Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Android动态旋转图

基于Android实现三维效果的动态旋转图

作者:Katie。

在电商、相册、视频封面、海报展示、启动页 Logo 等场景里,带真实透视感的 3D 旋转能明显提升界面质感,Android 自 3.x 起就支持基于属性的 3D 旋转,所以本文给大家介绍了如何基于Android实现三维效果的动态旋转图,需要的朋友可以参考下

一、项目背景详细介绍

在电商、相册、视频封面、海报展示、启动页 Logo 等场景里,带真实透视感的 3D 旋转能明显提升界面质感。常见需求:

Android 自 3.x 起就支持基于属性的 3D 旋转(rotationX/rotationY),配合 setCameraDistance() 能得到还不错的透视感;而传统 Camera + Matrix 则能实现更精细的像素级控制。

二、项目需求详细介绍

  1. 图片能连续、平滑地 3D 旋转(可配方向/速度)。
  2. 可选绕 X 或绕 Y 轴旋转。
  3. 可设置景深强度(近大远小的透视感)。
  4. 可暂停/恢复重复/往返等播放控制。
  5. 兼容 Android 5.0+,尽量避免兼容雷区。

三、相关技术详细介绍

四、实现思路详细介绍

方案A(首选):
1)XML/代码里设定较大的 cameraDistance
2)用 ObjectAnimator 驱动 rotationY(或 rotationX)从 0 → 360 循环;
3)可选 repeatCount/ModeInterpolator、时长。
优点:简单、兼容性好、硬件加速性能佳。

方案B(可精细控制):
1)自定义 Rotate3DImageView,在 onDraw() 里用 Camera.rotateX/rotateY + Matrix
2)可在绘制前/后裁剪半区,做上半/下半独立翻转;
3)ValueAnimator 驱动角度更新;
4)必要时设置 setLayerType(LAYER_TYPE_SOFTWARE/HARDWARE) 规避机型差异。

五、完整实现代码

// ======================= A. 推荐方案:属性动画 + cameraDistance =======================
// 文件:res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:foregroundGravity="center">
 
    <ImageView
        android:id="@+id/ivSpin"
        android:layout_width="220dp"
        android:layout_height="220dp"
        android:scaleType="centerCrop"
        android:src="@drawable/sample" />
 
    <!-- 可加控制按钮/文本,这里省略 -->
</FrameLayout>
 
// 文件:java/com/example/rotate3d/MainActivity.java
package com.example.rotate3d;
 
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
 
import androidx.appcompat.app.AppCompatActivity;
 
public class MainActivity extends AppCompatActivity {
 
    private ImageView ivSpin;
    private ObjectAnimator spinAnimatorY;
    private ObjectAnimator spinAnimatorX;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ivSpin = findViewById(R.id.ivSpin);
 
        // 1) 设置相机距离(越大透视越弱;数值过小会导致透视夸张/变形)
        // 建议:以屏幕密度为基准放大;经验值:8000f ~ 20000f(按像素 * density)
        float density = getResources().getDisplayMetrics().density;
        ivSpin.setCameraDistance(12000 * density); // 试着改大/改小感受透视差别
 
        // 2) 绕Y轴无限旋转(可换成 rotationX)
        spinAnimatorY = ObjectAnimator.ofFloat(ivSpin, "rotationY", 0f, 360f);
        spinAnimatorY.setDuration(3000); // 一圈3秒
        spinAnimatorY.setRepeatCount(ValueAnimator.INFINITE);
        spinAnimatorY.setInterpolator(new LinearInterpolator());
        spinAnimatorY.start();
 
        // 如需切换成绕X轴旋转,改用下面这段(示例先不启动)
        spinAnimatorX = ObjectAnimator.ofFloat(ivSpin, "rotationX", 0f, 360f);
        spinAnimatorX.setDuration(3000);
        spinAnimatorX.setRepeatCount(ValueAnimator.INFINITE);
        spinAnimatorX.setInterpolator(new LinearInterpolator());
 
        // 可依据交互,在按钮点击时:spinAnimatorY.pause()/resume()/cancel()
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        if (spinAnimatorY != null && spinAnimatorY.isRunning()) {
            spinAnimatorY.pause();
        }
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        if (spinAnimatorY != null && spinAnimatorY.isPaused()) {
            spinAnimatorY.resume();
        }
    }
}
 
 
// ======================= B. 进阶方案:自定义 View + Camera/Matrix =======================
// 亮点:可精细控制透视与局部翻转;适合卡片翻页、上半/下半独立翻转等
// 文件:java/com/example/rotate3d/Rotate3DImageView.java
package com.example.rotate3d;
 
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Camera;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;
import androidx.annotation.Nullable;
 
public class Rotate3DImageView extends View {
 
    public static final int AXIS_Y = 0;
    public static final int AXIS_X = 1;
 
    private Drawable drawable;
    private Bitmap bitmap;
    private final Camera camera = new Camera();
    private final Matrix matrix = new Matrix();
    private float degree = 0f;  // 当前角度
    private int axis = AXIS_Y;  // 旋转轴,默认Y
    private float cameraZ = -12_000f; // 相机Z,负值表示远离屏幕(像素维度)
    private boolean autoStart = true;
    private long duration = 3000L;
 
    private ValueAnimator animator;
 
    public Rotate3DImageView(Context context) { this(context, null); }
    public Rotate3DImageView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public Rotate3DImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (attrs != null) {
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Rotate3DImageView);
            axis = a.getInt(R.styleable.Rotate3DImageView_axis, AXIS_Y);
            cameraZ = a.getFloat(R.styleable.Rotate3DImageView_cameraZ, -12000f);
            autoStart = a.getBoolean(R.styleable.Rotate3DImageView_autoStart, true);
            duration = a.getInt(R.styleable.Rotate3DImageView_durationMs, 3000);
            a.recycle();
        }
        setWillNotDraw(false);
        setLayerType(LAYER_TYPE_HARDWARE, null); // 也可尝试 SOFTWARE 处理某些机型的Camera兼容
    }
 
    public void setImageDrawable(Drawable d) {
        this.drawable = d;
        if (d instanceof BitmapDrawable) {
            bitmap = ((BitmapDrawable) d).getBitmap();
        } else {
            bitmap = null;
        }
        requestLayout();
        invalidate();
    }
 
    public void setImageResource(int resId) {
        setImageDrawable(getResources().getDrawable(resId));
    }
 
    public void setAxis(int axis) {
        this.axis = axis;
        invalidate();
    }
 
    public void setDegree(float degree) {
        this.degree = degree;
        invalidate();
    }
 
    public void setCameraZ(float z) {
        this.cameraZ = z;
        invalidate();
    }
 
    public void setDuration(long durationMs) {
        this.duration = durationMs;
        if (animator != null) animator.setDuration(durationMs);
    }
 
    private void ensureAnimator() {
        if (animator != null) return;
        animator = ValueAnimator.ofFloat(0f, 360f);
        animator.setInterpolator(new LinearInterpolator());
        animator.setDuration(duration);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(a -> {
            degree = (float) a.getAnimatedValue();
            invalidate();
        });
    }
 
    @Override protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (autoStart) start();
    }
 
    @Override protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stop();
    }
 
    public void start() {
        ensureAnimator();
        if (!animator.isStarted()) animator.start();
    }
 
    public void pause() {
        if (animator != null && animator.isRunning()) animator.pause();
    }
 
    public void resumeAnim() {
        if (animator != null && animator.isPaused()) animator.resume();
    }
 
    public void stop() {
        if (animator != null) {
            animator.cancel();
        }
    }
 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int w = resolveSize(
                drawable == null ? 200 : Math.max(drawable.getIntrinsicWidth(), 1),
                widthMeasureSpec);
        int h = resolveSize(
                drawable == null ? 200 : Math.max(drawable.getIntrinsicHeight(), 1),
                heightMeasureSpec);
        setMeasuredDimension(w, h);
        if (drawable != null) {
            drawable.setBounds(0, 0, w, h);
        }
    }
 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (drawable == null) return;
 
        final int cx = getWidth() / 2;
        final int cy = getHeight() / 2;
 
        // 保存画布状态
        int saveCount = canvas.save();
 
        matrix.reset();
        camera.save();
 
        // 设置相机位置(Z 轴),单位是像素。负值远离屏幕,绝对值越大透视越弱
        // Camera#translate(0, 0, z) 不同厂商实现略有差异,必要时可按密度缩放
        camera.translate(0, 0, cameraZ);
 
        if (axis == AXIS_Y) {
            camera.rotateY(degree);
        } else {
            camera.rotateX(degree);
        }
        camera.getMatrix(matrix);
        camera.restore();
 
        // 将旋转中心平移到控件中心(Camera/Matrix 默认以(0,0)为中心)
        matrix.preTranslate(-cx, -cy);
        matrix.postTranslate(cx, cy);
 
        // 应用矩阵到画布
        canvas.concat(matrix);
 
        // 绘制图片
        drawable.draw(canvas);
 
        // 恢复画布
        canvas.restoreToCount(saveCount);
    }
}
 
 
// ======================= 自定义属性声明 =======================
// 文件:res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Rotate3DImageView">
        <!-- 0: Y轴;1: X轴 -->
        <attr name="axis" format="enum">
            <enum name="y" value="0"/>
            <enum name="x" value="1"/>
        </attr>
        <!-- Camera Z 位置(像素),负值表示远离屏幕,绝对值越大透视越弱 -->
        <attr name="cameraZ" format="float"/>
        <!-- 自动开始动画 -->
        <attr name="autoStart" format="boolean"/>
        <!-- 周期(毫秒) -->
        <attr name="durationMs" format="integer"/>
    </declare-styleable>
</resources>
 
// ======================= 使用自定义 View 的布局示例 =======================
// 文件:res/layout/activity_custom.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:foregroundGravity="center">
 
    <com.example.rotate3d.Rotate3DImageView
        android:id="@+id/iv3d"
        android:layout_width="240dp"
        android:layout_height="240dp"
        app:axis="y"
        app:cameraZ="-12000"
        app:autoStart="true"
        app:durationMs="2800" />
</FrameLayout>
 
// 文件:java/com/example/rotate3d/CustomActivity.java
package com.example.rotate3d;
 
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
 
public class CustomActivity extends AppCompatActivity {
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom);
 
        Rotate3DImageView v = findViewById(R.id.iv3d);
        v.setImageResource(R.drawable.sample);
        // 也可在代码里切换轴/时长/相机Z
        // v.setAxis(Rotate3DImageView.AXIS_X);
        // v.setCameraZ(-16000f);
        // v.setDuration(3500);
    }
}

六、代码与关键点解读

相机距离(方案A)

属性动画控制

Camera/Matrix(方案B)

性能 & 资源

七、项目详细总结

八、常见问题与解答(FAQ)

为什么我设置了 rotationY 但看不出 3D 透视?
→ 大概率是 cameraDistance 太大(透视过弱)或太小(畸变)。建议在 8000~20000 * density 内调参。

Camera 效果在某些手机发虚/锯齿?
→ 尝试 setLayerType(LAYER_TYPE_SOFTWARE, null)HARDWARE 切换;另外避免在动画中同时做大幅 scale

如何只做 180° 卡片翻转?
→ 把动画区间调到 0~180,结束时替换图片即可;或在 90° 时切换前后图层。

如何让旋转更丝滑?
→ 使用 LinearInterpolator 匀速,时长 2.5~3.5s;图片尽量使用与控件尺寸匹配的资源,减少 GPU 采样压力。

如何点击暂停/继续?
→ 方案A 直接 pause()/resume();方案B 对 ValueAnimator 调用相同方法或 cancel()/start()

九、扩展方向与优化建议

以上就是基于Android实现三维效果的动态旋转图的详细内容,更多关于Android动态旋转图的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文