图文剖析自定义View的绘制(以自定义滑动按钮为例)

自定义View一直是横在Android开发者面前的一道坎。

一、View和ViewGroup的关系

从View和ViewGroup的关系来看,ViewGroup继承View。

View的子类,多是功能型的控件,提供绘制的样式,比如imageView,TextView等,而ViewGroup的子类,多用于管理控件的大小,位置,如LinearLayout,RelativeLayout等,从下图可以看出



从实际应用中看,他们又是组合关系,我们在布局中,常常是一个ViewGroup嵌套多个ViewGroup或View,而被嵌套的ViewGroup又会嵌套多个ViewGroup或View

如下



二、View的绘制流程

从View源码来看,主要关系三个方法:

1、measure():测量
     一个final方法,控制控件的大小
2、layout():布局
         用来控制自己的布局位置
          有相对性,只相对于自己的父类布局,不关心祖宗布局
3、draw():绘制
          用来控制控件的显示样式

流程:  流程 measure --> layout --> draw


对应于我们要实现的方法是

onMeasure()

onLayout()

onDraw()

实际绘制中,我们的思考顺序一般是这样的:

是否需要控制控件的大小-->是-->onMeasure()
(1)如果这个自定义view不是ViewGroup,onMeasure()方法调用setMeasureDeminsion(width,height):用来设置自己的大小
(2)如果是ViewGroup,onMeasure()方法调用 ,child.measure()测量孩子的大小,给出孩子的期望大小值,之后-->setMeasureDeminsion(width,height):用来设置自己的大小

是否需要控制控件的摆放位置 -->是 -->onLayout ()


是否需要控制控件的样子 -->是 -->onDraw  ()-->canvas的绘制

下面是我绘制的流程图:



下面以自定义滑动按钮为例,说明自定义View的绘制流程


我们期待实现这样的效果:


拖动或点击按钮,开关向右滑动,变成


其中开关能随着手指的触摸滑动到相应位置,直到最后才固定在开关位置上


新建一个类继承自View,实现其两个构造方法

public class SwitchButtonView extends View {

    
    public SwitchButtonView(Context context) {
        this(context, null);
    }

    public SwitchButtonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

drawable资源中添加这两张图片


借此,我们可以用onMeasure()确定这个控件的大小

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        mSwitchButton = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button_background);
        setMeasuredDimension(mSwitchButton.getWidth(), mSwitchButton.getHeight());
    }

这个控件并不需要控制其摆放位置,略过onLayout();


接下来onDraw()确定其形状。

但我们需要根据我们点击控件的不同行为来确定形状,这需要重写onTouchEvent()

其中的逻辑是:

当按下时,触发事件MotionEvent.Action_Down,若此时状态为关:

(1)若手指触摸点(通过event.getX()得到)在按钮的 中线右侧,按钮向右滑动一段距离(event.getX()与开关控件一半宽度之差,具体看下文源码)

(2)若手指触摸点在按钮中线左侧,按钮依旧处于最左(即“关”的状态)。

若此时状态为开:

(1)若手指触摸点在按钮中线左侧,按钮向左滑动一段距离

(2)若手指触摸点在按钮中线右侧,按钮依旧处于最右(即“开”的状态)


当滑动时,触发时间MotionEvent.Action_MOVE,逻辑与按下时一致

注意,onTouchEvent()需要设置返回值 为 Return true,否则无法响应滑动事件


当手指收起时,若开关中线位于整个控件中线左侧,设置状态为关,反之,设置为开。

具体源码如下所示:(还对外提供了一个暴露此时开关状态的接口)


自定义View部分


package com.lian.switchtogglebutton;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by lian on 2016/3/20.
 */
public class SwitchButtonView extends View {

    private static final int STATE_NULL = 0;//默认状态
    private static final int STATE_DOWN = 1;
    private static final int STATE_MOVE = 2;
    private static final int STATE_UP = 3;

    private Bitmap mSlideButton;
    private Bitmap mSwitchButton;
    private Paint mPaint = new Paint();
    private int buttonState = STATE_NULL;
    private float mDistance;
    private boolean isOpened = false;
    private onSwitchListener mListener;

    public SwitchButtonView(Context context) {
        this(context, null);
    }

    public SwitchButtonView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        mSwitchButton = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button_background);
        setMeasuredDimension(mSwitchButton.getWidth(), mSwitchButton.getHeight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mSwitchButton!= null){
            canvas.drawBitmap(mSwitchButton, 0, 0, mPaint);
        }
        //buttonState的值在onTouchEvent()中确定
        switch (buttonState){
            case STATE_DOWN:
            case STATE_MOVE:
                if (!isOpened){
                    float middle = mSlideButton.getWidth() / 2f;
                    if (mDistance > middle) {
                        float max = mSwitchButton.getWidth() - mSlideButton.getWidth();
                        float left = mDistance - middle;
                        if (left >= max) {
                            left = max;
                        }
                        canvas.drawBitmap(mSlideButton,left,0,mPaint);
                    }

                    else {

                        canvas.drawBitmap(mSlideButton,0,0,mPaint);
                    }
                }else{
                    float middle = mSwitchButton.getWidth() - mSlideButton.getWidth() / 2f;
                    if (mDistance < middle){
                        float left = mDistance-mSlideButton.getWidth()/2f;
                        float min = 0;
                        if (left < 0){
                            left = min;
                        }
                        canvas.drawBitmap(mSlideButton,left,0,mPaint);
                    }else{
                        canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
                    }
                }



                break;

            case STATE_NULL:
            case STATE_UP:
                if (isOpened){
                    Log.d("开关","开着的");
                    canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
                }else{
                    Log.d("开关","关着的");
                    canvas.drawBitmap(mSlideButton,0,0,mPaint);
                }
                break;

            default:
                break;
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mDistance = event.getX();
                Log.d("DOWN","按下");
                buttonState = STATE_DOWN;
                invalidate();
                break;

            case MotionEvent.ACTION_MOVE:
                buttonState = STATE_MOVE;
                mDistance = event.getX();
                Log.d("MOVE","移动");
                invalidate();
                break;

            case MotionEvent.ACTION_UP:
                mDistance = event.getX();
                buttonState = STATE_UP;
                Log.d("UP","起开");
                if (mDistance >= mSwitchButton.getWidth() / 2f){
                    isOpened = true;
                }else {
                    isOpened = false;
                }
                if (mListener != null){
                    mListener.onSwitchChanged(isOpened);
                }
                invalidate();
                break;
            default:
                break;
        }

        return true;
    }

    public void setOnSwitchListener(onSwitchListener listener){
        this.mListener = listener;
    }

    public interface onSwitchListener{
        void onSwitchChanged(boolean isOpened);
    }
}


DemoActivity:

package com.lian.switchtogglebutton;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        SwitchButtonView switchButtonView = (SwitchButtonView) findViewById(R.id.switchbutton);
        switchButtonView.setOnSwitchListener(new SwitchButtonView.onSwitchListener() {
            @Override
            public void onSwitchChanged(boolean isOpened) {
                if (isOpened) {
                    Toast.makeText(MainActivity.this, "打开", Toast.LENGTH_SHORT).show();
                }else {
                    Toast.makeText(MainActivity.this, "关闭", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

布局:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.lian.switchtogglebutton.MainActivity">

    <com.lian.switchtogglebutton.SwitchButtonView
        android:id="@+id/switchbutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />
</RelativeLayout>


  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值