2

Two days ago, I happened to see that free front end open sourced a naked-eye 3D Banner carousel implementation scheme. I found it very interesting, so I also planned to study it.

在这里插入图片描述

1. Implementation principle

The realization principle comes from , the realization of the naked eye 3D effect of the

1.1 Stratification

When opening Android Stusio for layout analysis, you will find that their Banner uses two layers of views, corresponding to two Viewpagers, and these two Viewpagers also realize linkage, as shown in the following figure.
在这里插入图片描述
In addition to the linkage of Viewpager, their Banner also supports naked-eye 3D effects, which can follow the top to make changes in the display.

1.2 Displacement

Open the Ziruike App, and when users look at the Banner from different angles, they will see obvious dislocations. This kind of misalignment movement is actually realized by the sensor of the device itself. The specific implementation is to keep the background at the bottom always still, and then calculate the moving distance between the background and the foreground based on the inclination angle of the current device obtained from the device sensor. , And then perform the background and foreground moving actions, the schematic diagram is as follows.
在这里插入图片描述
The relevant code is as follows:

1. Sensor code
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. Calculate the offset angle code
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. Perform relative offset calculation
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 implementation

2.1 Sensor monitoring

In fact, the core of realizing the naked eye 3D effect is the monitoring of the sensor. This free sensor Layout has been open sourced. The SensorLayout calculates the displacement of the View by monitoring the sensor, and then slides through the Scroller. The first choice is to add a sensor monitoring method, as shown below Show.

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);
    }
}

Then, use the Scroller to move the View when the sensor changes, as shown below.

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

The mDirection in the code represents the direction of movement. This parameter will be open to the user to set whether to follow the sensor or move in the opposite direction.

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

The complete code of SensorLayout is as follows:

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 example

In fact, after understanding the principle of naked eye 3D, we can easily achieve this effect by using SensorLayout. The following is the use of SensorLayout to achieve the naked eye 3D effect of a single page, just use the SensorLayout to wrap the corresponding picture.

<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 naked eye 3D carousel example

Through the previous analysis, the naked eye 3D of Ziru APP uses two ViewPagers, and then allows them to achieve linkage. In fact, we can use ImageView for the background layer, and then use the ViewPager in the foreground layer to achieve the effect of 3D carousel. By monitoring the ViewPager of the foreground layer, we can change the background layer to use ImageView. The layout file code is as follows:

<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>

Then use ViewPager+PageAdapter to implement carousel. Of course, you can also use some carousel libraries to reduce the code, such as convenientbanner, the final effect is shown in the figure below.
在这里插入图片描述

The code link is as follows: https://github.com/xiangzhihong/AndroidDemo


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》