放射科小黄 發表於 2025-9-9 10:12:15

基于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&deg; 翻面)。</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 &rarr; 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
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;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"&gt;

    &lt;ImageView
      android:id="@+id/ivSpin"
      android:layout_width="220dp"
      android:layout_height="220dp"
      android:scaleType="centerCrop"
      android:src="@drawable/sample" /&gt;

    &lt;!-- 可加控制按钮/文本,这里省略 --&gt;
&lt;/FrameLayout&gt;

// 文件: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 &amp;&amp; spinAnimatorY.isRunning()) {
            spinAnimatorY.pause();
      }
    }

    @Override
    protected void onResume() {
      super.onResume();
      if (spinAnimatorY != null &amp;&amp; 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 -&gt; {
            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 &amp;&amp; animator.isRunning()) animator.pause();
    }

    public void resumeAnim() {
      if (animator != null &amp;&amp; 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
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;resources&gt;
    &lt;declare-styleable name="Rotate3DImageView"&gt;
      &lt;!-- 0: Y轴;1: X轴 --&gt;
      &lt;attr name="axis" format="enum"&gt;
            &lt;enum name="y" value="0"/&gt;
            &lt;enum name="x" value="1"/&gt;
      &lt;/attr&gt;
      &lt;!-- Camera Z 位置(像素),负值表示远离屏幕,绝对值越大透视越弱 --&gt;
      &lt;attr name="cameraZ" format="float"/&gt;
      &lt;!-- 自动开始动画 --&gt;
      &lt;attr name="autoStart" format="boolean"/&gt;
      &lt;!-- 周期(毫秒) --&gt;
      &lt;attr name="durationMs" format="integer"/&gt;
    &lt;/declare-styleable&gt;
&lt;/resources&gt;

// ======================= 使用自定义 View 的布局示例 =======================
// 文件:res/layout/activity_custom.xml
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;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"&gt;

    &lt;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" /&gt;
&lt;/FrameLayout&gt;

// 文件: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, &quot;rotationY&quot;, 0f, 360f)</code> 连续旋转;</li><li><code>LinearInterpolator</code> 让转动匀速;可换 <code>AccelerateDecelerate</code> 做&ldquo;呼吸感&rdquo;。</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> 控制景深;也可不平移仅靠旋转,效果会更&ldquo;紧&rdquo;。</li><li>如果遇到某些机型显示异常,可试试 <code>setLayerType(LAYER_TYPE_SOFTWARE, null)</code> 或保持 <code>HARDWARE</code>,二者择一以实际效果为准。</li></ul>
<p><strong>性能 &amp; 资源</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>,实现简单、性能稳、展示效果已足够&ldquo;3D&rdquo;。</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 />&rarr; 大概率是 <code>cameraDistance</code> 太大(透视过弱)或太小(畸变)。建议在 <code>8000~20000 * density</code> 内调参。</p>
<p><strong><code>Camera</code> 效果在某些手机发虚/锯齿?</strong><br />&rarr; 尝试 <code>setLayerType(LAYER_TYPE_SOFTWARE, null)</code> 或 <code>HARDWARE</code> 切换;另外避免在动画中同时做大幅 <code>scale</code>。</p>
<p><strong>如何只做 180&deg; 卡片翻转?</strong><br />&rarr; 把动画区间调到 <code>0~180</code>,结束时替换图片即可;或在 90&deg; 时切换前后图层。</p>
<p><strong>如何让旋转更丝滑?</strong><br />&rarr; 使用 <code>LinearInterpolator</code> 匀速,时长 2.5~3.5s;图片尽量使用与控件尺寸匹配的资源,减少 GPU 采样压力。</p>
<p><strong>如何点击暂停/继续?</strong><br />&rarr; 方案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&nbsp;App获取屏幕旋转角度的方法</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: 基于Android实现三维效果的动态旋转图