基于Android實(shí)現(xiàn)三維效果的動態(tài)旋轉(zhuǎn)圖
一、項(xiàng)目背景詳細(xì)介紹
在電商、相冊、視頻封面、海報展示、啟動頁 Logo 等場景里,帶真實(shí)透視感的 3D 旋轉(zhuǎn)能明顯提升界面質(zhì)感。常見需求:
- 圖片繞 X/Y 軸持續(xù)旋轉(zhuǎn)(封面展示、加載動效)。
- 卡片翻轉(zhuǎn)(繞 X 或 Y 軸 180° 翻面)。
- 帶景深的 3D 旋轉(zhuǎn)(近大遠(yuǎn)小,具備透視壓縮)。
Android 自 3.x 起就支持基于屬性的 3D 旋轉(zhuǎn)(rotationX/rotationY),配合 setCameraDistance() 能得到還不錯的透視感;而傳統(tǒng) Camera + Matrix 則能實(shí)現(xiàn)更精細(xì)的像素級控制。
二、項(xiàng)目需求詳細(xì)介紹
- 圖片能連續(xù)、平滑地 3D 旋轉(zhuǎn)(可配方向/速度)。
- 可選繞 X 或繞 Y 軸旋轉(zhuǎn)。
- 可設(shè)置景深強(qiáng)度(近大遠(yuǎn)小的透視感)。
- 可暫停/恢復(fù)、重復(fù)/往返等播放控制。
- 兼容 Android 5.0+,盡量避免兼容雷區(qū)。
三、相關(guān)技術(shù)詳細(xì)介紹
- 屬性動畫:
ObjectAnimator/ValueAnimator控制rotationX/rotationY。 - 透視距離:
View.setCameraDistance(float),距離越大透視越弱,單位是像素乘以屏幕密度系數(shù)。 - 插值器:
LinearInterpolator(勻速)、AccelerateDecelerateInterpolator(緩入緩出)。 - Camera/Matrix:
android.graphics.Camera做 3D 變換、Matrix應(yīng)用到Canvas。 - 硬件加速:屬性動畫天然兼容;
Camera+Matrix某些機(jī)型需要切換圖層類型。
四、實(shí)現(xiàn)思路詳細(xì)介紹
方案A(首選):
1)XML/代碼里設(shè)定較大的 cameraDistance;
2)用 ObjectAnimator 驅(qū)動 rotationY(或 rotationX)從 0 → 360 循環(huán);
3)可選 repeatCount/Mode、Interpolator、時長。
優(yōu)點(diǎn):簡單、兼容性好、硬件加速性能佳。
方案B(可精細(xì)控制):
1)自定義 Rotate3DImageView,在 onDraw() 里用 Camera.rotateX/rotateY + Matrix;
2)可在繪制前/后裁剪半?yún)^(qū),做上半/下半獨(dú)立翻轉(zhuǎn);
3)ValueAnimator 驅(qū)動角度更新;
4)必要時設(shè)置 setLayerType(LAYER_TYPE_SOFTWARE/HARDWARE) 規(guī)避機(jī)型差異。
五、完整實(shí)現(xiàn)代碼
// ======================= 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) 設(shè)置相機(jī)距離(越大透視越弱;數(shù)值過小會導(dǎo)致透視夸張/變形)
// 建議:以屏幕密度為基準(zhǔn)放大;經(jīng)驗(yàn)值:8000f ~ 20000f(按像素 * density)
float density = getResources().getDisplayMetrics().density;
ivSpin.setCameraDistance(12000 * density); // 試著改大/改小感受透視差別
// 2) 繞Y軸無限旋轉(zhuǎn)(可換成 rotationX)
spinAnimatorY = ObjectAnimator.ofFloat(ivSpin, "rotationY", 0f, 360f);
spinAnimatorY.setDuration(3000); // 一圈3秒
spinAnimatorY.setRepeatCount(ValueAnimator.INFINITE);
spinAnimatorY.setInterpolator(new LinearInterpolator());
spinAnimatorY.start();
// 如需切換成繞X軸旋轉(zhuǎn),改用下面這段(示例先不啟動)
spinAnimatorX = ObjectAnimator.ofFloat(ivSpin, "rotationX", 0f, 360f);
spinAnimatorX.setDuration(3000);
spinAnimatorX.setRepeatCount(ValueAnimator.INFINITE);
spinAnimatorX.setInterpolator(new LinearInterpolator());
// 可依據(jù)交互,在按鈕點(diǎn)擊時: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. 進(jìn)階方案:自定義 View + Camera/Matrix =======================
// 亮點(diǎn):可精細(xì)控制透視與局部翻轉(zhuǎn);適合卡片翻頁、上半/下半獨(dú)立翻轉(zhuǎn)等
// 文件: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; // 當(dāng)前角度
private int axis = AXIS_Y; // 旋轉(zhuǎn)軸,默認(rèn)Y
private float cameraZ = -12_000f; // 相機(jī)Z,負(fù)值表示遠(yuǎn)離屏幕(像素維度)
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 處理某些機(jī)型的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;
// 保存畫布狀態(tài)
int saveCount = canvas.save();
matrix.reset();
camera.save();
// 設(shè)置相機(jī)位置(Z 軸),單位是像素。負(fù)值遠(yuǎn)離屏幕,絕對值越大透視越弱
// Camera#translate(0, 0, z) 不同廠商實(shí)現(xiàn)略有差異,必要時可按密度縮放
camera.translate(0, 0, cameraZ);
if (axis == AXIS_Y) {
camera.rotateY(degree);
} else {
camera.rotateX(degree);
}
camera.getMatrix(matrix);
camera.restore();
// 將旋轉(zhuǎn)中心平移到控件中心(Camera/Matrix 默認(rèn)以(0,0)為中心)
matrix.preTranslate(-cx, -cy);
matrix.postTranslate(cx, cy);
// 應(yīng)用矩陣到畫布
canvas.concat(matrix);
// 繪制圖片
drawable.draw(canvas);
// 恢復(fù)畫布
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 位置(像素),負(fù)值表示遠(yuǎn)離屏幕,絕對值越大透視越弱 -->
<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);
// 也可在代碼里切換軸/時長/相機(jī)Z
// v.setAxis(Rotate3DImageView.AXIS_X);
// v.setCameraZ(-16000f);
// v.setDuration(3500);
}
}六、代碼與關(guān)鍵點(diǎn)解讀
相機(jī)距離(方案A)
setCameraDistance(12000 * density)是非常關(guān)鍵的一步。- 距離越小透視越強(qiáng)(近大遠(yuǎn)小更明顯),太小會變形嚴(yán)重;距離越大透視越弱,趨近于平面旋轉(zhuǎn)。
- 常見經(jīng)驗(yàn)范圍:
8000f ~ 20000f(乘以density)。
屬性動畫控制
- 使用
ObjectAnimator.ofFloat(view, "rotationY", 0f, 360f)連續(xù)旋轉(zhuǎn); LinearInterpolator讓轉(zhuǎn)動勻速;可換AccelerateDecelerate做“呼吸感”。pause()/resume()方便在onPause/onResume做生命周期管理,避免后臺耗電。
Camera/Matrix(方案B)
- 在
onDraw()中使用Camera.rotateY/rotateX后要平移到控件中心旋轉(zhuǎn):preTranslate(-cx,-cy) / postTranslate(cx,cy); camera.translate(0,0,cameraZ)控制景深;也可不平移僅靠旋轉(zhuǎn),效果會更“緊”。- 如果遇到某些機(jī)型顯示異常,可試試
setLayerType(LAYER_TYPE_SOFTWARE, null)或保持HARDWARE,二者擇一以實(shí)際效果為準(zhǔn)。
性能 & 資源
- 方案A 基本不需要擔(dān)心性能(GPU 動畫,輕量);
- 方案B 每幀重繪,盡量避免做額外開銷(如 Bitmap 頻繁創(chuàng)建)。
- 圖片過大時請用
centerCrop與合適分辨率,避免內(nèi)存抖動。
七、項(xiàng)目詳細(xì)總結(jié)
- 首選:屬性動畫 +
cameraDistance,實(shí)現(xiàn)簡單、性能穩(wěn)、展示效果已足夠“3D”。 - 進(jìn)階:
Camera+Matrix給你更高自由度(半?yún)^(qū)翻轉(zhuǎn)、復(fù)雜翻書效果),代價是自己管理繪制與兼容。 - 通用建議:合理設(shè)置透視距離與動畫時長,注意生命周期暫?;謴?fù),避免后臺白跑。
八、常見問題與解答(FAQ)
為什么我設(shè)置了 rotationY 但看不出 3D 透視?
→ 大概率是 cameraDistance 太大(透視過弱)或太?。ɑ儯?。建議在 8000~20000 * density 內(nèi)調(diào)參。
Camera 效果在某些手機(jī)發(fā)虛/鋸齒?
→ 嘗試 setLayerType(LAYER_TYPE_SOFTWARE, null) 或 HARDWARE 切換;另外避免在動畫中同時做大幅 scale。
如何只做 180° 卡片翻轉(zhuǎn)?
→ 把動畫區(qū)間調(diào)到 0~180,結(jié)束時替換圖片即可;或在 90° 時切換前后圖層。
如何讓旋轉(zhuǎn)更絲滑?
→ 使用 LinearInterpolator 勻速,時長 2.5~3.5s;圖片盡量使用與控件尺寸匹配的資源,減少 GPU 采樣壓力。
如何點(diǎn)擊暫停/繼續(xù)?
→ 方案A 直接 pause()/resume();方案B 對 ValueAnimator 調(diào)用相同方法或 cancel()/start()。
九、擴(kuò)展方向與優(yōu)化建議
- 組合動效:在旋轉(zhuǎn)同時,疊加輕微
scale/alpha做呼吸感。 - 多圖輪播:配合
ViewPager2或RecyclerView,在切換頁時加 3D 翻頁過渡。 - 曲線速度:自定義
TimeInterpolator(如先快后慢)營造動勢。 - 數(shù)據(jù)驅(qū)動:把
degree暴露為可綁定屬性(DataBinding/Compose),做可控進(jìn)度展示。 - Compose 版本:用
Modifier.graphicsLayer { rotationY = ...; cameraDistance = ... }+rememberInfiniteTransition實(shí)現(xiàn)同等效果。
以上就是基于Android實(shí)現(xiàn)三維效果的動態(tài)旋轉(zhuǎn)圖的詳細(xì)內(nèi)容,更多關(guān)于Android動態(tài)旋轉(zhuǎn)圖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android提高之AudioRecord實(shí)現(xiàn)助聽器的方法
這篇文章主要介紹了Android中AudioRecord實(shí)現(xiàn)助聽器的方法,對進(jìn)行Android項(xiàng)目開發(fā)有一定的借鑒價值,需要的朋友可以參考下2014-08-08
Android AnalogClock簡單使用方法實(shí)例
這篇文章主要介紹了Android AnalogClock簡單使用方法,結(jié)合實(shí)例形式簡單分析了AnalogClock的布局調(diào)用技巧,需要的朋友可以參考下2016-01-01
使用Android開發(fā)接入第三方原生SDK實(shí)現(xiàn)微信登錄
這篇文章主要介紹了使用Android開發(fā)接入第三方原生SDK實(shí)現(xiàn)微信登錄,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android編程實(shí)現(xiàn)仿心跳動畫效果的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)仿心跳動畫效果的方法,實(shí)例分析了Android基于線程實(shí)現(xiàn)動畫過度效果的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11
Android 藍(lán)牙連接 ESC/POS 熱敏打印機(jī)打印實(shí)例(ESC/POS指令篇)
這篇文章主要介紹了Android 藍(lán)牙連接 ESC/POS 熱敏打印機(jī)打印實(shí)例(ESC/POS指令篇),具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04
Android開發(fā)之基于DialogFragment創(chuàng)建對話框的方法示例
這篇文章主要介紹了Android開發(fā)之基于DialogFragment創(chuàng)建對話框的方法,結(jié)合實(shí)例形式分析了DialogFragment創(chuàng)建對話框的具體功能與布局相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-08-08
Android使用java實(shí)現(xiàn)網(wǎng)絡(luò)連通性檢查詳解
這篇文章主要為大家詳細(xì)介紹了Android使用java實(shí)現(xiàn)網(wǎng)絡(luò)連通性檢查的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-12-12
Android中Gallery和ImageSwitcher的使用實(shí)例
今天小編就為大家分享一篇關(guān)于Android中Gallery和ImageSwitcher的使用實(shí)例,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03
Android table布局開發(fā)實(shí)現(xiàn)簡單計算器
這篇文章主要為大家詳細(xì)介紹了Android table布局開發(fā)實(shí)現(xiàn)簡單計算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-05-05

