前兩天,偶然看到自如大前端開源了一個裸眼3D的Banner輪播圖實作方案,覺得非常有意思,于是也打算研究一下,

1,實作原理
實作原理來自自如客APP裸眼3D效果的實作
1.1 分層
打開Android Stusio進行布局分析時會發現,他們的Banner使用了兩層視圖,對應兩個Viewpager,并且這兩個Viewpager還實作了聯動,如下圖所示,

除了Viewpager的聯動,他們的Banner還支持裸眼3D效果,能夠跟隨陀螺進行顯示上的變化,
1.2 位移
打開自如客App,當用戶在不同的角度上看Banner時會看到明顯的錯位移動,這種錯位移動其實借助的是設備本身的傳感器來實作的,具體實作方式是讓底部的背景始終保持不動,然后根據從設備傳感器獲取當前設備對應的傾斜角,計算出背景和前景的移動距離,進而執行背景和前景移動的動作,示意圖如下,

相關的代碼如下:
1, 傳感器代碼
mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
// 重力傳感器
mAcceleSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// 地磁場傳感器
mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mSensorManager.registerListener(this, mAcceleSensor, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(this, mMagneticSensor, SensorManager.SENSOR_DELAY_GAME);
2,計算偏移角度代碼
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mAcceleValues = event.values;
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mMageneticValues = event.values;
}
float[] values = new float[3];
float[] R = new float[9];
SensorManager.getRotationMatrix(R, null, mAcceleValues, mMageneticValues);
SensorManager.getOrientation(R, values);
// x軸的偏轉角度
values[1] = (float) Math.toDegrees(values[1]);
// y軸的偏轉角度
values[2] = (float) Math.toDegrees(values[2]);
3,執行相對偏移計算
if (mDegreeY <= 0 && mDegreeY > mDegreeYMin) {
hasChangeX = true;
scrollX = (int) (mDegreeY / Math.abs(mDegreeYMin) * mXMoveDistance*mDirection);
} else if (mDegreeY > 0 && mDegreeY < mDegreeYMax) {
hasChangeX = true;
scrollX = (int) (mDegreeY / Math.abs(mDegreeYMax) * mXMoveDistance*mDirection);
}
if (mDegreeX <= 0 && mDegreeX > mDegreeXMin) {
hasChangeY = true;
scrollY = (int) (mDegreeX / Math.abs(mDegreeXMin) * mYMoveDistance*mDirection);
} else if (mDegreeX > 0 && mDegreeX < mDegreeXMax) {
hasChangeY = true;
scrollY = (int) (mDegreeX / Math.abs(mDegreeXMax) * mYMoveDistance*mDirection);
}
smoothScrollTo(hasChangeX ? scrollX : mScroller.getFinalX(), hasChangeY ? scrollY : mScroller.getFinalY());
2,Android實作
2.1 傳感器監聽
其實,實作裸眼3D效果最核心的就是傳感器的監聽,這個自如客SensorLayout已經進行了開源,SensorLayout通過監聽傳感器來計算View的位移,然后通過Scroller進行滑動,首選我們添加一個傳感器監聽的方法,如下所示,
public SensorLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
// 重力傳感器
if (mSensorManager != null) {
Sensor accelerateSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// 地磁場傳感器
Sensor magneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mSensorManager.registerListener(this, accelerateSensor, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(this, magneticSensor, SensorManager.SENSOR_DELAY_GAME);
}
}
然后,在傳感器發生變化的時候通過Scroller來移動View,如下所示,
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mAccelerateValues = event.values;
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mMagneticValues = event.values;
}
float[] values = new float[3];
float[] R = new float[9];
if (mMagneticValues != null && mAccelerateValues != null)
SensorManager.getRotationMatrix(R, null, mAccelerateValues, mMagneticValues);
SensorManager.getOrientation(R, values);
// x軸的偏轉角度
values[1] = (float) Math.toDegrees(values[1]);
// y軸的偏轉角度
values[2] = (float) Math.toDegrees(values[2]);
double degreeX = values[1];
double degreeY = values[2];
if (degreeY <= 0 && degreeY > mDegreeYMin) {
hasChangeX = true;
scrollX = (int) (degreeY / Math.abs(mDegreeYMin) * mXMoveDistance * mDirection);
} else if (degreeY > 0 && degreeY < mDegreeYMax) {
hasChangeX = true;
scrollX = (int) (degreeY / Math.abs(mDegreeYMax) * mXMoveDistance * mDirection);
}
if (degreeX <= 0 && degreeX > mDegreeXMin) {
hasChangeY = true;
scrollY = (int) (degreeX / Math.abs(mDegreeXMin) * mYMoveDistance * mDirection);
} else if (degreeX > 0 && degreeX < mDegreeXMax) {
hasChangeY = true;
scrollY = (int) (degreeX / Math.abs(mDegreeXMax) * mYMoveDistance * mDirection);
}
smoothScroll(hasChangeX ? scrollX : mScroller.getFinalX(), hasChangeY ? scrollY : mScroller.getFinalY());
}
代碼中的mDirection表示的是移動的方向,這個引數會開放給使用方,用來設定跟隨傳感器移動還是與傳感器反向移動,
public void smoothScroll(int destX, int destY) {
int scrollY = getScrollY();
int delta = destY - scrollY;
mScroller.startScroll(destX, scrollY, 0, delta, 200);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
SensorLayout完整的代碼如下:
public class SensorLayout extends FrameLayout implements SensorEventListener {
private final SensorManager mSensorManager;
private float[] mAccelerateValues;
private float[] mMagneticValues;
private final Scroller mScroller;
private double mDegreeYMin = -50;
private double mDegreeYMax = 50;
private double mDegreeXMin = -50;
private double mDegreeXMax = 50;
private boolean hasChangeX;
private int scrollX;
private boolean hasChangeY;
private int scrollY;
private static final double mXMoveDistance = 40;
private static final double mYMoveDistance = 20;
private int mDirection = 1;
public SensorLayout(@NonNull Context context) {
this(context, null);
}
public SensorLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SensorLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScroller = new Scroller(context);
mSensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE);
// 重力傳感器
if (mSensorManager != null) {
Sensor accelerateSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// 地磁場傳感器
Sensor magneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mSensorManager.registerListener(this, accelerateSensor, SensorManager.SENSOR_DELAY_GAME);
mSensorManager.registerListener(this, magneticSensor, SensorManager.SENSOR_DELAY_GAME);
}
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mAccelerateValues = event.values;
}
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mMagneticValues = event.values;
}
float[] values = new float[3];
float[] R = new float[9];
if (mMagneticValues != null && mAccelerateValues != null)
SensorManager.getRotationMatrix(R, null, mAccelerateValues, mMagneticValues);
SensorManager.getOrientation(R, values);
// x軸的偏轉角度
values[1] = (float) Math.toDegrees(values[1]);
// y軸的偏轉角度
values[2] = (float) Math.toDegrees(values[2]);
double degreeX = values[1];
double degreeY = values[2];
if (degreeY <= 0 && degreeY > mDegreeYMin) {
hasChangeX = true;
scrollX = (int) (degreeY / Math.abs(mDegreeYMin) * mXMoveDistance * mDirection);
} else if (degreeY > 0 && degreeY < mDegreeYMax) {
hasChangeX = true;
scrollX = (int) (degreeY / Math.abs(mDegreeYMax) * mXMoveDistance * mDirection);
}
if (degreeX <= 0 && degreeX > mDegreeXMin) {
hasChangeY = true;
scrollY = (int) (degreeX / Math.abs(mDegreeXMin) * mYMoveDistance * mDirection);
} else if (degreeX > 0 && degreeX < mDegreeXMax) {
hasChangeY = true;
scrollY = (int) (degreeX / Math.abs(mDegreeXMax) * mYMoveDistance * mDirection);
}
smoothScroll(hasChangeX ? scrollX : mScroller.getFinalX(), hasChangeY ? scrollY : mScroller.getFinalY());
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
public void smoothScroll(int destX, int destY) {
int scrollY = getScrollY();
int delta = destY - scrollY;
mScroller.startScroll(destX, scrollY, 0, delta, 200);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void unregister() {
mSensorManager.unregisterListener(this);
}
public void setDegreeYMin(double degreeYMin) {
mDegreeYMin = degreeYMin;
}
public void setDegreeYMax(double degreeYMax) {
mDegreeYMax = degreeYMax;
}
public void setDegreeXMin(double degreeXMin) {
mDegreeXMin = degreeXMin;
}
public void setDegreeXMax(double degreeXMax) {
mDegreeXMax = degreeXMax;
}
public void setDirection(@ADirection int direction) {
mDirection = direction;
}
@IntDef({DIRECTION_LEFT, DIRECTION_RIGHT})
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PARAMETER)
public @interface ADirection {
}
public static final int DIRECTION_LEFT = 1;
public static final int DIRECTION_RIGHT = -1;
}
2.2 SensorLayout示例
其實,明白裸眼3D的原理后,我們使用SensorLayout就可以很容易實作這種效果,下面是使用SensorLayout實作單個頁面的裸眼3D效果,只需要使用SensorLayout包裹對應的圖片即可,
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.xzh.vrgame.banner3d.SensorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="25dp">
<ImageView
android:id="@+id/iv_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:scaleX="1.3"
android:src="@drawable/background1"/>
</com.xzh.vrgame.banner3d.SensorLayout>
<ImageView
android:id="@+id/iv_mid"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="bottom"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:scaleType="fitXY"
android:src="@drawable/mid1"/>
<com.xzh.vrgame.banner3d.SensorLayout
android:id="@+id/sensor_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<ImageView
android:id="@+id/iv_foreground"
android:layout_width="match_parent"
android:layout_height="100dp"
android:scaleType="fitXY"
android:src="@drawable/foreground1"/>
</com.xzh.vrgame.banner3d.SensorLayout>
</FrameLayout>

2.3 ViewPager裸眼3D輪播圖示例
通過前面的分析,自如APP的裸眼3D用到了兩個ViewPager,然后讓他們實作聯動,其實,我們可以把背景層使用ImageView,然后前景層再使ViewPager也可以實作3D輪播的效果,通過監聽前景層的ViewPager,來改變背景層使用ImageView,布局檔案代碼如下:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.xzh.vrgame.banner3d.SensorLayout
android:id="@+id/sensor_layout"
android:layout_width="match_parent"
android:layout_height="200dp">
<ImageView
android:id="@+id/iv_background"
android:layout_width="match_parent"
android:scaleType="centerCrop"
android:scaleX="1.3"
android:layout_height="match_parent" />
</com.xzh.vrgame.banner3d.SensorLayout>
<com.xzh.vrgame.widget.AutoPlayViewPager
android:id="@+id/avp_foreground"
android:layout_width="match_parent"
android:layout_height="220dp" />
</FrameLayout>
然后就是使用ViewPager+PageAdapter實作輪播,當然,大家也可以使用一些輪播的庫減少代碼,比如convenientbanner,最終效果如下圖所示,

代碼鏈接如下:https://github.com/xiangzhihong/AndroidDemo
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/292213.html
標籤:其他
上一篇:Vue axios實體
