美文网首页Android开发经验谈Android开发Android技术知识
Android自定义控件之角度传感器实现3d景深效果

Android自定义控件之角度传感器实现3d景深效果

作者: 书柜里的松鼠 | 来源:发表于2018-08-03 17:30 被阅读55次

    先看下效果。


    Android自定义控件之角度传感器实现3d景深效果
    1.自定义控件

    先建一个自定义控件并需要实现SensorEventListener接口,这部分就不啰嗦了,直接看代码:

    package net.codepig.customviewdemo.view;
    
    import android.content.Context;
    import android.hardware.Sensor;
    import android.hardware.SensorEvent;
    import android.hardware.SensorEventListener;
    import android.hardware.SensorManager;
    import android.util.AttributeSet;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.widget.LinearLayout;
    import android.widget.TextView;
    
    import net.codepig.customviewdemo.R;
    
    import java.util.List;
    
    /**
     * 传感器控制景深效果
     */
    public class SensorDepth3D extends LinearLayout implements SensorEventListener{
        private Context _context;
        private TextView txt1,txt2,txt3;
    
        private int mWidth;//容器的宽度
        private int mHeight;//容器的高度
    
        private float[] angle = new float[3];
        private SensorManager sm;
        private String content;
        private Sensor mSensor;
    
        public SensorDepth3D(Context context){
            super(context);
            this._context = context;
            init(context);
        }
    
        public SensorDepth3D(Context context, AttributeSet attrs) {
            super(context, attrs);
            this._context = context;
            init(context);
        }
    
        public SensorDepth3D(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this._context = context;
            init(context);
        }
    
        private void init(Context mContext){
            LayoutInflater.from(mContext).inflate(R.layout.sensor_depth_3d,this, true);
            txt1=findViewById(R.id.txt1);
            txt2=findViewById(R.id.txt2);
            txt3=findViewById(R.id.txt3);
        }
    
        //测量子View
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            mWidth = getMeasuredWidth();
            mHeight = getMeasuredHeight();
        }
    
        //排列子View的位置
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int childTop = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    child.layout(0, childTop,child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
                    childTop = childTop + child.getMeasuredHeight();
                }
            }
        }
    
        @Override
        public void onSensorChanged(SensorEvent event) {
        }
    
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
            // 精度改变
        }
        
        /**
         * 解除监听
         */
        public void unregisterListener(){
            sm.unregisterListener(this);
        }
    }
    
    

    控件布局

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:id="@+id/txt1"
            android:text="00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/txt2"
            android:text="00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/txt3"
            android:text="00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
    

    mainActivity里使用控件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:MaskImage="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical">
        <net.codepig.customviewdemo.view.SensorDepth3D
            android:id="@+id/sensorDepth3d"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>
    
    2.添加传感器及监听

    这里通过SensorManager.getDefaultSensor方法来调用传感器。
    与旋转相关的传感器有好几个,比如Sensor.TYPE_GYROSCOPE是陀螺仪,适合监听自身实时运动;TYPE_ORIENTATION是方向。(然而已经从api 16开始被弃用);TYPE_ROTATION_VECTOR是旋转矢量,但是数值比较不直观。
    我们这里用到加速度传感器TYPE_ACCELEROMETER和磁场传感器TYPE_MAGNETIC_FIELD来计算角度。

    private float[] accelerometerValues = new float[3];
    private float[] magneticValues = new float[3];
    
     /**
         * 根据传入的类型初始化传感器
         * @param ctx
         * @param type
         */
        private void initSensor(Context ctx) {
            sm = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
            gSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);//加速度传感器
            mSensor = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);//磁场传感器
            registerListener();
        }
    
        public void registerListener(){
            sm.registerListener(this, gSensor, SensorManager.SENSOR_DELAY_NORMAL);//注册传感器,第一个参数为监听器,第二个是传感器类型,第三个是刷新速度
            sm.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
        }
    
        @Override
        public void onSensorChanged(SensorEvent event) {
            if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
                accelerometerValues = event.values.clone();
            } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
                magneticValues = event.values.clone();
            }
    
            //获取地磁与加速度传感器组合的旋转矩阵
            float[] R = new float[9];
            float[] values = new float[3];
            SensorManager.getRotationMatrix(R, null, accelerometerValues,magneticValues);
            SensorManager.getOrientation(R, values);
    
            //values[0]->Z轴、values[1]->X轴、values[2]->Y轴
            //使用前请进行转换,因为获取到的值是弧度,示例如下
            txt1.setText("angleZ:"+Math.toDegrees(values[0]));
            txt2.setText("angleX:"+Math.toDegrees(values[1]));
            txt3.setText("angleY:"+Math.toDegrees(values[2]));
        }
    

    记得结束的时候要解除监听

    /**
         * 解除监听
         */
        public void unregisterListener(){
            sm.unregisterListener(this);
        }
    

    摇摆一下手机,可以看到旋转的角度了。

    3.根据角度移动不同层次上的图片

    这里要用到一点中学三角函数知识。
    这里简便起见就不用图片了,整三个不同颜色的圆表示一下。
    先在布局空间里整三个圆形ball0,ball1,ball2:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:id="@+id/txt1"
            android:text="00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/txt2"
            android:text="00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/txt3"
            android:text="00"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <RelativeLayout
            android:gravity="center"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <View
                android:id="@+id/ball0"
                android:background="@drawable/ball_front"
                android:layout_width="100dp"
                android:layout_height="100dp"/>
            <View
                android:id="@+id/ball1"
                android:background="@drawable/ball_middle"
                android:layout_width="100dp"
                android:layout_height="100dp"/>
            <View
                android:id="@+id/ball2"
                android:background="@drawable/ball_behide"
                android:layout_width="100dp"
                android:layout_height="100dp"/>
        </RelativeLayout>
    </LinearLayout>
    

    在初始化里根据想要的深度设置一下大小的缩放

    private View ball0,ball1,ball2;
    private double[] angle = new double[3];
    private int zScale=0.8f;//纵向缩放比例,值越大则场景越深
    private float dScale=50;//基础偏移量
    //中心点坐标
    private int x0=0;
    private int y0=0;
    
        private void init(Context mContext){
            txt1=findViewById(R.id.txt1);
            txt2=findViewById(R.id.txt2);
            txt3=findViewById(R.id.txt3);
            ball0=findViewById(R.id.ball0);
            ball1=findViewById(R.id.ball1);
            ball2=findViewById(R.id.ball2);
            ball1.setScaleX(zScale);
            ball1.setScaleY(zScale);
            ball2.setScaleX(zScale*zScale);
            ball2.setScaleY(zScale*zScale);
            initSensor(_context);
        }
    

    onMeasure里设置一下中心点:

    @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            mWidth = getMeasuredWidth();
            mHeight = getMeasuredHeight();
            x0=mWidth/2-ball0.getMeasuredWidth()/2;
            y0=mHeight/2-ball0.getMeasuredHeight()/2;
            Log.d("LOGCAT","screenSize:"+curWidth+"_"+mHeight+"_"+ball0.getMeasuredWidth());
        }
    

    然后根据角度的变化设置圆形相对中心的偏移量:
    注意这里的三角函数需要使用弧度值

        @Override
        public void onSensorChanged(SensorEvent event) {
            if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
                accelerometerValues = event.values.clone();
            } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
                magneticValues = event.values.clone();
            }
    
            //获取地磁与加速度传感器组合的旋转矩阵
            float[] R = new float[9];
            float[] values = new float[3];
            SensorManager.getRotationMatrix(R, null, accelerometerValues,magneticValues);
            SensorManager.getOrientation(R, values);
    
            //values[0]->Z轴、values[1]->X轴、values[2]->Y轴
            angle[0]=values[0];
            angle[1]=values[1];
            angle[2]=values[2];
            //弧度转换为角度
            txt1.setText("angleZ:"+(int) Math.toDegrees(angle[0]));
            txt2.setText("angleX:"+(int) Math.toDegrees(angle[1]));
            txt3.setText("angleY:"+(int) Math.toDegrees(values[2]));
    
            //计算位置偏移量
            double dX=dScale*Math.sin(values[2]);
            double dY=dScale*Math.sin(values[1]);
    //        Log.d("LOGCAT","d:"+dX+dY);
    //        这里加上了屏幕中心点的位置
            ball0.setX(x0);
            ball0.setY(y0);
            ball1.setX((float) (x0-dX));
            ball1.setY((float) (y0+dY));
            ball2.setX((float) (x0-dX*2));
            ball2.setY((float) (y0+dY*2));
        }
    

    运行一下,转动手机就可以看到模拟的景深立体效果了。

    4.设置基准面。

    目前这个效果还有一个问题,就是基准面是根据实际的地理方向定的。所以对于拿起来的时候手机可能是任何方向的实际情况来说,效果不是太好。所以这里再加个校准基准平面的功能。
    这里用按钮来操作,按下按钮的时候以当前手机所在平面为基准。
    按钮的代码就不多说了,总之就是按下按钮记录当前角度:

    resetBtn.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    //记录基准角度
                    angle0[0]=angle[0];
                    angle0[1]=angle[1];
                    angle0[2]=angle[2];
                }
            });
    

    然后在刚才的偏移量计算里加入角度的修正:

    //计算偏移量
            double dX=dScale*Math.sin(values[2]-angle0[2]);
    //        Log.d("LOGCAT","angleY"+(values[1]-angle0[1]));
            double dY=dScale*Math.sin(values[1]-angle0[1]);
    //        Log.d("LOGCAT","d:"+dX+dY);
    //        这里加上了屏幕中心点的位置
            ball0.setX(x0);
            ball0.setY(y0);
            ball1.setX((float) (x0-dX));
            ball1.setY((float) (y0+dY));
            ball2.setX((float) (x0-dX*2));
            ball2.setY((float) (y0+dY*2));
    
    5.题外话。

    由于这里偏移量用的是绝对数值,所以不同的机型表现可能会不一样。
    另外,那个角度的值,其中一个是0180度范围变化,一个是090度变化,不知道为什么要这样。也许还是用陀螺仪的角速度来计算的更靠谱?


    相关github项目地址:
    https://github.com/codeqian/customViewDemo/blob/master/app/src/main/java/net/codepig/customviewdemo/view/SensorDepth3D.java

    相关文章

      网友评论

        本文标题:Android自定义控件之角度传感器实现3d景深效果

        本文链接:https://www.haomeiwen.com/subject/resojftx.html