Android仿自如客APP裸眼3D效果

前兩天,偶然看到自如大前端開源了一個裸眼3D的Banner輪播圖實現方案,以爲很是有意思,因而也打算研究一下。前端

在這裏插入圖片描述

1,實現原理

實現原理來自自如客APP裸眼3D效果的實現android

1.1 分層

打開Android Stusio進行佈局分析時會發現,他們的Banner使用了兩層視圖,對應兩個Viewpager,而且這兩個Viewpager還實現了聯動,以下圖所示。
在這裏插入圖片描述
除了Viewpager的聯動,他們的Banner還支持裸眼3D效果,可以跟隨陀螺進行顯示上的變化。git

1.2 位移

打開自如客App,當用戶在不一樣的角度上看Banner時會看到明顯的錯位移動。這種錯位移動其實藉助的是設備自己的傳感器來實現的,具體實現方式是讓底部的背景始終保持不動,而後根據從設備傳感器獲取當前設備對應的傾斜角,計算出背景和前景的移動距離,進而執行背景和前景移動的動做,示意圖以下。
在這裏插入圖片描述
相關的代碼以下:github

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進行滑動,首選咱們添加一個傳感器監聽的方法,以下所示。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;
}

2.2 SensorLayout示例

其實,明白裸眼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>

在這裏插入圖片描述

2.3 ViewPager裸眼3D輪播圖示例

經過前面的分析,自如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,最終效果以下圖所示。
在這裏插入圖片描述

代碼連接以下:https://github.com/xiangzhihong/AndroidDemo

相關文章
相關標籤/搜索