自定义控件-仿iphone之ToggleButton&VoiceSeekBar

由于项目中需要使用开关切换按钮,和声音滑动控件,但是原生Android5.0版本以下的控件实在是太挫了。虽然网上已经有很多关于这两个控件的blog,但是我实在是找不到像iPhone这样简洁样式的,不过咱们程序员总不能这点问题就被难道撒···所以我决定仿照iphone的样式自己写这两个控件,。

效果图如下:
这里写图片描述
这里写图片描述

一、ToggleButton

先直接上代码,后面会一步步分析

package com.zuck.definitionview.views;

import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.OvershootInterpolator;

/**
 * 仿iPhone ToggleButton
 * 
 * 2015-7-13
 * 
 * @author zuck
 *
 */
public class ToggleButton extends View{
    private float radius;
    // 开启颜色
    private int onColor;
    // 关闭颜色
    private int offColor;
    // 灰色带颜色
    private int offBorderColor;
    // 手柄颜色
    private int spotColor;
    // 边框颜色
    private int borderColor;
    // 画笔
    private Paint paint ;
    // 开关状态
    private boolean toggleOn = false;
    // 边框大小 默认为2px
    private int borderWidth = 2;
    // 垂直中心
    private float centerY;
    // 按钮的开始和结束位置
    private float startX, endX;
    // 手柄X位置的最小和最大值
    private float spotMinX, spotMaxX;
    // 手柄大小
    private int spotSize ;
    ///  手柄X位置
    private float spotX;
    // 关闭时内部灰色带高度
    private float offLineWidth;

    private RectF rect = new RectF();

    //开关切换监听器
    private OnToggleChanged listener;

    //属性动画
    private ValueAnimator animation;

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

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

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


    public void init() {
        initPaint();
        initColor();
        initAnimation();
        this.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View arg0) {
                toggle();
            }
        });
    }

    private void initPaint() {
        //初始化画笔(抗抖动)
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //绘制风格为填充
        paint.setStyle(Style.FILL);
        //笔触风格为圆角
        paint.setStrokeCap(Cap.ROUND);
    }

    private void initColor() {
        onColor = Color.parseColor("#4ebb7f");
        offColor = Color.parseColor("#dadbda");
        offBorderColor = Color.parseColor("#ffffff");
        spotColor = Color.parseColor("#ffffff");
        //因为开始为关闭状态,所以这里边框背景色初始化为关闭状态颜色
        borderColor = offColor;
    }

    @SuppressLint("NewApi")
    private void initAnimation() {
        animation = new ValueAnimator();
        animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                calculateToggleEffect(Double.parseDouble(animation.getAnimatedValue().toString()));
            }
        });
        //OvershootInterpolator : 结束时会超过给定数值,但是最后一定返回给定值
        animation.setInterpolator(new OvershootInterpolator(1.5f)); 
        animation.setDuration(500);
    }

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

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        Resources r = Resources.getSystem();
        if(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){
            widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 55, r.getDisplayMetrics());
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
        }

        if(heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST){
            heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        final int width = getWidth();
        final int height = getHeight();
        //由宽高计算圆角的半径
        radius = Math.min(width, height) * 0.5f;
        centerY = radius;
        startX = radius;
        endX = width - radius;
        spotMinX = startX + borderWidth;
        spotMaxX = endX - borderWidth;
        spotSize = height - 4 * borderWidth;
        spotX = toggleOn ? spotMaxX : spotMinX;
        offLineWidth = 0;
    }

    private int clamp(int value, int low, int high) {
        return Math.min(Math.max(value, low), high);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //1.绘制最外层边框背景-圆角矩形
        //绘制圆角矩形背景大小为测量的宽高
        rect.set(0, 0, getWidth(), getHeight());
        paint.setColor(borderColor);
        canvas.drawRoundRect(rect, radius, radius, paint);

        if(offLineWidth > 0){
            //1.1绘制整个开关区域中除手柄外的白色区域带
            final float cy = offLineWidth * 0.5f;
            rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy);
            paint.setColor(offBorderColor);
            canvas.drawRoundRect(rect, cy, cy, paint);
        }

        //2.绘制圆形手柄边框区域
        rect.set(spotX - 1 - radius, centerY - radius, spotX + 1.1f + radius, centerY + radius);
        paint.setColor(borderColor);
        canvas.drawRoundRect(rect, radius, radius, paint);

        //3.绘制圆形手柄区域
        //圆形手柄的半径大小(不包含边框)
        final float spotR = spotSize * 0.5f;
        rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR);
        paint.setColor(spotColor);
        canvas.drawRoundRect(rect, spotR, spotR, paint);
    }

    private double mapValueFromRangeToRange(double value, double fromLow,
            double fromHigh, double toLow, double toHigh) {
        double fromRangeSize = fromHigh - fromLow;
        double toRangeSize = toHigh - toLow;
        double valueScale = (value - fromLow) / fromRangeSize;
        return toLow + (valueScale * toRangeSize);
    }

    /**
     * @param value
     */
    private void calculateToggleEffect(final double value) {
        final float mapToggleX = (float) mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX);
        spotX = mapToggleX;

        float mapOffLineWidth = (float) mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize);

        offLineWidth = mapOffLineWidth;

        //开启时候的背景色
        final int fr = Color.red(onColor);
        final int fg = Color.green(onColor);
        final int fb = Color.blue(onColor);

        //关闭后的背景色
        final int tr = Color.red(offColor);
        final int tg = Color.green(offColor);
        final int tb = Color.blue(offColor);

        //border颜色渐变
        int sr = (int) mapValueFromRangeToRange(1 - value, 0, 1, fr, tr);
        int sg = (int) mapValueFromRangeToRange(1 - value, 0, 1, fg, tg);
        int sb = (int) mapValueFromRangeToRange(1 - value, 0, 1, fb, tb);

        sr = clamp(sr, 0, 255);
        sg = clamp(sg, 0, 255);
        sb = clamp(sb, 0, 255);

        borderColor = Color.rgb(sr, sg, sb);
        //重绘
        if (Looper.myLooper() == Looper.getMainLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

    @SuppressLint("NewApi")
    private void takeToggleAction(boolean isOn){
        if(isOn) {
            animation.setFloatValues(0.f, 1.f);
        } else {
            animation.setFloatValues(1.f, 0.f);
        }
        animation.start();
    }

    /**
     * 切换开关
     */
    public void toggle() {
        toggleOn = !toggleOn;
        takeToggleAction(toggleOn);
        if(listener != null){//触发toggle事件
            listener.onToggle(toggleOn);
        }
    }

    /**
     * 切换为打开状态
     */
    public void toggleOn() {
        toggleOn = true;
        takeToggleAction(toggleOn);
        if(listener != null){//触发toggle事件
            listener.onToggle(toggleOn);
        }
    }

    /**
     * 切换为关闭状态
     */
    public void toggleOff() {
        toggleOn = false;
        takeToggleAction(toggleOn);
        if(listener != null){//触发toggle事件
            listener.onToggle(toggleOn);
        }
    }

    /**
     * 设置显示成打开样式,不会触发toggle事件
     */
    public void setToggleOn(){
        toggleOn = true;
        calculateToggleEffect(1.0f);
    }

    /**
     * 设置显示成关闭样式,不会触发toggle事件
     */
    public void setToggleOff() {
        toggleOn = false;
        calculateToggleEffect(0.0f);
    }

    /**
     * 状态切换监听器
     */
    public interface OnToggleChanged{
        public void onToggle(boolean on);
    }

    public void setOnToggleChanged(OnToggleChanged onToggleChanged) {
        listener = onToggleChanged;
    }

}

这个控件整体看来,还是比较简单的,开始我们仍然是初始化一些需要用到的画笔、颜色、以及属性动画。这里画笔和颜色就不说了,跟切白菜一样。
简单说说动画:为什么要用到属性动画,大家可以仔细看看上面的效果图,当点击控件的时候手柄会从最左端移动到最右端,完成一个打开的效果。或者从最右端移动到最左端,完成一个关闭的效果。并且在手柄位移到最左端或者最右端的时候还会有一个回弹的效果。而这些全部都是依据属性动画

    private void initAnimation() {
        animation = new ValueAnimator();
        animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                calculateToggleEffect(Double.parseDouble(animation.getAnimatedValue().toString()));
            }
        });
        //OvershootInterpolator : 结束时会超过给定数值,但是最后一定返回给定值
        animation.setInterpolator(new OvershootInterpolator(1.5f)); 
        animation.setDuration(500);
    }

这里属性动画大家应该都很熟悉了,如果还有不懂他的基本用法的,可以去这个链接去学习下关于属性动画的使用:Android属性动画完全解析(上),初识属性动画的基本用法。现在我们只看OvershootInterpolator这个API。

这里写图片描述

从上图可以看到OvershootInterpolator继承BaseInterpolator。那么Interpolator是什么?引用郭霖blog中的一句话:“Interpolator这个东西很难进行翻译,直译过来的话是补间器的意思,它的主要作用是可以控制动画的变化速率”。那么按我的理解就是控制view动画效果的节奏。
从这个继承关系图我们可以看到,Android为我们提供了很多Interpolator,有兴趣的童鞋可以挨个试一试。看看他们的效果。这里带大家看看OvershootInterpolator:

/**
* An interpolator where the change flings forward and overshoots the last value
* then comes back.
*/

这是API中关于OvershootInterpolator的介绍。我的英文稀烂,大致意思是:值在变化时,快结束的时候值会超过给定的最大值,然后再返回给定的最大值。也就是说,当我们有如下设置的时候

animation.setFloatValues(0.f, 1.f);

onAnimationUpdate()回调方法最后会按 0.9f->1.0f->1.1f->1.2f->….1.0f。这样的方式返回数值。通过这样的数值去控制手柄的运行轨迹从而达到一个回弹的效果。
还有一点:在我创建OvershootInterpolator的时候传入一个tension参数,tension表示张力,默认为2.0f。tension越大,那么最后超过给定最大边界值后继续增大数值也就越大,反之越小。


ok,以上就是关于初始化操作。下面也就是按流程一步一步看了:onMeasure() -> onLayout() -> onDraw();
这些都是老生长谈了:

onMeasure

在onMeasure方法中去测量控件的大小这块逻辑很简单。这里只处理了AT_MOST和UNSPECIFIED两种模式,当我们把宽度或者高度设置为wrap_content或者设置了weight,那么对应我们让ToggleButton的长宽也为默认值。其它方式,测量方法中不做干涉。用户设置的宽高是多少,那么控件的宽高就是多少。

onLayout

在onLayout方法中去计算了该控件在绘制的时候需要用到的几个参数,例如包含边框的手柄半径、手柄的开始位置、结束位置、手柄大小等等…看看也没啥好说的,都是一系列计算。

onDraw

最后看onDraw方法,控件的绘制主要分为四步,上述代码中有明确的注释:

1.绘制控件最外层边框,这里注意这个边框是圆矩形,他们的rx 和ry的值也就是在onLayout中计算的radius值
2. 绘制圆角矩形内除去手柄区域以外的区域,该区域的大小是由offLineWidth去控制的,而关于offLineWidth是怎么算出来的,等下会跟大家讲明白的
3. 绘制手柄的边框区域,该区域的位置是由手柄的位置spotX去控制的。而spotX的计算,等下和offLineWidth的计算一并跟大家说明白(ps:为什么要一起,因为计算他们两个的值都是用的同一种算法)
4. 绘制手柄区域和绘制手柄边框区域几乎是一致的,唯一不同的是绘制手柄区域的半径大小比绘制手柄边框区域的大小要小2个单位值,因为他们绘制的位置是一样的,而大小不一样,这样也就让整个手柄有了边框的效果同时看起来更加有扁平感了。同时该手柄区域的位置也是由spotX去控制的

到此从init() 到 onMeasure() -> onLayout() -> onDraw()这些方法逻辑思路在大家心中应该是比较清晰明了的。


calculateToggleEffect -> mapValueFromRangeToRange

下面就来说说该控件中最重要的算法问题了,其实这个算法也是相当的简单
当我们点击控件,内部是怎样驱动手柄左右位移的呢,圆矩形内颜色是怎样转变的呢?这一切要归功于calculateToggleEffect()这个方法,上面讲述为什么要使用属性动画的时候。我曾贴过一小段代码,在onAnimationUpdate()回调方法中我们调用了calculateToggleEffect()方法,根据属性动画回传给我们的值去计算控制位移、颜色变化等效果的三个参数值

    private void calculateToggleEffect(final double value) {
        final float mapToggleX = (float) mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX);
        spotX = mapToggleX;

        float mapOffLineWidth = (float) mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize);

        offLineWidth = mapOffLineWidth;

        //开启时候的背景色
        final int fr = Color.red(onColor);
        final int fg = Color.green(onColor);
        final int fb = Color.blue(onColor);

        //关闭后的背景色
        final int tr = Color.red(offColor);
        final int tg = Color.green(offColor);
        final int tb = Color.blue(offColor);

        //border颜色渐变
        int sr = (int) mapValueFromRangeToRange(1 - value, 0, 1, fr, tr);
        int sg = (int) mapValueFromRangeToRange(1 - value, 0, 1, fg, tg);
        int sb = (int) mapValueFromRangeToRange(1 - value, 0, 1, fb, tb);

        sr = clamp(sr, 0, 255);
        sg = clamp(sg, 0, 255);
        sb = clamp(sb, 0, 255);

        borderColor = Color.rgb(sr, sg, sb);
        //重绘
        if (Looper.myLooper() == Looper.getMainLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

这段方法中,大家可以看到我们分别去计算了spotX、offLineWidth、borderColor这三个实例变量的值
而计算这三个实例变量的值主要是由mapValueFromRangeToRange()方法来完成,那么mapValueFromRangeToRange()方法是个什么鬼?

    private double mapValueFromRangeToRange(double value, double fromLow,
            double fromHigh, double toLow, double toHigh) {
        double fromRangeSize = fromHigh - fromLow;
        double toRangeSize = toHigh - toLow;
        double valueScale = (value - fromLow) / fromRangeSize;
        return toLow + (valueScale * toRangeSize);
    }

一眼望去,各种加减乘除,不喜欢算法的童鞋头都大了,其实这些运算是相当的简单,主要的思想就是:以一个范围内的值,去映射另外一个给定的范围。通俗讲就是:给你一个范围[0,1]rangeA,在给你一个用于映射的范围[50,100]rangeB,那么现在给你一个在rangeA中的值 0.3,你去在rangeB中计算出对应比例的值。这个够简单吧。

result = 50 + ((0.3-0) / (1- 0))*(100 - 50) ;

上面的代码中只是将每一步拆分开来,大家可以合在一起看看是不是与上面的式子相等,ok,这样我们就计算出spotX和offLineWidth的值,所以spotX的值也就是会在spotMinX 到 spotMaxX的范围中递增或者递减(因为属性动画回传过来的值也是递增或者递减)。同理offLineWidth和背景色的计算也是一样。我们搞懂mapValueFromRangeToRange后calculateToggleEffect()方法中的逻辑就非常非常简单了,我就不累赘了~那么到此为止ToggleButton的实现已经带大家全部分析完毕。

二、VoiceSeekBar

直接上代码
package com.zuck.definitionview.views;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import com.zuck.definitionview.R;

/**
 * 仿iPhone VoiceSeekBar
 * 
 * 2015-7-16
 * 
 * @author zuck
 *
 */
@SuppressLint("NewApi")
public class VoiceSeekBar extends View {

    /**
     * 手柄半径比率
     */
    private static final float SPOTRADIUSRATE = 13.f/40.f;

    /**
     * 横条默认高度
     */
    private static final int BAR_HEIGHT = 2;

    /**
     * 默认间隙宽度
     */
    private static final int SPACE = 30;

    /**
     * 最高级别[级别从0开始,并且级别的段数则为:MAXLEVEL + 1]
     */
    private static final int MAXLEVEL = 15;

    /**
     * 条形棒宽度
     */
    private int barWidth;

    /**
     * 手柄的区域
     */
    private Region spotRegion;

    /**
     * 绘制条形棒的矩形
     */
    private Rect rect;

    // 左边条形棒的颜色,右边条形棒的颜色,手柄的颜色
    private int leftBarColor, rightBarColor, spotColor;

    // 手指按下的时候x坐标
    private float pressX;

    // 条形棒矩形的左上角定点
    private int barLeft ,barTop;

    // 左侧符号的X坐标,Y坐标
    private int signX,signY;

    // 手柄半径
    private float radius;

    private Paint paint, spotPaint;

    private Bitmap signLeft, signRigh;

    private int currentLevel;

    private OnSeekBarChangeListener onSeekBarChangeListener;

    public interface OnSeekBarChangeListener {

        void onProgressChanged(VoiceSeekBar seekBar, int progress);

        void onStartTrackingTouch(VoiceSeekBar seekBar);

        void onStopTrackingTouch(VoiceSeekBar seekBar);

    }

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

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

    public VoiceSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);  
        init();

    }

    private void init() {
        //初始化当前级别
        currentLevel = 0;

        // 初始化加载左边与右边的两张符号图标
        signLeft = BitmapFactory.decodeResource(getResources(), R.drawable.minus);
        signRigh = BitmapFactory.decodeResource(getResources(), R.drawable.plus);

        // 手柄左边/右边的条形棒颜色
        leftBarColor = Color.parseColor("#4ebb7f");
        rightBarColor = Color.parseColor("#dadbda");
        // 手柄颜色
        spotColor = rightBarColor;

        paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        paint.setStyle(Paint.Style.FILL);
        paint.setStrokeCap(Paint.Cap.ROUND);

        spotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        spotPaint.setColor(spotColor);
        spotPaint.setStyle(Paint.Style.FILL);
        // 为手柄添加阴影效果
        spotPaint.setShadowLayer(10, 0, 1, Color.DKGRAY);

        // 手柄所在的区域
        spotRegion = new Region();
        rect = new Rect();
    }

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

        Resources r = Resources.getSystem();
        //测量高度,高度默认为35
        int heigthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 35, r.getDisplayMetrics());
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(heigthSize, MeasureSpec.EXACTLY);

        //测量宽度
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int minWidth = (int) (signLeft.getWidth() + signRigh.getWidth() + SPACE * 4 +  heigthSize * SPOTRADIUSRATE);
        widthSize = Math.max(minWidth, widthSize);
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        final int width = getWidth();
        final int height = getHeight();

        //手柄的半径
        radius = height * SPOTRADIUSRATE;

        //条形棒宽度
        barWidth = (int) (width - (signLeft.getWidth() + signRigh.getWidth() + SPACE * 4 + radius));

        //计算条形棒开始绘制的左上角顶点位置
        barLeft = (int) Math.ceil((width - barWidth) / 2.f);
        barTop = (int) Math.ceil((height - BAR_HEIGHT) / 2.f);

        //计算左边符号绘制的位置
        signX = (int) (barLeft - signLeft.getWidth() - SPACE);
        signY = (height - signLeft.getHeight()) / 2;

        calcCurrentPressX();
    }

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

        //0.绘制左右两边的符号
        canvas.drawBitmap(signLeft, signX, signY, paint);
        canvas.drawBitmap(signRigh, barWidth + barLeft + SPACE, signY, paint);

        //1.绘制圆形手柄
        //计算手柄圆点坐标x值
        float cx = 0;
        float cy = barTop + BAR_HEIGHT / 2; 
        if(pressX <= radius + barLeft){//手指触摸的范围在:从最左侧到横条开始的一个手柄半径距离范围
            cx = barLeft + radius;
        }else if (pressX > barLeft + radius && pressX <= barWidth + barLeft - radius){//手指触摸点在 横条最左侧开始加上一个手柄半径距离到横条最右侧减去一个手柄半径距离范围之内
            cx = pressX;
        } else {//手指触摸点超过横条最右侧减去一个手柄半径距离的范围
            cx = barLeft + barWidth - radius;
        }
        canvas.drawCircle(cx, cy, radius, spotPaint);

        //2.绘制左边的条形棒
        int leftBarRight = (int) (cx - radius);
        int barBottom = barTop + BAR_HEIGHT;
        rect.set(barLeft, barTop, leftBarRight, barBottom);
        paint.setColor(leftBarColor);
        canvas.drawRect(rect, paint);

        //3.绘制右边的条形棒
        int rightBarLeft = (int) (leftBarRight + radius * 2);
        rect.set(rightBarLeft, barTop, barWidth + barLeft, barBottom);
        paint.setColor(rightBarColor);
        canvas.drawRect(rect, paint);

        //4.记录当前手柄所在的区域(区域的范围扩大一个半径范围)
        int regionLeft = (int)(cx - 2 * radius);
        spotRegion.set(regionLeft, (int) -radius, (int)(radius * 4) + regionLeft, (int)(radius * 4));

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.dispatchTouchEvent(ev);
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            pressX = event.getX();
            float pressY = event.getY();
            if(!spotRegion.contains((int)pressX, (int)pressY)) {
                return false;
            }
            if(onSeekBarChangeListener != null) 
            onSeekBarChangeListener.onStartTrackingTouch(this);
            break;
        case MotionEvent.ACTION_MOVE:
            pressX = event.getX();
            int level = CalcCurrentLevel();
            if(onSeekBarChangeListener != null && currentLevel != level) {
                currentLevel = level;
                onSeekBarChangeListener.onProgressChanged(this, level);
            }
            invalidate();
            break;
        case MotionEvent.ACTION_UP:
            if(onSeekBarChangeListener != null) 
            onSeekBarChangeListener.onStopTrackingTouch(this);
            break;
        }
        return true;
    }

    /**
     * 设置当前级别
     * @param currentLevel
     */
    public void setCurrentLevel(int currentLevel) {
        if(currentLevel < 0){
            this.currentLevel = 0;
        } else {
            this.currentLevel = currentLevel >= MAXLEVEL ? MAXLEVEL : currentLevel;
        }
    }

    /**
     * 获取当前级别
     * @return
     */
    public int getCurrentLevel() {
        return currentLevel;
    }

    /**
     * 根据当前级别计算当前pressX值(pressX用来计算手柄所在位置)
     */
    private void calcCurrentPressX() {
        pressX = barWidth * ((float)currentLevel / MAXLEVEL) + barLeft;
    }

    /**
     *  计算当前的级别
     * @return
     */
    private int CalcCurrentLevel() {
        int level ;
        if(pressX <= radius + barLeft){//手指触摸的范围在:从最左侧到横条开始的一个手柄半径距离范围
            level = 0;
        }else if (pressX > barLeft + radius && pressX <= barWidth + barLeft - radius){//手指触摸点在 横条最左侧开始加上一个手柄半径距离到横条最右侧减去一个手柄半径距离范围之内
            level = (int) (((pressX - barLeft) * MAXLEVEL) / barWidth);
        } else {//手指触摸点超过横条最右侧减去一个手柄半径距离的范围
            level = MAXLEVEL;
        }
        return level;
    }

    public void setOnSeekBarChangeListener(OnSeekBarChangeListener onSeekBarChangeListener) {
        this.onSeekBarChangeListener = onSeekBarChangeListener;
    }

}

该控件实现逻辑思路与ToggleButton几乎一致。我也就不打算讲述了。记录在这里,供以后方便查看。以上代码,可以直接copy粘贴到项目中使用。改下包名即可。
代码传送门

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值