先看下效果。
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.题外话。
由于这里偏移量用的是绝对数值,所以不同的机型表现可能会不一样。
另外,那个角度的值,其中一个是0到180度范围变化,一个是0到90度变化,不知道为什么要这样。也许还是用陀螺仪的角速度来计算的更靠谱?
相关github项目地址:
https://github.com/codeqian/customViewDemo/blob/master/app/src/main/java/net/codepig/customviewdemo/view/SensorDepth3D.java