基于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动态旋转图的资料请关注脚本之家其它相关文章!
