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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。