自定义形状seekbar学习--方向盘view

上一篇文章实现了档位的view例子,这一篇再来实现一个方向盘的view,主要实现一个需要跟手转动的图片,并且返回转动的角度,主要思路就是在重绘时进行前景的转动设置。

下面我根据我自己的工程讲解一下

先上一张我自己的类图

View控件主要有3个类,

SeekBarBackgroundView.java画背景类,可以设置背景,这个类里面什么也没做,只是继承了view,提供background的设置属性

SeekBarForegroundView.java画前景类,例子里显示了方向盘的图片,转动这个view达到方向盘转动的效果

SteeringWheelSeekBar.java触摸事件的处理,并提供对外接口

自定义属性

我们需要在前景view自定义一些属性,需要在values目录新建attr.xml文件

里面写上我们自定义的属性,这些属性可以在xml中使用,在代码中进行获取,处理

<resources>

    <declare-styleable name="SteeringWheelSeekBar">
        <attr name="foregroundSize" format="dimension" />
        <attr name="foreground" format="reference" />
    </declare-styleable>

</resources>

具体的格式和类型可以看这个文章,

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调用的角度设置函数。

/**
 * @param degrees 旋转角度
 */
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));
    }
}

完整代码如下:

/**
 * @return 返回相对于按下点的旋转角度
 */
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如下

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">


        <com.example.myseekbar.SteeringWheelSeekBar
            android:id="@+id/seek_bar"
            android:layout_width="600dp"
            android:layout_height="300dp"
            android:layout_marginTop="40dp"
            android:background="@color/purple_200"
            app:foregroundSize="200dp"
            app:foreground="@drawable/test6"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="旋转角度"
            android:textSize="30sp" />

        <TextView
            android:id="@+id/seek_bar_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="0"
            android:textSize="30sp" />

    </androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>

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的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值