Android自定义View-画直线、折线
需求:
1.在视频界面上,画出1米警戒线、2米警戒线、3米警戒线。
2.绘制车身轮廓线。
3.要求绘制的线段可调整位置。
效果如图:
解决方案:
1.自定义一个SurfaceView。
2.SurfaceView实现onDraw(),进行绘制。
3.警戒线为直线,车身轮廓线使用四个点坐标,描绘出车身轮廓。
4.拖动直线两端端点,或者车身轮廓坐标任意一点,都可改变直线。从而实现线段可随意调整位置。
5.当手指点下屏幕时,计算点击位置坐标距离哪个可移动的端点最近,如果超过一定距离则不移动端点。如果在可移动范围内,则将该坐标点设为移动结束时的x、y轴坐标点。
创建自定义View
- 创建自定义View
MyBSDSurfaceView 继承SurfaceView。 - 实现SurfaceHolder.Callback
是为了监听界面的变化,考虑手机上拖动画线不方便,需要加上双击切换为横屏的方法。横竖屏切换,还需要对坐标进行分辨率的转换,主要处理逻辑在surfaceChanged()中。
@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {// 检查当前屏幕方向int currentOrientation = context.getResources().getConfiguration().orientation;int orientation;if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {// 如果是横屏模式orientation = 1;Log.i("---","横屏,viewHeight = " + height + ",viewWidth = " + width);} else {orientation = 0;Log.i("---","竖屏,viewHeight = " + height + ",viewWidth = " + width);}// 第一次界面预览,横竖屏发生变化,重新计算坐标转换if (currScreenHeight == 0 || currScreenWidth == 0) {// 判断车身点和基准线点为0,则认为没有预置数据,给出一个初始画线initLine(width, height);}if (mCurrOrientation != orientation) {// 横竖屏切换了,进行坐标转换,当前屏幕分辨率 转化为 目标屏幕分辨率mCurrOrientation = orientation;float srcWidth = currScreenWidth;// 屏幕切换前的 分辨率float srcHeight = currScreenHeight;// 屏幕切换前的 分辨率pointCar1 = convertResolution(pointCar1, srcWidth, srcHeight, width, height);pointCar2 = convertResolution(pointCar2, srcWidth, srcHeight, width, height);pointCar3 = convertResolution(pointCar3, srcWidth, srcHeight, width, height);pointCar4 = convertResolution(pointCar4, srcWidth, srcHeight, width, height);// 高危险线highStartPoint = convertResolution(highStartPoint, srcWidth, srcHeight, width, height);highEndPoint = convertResolution(highEndPoint, srcWidth, srcHeight, width, height);// 中危险线midStartPoint = convertResolution(midStartPoint, srcWidth, srcHeight, width, height);midEndPoint = convertResolution(midEndPoint, srcWidth, srcHeight, width, height);// 低危险线lowStartPoint = convertResolution(lowStartPoint, srcWidth, srcHeight, width, height);lowEndPoint = convertResolution(lowEndPoint, srcWidth, srcHeight, width, height);}currScreenWidth = width;currScreenHeight = height;invalidate();}
这里有处需要注意,那就是横竖屏切换时,执行了surfaceCreated(),SurfaceView被重新创建了,需要在AndroidManifest文件对应的activity中加上配置。否则每次切换横竖屏currScreenHeight、currScreenWidth都是0。
android:configChanges="orientation|screenSize|screenLayout"
android:configChanges=“orientation|screenSize|screenLayout” 是一个 Android 配置变化属性,它用于应对屏幕旋转和尺寸变化时的问题。
这个属性的值是一个字符串,包含了应用需要处理的配置变化类型。在这个例子中,应用会处理以下三种类型的配置变化:
orientation:屏幕方向的变化,即从竖屏到横屏或从横屏到竖屏。
screenSize:屏幕大小的变化,例如从全屏模式到非全屏模式,或者从非全屏模式到全屏模式。
screenLayout:这是一种特殊的屏幕大小变化,当屏幕的宽高比发生改变时会触发。例如,当一个设备从横向模式切换到纵向模式时,或者反过来。
当这个属性被设置后,系统不会自动重启应用或销毁活动(即不会重新创建活动),而是会调用活动的 onConfigurationChanged() 方法来通知配置已经改变。这样,应用就可以在那个方法中处理配置变化,而不是重新创建活动。
-
实现View.OnTouchListener的监听事件
监听屏幕的点击事件,在onTouch中处理屏幕的点击事件。并实现双击屏幕切换横屏,需要一个外部监听doubleClickListener将双击事件传递给上层,进行切换界面后的逻辑处理。ACTION_DOWN:屏幕被点击时,获取当前点击点坐标。并计算该点距离初始点位哪个比较近,并将这个点位设置成可移动的点。
ACTION_MOVE:手指在屏幕上移动事件。将移动坐标赋值给moveLine这样会有直线的实时变化。
ACTION_UP、ACTION_CANCEL:结束移动。
@Overridepublic boolean onTouch(View v,MotionEvent event) {int action = event.getAction();float x = event.getX();float y = event.getY();Point touchPoint = new Point(x, y);switch (action) {case MotionEvent.ACTION_DOWN:// 找出点击点,距离最近的 pointcurrStates = setMoveLine(touchPoint);Log.i("---", "ACTION_DOWN : (" + touchPoint.x + "," + touchPoint.y + ")");long clickTime = System.currentTimeMillis();if (clickTime - mLastClickTime < DOUBLE_CLICK_TIME_DELTA) {doubleClickListener.doubleClick();Log.i("---", "doubleClickListener");}mLastClickTime = clickTime;return true;case MotionEvent.ACTION_MOVE:if (isDragging && movingPoint != null) {movingPoint.x = (int) event.getX();movingPoint.y = (int) event.getY();invalidate();}Log.i("---", "ACTION_MOVE : (" + touchPoint.x + "," + touchPoint.y + ")");return true;case MotionEvent.ACTION_UP:isDragging = false;movingPoint = null;Log.i("---", "ACTION_UP : (" + touchPoint.x + "," + touchPoint.y + ")");return true;case MotionEvent.ACTION_CANCEL:isDragging = false;movingPoint = null;Log.i("---", "ACTION_CANCEL : (" + touchPoint.x + "," + touchPoint.y + ")");return true;}return false;}
- 绘制点位连接成线
注意SurfaceView中是阻止了其自身的绘制操作的,需要在初始化时加上设置setWillNotDraw(false); 不在阻止界面绘制。
初始化一个画笔:
// 设置画笔属性paint.setColor(Color.RED); // 设置直线颜色为红色paint.setStrokeWidth(4);paint.setStyle(Paint.Style.STROKE);
在onDraw()中实现绘制
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 画三条直线path.reset();paint.setColor(Color.GREEN); // 设置直线颜色为绿色canvas.drawLine(lowStartPoint.x, lowStartPoint.y, lowEndPoint.x, lowEndPoint.y, paint);path.reset();paint.setColor(Color.YELLOW); // 设置直线颜色为黄色canvas.drawLine(midStartPoint.x, midStartPoint.y, midEndPoint.x, midEndPoint.y, paint);canvas.drawPath(path, paint); // 画直线path.reset();paint.setColor(Color.RED); // 设置直线颜色为红色canvas.drawLine(highStartPoint.x, highStartPoint.y, highEndPoint.x, highEndPoint.y, paint);canvas.drawPath(path, paint); // 画直线// 画折线path.reset();paint.setColor(Color.BLUE); // 设置折线颜色为蓝色paint.setStyle(Paint.Style.FILL);// 设置圆点实心canvas.drawCircle(pointCar1.x, pointCar1.y, radius, paint);canvas.drawCircle(pointCar2.x, pointCar2.y, radius, paint);canvas.drawCircle(pointCar3.x, pointCar3.y, radius, paint);canvas.drawCircle(pointCar4.x, pointCar4.y, radius, paint);canvas.drawLine(pointCar1.x, pointCar1.y, pointCar2.x, pointCar2.y, paint);canvas.drawLine(pointCar2.x, pointCar2.y, pointCar3.x, pointCar3.y, paint);canvas.drawLine(pointCar3.x, pointCar3.y, pointCar4.x, pointCar4.y, paint);}
自定义View使用
- 创建布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"tools:context=".MainActivity"><com.tiamaes.test.MyBSDSurfaceViewandroid:id="@+id/myBSDSurfaceView"android:layout_width="match_parent"android:layout_height="0dp"android:visibility="visible"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintDimensionRatio="h,16:9"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>
- Activity调用
public class MainActivity extends AppCompatActivity {MyBSDSurfaceView myBSDSurfaceView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);myBSDSurfaceView = findViewById(R.id.myBSDSurfaceView);myBSDSurfaceView.setDoubleClickListener(new MyBSDSurfaceView.DoubleClickListener() {@Overridepublic void doubleClick() {if (!isLand()) {// 横屏showFullScreenMode();} else {// 竖屏showPortraitMode();}}});}/*** 检查屏幕是否为横屏* @return*/private boolean isLand() {return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;}/*** 展示横屏*/private void showFullScreenMode() {setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);if (getSupportActionBar() != null) {getSupportActionBar().hide();}}/*** 展示竖屏*/private void showPortraitMode() {setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);if (getSupportActionBar() != null) {getSupportActionBar().show();}}@Overridepublic void onConfigurationChanged(Configuration newConfig) {if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {//设置横屏的UILog.d("---", "onConfigurationChanged ORIENTATION_LANDSCAPE");//set full screen modeint uiOptions = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;getWindow().getDecorView().setSystemUiVisibility(uiOptions);getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);WindowManager.LayoutParams layoutParams = getWindow().getAttributes();layoutParams.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN;getWindow().setAttributes(layoutParams);} else {//设置竖屏的UILog.d("---", "onConfigurationChanged ORIENTATION_PORTRAIT");//exit from full screengetWindow().getDecorView().setSystemUiVisibility(View.VISIBLE);WindowManager.LayoutParams attrs = getWindow().getAttributes();attrs.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);getWindow().setAttributes(attrs);}super.onConfigurationChanged(newConfig);}
}
完整工程代码:demo