前两天,偶尔看到自若大前端开源了一个裸眼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,如下所示。

@Overridepublic 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();}@Overridepublic 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