上一篇文章实现了档位的view例子,这一篇再来实现一个方向盘的view,主要实现一个需要跟手转动的图片,并且返回转动的角度,主要思路就是在重绘时进行前景的转动设置。
下面我根据我自己的工程讲解一下
先上一张我自己的类图
View控件主要有3个类,
SeekBarBackgroundView.java画背景类,可以设置背景,这个类里面什么也没做,只是继承了view,提供background的设置属性
SeekBarForegroundView.java画前景类,例子里显示了方向盘的图片,转动这个view达到方向盘转动的效果
SteeringWheelSeekBar.java触摸事件的处理,并提供对外接口
自定义属性我们需要在前景view自定义一些属性,需要在values目录新建attr.xml文件
里面写上我们自定义的属性,这些属性可以在xml中使用,在代码中进行获取,处理
具体的格式和类型可以看这个文章,
Android中自定义属性attr.xml的格式详解 - kim_liu - 博客园
Android自定义控件——自定义属性_Vincent的专栏-CSDN博客_android 自定义控件属性
这个文章把常用的类型都举例了,使用处理方法在后面的代码中会介绍。
显示前景前景就是一个简单的view,view可以设置大小和图片,在描画时进行旋转角度的设置。
在构造函数中将属性传入。
public SeekBarForegroundView(Context context, int foregroundSize, int foreground) { super(context); this.foregroundSize = foregroundSize; this.foreground = foreground; init(); }
private void init() { paint = new Paint(); //设置抗锯齿,防止过多的失真 paint.setAntiAlias(true); if (foreground != 0) { bitmapPaint = BitmapFactory.decodeResource(this.getResources(), foreground); // 指定图片绘制区域(原图大小) src = new Rect(0, 0, bitmapPaint.getWidth(), bitmapPaint.getHeight()); // 指定图片在屏幕上显示的区域(ballSize大小) dst = new Rect(0, 0, foregroundSize, foregroundSize); } }
初始化的时候对前景图片进行判断,如果是默认值,也就是0则不绘制图片。
在描画中判断如果没有前景图片则绘制一个纯色圆。
如果有前景色则设置旋转属性。
@Override protected void onDraw(Canvas canvas) { if (foreground == 0) { canvas.drawCircle((float) getMeasuredWidth() / 2, (float) getMeasuredWidth() / 2, (float) getMeasuredWidth() / 2, paint); } else { // 旋转 matrix.setRotate(deg, (float) foregroundSize / 2, (float) foregroundSize / 2); canvas.setMatrix(matrix); canvas.drawBitmap(bitmapPaint, src, dst, paint); } }
在提供一个给group view调用的角度设置函数。
public void setDegrees(float degrees) { deg = degrees; postInvalidate(); }ViewGroup代码
主要读取属性值,并实例两个view,还需要处理touch逻辑,并给应用通知角度。
在构造中读取属性值,并实例两个view。
public SteeringWheelSeekBar(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.SteeringWheelSeekBar); foregroundSize = array.getDimensionPixelSize(R.styleable.SteeringWheelSeekBar_foregroundSize, 90); foregroundId = array.getResourceId(R.styleable.SteeringWheelSeekBar_foreground, 0); array.recycle(); init(); } private void init() { centerPoint = new PointF(); pressPoint = new PointF(); movePoint = new PointF(); // 背景view SeekBarBackgroundView backgroundView = new SeekBarBackgroundView(context); backgroundView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); addView(backgroundView); foregroundView = new SeekBarForegroundView(context, foregroundSize, foregroundId); addView(foregroundView); }
处理touch事件
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: float downX = event.getX(); float downY = event.getY(); pressPoint.set(downX, downY); return true; case MotionEvent.ACTION_MOVE: movePoint.set(event.getX(), event.getY()); // 设置旋转角度 foregroundView.setDegrees(deg + getRotationDegrees()); // 当前角度 float currentDegrees = deg + getRotationDegrees(); if (lastDegrees != currentDegrees) { lastDegrees = currentDegrees; if (listener != null) { listener.onProgressChanged((int) currentDegrees); } } break; case MotionEvent.ACTION_UP: movePoint.set(event.getX(), event.getY()); deg += getRotationDegrees(); if (listener != null) { listener.onProgressChanged((int) deg); } performClick(); break; default: // 当手指移出View时,目前好像不会进入到这个case中 Log.w("TAG", "onTouchEvent default: " + event.getX() + "," + event.getY()); movePoint.set(event.getX(), event.getY()); deg += getRotationDegrees(); if (listener != null) { listener.onProgressChanged((int) deg); } break; } return super.onTouchEvent(event); }
这里有一个地方需要用到数学知识,就是根据中心点坐标,按下点坐标和移动点坐标计算出旋转角度,并设置给前景view去描画。
设置旋转角度时需要设置顺时针旋转还是逆时针旋转,逆时针旋转为负角度,顺时针为正角度。
角度的计算通过高中知识,已知三边,计算夹角公式,cosA=(b平方+c平方-a平方)/2cb
double AB; // 原点到按下点线段 double AC; // 原点到移动点线段 double BC; // 按下点到移动点线段 AB = Math.sqrt(Math.pow(pressPoint.x - centerPoint.x, 2) + Math.pow(pressPoint.y - centerPoint.y, 2)); AC = Math.sqrt(Math.pow(movePoint.x - centerPoint.x, 2) + Math.pow(movePoint.y - centerPoint.y, 2)); BC = Math.sqrt(Math.pow(movePoint.x - pressPoint.x, 2) + Math.pow(movePoint.y - pressPoint.y, 2)); double temp = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AC * AB);
而旋转方向通过移动点是否在中心点与按下点连线的上下方判断,但是在斜率的正负会影响大于小于的判断,所以需要先判断斜率,然后再判断上下方。
// 计算顺时针逆时针 // 由于原点x与按下点x大小影响斜率正负,斜率不同计算线段上下方算法不同,斜率不同判断逻辑相反 // ------------->x // | 1 | 2 // | | // |------o------ // | | // ↓ 4 | 3 // y // 计算移动点在AB线段上方还是下方 float yyy = (centerPoint.y - pressPoint.y) / (centerPoint.x - pressPoint.x) * movePoint.x + (pressPoint.y * centerPoint.x - centerPoint.y * pressPoint.x) / (centerPoint.x - pressPoint.x); if (centerPoint.x < pressPoint.x) { // 2象限 3象限 if (yyy > movePoint.y) { // 顺时针 return -(float) (Math.acos(temp) * (180 / Math.PI)); } else { // 逆时针 return (float) (Math.acos(temp) * (180 / Math.PI)); } } else { // 1象限 4象限 if (yyy > movePoint.y) { // 顺时针 return (float) (Math.acos(temp) * (180 / Math.PI)); } else { // 逆时针 return -(float) (Math.acos(temp) * (180 / Math.PI)); } }
完整代码如下:
private float getRotationDegrees() { double AB; // 原点到按下点线段 double AC; // 原点到移动点线段 double BC; // 按下点到移动点线段 AB = Math.sqrt(Math.pow(pressPoint.x - centerPoint.x, 2) + Math.pow(pressPoint.y - centerPoint.y, 2)); AC = Math.sqrt(Math.pow(movePoint.x - centerPoint.x, 2) + Math.pow(movePoint.y - centerPoint.y, 2)); BC = Math.sqrt(Math.pow(movePoint.x - pressPoint.x, 2) + Math.pow(movePoint.y - pressPoint.y, 2)); double temp = (Math.pow(AB, 2) + Math.pow(AC, 2) - Math.pow(BC, 2)) / (2 * AC * AB); // 计算顺时针逆时针 // 由于原点x与按下点x大小影响斜率正负,斜率不同计算线段上下方算法不同,斜率不同判断逻辑相反 // ------------->x // | 1 | 2 // | | // |------o------ // | | // ↓ 4 | 3 // y // 计算移动点在AB线段上方还是下方 float yyy = (centerPoint.y - pressPoint.y) / (centerPoint.x - pressPoint.x) * movePoint.x + (pressPoint.y * centerPoint.x - centerPoint.y * pressPoint.x) / (centerPoint.x - pressPoint.x); if (centerPoint.x < pressPoint.x) { // 2象限 3象限 if (yyy > movePoint.y) { // 顺时针 return -(float) (Math.acos(temp) * (180 / Math.PI)); } else { // 逆时针 return (float) (Math.acos(temp) * (180 / Math.PI)); } } else { // 1象限 4象限 if (yyy > movePoint.y) { // 顺时针 return (float) (Math.acos(temp) * (180 / Math.PI)); } else { // 逆时针 return -(float) (Math.acos(temp) * (180 / Math.PI)); } } }
然后就是设置一个listener给应用通知旋转角度。
// 设置最终档位监听 public void setListener(SteeringWheelSeekBar.onProgressChangedListener listener) { this.listener = listener; }
这个listener在touch事件中去触发。
至此,我们就把所有需要的完成了,只需要写一个demo测试一下。
Xml如下
Mainactivity如下
package com.example.myseekbar; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView textView = findViewById(R.id.seek_bar_progress); SteeringWheelSeekBar seekBar = findViewById(R.id.seek_bar); seekBar.setListener(level -> textView.setText(String.valueOf(level))); } }
效果如下
这里只讲述了一些算法和逻辑,具体流程需要学习上面的博文,一些代码细节还需要自己研究源码,源码如下
SteeringWheelSeekBar: 自定义view学习,实现汽车方向盘view,返回旋转角度,对应博文 https://blog.csdn.net/andylauren/article/details/122169990
希望通过这个代码的学习能够加深对自定义view的理解。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)