前兩天,偶然看到自如大前端開源了一個裸眼3D的Banner輪播圖實現方案,以爲很是有意思,因而也打算研究一下。前端
實現原理來自自如客APP裸眼3D效果的實現android
打開Android Stusio進行佈局分析時會發現,他們的Banner使用了兩層視圖,對應兩個Viewpager,而且這兩個Viewpager還實現了聯動,以下圖所示。
除了Viewpager的聯動,他們的Banner還支持裸眼3D效果,可以跟隨陀螺進行顯示上的變化。git
打開自如客App,當用戶在不一樣的角度上看Banner時會看到明顯的錯位移動。這種錯位移動其實藉助的是設備自己的傳感器來實現的,具體實現方式是讓底部的背景始終保持不動,而後根據從設備傳感器獲取當前設備對應的傾斜角,計算出背景和前景的移動距離,進而執行背景和前景移動的動做,示意圖以下。
相關的代碼以下:github
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);
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]);
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());
其實,實現裸眼3D效果最核心的就是傳感器的監聽,這個自如客SensorLayout已經進行了開源,SensorLayout經過監聽傳感器來計算View的位移,而後經過Scroller進行滑動,首選咱們添加一個傳感器監聽的方法,以下所示。segmentfault
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,以下所示。app
@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表示的是移動的方向,這個參數會開放給使用方,用來設置跟隨傳感器移動仍是與傳感器反向移動。ide
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; }
其實,明白裸眼3D的原理後,咱們使用SensorLayout就能夠很容易實現這種效果。下面是使用SensorLayout實現單個頁面的裸眼3D效果,只須要使用SensorLayout包裹對應的圖片便可。post
<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>
經過前面的分析,自如APP的裸眼3D用到了兩個ViewPager,而後讓他們實現聯動。其實,咱們能夠把背景層使用ImageView,而後前景層再使ViewPager也能夠實現3D輪播的效果,經過監聽前景層的ViewPager,來改變背景層使用ImageView。佈局文件代碼以下:this
<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,最終效果以下圖所示。