Android自定义控件:滑动开关机

最近重构代码,发现了之前偷懒遗留的一个问题。有一个控制设备开关机的控件,由于之前赶项目交期,匆匆忙忙直接在Activity中重写onTouch事件,效果虽然也实现了,但是肯定不是很好的,今天重新将这个小玩意重新封装成一个自定义控件,话不多说,先看看实现的效果。


这里写图片描述

其实看样子都知道,是一个蛮简单的自定义控件,至于为什么要写这篇博客呢,因为也有段时间没有搞自定义控件了,一时手痒,哈哈 = =,温故而知新,总结一下总归是有好处的。

既然是自定义控件,那么自然少不了自定义属性,先贴上attrs.xml代码。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SlideSwitchView">
        <attr name="slide_button" format="reference"/>
        <attr name="android:text"/>
        <attr name="android:textSize"/>
        <attr name="android:textColor"/>
    </declare-styleable>
</resources>

这里定义了四个属性,分别是文本内容,文本字号,文本颜色以及滑动button的背景图片,如果有拓展需求的话,也可以很简单的拓展。

然后再看布局代码, 简单粗暴,就只有一个控件。

<?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:swipe="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.zyw.horrarndoo.swipebutton.MainActivity">

    <com.zyw.horrarndoo.swipebutton.SlideSwitchView
        android:layout_centerInParent="true"
        android:id="@+id/slide_switch_view"
        swipe:slide_button="@mipmap/slide"
        android:text = "Slide to power on"
        android:textSize = "22sp"
        android:textColor = "@android:color/holo_green_dark"
        android:layout_width="wrap_content"
        android:layout_height="60dp"/>

</RelativeLayout>

下面开始写我们的自定义控件,写代码之前,不妨先撸一撸思路。

  1. 我们这个自定义控件跟现有的控件实际上是没有什么联系的,所以我们这里自定义控件继承自View;
  2. 由于button是要随手势拖动的,所以我们肯定是要重写onTouchEvent方法的,在onTouchEvent中动态的更新button的X坐标值;
  3. 既然界面会涉及到重绘,所以我们肯定需要重写onDraw方法,为了实现button拖动,这里我们通过drawBitmap的方法动态绘制button;
  4. 由于我们这个控件会放到各个界面中去用,我们肯定就需要重写onMeasure方法,动态的确定button的宽高以及text的长度等;
  5. 外界需要知道控件滑动的结果,我们这里定义一个接口将状态回调出去;

思路撸完,上代码。

/**
 * 自定义滑动开关
 * <p>
 * Created by Horrarndoo on 2017/6/1.
 */
public class SlideSwitchView extends View {
    private Bitmap slideButtonBitmap; // 滑块图片
    private Paint mPaint; // 画笔
    private float currentX; //当前滑动的x坐标
    private int mBaseLineY; //  text基准线
    private String mTextContent; //text内容

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

    public SlideSwitchView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlideSwitchView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initPaint();
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideSwitchView);
        setSlideButtonResource(ta.getResourceId(R.styleable.SlideSwitchView_slide_button, -1));
        setText(ta.getString(R.styleable.SlideSwitchView_android_text));
        setTextSize(ta.getDimension(R.styleable.SlideSwitchView_android_textSize,30));
        setTextColor(ta.getColor(R.styleable.SlideSwitchView_android_textColor, Color.BLACK));
        ta.recycle();
    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextAlign(Paint.Align.LEFT);
        mPaint.setAntiAlias(true);
    }

    /**
     * 初始化text居中基准线
     */
    private void initTextBaseLine() {
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的top
        float bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom
        mBaseLineY = (int) (getMeasuredHeight() / 2 - top / 2 - bottom / 2);//基线中间点的y轴计算公式
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST) {
            int newWidth = (int) (slideButtonBitmap.getWidth() * 2 + getTextWidth());
            if (width >= newWidth)
                width = newWidth;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            if (height < slideButtonBitmap.getHeight()) {
                // 获得图片的宽高
                int widthSlide = slideButtonBitmap.getWidth();
                int heightSlide = slideButtonBitmap.getHeight();
                float scaleHeight = height * 1.0f / slideButtonBitmap.getHeight();
                Matrix matrix = new Matrix();
                matrix.postScale(scaleHeight, scaleHeight);
                slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
                        heightSlide, matrix, true);
                invalidate();
            }
        }

        if (slideButtonBitmap.getWidth() > (width - getTextWidth()) / 2) {
            // 获得图片的宽高
            int widthSlide = slideButtonBitmap.getWidth();
            int heightSlide = slideButtonBitmap.getHeight();
            float scaleWidth = (width - getTextWidth()) / 2 / slideButtonBitmap.getWidth();
            Matrix matrix = new Matrix();
            matrix.postScale(scaleWidth, scaleWidth);
            slideButtonBitmap = Bitmap.createBitmap(slideButtonBitmap, 0, 0, widthSlide,
                    heightSlide, matrix, true);
            invalidate();
        }

        setMeasuredDimension(width, slideButtonBitmap.getHeight());
        initTextBaseLine();
    }

    // Canvas 画布, 画板. 在上边绘制的内容都会显示到界面上.
    @Override
    protected void onDraw(Canvas canvas) {
        // 1. 绘制text
        canvas.drawText(mTextContent, slideButtonBitmap.getWidth(), mBaseLineY, mPaint);

        // 2. 绘制滑块
        if (isTouchMode) {
            // 根据当前用户触摸到的位置画滑块
            // 让滑块向左移动自身一半大小的位置
            float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;

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

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

            canvas.drawBitmap(slideButtonBitmap, newLeft, 0, mPaint);
        } else {
            //还原button位置
            canvas.drawBitmap(slideButtonBitmap, 0, 0, mPaint);
        }

    }

    boolean isTouchMode = false;
    private OnSwitchStateUpdateListener onSwitchStateUpdateListener;

    // 重写触摸事件, 响应用户的触摸.
    @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 = getMeasuredWidth() / 2.0f;

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

                // 如果开关状态变化了, 通知界面
                if (isStateChanged && onSwitchStateUpdateListener != null) {
                    onSwitchStateUpdateListener.onStateUpdate();
                }
                break;

            default:
                break;
        }

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

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

    /**
     * 设置滑块图片资源
     *
     * @param slideButton 滑块图片资源
     */
    public void setSlideButtonResource(int slideButton) {
        slideButtonBitmap = BitmapFactory.decodeResource(getResources(), slideButton);
    }

    /**
     * 设置text字号大小
     *
     * @param textSize text字号大小
     */
    public void setTextSize(float textSize) {
        mPaint.setTextSize(textSize);
        mPaint.setStrokeWidth(textSize / 15.f);
    }

    /**
     * 设置text内容
     *
     * @param text text内容
     */
    public void setText(String text) {
        mTextContent = text;
    }

    /**
     * 设置text颜色
     *
     * @param color text颜色资源
     */
    public void setTextColor(int color) {
        mPaint.setColor(color);
    }

    /**
     * 获取text文字宽度
     *
     * @return text文字宽度
     */
    private float getTextWidth() {
        return mPaint.measureText(mTextContent);
    }

    /**
     * 获取text文字高度
     *
     * @return text文字高度
     */
    private float getTextHeight() {
        return mPaint.getFontMetrics().bottom - mPaint.getFontMetrics().top;
    }

    public interface OnSwitchStateUpdateListener {
        // 状态回调
        void onStateUpdate();
    }

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

这里我们主要看看onTouchEvent方法和onMeasure方法。
在onTouchEvent中,我们在ACTION_MOVE中不断的刷新button的X坐标,然后刷新button的位置,达到拖动button滑动的效果 ,最后在ACTION_UP中判断滑动位置是否超过控件一半位置来确定是否传递滑动状态变化给回调。

在onMeasure中,我们计算了几种情况。

  1. 首先要保证宽度为wrap_content的时候,控件要有滑动的空间,所以在wrap_content的时候,设置控件宽度值为button.width*2+text.width,保证控件最低的宽度值。
  2. 控件高度确定的情况下,button的宽高也根据控件宽高做相应比例的缩放,避免图片超出控件范围。
  3. 最后要计算图片宽度,保证view.width = button.width*2+text.width。

最后一步,在Activity中调用。

public class MainActivity extends AppCompatActivity {
    private SlideSwitchView mSlideSwitchView;
    private boolean mIsPowerOn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSlideSwitchView = (SlideSwitchView) findViewById(R.id.slide_switch_view);
        mSlideSwitchView.setOnSwitchStateUpdateListener(new SlideSwitchView.OnSwitchStateUpdateListener() {
            @Override
            public void onStateUpdate() {
                mIsPowerOn = !mIsPowerOn;
                String content = mIsPowerOn ? "Slide to power off" : "Slide to power on";
                mSlideSwitchView.setText(content);
            }
        });
    }
}

附上完整demo地址:https://github.com/Horrarndoo/SwipeButton

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 6、View的绘制 (1)当测量好一个View之后,我们就可以简单的重写 onDraw()方法,并在 Canvas...
    b5e7a6386c84阅读 1,858评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,042评论 25 707
  • 星星不常在,人亦许久未谋面,黑夜之中陪伴我的只有星星和记忆中的你。 艳阳下,人行道旁,一边与朋友笑谈其他,一边在心...
    馒头面具阅读 231评论 0 0
  • 前情提要:恋你十年,未曾改变(四十八) 我把冰箱里剩下的食材全都翻了出来,做了几个坤喜欢吃的小菜。解下围裙的那一...
    lemoney阅读 584评论 3 4
  • 有人说你生不逢时, 一开花就遭遇严寒天气; 有人说你孤芳自赏, 独自开放,与百花脱离; 有人说你没有朋友, 蜂儿蝶...
    竹林洒阳光阅读 233评论 2 2