基于Android实现三维效果的动态旋转图
作者:Katie。
一、项目背景详细介绍
在电商、相册、视频封面、海报展示、启动页 Logo 等场景里,带真实透视感的 3D 旋转能明显提升界面质感。常见需求:
- 图片绕 X/Y 轴持续旋转(封面展示、加载动效)。
- 卡片翻转(绕 X 或 Y 轴 180° 翻面)。
- 带景深的 3D 旋转(近大远小,具备透视压缩)。
Android 自 3.x 起就支持基于属性的 3D 旋转(rotationX/rotationY
),配合 setCameraDistance()
能得到还不错的透视感;而传统 Camera + Matrix
则能实现更精细的像素级控制。
二、项目需求详细介绍
- 图片能连续、平滑地 3D 旋转(可配方向/速度)。
- 可选绕 X 或绕 Y 轴旋转。
- 可设置景深强度(近大远小的透视感)。
- 可暂停/恢复、重复/往返等播放控制。
- 兼容 Android 5.0+,尽量避免兼容雷区。
三、相关技术详细介绍
- 属性动画:
ObjectAnimator
/ValueAnimator
控制rotationX/rotationY
。 - 透视距离:
View.setCameraDistance(float)
,距离越大透视越弱,单位是像素乘以屏幕密度系数。 - 插值器:
LinearInterpolator
(匀速)、AccelerateDecelerateInterpolator
(缓入缓出)。 - Camera/Matrix:
android.graphics.Camera
做 3D 变换、Matrix
应用到Canvas
。 - 硬件加速:属性动画天然兼容;
Camera+Matrix
某些机型需要切换图层类型。
四、实现思路详细介绍
方案A(首选):
1)XML/代码里设定较大的 cameraDistance
;
2)用 ObjectAnimator
驱动 rotationY
(或 rotationX
)从 0 → 360 循环;
3)可选 repeatCount/Mode
、Interpolator
、时长。
优点:简单、兼容性好、硬件加速性能佳。
方案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)
setCameraDistance(12000 * density)
是非常关键的一步。- 距离越小透视越强(近大远小更明显),太小会变形严重;距离越大透视越弱,趋近于平面旋转。
- 常见经验范围:
8000f ~ 20000f
(乘以density
)。
属性动画控制
- 使用
ObjectAnimator.ofFloat(view, "rotationY", 0f, 360f)
连续旋转; LinearInterpolator
让转动匀速;可换AccelerateDecelerate
做“呼吸感”。pause()/resume()
方便在onPause/onResume
做生命周期管理,避免后台耗电。
Camera/Matrix(方案B)
- 在
onDraw()
中使用Camera.rotateY/rotateX
后要平移到控件中心旋转:preTranslate(-cx,-cy) / postTranslate(cx,cy)
; camera.translate(0,0,cameraZ)
控制景深;也可不平移仅靠旋转,效果会更“紧”。- 如果遇到某些机型显示异常,可试试
setLayerType(LAYER_TYPE_SOFTWARE, null)
或保持HARDWARE
,二者择一以实际效果为准。
性能 & 资源
- 方案A 基本不需要担心性能(GPU 动画,轻量);
- 方案B 每帧重绘,尽量避免做额外开销(如 Bitmap 频繁创建)。
- 图片过大时请用
centerCrop
与合适分辨率,避免内存抖动。
七、项目详细总结
- 首选:属性动画 +
cameraDistance
,实现简单、性能稳、展示效果已足够“3D”。 - 进阶:
Camera+Matrix
给你更高自由度(半区翻转、复杂翻书效果),代价是自己管理绘制与兼容。 - 通用建议:合理设置透视距离与动画时长,注意生命周期暂停恢复,避免后台白跑。
八、常见问题与解答(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()
。
九、扩展方向与优化建议
- 组合动效:在旋转同时,叠加轻微
scale
/alpha
做呼吸感。 - 多图轮播:配合
ViewPager2
或RecyclerView
,在切换页时加 3D 翻页过渡。 - 曲线速度:自定义
TimeInterpolator
(如先快后慢)营造动势。 - 数据驱动:把
degree
暴露为可绑定属性(DataBinding/Compose),做可控进度展示。 - Compose 版本:用
Modifier.graphicsLayer { rotationY = ...; cameraDistance = ... }
+rememberInfiniteTransition
实现同等效果。
以上就是基于Android实现三维效果的动态旋转图的详细内容,更多关于Android动态旋转图的资料请关注脚本之家其它相关文章!