自定义控件-TouchButton

入门安卓,自定义这块也是啃了好多知识,想做个文章勉励自己坚持下去,废话不多说,这次做的是一个简单的选择控件,效果如下:

动图

控件效果:

1 ,可设置Item,类型是List<String>
2,多种属性可设置,背景,文本大小,文本颜色,具体看代码
2,可点击,并且可重复点击,附带动画效果,动画时长可自定

开始:

知道大概效果,我们先来看代码吧:
由于个人习惯,我会先写要用到的属性,也有些人喜欢先写代码再根据需求最后总结写attrs.xml

touch_button_attrs.xml :(自定义属性的名称都很浅显易懂是吧哈哈)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TouchButton">
        <attr name="tbRoundDistance" format="dimension"></attr>
        <attr name="tbBackgroupColor" format="color|reference"></attr>
        <attr name="tbLineColor" format="color|reference"></attr>
        <attr name="tbLineWidth" format="dimension"></attr>
        <attr name="tbSelectBackgroupColor" format="color|reference"></attr>
        <attr name="tbNormalBackgroupColor" format="color|reference"></attr>
        <attr name="tbNormalTextSize" format="dimension"></attr>
        <attr name="tbNormalTextColor" format="color|reference"></attr>
        <attr name="tbSelectTextSize" format="dimension"></attr>
        <attr name="tbSelectTextColor" format="color|reference"></attr>
        <attr name="tbIsRound" format="boolean"></attr>
        <attr name="tbSelectIndex" format="integer"></attr>
        <attr name="tbDuration" format="integer"></attr>
    </declare-styleable>
</resources>

下面进入正文

自定义控件,少不了要继承View,并且重写它的构造函数:

public class TouchButton extends View {
        public TouchButton(Context context) {
        this(context, null);
    }

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

    public TouchButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 获取layout里面设置的属性,如果未设置则设置默认值
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TouchButton);

        mDistance = a.getDimension(R.styleable.TouchButton_tbRoundDistance, dp2px(5));
        mBackGroupColor = a.getColor(R.styleable.TouchButton_tbBackgroupColor, getResources().getColor(R.color.red));
        mLineColor = a.getColor(R.styleable.TouchButton_tbLineColor, getResources().getColor(R.color.black));
        mLineWidth = a.getDimension(R.styleable.TouchButton_tbLineWidth, dp2px(1));
        mSelectBackgroupColor = a.getColor(R.styleable.TouchButton_tbSelectBackgroupColor, getResources().getColor(R.color.blue));
        mNormalBackgroupColor = a.getColor(R.styleable.TouchButton_tbNormalBackgroupColor, getResources().getColor(R.color.transparent));
        mNormalTextSize = a.getDimensionPixelSize(R.styleable.TouchButton_tbNormalTextSize, sp2px(16));
        mNormalTextColor = a.getColor(R.styleable.TouchButton_tbNormalTextColor, getResources().getColor(R.color.black));
        mSelectTextSize = a.getDimensionPixelSize(R.styleable.TouchButton_tbSelectTextSize, sp2px(16));
        mSelectTextColor = a.getColor(R.styleable.TouchButton_tbSelectTextColor, getResources().getColor(R.color.white));
        mIsRound = a.getBoolean(R.styleable.TouchButton_tbIsRound, true);
        detalIndex = mPreIndex = mSelectIndex = a.getInt(R.styleable.TouchButton_tbSelectIndex, 0);
        mDuration = a.getInt(R.styleable.TouchButton_tbDuration, 500);
        // 释放资源(这步大家要记得加上,本人有时候会忘记加了哈哈)
        a.recycle();

        init();
    }
}

然后第二步当然是重写onMeasure()方法,来更正宽高,因为layout.xml中如果设置了宽高属性为wrap_content,系统会默认把它当作 MeasureSpec.EXACTLY,这样我们计算出来的宽高就跟我们的不一样了,所以我们要处理下这种情况

    // 代码很简单,如果不是 EXACTLY ,就给它指定的宽高
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int modeW = MeasureSpec.getMode(widthMeasureSpec);
        int sizeW = MeasureSpec.getSize(widthMeasureSpec);
        int modeH = MeasureSpec.getMode(heightMeasureSpec);
        int sizeH = MeasureSpec.getSize(heightMeasureSpec);

        int height = 0;
        int width = 0;
        switch (modeW) {
            case MeasureSpec.EXACTLY:
                width = sizeW;
                break;
            case MeasureSpec.AT_MOST:
                width = dp2px(250);
                break;
            default:
                width = dp2px(250);
                break;
        }

        switch (modeH) {
            case MeasureSpec.EXACTLY:
                height = sizeH;
                break;
            case MeasureSpec.AT_MOST:
                height = dp2px(40);
                break;
            default:
                height = dp2px(40);
                break;
        }
        // 这个必须加上,这个设置后我们才能获取到我们设置的宽高
        setMeasuredDimension(width, height);

    }

上面都是基础代码,核心代码请看下面:

onDraw()

onDraw() 方法,我们之前所做的操作都是为了onDraw方法打基础,onDraw() 是真正把东西展示出来的,代码如下:

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

        // 画背景
        drawBackgroup(canvas);
        // 画文本和边框
        drawText(canvas);

    }

我把步骤分了出来,先画背景,再去画文本和文本的边框

drawBackgroup() : 背景可以有两种模式,一个圆形边角,一个是正方形边角,类似下图

这里写图片描述

private void drawBackgroup(Canvas canvas) {

        // 背景的边框,计算应该会计算把哈哈
        mBackGroupRectF.left = mLineWidth / 2;
        mBackGroupRectF.top = mLineWidth / 2;
        mBackGroupRectF.right = getMeasuredWidth() - mLineWidth / 2;
        mBackGroupRectF.bottom = getMeasuredHeight() - mLineWidth / 2;
        // 圆边角的半径,(控件高度 - 上下两条边线宽度) / 2
        float mRadius = getMeasuredHeight() / 2 - mLineWidth / 2;

        // 如果是圆形边角
        if (mIsRound) {
            mBackGroupPaint.setStyle(Paint.Style.FILL);
            mBackGroupPaint.setColor(mBackGroupColor);
            canvas.drawRoundRect(mBackGroupRectF,
                    mRadius, mRadius,
                    mBackGroupPaint);
            mBackGroupPaint.setStyle(Paint.Style.STROKE);
            mBackGroupPaint.setColor(mLineColor);
            mBackGroupPaint.setStrokeWidth(mLineWidth);
            canvas.drawRoundRect(mBackGroupRectF,
                    mRadius, mRadius,
                    mBackGroupPaint);

        }
        // 默认方形
        else {
            mBackGroupPaint.setStyle(Paint.Style.FILL);
            mBackGroupPaint.setColor(mBackGroupColor);
            canvas.drawRect(mBackGroupRectF, mBackGroupPaint);
            mBackGroupPaint.setStyle(Paint.Style.STROKE);
            mBackGroupPaint.setColor(mLineColor);
            mBackGroupPaint.setStrokeWidth(mLineWidth);
            canvas.drawRect(mBackGroupRectF, mBackGroupPaint);
        }
    }

上面为什么画两次边框,第一次边框是还没有设置画笔宽度,第二次是设置了宽度,比如正方形边角模式,
第一次画边框:我们设置要红色底色
这里写图片描述

第二次画边框:我们要边线的粗为5dp
这里写图片描述
因为考虑到有人应该有这需求:需要加粗的边线

drawText() : 设置文本和选中的文本的边框

 private void drawText(Canvas canvas) {
        // 第一步:数据源为空则返回,不画了
        if (mStrList == null || mStrList.size() <= 0 || mStrCount <= 0) {
            return;
        }
        // 第二步:每个item的宽度
        float mSquareWidth = (getMeasuredWidth() - mDistance * 2 - mLineWidth * 2) / mStrCount;
        // 每个item的高度(代码中好像还没用过)
        float mSquareHeight = getMeasuredHeight() - mDistance * 2;
        // 文本baseLine位置
        float baseX = 0;
        float baseY = 0;

        // 第三步:画选中的左边
        for (int i = 0; i < mSelectIndex; i++) {
            String itemStr = mStrList.get(i);
            // 每一个item的baselineX坐标
            baseX = mDistance + mSquareWidth / 2 + i * mSquareWidth;
            mTextPaint.setTextSize(mNormalTextSize);
            mTextPaint.setColor(mNormalTextColor);
            // 每一个item的baselineY坐标
            baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(mTextPaint.descent() - Math.abs(mTextPaint.ascent()))) / 2));
            canvas.drawText(itemStr, baseX, baseY, mTextPaint);
        }

        // 第四步:画选中的右边
        for (int i = mSelectIndex + 1; i < mStrCount; i++) {
            String itemStr = mStrList.get(i);
            // 每一个item的baselineX坐标
            baseX = mDistance + mSquareWidth / 2 + i * mSquareWidth;
            mTextPaint.setTextSize(mNormalTextSize);
            mTextPaint.setColor(mNormalTextColor);
            // 每一个item的baselineY坐标
            baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(mTextPaint.descent() - Math.abs(mTextPaint.ascent()))) / 2));
            canvas.drawText(itemStr, baseX, baseY, mTextPaint);
        }

        // 第五步: 画选中的条目
        String itemStr = mStrList.get(mSelectIndex);
        // 选中的方框
        mTextBackGroupRectF.left = mLineWidth + mDistance + mSelectIndex* mSquareWidth;
        mTextBackGroupRectF.top = mDistance + mLineWidth;
        mTextBackGroupRectF.right = mTextBackGroupRectF.left + mSquareWidth;
        mTextBackGroupRectF.bottom = getMeasuredHeight() - mDistance - mLineWidth;

        mTextPaint.setTextSize(mSelectTextSize);
        mTextPaint.setColor(mSelectTextColor);
        // item的y基坐标
        baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(mTextPaint.descent() - Math.abs(mTextPaint.ascent()))) / 2));

        if (mIsRound) {
            mBackGroupPaint.setColor(mSelectBackgroupColor);
            mBackGroupPaint.setStyle(Paint.Style.FILL);
            canvas.drawRoundRect(mTextBackGroupRectF, (getMeasuredHeight() - mDistance * 2) / 2, (getMeasuredHeight() - mDistance * 2) / 2, mBackGroupPaint);
            canvas.drawText(itemStr, mTextBackGroupRectF.left + mSquareWidth / 2, baseY, mTextPaint);
        } else {
            mBackGroupPaint.setColor(mSelectBackgroupColor);
            mBackGroupPaint.setStyle(Paint.Style.FILL);
            canvas.drawRect(mTextBackGroupRectF, mBackGroupPaint);
            canvas.drawText(itemStr, mTextBackGroupRectF.left + mSquareWidth / 2, baseY, mTextPaint);
        }

    }

代码中最重要的就是文本的绘制,要居中:
也就是这个

// 每一个item的x基坐标
baseX = mDistance + mSquareWidth / 2 + i * mSquareWidth;
// item的y基坐标
baseY = (int) ((canvas.getHeight() / 2) + ((Math.abs(mTextPaint.descent() - Math.abs(mTextPaint.ascent()))) / 2));

mSquareWidth 也就是每一个item的宽度,用整个View的宽度-左右两个边距-左右两条粗的边线,也就是上面所说的可能要用到的需求,然后得出全部item占用的宽度,/mStrCount,就算出每个item的宽度

float mSquareWidth = (getMeasuredWidth() - mDistance * 2 - mLineWidth * 2) / mStrCount;

对于文本的绘制,本篇先不讲,我大概说下,就是我们在界面画文本的时候,文本是从所谓的基线开始画的,一般我们不设置画笔居中画的时候,就用控件的宽度 / 2-文本的宽度 / 2,得到起始点x,或者我们可以通过设置画笔 :mTextPaint.setTextAlign(Paint.Align.CENTER);,这样我们就不用去关心x的偏移量了,直接给它个控件中心x就行了,所以x还是比较简单,也容易理解,比较麻烦的是Y,不过用(控件的高度 + 文本的高度)/ 2 就可以得到了,文本的高度可以用文本的下坡度(descent)减去文本的上波度(ascent),因为下坡度为正,上坡度为负,所以一减就是文本高度了,当然,文本高度不包括这些,真的要算还得用bottom和top,不过粗略计算可以用前面两个了,下图可方便你理解文本的基线,以及其他:
这里写图片描述

别跑太远,还是讲解代码吧,再次进入正题:

首先:
进入drawTex后,
1, 第一步先判断数据源,不存在我们就不需要绘制了
2,计算每个item的宽度,为下面做准备
3,先画选中项的左侧条目,设置未选中的文本大小和颜色,找好基线,然后画
4,再画选中项的右侧条目,同上
5,画选中项,先画背景框,再画文本
6,结束

问题:
有人可能在第三步的时候,就很懵逼,这么多条目,我怎么知道它们的文本绘画的起点呢,看下图
这里写图片描述
所以在for循环里面,i就是下标,一半的item宽度是每个item都需要的,所以可以推出,第三个item(下标为2)就要mLineWidth + mDistance + mSquareWidth / 2 + mSquareWidth * i(此时 i 为2),

点击事件onTouchEvent()

上面我们只是做到了静态显示,要让控件动起来,我们还需要重写重写onTouchEvent事件,在这里我也不多说事件分发机制了,这网上好的文章有很多,大家可以百度百度哈哈
在我们的控件中,我们只需要处理按下的事件就可以了:

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        int action = event.getAction();
        // 动画是否还在执行,还在执行则不进入下一步,直接返回
        if (isAnimationActive) {
            return false;
        }
        float x = event.getX();
        float y = event.getY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:

                if (isCanTouch(x, y)) {
                    mSelectIndex = touchInSelect(x, y);
                }
                if (mPreIndex == mSelectIndex) {
                    if(listener != null){
                        listener.onSelect(mSelectIndex, getSelectItem(), false);
                    }
                } else {
                    mPreIndex == mSelectIndex;
                    if(listener != null){
                        listener.onSelect(mSelectIndex, getSelectItem(), true);
                    }
                }
                // 再次绘制
                invalidate();
                break;
        }

        return super.onTouchEvent(event);
    }

上面有两个方法,一个是isCanTouch(x, y),是判断触摸的地方是不是在item的宽度上,我们知道item的整个宽度是整个View的宽度,减掉边线宽度和边距的

// 是否在触摸在View内
    public boolean isCanTouch(float x, float y) {
        if (x > mLineWidth + mDistance
                && x < getMeasuredWidth() - mLineWidth - mDistance
                && y > mLineWidth + mDistance
                && y < getMeasuredHeight() - mLineWidth - mDistance) {
            return true;
        }
        return false;
    }

一个是touchInSelect(x, y),也很简单,你点击的x,减去最右边的边线和边距,除以一个item的宽度,就得出点击所在的下标 selectIndex

 // 选中的Index
public int touchInSelect(float x, float y) {
        int selectIndex = (int) ((x - mLineWidth - mDistance) / ((getMeasuredWidth() - mDistance * 2 - mLineWidth * 2) / mStrCount));
        return selectIndex;
    }

上面的点击事件你如果试了,你会发现控件可以用的,效果如下:
这里写图片描述

咋一看还不错,至少有效果了,可是你操作起来会觉得很生硬,不生动,我们可以在这个基础上加上我们的动画,来达到第一张图片的动画效果,使控件生动些。

动画:ObjectAnimator

属性动画有两类,一种是ValueAnimator, 一种是我们要用到的ObjectAnimator,具体怎么使用我就不在这里详解了,百度一堆哈哈,都是前人的心血,都很不错。我在这里大概说下ObjectAnimator的使用,
我分三步:
第一步:实例化ObjectAnimator

ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(this, "detalIndex", mPreIndex, mSelectIndex)
                            .setDuration(mDuration);

其中第一个参数就是要操作的对象本身,第二个参数是对象的一个public属性,这个比较好用,我们可以自己自定义的,灵活用这个,会让你做好多你想要的动画,这个属性会在动画执行过程中一直变化,变化的变化量取决于后面两个参数,第三个参数是变化开始前的值,第四个参数是变化后的值,也就是目标值,
setDuration() 顾名思义就是设置变化过程所需的时间

第二步:设置监听

        mObjectAnimator.addListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            super.onAnimationEnd(animation);
                            isAnimationActive = false;
                            mPreIndex = mSelectIndex;
                            if(listener != null){
                                listener.onSelect(mSelectIndex, getSelectItem(), true);
                            }
                        }

                        @Override
                        public void onAnimationStart(Animator animation) {
                            super.onAnimationStart(animation);
                            isAnimationActive = true;
                        }
                    });

onAnimationStart() : 动画开始的时候调用
onAnimationEnd() : 动画结束的时候调用

在这里我们需要在开始的时候设置标志位,表示动画还在执行,在结束的时候,再去更换标志位,作用是防止快速点击,导致上一次动画还没执行完就再次执行

第三步:开启动画

mObjectAnimator.start();

好了,动画也不是很难,慢慢理解吧。

在设置动画的时候,我们需要改变下一个属性,在第一步可以看到,我们动画操作的是detalIndex属性,这个属性我们需要替换下onDraw() 里面画文本那里,如下:
这里写图片描述

有人可能有疑惑了,举个例子,比如初始化View的时候我是下标0,这个时候preIndex = selectIndex = 0,当我第一次点击下标1,这个时候selectIndex = 1, pre还是0, 0 - 1 的过程有个时间段变化的,这个变化的过程的值刚好就是detailIndex,所以我的文本方框才有动画效果,最后这个detalIndex是会从0 在500ms内变到1,也就是0,0.1, 0.2…一直变化到目标值,这样完整的控件就出来了。。。。。。。

这里写图片描述

这里写图片描述

需要整份源码的等我上传到Github上吧,其实上面源码也都给出来了,哇好好休息,明天继续加油!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值