自定义操作杆教程

2019-01-27  本文已影响0人  虚心学习的小来子

自定义操作杆教程

同学们都玩过王者荣耀吧,里面有控制人物走动的操作杆,但是大家不要误以为这是Android里面自带的控件,需要我们开发者自己实现。实现一个操作杆的步骤较为麻烦,但是我在这里大概地将其分为以下四个步骤:

  1. 画出基本的view
  2. 实现根据手指触摸的位置改变view的位置
  3. 写出不同方向上面的回调函数
  4. 限定触摸的区域

可能大家目前看上面的四个步骤会很懵,但是别着急,我们慢慢道来。

画出基本的view

看到这里之前,大家需要对自定义View有一定的了解,起码要知道 paint 和 canvas 这两个自定义view必须要用到的东西,在这里我向大家推荐学习自定义
view 的教程,大神 GcsSloop 的博客,讲的十分详细,如果没有相应的基础,大家看完前两章就可以把这篇博客大部分看懂了。

我们暂把我们的自定义操作杆叫做 StickView ,这样我们就可以,我们先写出一个自定义View的框架。


public class StickView extends ControlView {

    private Paint paint = new Paint();
        
    public StickControlView(Context context) {
        this(context, null);
    }

    public StickControlView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StickControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initialData();
    }
    
    private void initialData() {
                
    }
    
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
    }
    
}

自定义 View 最重要的就是重写 View 的 onDraw 这个方法,但是在此之前我们要获取其定义 View 的宽和高,我们需要在 onSizeChanged 这个方法里面获取到这个 View 的宽和高,接着我们就只看这两个方法。

protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        centerPoint.x = w / 2;
        centerPoint.y = h / 2;
        stickRadius =  (w > h ? h : w) / 8;
        edgeRadius = (w > h ? h : w) * 2 / 5;
        resetStick();
        initialRegion();
    }
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawEdge(canvas);
        drawStick(canvas);
    }

在上面的代码 onSizeChanged 中我分别定义了操作杆本身的半径和边境的半径,接着在 onDraw 当中我们分别写出了如何画出边界和操作杆的方法,这两个方法都是我自定义的,代码如下:

private void drawEdge(Canvas canvas) {

        paint.reset();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(getResources().getColor(R.color.edge_color, null));
        paint.setStrokeWidth(1);
        paint.setAntiAlias(true);
        canvas.drawCircle(centerPoint.x, centerPoint.y, edgeRadius, paint);

    }

    private void drawStick(Canvas canvas) {

        paint.reset();
        paint.setColor(getResources().getColor(R.color.stick_color, null));
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        canvas.drawCircle(stickX, stickY, stickRadius, paint);

    }

到这一步,我们就可以看到操作杆大概的样子了!


操作杆的效果图

实现根据手指触摸的位置改变view的位置

这里的改变 View 的位置大家想想也知道,就是改变操作杆的位置,想要改变操作杆的位置其实很简单,其实这里关键的就是有调用 View 的一个叫做 invalidate 的方法,这个方法就是重新调用 onDraw 这个方法,也就是我们总是说的重绘,所以我们的思路是定义操作杆中心的 X 坐标和 Y 坐标,让这两个坐标等于我们手指所在的坐标,但是关键的是我们要还要重写 View 另一个方法,就是控制 View 的触摸事件,需要 onTouchEvent 这个方法。

public boolean onTouchEvent(MotionEvent event) {

        int x = (int)event.getX();
        int y = (int)event.getY();

        int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (stickRegion.contains(x, y)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (edgeRegion.contains(x, y)) {
                    updateStick(x, y);
                } else {
                    resetStick();
                }
                break;
            case MotionEvent.ACTION_UP:
                resetStick();
                break;
        }

        return super.onTouchEvent(event);
    }

我们注意到需要在 ACTION_DOWN 事件要返回 true ,这是因为想要消费一个事件序列,我们必须要消费 ACTION_DOWN ,返回 true 就是消费,否则就是不消费,我们关键就是要修改 ACTION_MOVE 这个方法,先不管edgeRegion.contains(x, y),我们过会会讲的,最重要的就是 updateStick(x, y) 这个方法是我们想要的修改操作杆的位置的方法,resetStick 这个方法就是将操作杆恢复到中间,接下来就直接上代码吧。

private void updateStick(int x, int y) {

        int dx = x - centerPoint.x;
        int dy = y - centerPoint.y;

        stickX = x - dx / 4;
        stickY = y - dy / 4;

        double degree = calculateAngle(dx, dy);
        invalidate();
        Log.d(TAG, "" + degree);

    }
private void resetStick() {

        stickX = width / 2;
        stickY = height / 2;
        invalidate();

    }

写出不同方向上面的回调函数

所谓的方向的回调方法并不是必须得,每个人都有自己的理解,在这里我就简单的分成四个,也就是上下左右方向。我们定义一个 enum 类,定义了四个枚举变量,分别是 UP, LEFT, DOWN, RIGHT ,接着又写了一些监听的函数,最后我们就可以在想要获取方向的地方获取到回调,具体的代码如下。

 public enum Direction {
        UP, LEFT, DOWN, RIGHT
    }

    public interface OnDirectionListener {
        void onDirection(Direction direction);
    }

    public void setOnDirectionListener(OnDirectionListener onDirectionListener) {
        this.onDirectionListener = onDirectionListener;
    }

接着是在 updateStick 里面增加一段代码,具体代码如下:

if (onDirectionListener != null) {
            if (degree > 0 && degree < 45 || degree > 325 && degree < 360) {
                onDirectionListener.onDirection(Direction.RIGHT);
            } else if (degree > 45 && degree < 135){
                onDirectionListener.onDirection(Direction.DOWN);
            } else if (degree > 135 && degree < 225) {
                onDirectionListener.onDirection(Direction.LEFT);
            } else if (degree > 225 && degree < 325){
                onDirectionListener.onDirection(Direction.UP);
            }
        }

当然至于这些判断的角度是怎么计算出来的,其实很简单,先计算 tan 的函数,然后将这个 tan 的值转换成角度,这些方法 Java 的 Math 包都有提供,这里把那部分重点的列出来。

 private double calculateAngle(float dx, float dy) {
        double degrees = Math.toDegrees(Math.atan2(dy, dx));
        return degrees < 0 ? Math.floor(degrees + 360) : Math.floor(degrees);
    }

最后比如说我们在Activity 和 Fragment 中使用就可以像一个正常的 View 那样监听它的角度的变化,可以参考 OnClickListener 。

StickView stickView = (StickView) findViewById(R.id.sv_my);
        stickView.setOnDirectionListener(new StickView.OnDirectionListener() {
            @Override
            public void onDirection(StickView.Direction direction) {
                switch (direction) {
                    case UP:
                        Log.d(TAG, "调用了 UP");
                        break;
                    case DOWN:
                        Log.d(TAG, "调用了 DOWN");
                        break;
                    case LEFT:
                        Log.d(TAG, "调用了 LEFT");
                        break;
                    case RIGHT:
                        Log.d(TAG, "调用了 RIGHT");
                        break;
                }
            }
        });

调用的 Log 如下 :

2019-01-27 19:56:46.636 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.653 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.669 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.686 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.702 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.719 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.736 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.752 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.770 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 UP
2019-01-27 19:56:46.774 18539-18539/com.example.yuanyuanlai.uav D/ViewRootImpl@18d04f6[MainActivity]: ViewPostIme pointer 1
2019-01-27 19:56:48.088 18539-18539/com.example.yuanyuanlai.uav D/ViewRootImpl@18d04f6[MainActivity]: ViewPostIme pointer 0
2019-01-27 19:56:48.139 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 LEFT
2019-01-27 19:56:48.155 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 LEFT
2019-01-27 19:56:48.172 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 LEFT
2019-01-27 19:56:48.188 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 LEFT
2019-01-27 19:56:48.205 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 LEFT
2019-01-27 19:56:48.216 18539-18539/com.example.yuanyuanlai.uav D/MainActivity: 调用了 LEFT
......

限定触摸的区域

最后一个正常的操作杆,还是得限定下它的触摸区域,首先不能没触摸到操作杆,那个杆子就动了;也不能触摸的时候,手已经离操作杆很远了,还是可以改变距离,这样显得不是很合理,我们就分别规范边界区域和操作杆的边界。首先为什么规范这两个,我们来分析下,之前说了不能没有触摸到操作杆,所以我们操作杆的边界只在 ACTION_DOWN 的时候判断是否消费事件,接着为什么为边界区域定义一个边界呢,因为我们在移动操作杆的时候不行要操作杆移动的太远。好了,直接上代码吧。

private void initialRegion() {

        Path stickPath = new Path();
        stickPath.addCircle(centerPoint.x, centerPoint.y, stickRadius, Path.Direction.CW);
        Region globalRegion = new Region(centerPoint.x - stickRadius, centerPoint.y - stickRadius, centerPoint.x + stickRadius, centerPoint.y + stickRadius);
        stickRegion.setPath(stickPath, globalRegion);

        Path edgePath = new Path();
        edgePath.addCircle(centerPoint.x, centerPoint.y, edgeRadius * 2, Path.Direction.CW);
        globalRegion = new Region(centerPoint.x - edgeRadius * 2, centerPoint.y - edgeRadius * 2, centerPoint.x + edgeRadius * 2, centerPoint.y + edgeRadius * 2);
        edgeRegion.setPath(edgePath, globalRegion);

    }

上面的代码是我们定义两个边界,注意因为这里我们用到了长度,所以必须放在我们获取到长度地方的后面,这里我们把它放到 onSizeChanged 那里,大家可以在上面看到。也许大家会感到奇怪,为什么会有一个 globalRegion ,这里因为我们的边界是一个不规矩的区域,所以我们先要定义一个区域,和我们的path取交集,所以尽量要比 path 大一点。
具体怎么调用这个 region 其实也很简单,我们就是用它来判断 x 和 y 是否在规定的区域范围里面。上例子!!

stickRegion.contains(x, y)

总结

自定义 View 涉及的点还是很多的,想要完成一个别人看似很简单的效果,就要花很多心思,并且这里实现的是一个简单的操作杆,想要实现那种市场上面可以看见的操作杆,大家需要花很多的心思,去完成一些细节,学习 View 方面的知识还是要系统一点,因为它真的对 Android 来说很重要。

最后贴出所有代码

public class StickView extends View {

    private OnDirectionListener onDirectionListener;

    public enum Direction {
        UP, LEFT, DOWN, RIGHT
    }

    public interface OnDirectionListener {
        void onDirection(Direction direction);
    }

    public void setOnDirectionListener(OnDirectionListener onDirectionListener) {
        this.onDirectionListener = onDirectionListener;
    }

    private int width, height;

    private final static String TAG = "StickView";

    private int edgeRadius;

    private int stickRadius;

    private Region stickRegion = new Region();
    private Region edgeRegion = new Region();

    private int stickX, stickY;

    private Point centerPoint = new Point();

    private Paint paint = new Paint();

    public StickView(Context context) {
        super(context);
    }

    public StickView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public StickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawEdge(canvas);
        drawStick(canvas);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        centerPoint.x = w / 2;
        centerPoint.y = h / 2;
        stickRadius =  (w > h ? h : w) / 8;
        edgeRadius = (w > h ? h : w) * 2 / 5;
        resetStick();
        initialRegion();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int x = (int)event.getX();
        int y = (int)event.getY();

        int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (stickRegion.contains(x, y)) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (edgeRegion.contains(x, y)) {
                    updateStick(x, y);
                } else {
                    resetStick();
                }
                break;
            case MotionEvent.ACTION_UP:
                resetStick();
                break;
        }

        return super.onTouchEvent(event);
    }

    private void drawEdge(Canvas canvas) {

        paint.reset();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(getResources().getColor(R.color.edge_color, null));
        paint.setStrokeWidth(1);
        paint.setAntiAlias(true);
        canvas.drawCircle(centerPoint.x, centerPoint.y, edgeRadius, paint);

    }

    private void drawStick(Canvas canvas) {

        paint.reset();
        paint.setColor(getResources().getColor(R.color.stick_color, null));
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        canvas.drawCircle(stickX, stickY, stickRadius, paint);

    }

    private void resetStick() {

        stickX = width / 2;
        stickY = height / 2;
        invalidate();

    }

    private void initialRegion() {

        Path stickPath = new Path();
        stickPath.addCircle(centerPoint.x, centerPoint.y, stickRadius, Path.Direction.CW);
        Region globalRegion = new Region(centerPoint.x - stickRadius, centerPoint.y - stickRadius, centerPoint.x + stickRadius, centerPoint.y + stickRadius);
        stickRegion.setPath(stickPath, globalRegion);

        Path edgePath = new Path();
        edgePath.addCircle(centerPoint.x, centerPoint.y, edgeRadius * 2, Path.Direction.CW);
        globalRegion = new Region(centerPoint.x - edgeRadius * 2, centerPoint.y - edgeRadius * 2, centerPoint.x + edgeRadius * 2, centerPoint.y + edgeRadius * 2);
        edgeRegion.setPath(edgePath, globalRegion);

    }

    private void updateStick(int x, int y) {

        int dx = x - centerPoint.x;
        int dy = y - centerPoint.y;

        stickX = x - dx / 4;
        stickY = y - dy / 4;

        double degree = calculateAngle(dx, dy);
        invalidate();

        if (onDirectionListener != null) {
            if (degree > 0 && degree < 45 || degree > 325 && degree < 360) {
                onDirectionListener.onDirection(Direction.RIGHT);
            } else if (degree > 45 && degree < 135){
                onDirectionListener.onDirection(Direction.DOWN);
            } else if (degree > 135 && degree < 225) {
                onDirectionListener.onDirection(Direction.LEFT);
            } else if (degree > 225 && degree < 325){
                onDirectionListener.onDirection(Direction.UP);
            }
        }

        Log.d(TAG, "" + degree);

    }

    private double calculateAngle(float dx, float dy) {
        double degrees = Math.toDegrees(Math.atan2(dy, dx));
        return degrees < 0 ? Math.floor(degrees + 360) : Math.floor(degrees);
    }

}

上一篇 下一篇

猜你喜欢

热点阅读