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