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

Posted liguangsunls

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自己定义控件-仿iphone之ToggleButton&VoiceSeekBar相关的知识,希望对你有一定的参考价值。

由于项目中须要使用开关切换button,和声音滑动控件,可是原生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;
    // button的開始和结束位置
    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方法中去測量控件的大小这块逻辑非常easy。

这里仅仅处理了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()方法中的逻辑就非常非常easy了。我就不累赘了~那么到此为止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粘贴到项目中使用。

改下包名就可以。


代码传送门



















以上是关于自己定义控件-仿iphone之ToggleButton&amp;VoiceSeekBar的主要内容,如果未能解决你的问题,请参考以下文章

[原创]自定义控件之AndroidSegmentControlView,仿IOS平台UISegmentControlView,继承自View

WPF自定义控件之仿Win8滚动条--ScrollViewer

Android进阶之自定义View实战仿iOS UISwitch控件实现

Android进阶之自定义View实战仿iOS UISwitch控件实现

利用swiper仿iphone时间设置滚轮控件

Android仿华为天气绘制刻度盘