一步一步实现自定义控件(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Greathfs/article/details/52761319

前言

今天我们要实现的是是一个自定义开关,之前都是集成已有的控件,今天这个却有些不太一样,这个是继承View的,也就真的是完全自定义控件了

效果图

这里写图片描述

实现

  • 首先写一个类继承View,重写三个构造方法
public class ToggleView extends View {

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

 public ToggleView(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

 public ToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

}

OK,解释下这三个构造方法:

  • 第一个用于代码创建控件
  • 第二个用于xml中使用,可指定自定义属性
  • 第三个用于xml中使用,可指定自定义属性,如果指定了样式,则走此构造函数

接下来先简单说下Android的绘制流程,大体说下:
Android绘制控件在界面打开时,生命周期onResume()之后
measure -> layout -> draw
对应的就是 测量 -> 摆放 ->绘制
对应的方法就是 onMeasure -> onLayout -> onDraw重写这些方法,实现自定义控件
其中:
View只需要两个方法:
onMeasure()(指定自己的宽高) -> onDraw()(绘制自己的内容)
ViewGroup需要三个方法:
onMeasure()(指定自己的宽高,所有子View的宽高) ->onLayout()(摆放所有子View) -> onDraw()(绘制自己的内容)

OK,之后我们把我们自定义的控件先添加到住布局中,这里注意,要全路经带包名的

 <com.example.hfs.toggleview.ui.ToggleView
        android:id="@+id/toggle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

然后找到它,并且给它设置一些属性,没有的方法我们在我们自定义控件中添加旧OK了

        mToggleView = (ToggleView) findViewById(R.id.toggle);
        //设置背景
        mToggleView.setSwitchBackgroundResource(R.mipmap.switch_background);
        //设置滑块背景
        mToggleView.setSlideButtonResource(R.mipmap.slide_button);
        //设置开关状态
        mToggleView.setSwitchState(false);

设置背景了,我们就可以把它转成一个Bitmap对象

//设置背景
 public void setSwitchBackgroundResource(int background) {

        mSwitchBackgroundResouce = BitmapFactory.decodeResource(getResources(),background);
    }
    //设置滑块背景
public void setSlideButtonResource(int button) {
        mSlideButtonBitmap = BitmapFactory.decodeResource(getResources(),button);

    }

接下来我们就要重写那两个方法了onMeasure()和onDraw()

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

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

这里我们只需要显示我们开关的宽高就可以了,所以我们得修改下onMeasure的返回值,返回刚才我们设置的背景的宽高就行了

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(mSwitchBackgroundResouce.getWidth(),mSwitchBackgroundResouce.getHeight());
    }

接下里就到onDraw方法了,Canvas 是画布的意思,我们通过它就可以绘制出我们想要的图形,油画布了还需要一只画笔,画笔可以共用,所以我们在每个构造方法都加一个init()方法,初始化画笔,同时以后也要初始化一些其它的东西


    private void init() {
        mPaint = new Paint();
    }

有了画布和画笔就开始绘制我们的控件了:

第一步:画背景

canvas.drawBitmap(mSwitchBackgroundResouce,0,0,mPaint);

第二步:画滑块
这步的注意下,因为开关的滑块不是固定的,开关有两种状态,在不同的状态下,滑块的位置是不同的,同样画的也是不同的,这个状态值就是我们之前传过来的boolean值,接下来我们就要通过这个值来进行绘制

public void setSwitchState(boolean state) {
        this.mSwitchState=state;//给状态值赋值
    }

有了状态值就开始画滑块:

//根据开关状态直接设置图片位置
if (mSwitchState){
            int newLeft=mSwitchBackgroundResouce.getWidth()-mSlideButtonBitmap.getWidth();//获取左边的坐标
            canvas.drawBitmap(mSlideButtonBitmap,newLeft,0,mPaint);
        }else{
            canvas.drawBitmap(mSlideButtonBitmap,0,0,mPaint);
        }

OK,控件基本已经画出来了,现在我们点击它还不会动,因为缺少触摸事件的监听,所以接下来我们就要给它设置触摸监听,因为我们是自定义的控件,我们就直接在我们的自定义控件内重写方法了,外面不需要管里面是怎么实现的

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                currentX = event.getX();//获取当前位置
                break;
            case MotionEvent.ACTION_MOVE:
                currentX = event.getX();//获取当前位置
                break;
            case MotionEvent.ACTION_UP:
                currentX = event.getX();//获取当前位置
                break;
        }
        return true;
    }

我们之前都是都过一个boolean值来画滑块的,但是实际中我们却要根据用户的滑动位置来画滑块,所以这里我们要设置一个boolean值,表示当前是否是滑动状态,我们按下去的时候置为true,表示滑动状态,松手置为false,表示不是滑动状态了,同时还要重绘界面,当我们处在滑动状态时,我们就按照滑动状态绘制滑块;当我们不在滑动状态时,我们还是要按照之前开关状态绘制滑块
说下重绘界面方法:

// 会引发onDraw()被调用, 里边的变量会重新生效.界面会更新
invalidate(); 

贴下完整的onDraw()方法:

// Canvas 画布, 画板. 在上边绘制的内容都会显示到界面上.
    @Override
    protected void onDraw(Canvas canvas) {
        // 1. 绘制背景
        canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);

        // 2. 绘制滑块

        if(isTouchMode){
            // 根据当前用户触摸到的位置画滑块

            // 让滑块向左移动自身一半大小的位置
            float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;

            int maxLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();

            // 限定滑块范围
            if(newLeft < 0){
                newLeft = 0; // 左边范围
            }else if (newLeft > maxLeft) {
                newLeft = maxLeft; // 右边范围
            }

            canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
        }else {
            // 根据开关状态boolean, 直接设置图片位置
            if(mSwitchState){// 开
                int newLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
                canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
            }else {// 关
                canvas.drawBitmap(slideButtonBitmap, 0, 0, paint);
            }
        }

    }

    boolean isTouchMode = false;
    // 重写触摸事件, 响应用户的触摸.
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            isTouchMode = true;
            currentX = event.getX();
            break;
        case MotionEvent.ACTION_MOVE:
            currentX = event.getX();
            break;
        case MotionEvent.ACTION_UP:
            isTouchMode = false;
            currentX = event.getX();

            float center = switchBackgroupBitmap.getWidth() / 2.0f;

            // 根据当前按下的位置, 和控件中心的位置进行比较. 
            boolean state = currentX > center;

            break;

        default:
            break;
        }

        // 重绘界面
        invalidate(); // 会引发onDraw()被调用, 里边的变量会重新生效.界面会更新

        return true; // 消费了用户的触摸事件, 才可以收到其他的事件.
    }

OK,接下来我么还差一个接口监听,类似于View的setOnClickListener一样,当我们的开关状态改变时我们得需要通知外面,这里就是通过接口回调来实现的
首先,在我们自定义的类中定义一个接口,同时提供set方法

public interface OnSwitchStateUpdateListener{
        // 状态回调, 把当前状态传出去
        void onStateUpdate(boolean state);
    }

    public void setOnSwitchStateUpdateListener(
            OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
                this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
    }

OK,那么我们在哪里调用呢?没错,就是在我们松开手指的时候,我们就要把状态值传出去:

case MotionEvent.ACTION_UP:
            isTouchMode = false;
            System.out.println("event: ACTION_UP: " + event.getX());
            currentX = event.getX();

            float center = switchBackgroupBitmap.getWidth() / 2.0f;

            // 根据当前按下的位置, 和控件中心的位置进行比较. 
            boolean state = currentX > center;

            // 如果开关状态变化了, 通知界面. 里边开关状态更新了.
            if(state != mSwitchState && onSwitchStateUpdateListener != null){
                // 把最新的boolean, 状态传出去了
                onSwitchStateUpdateListener.onStateUpdate(state);
            }

            mSwitchState = state;
            break;

OK,这样我们就可以在MainActivity中拿到这个状态值了

// 设置开关更新监听
        mToggleView.setOnSwitchStateUpdateListener(new OnSwitchStateUpdateListener(){

            @Override
            public void onStateUpdate(boolean state) {
                Toast.makeText(getApplicationContext(), "state: " + state, 0).show();
            }

        });

好了,这样看来整体上我们的这个控件好像写完了,其实没有,因为一般自定义控件设置属性都不会在代码中设置的,就是MainActivity中那几行代码,一般都是在xml中配置这些,所以我们也要这么做.
首先,我们要在res目录下的values目录下新建一个attrs.xml,之后我们就可以声明我们的自定义属性了

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="ToggleView">
        <attr name="switch_background" format="reference" />
        <attr name="slide_button" format="reference" />
        <attr name="switch_state" format="boolean" />
    </declare-styleable>

</resources>

这里有兴趣的童鞋可以看下Android的一些源码,比如View的,为什么有些控件我们在xml中能直接使用一些属性,因为Android已经帮我们定义好了一些属性,定义的格式和我们上面那段一样的

OK,声明完我们需要的属性后,我们就可以在xml中使用了,怎么使用呢?首先需要我们自己命名一个命名空间,名字随意,然后就可以使用了:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.hfs.toggleview.MainActivity">

    <com.example.hfs.toggleview.ui.ToggleView
        android:id="@+id/toggle"
        android:layout_centerInParent="true"
        app:switch_background="@mipmap/switch_background"
        app:slide_button="@mipmap/slide_button"
        app:switch_state="false"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>

这里的app就是我们自己的命名空间

这样我们就可以把那三行代码删除了,同时也可以删除我们自定义类中对应的方法了,于是MainActivity代码就很简单了

public class MainActivity extends AppCompatActivity {

    private ToggleView mToggleView;

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

        mToggleView = (ToggleView) findViewById(R.id.toggle);

        mToggleView.setOnSwitchStateUpdateListener(new ToggleView.onSwitchStateUpdateListener() {
            @Override
            public void onStateUpdate(boolean state) {
                if (state) {
                    Toast.makeText(MainActivity.this, "开关打开", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(MainActivity.this, "开关关闭", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }
}

OK,这样我们的一个简单自定义开关就写完了,这只是个例子,起到抛砖引用作用

源码:
https://github.com/Greathfs/ToggleView

猜你喜欢

转载自blog.csdn.net/Greathfs/article/details/52761319