基于Android实现三维效果的动态旋转图
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>一、项目背景详细介绍</li><li>二、项目需求详细介绍</li><li>三、相关技术详细介绍</li><li>四、实现思路详细介绍</li><li>五、完整实现代码</li><li>六、代码与关键点解读</li><li>七、项目详细总结</li><li>八、常见问题与解答(FAQ)</li><li>九、扩展方向与优化建议</li></ul></div><p class="maodian"></p><h2>一、项目背景详细介绍</h2><p>在电商、相册、视频封面、海报展示、启动页 Logo 等场景里,带<strong>真实透视感</strong>的 3D 旋转能明显提升界面质感。常见需求:</p>
<ul><li>图片<strong>绕 X/Y 轴</strong>持续旋转(封面展示、加载动效)。</li><li><strong>卡片翻转</strong>(绕 X 或 Y 轴 180° 翻面)。</li><li><strong>带景深</strong>的 3D 旋转(近大远小,具备透视压缩)。</li></ul>
<p>Android 自 3.x 起就支持基于属性的 3D 旋转(<code>rotationX/rotationY</code>),配合 <code>setCameraDistance()</code> 能得到还不错的透视感;而传统 <code>Camera + Matrix</code> 则能实现更精细的像素级控制。</p>
<p class="maodian"></p><h2>二、项目需求详细介绍</h2>
<ol><li>图片能<strong>连续、平滑</strong>地 3D 旋转(可配方向/速度)。</li><li>可选<strong>绕 X 或绕 Y</strong> 轴旋转。</li><li>可设置<strong>景深强度</strong>(近大远小的透视感)。</li><li><strong>可暂停/恢复</strong>、<strong>重复/往返</strong>等播放控制。</li><li>兼容 Android 5.0+,尽量避免兼容雷区。</li></ol>
<p class="maodian"></p><h2>三、相关技术详细介绍</h2>
<ul><li><strong>属性动画</strong>:<code>ObjectAnimator</code> / <code>ValueAnimator</code> 控制 <code>rotationX/rotationY</code>。</li><li><strong>透视距离</strong>:<code>View.setCameraDistance(float)</code>,距离越大透视越弱,单位是像素乘以屏幕密度系数。</li><li><strong>插值器</strong>:<code>LinearInterpolator</code>(匀速)、<code>AccelerateDecelerateInterpolator</code>(缓入缓出)。</li><li><strong>Camera/Matrix</strong>:<code>android.graphics.Camera</code> 做 3D 变换、<code>Matrix</code> 应用到 <code>Canvas</code>。</li><li><strong>硬件加速</strong>:属性动画天然兼容;<code>Camera+Matrix</code> 某些机型需要切换图层类型。</li></ul>
<p class="maodian"></p><h2>四、实现思路详细介绍</h2>
<p><strong>方案A</strong>(首选):<br />1)XML/代码里设定较大的 <code>cameraDistance</code>;<br />2)用 <code>ObjectAnimator</code> 驱动 <code>rotationY</code>(或 <code>rotationX</code>)从 0 → 360 循环;<br />3)可选 <code>repeatCount/Mode</code>、<code>Interpolator</code>、时长。<br />优点:简单、兼容性好、硬件加速性能佳。</p>
<p><strong>方案B</strong>(可精细控制):<br />1)自定义 <code>Rotate3DImageView</code>,在 <code>onDraw()</code> 里用 <code>Camera.rotateX/rotateY</code> + <code>Matrix</code>;<br />2)可在绘制前/后裁剪半区,做上半/下半独立翻转;<br />3)<code>ValueAnimator</code> 驱动角度更新;<br />4)必要时设置 <code>setLayerType(LAYER_TYPE_SOFTWARE/HARDWARE)</code> 规避机型差异。</p>
<p class="maodian"></p><h2>五、完整实现代码</h2>
<div class="jb51code"><pre class="brush:java;">// ======================= 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);
}
}</pre></div>
<p class="maodian"></p><h2>六、代码与关键点解读</h2>
<p><strong>相机距离(方案A)</strong></p>
<ul><li><code>setCameraDistance(12000 * density)</code> 是非常关键的一步。</li><li>距离越小透视越强(近大远小更明显),太小会变形严重;距离越大透视越弱,趋近于平面旋转。</li><li>常见经验范围:<code>8000f ~ 20000f</code>(乘以 <code>density</code>)。</li></ul>
<p><strong>属性动画控制</strong></p>
<ul><li>使用 <code>ObjectAnimator.ofFloat(view, "rotationY", 0f, 360f)</code> 连续旋转;</li><li><code>LinearInterpolator</code> 让转动匀速;可换 <code>AccelerateDecelerate</code> 做“呼吸感”。</li><li><code>pause()/resume()</code> 方便在 <code>onPause/onResume</code> 做生命周期管理,避免后台耗电。</li></ul>
<p><strong>Camera/Matrix(方案B)</strong></p>
<ul><li>在 <code>onDraw()</code> 中使用 <code>Camera.rotateY/rotateX</code> 后要<strong>平移到控件中心</strong>旋转:<code>preTranslate(-cx,-cy) / postTranslate(cx,cy)</code>;</li><li><code>camera.translate(0,0,cameraZ)</code> 控制景深;也可不平移仅靠旋转,效果会更“紧”。</li><li>如果遇到某些机型显示异常,可试试 <code>setLayerType(LAYER_TYPE_SOFTWARE, null)</code> 或保持 <code>HARDWARE</code>,二者择一以实际效果为准。</li></ul>
<p><strong>性能 & 资源</strong></p>
<ul><li>方案A 基本不需要担心性能(GPU 动画,轻量);</li><li>方案B 每帧重绘,尽量避免做额外开销(如 Bitmap 频繁创建)。</li><li>图片过大时请用 <code>centerCrop</code> 与合适分辨率,避免内存抖动。</li></ul>
<p class="maodian"></p><h2>七、项目详细总结</h2>
<ul><li><strong>首选</strong>:属性动画 + <code>cameraDistance</code>,实现简单、性能稳、展示效果已足够“3D”。</li><li><strong>进阶</strong>:<code>Camera+Matrix</code> 给你更高自由度(半区翻转、复杂翻书效果),代价是自己管理绘制与兼容。</li><li><strong>通用建议</strong>:合理设置透视距离与动画时长,注意生命周期暂停恢复,避免后台白跑。</li></ul>
<p class="maodian"></p><h2>八、常见问题与解答(FAQ)</h2>
<p><strong>为什么我设置了 <code>rotationY</code> 但看不出 3D 透视?</strong><br />→ 大概率是 <code>cameraDistance</code> 太大(透视过弱)或太小(畸变)。建议在 <code>8000~20000 * density</code> 内调参。</p>
<p><strong><code>Camera</code> 效果在某些手机发虚/锯齿?</strong><br />→ 尝试 <code>setLayerType(LAYER_TYPE_SOFTWARE, null)</code> 或 <code>HARDWARE</code> 切换;另外避免在动画中同时做大幅 <code>scale</code>。</p>
<p><strong>如何只做 180° 卡片翻转?</strong><br />→ 把动画区间调到 <code>0~180</code>,结束时替换图片即可;或在 90° 时切换前后图层。</p>
<p><strong>如何让旋转更丝滑?</strong><br />→ 使用 <code>LinearInterpolator</code> 匀速,时长 2.5~3.5s;图片尽量使用与控件尺寸匹配的资源,减少 GPU 采样压力。</p>
<p><strong>如何点击暂停/继续?</strong><br />→ 方案A 直接 <code>pause()/resume()</code>;方案B 对 <code>ValueAnimator</code> 调用相同方法或 <code>cancel()/start()</code>。</p>
<p class="maodian"></p><h2>九、扩展方向与优化建议</h2>
<ul><li><strong>组合动效</strong>:在旋转同时,叠加轻微 <code>scale</code>/<code>alpha</code> 做呼吸感。</li><li><strong>多图轮播</strong>:配合 <code>ViewPager2</code> 或 <code>RecyclerView</code>,在切换页时加 3D 翻页过渡。</li><li><strong>曲线速度</strong>:自定义 <code>TimeInterpolator</code>(如先快后慢)营造动势。</li><li><strong>数据驱动</strong>:把 <code>degree</code> 暴露为可绑定属性(DataBinding/Compose),做可控进度展示。</li><li><strong>Compose 版本</strong>:用 <code>Modifier.graphicsLayer { rotationY = ...; cameraDistance = ... }</code> + <code>rememberInfiniteTransition</code> 实现同等效果。</li></ul>
<p>以上就是基于Android实现三维效果的动态旋转图的详细内容,更多关于Android动态旋转图的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>Android使用Matrix旋转图片模拟碟片加载过程</li><li>Android imageVIew实现镜像旋转的方法</li><li>Android开发sensor旋转屏问题解决示例</li><li>Android App获取屏幕旋转角度的方法</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]