美文网首页Android开发Android开发Android知识
自定义控件之---SlideView开关控件

自定义控件之---SlideView开关控件

作者: 旧时光KK | 来源:发表于2016-03-30 22:24 被阅读1205次

    1.演示:

    别的不谈,先看下效果:

    slideview.gif

    2.分析:

    在做自定义控件之前,最重要的就是分析你要实现的控件的功能以及效果。将他们拆分成各个模块,然后一一实现。这里我们分析一下这个SlideView。

    • (1) 由一个圆角矩形背景以及一个圆形滑块组成。
    • (2) 圆形滑块可以左右滑动,在滑动时,背景有一个渐变的效果。即圆形滑块使用了平移动画,背景使用了透明度动画。
    • (3) 圆形滑块没有紧贴背景的矩形,有一定的间隙。

    3.实现:

    剖析完控件之后,我们就可以按步骤一步步来实现了。

    控件测量与绘制

    首先建立一个SlideView,继承我们的View小哥~

    public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
      }
    
      public SlideView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
      }
    
      public SlideView(Context context) {
        this(context, null);
      }
    

    在init方法中初始化我们的画笔

      // 绘制背景
      private Paint bgPaint;
      // 绘制圆形滑块
      private Paint circlePaint;
      //关闭时默认背景颜色
      public static final int CLOSE_PAINT_COLOR = 0x667f7f7f;
      //打开时默认背景颜色
      public static final int OPEN_PAINT_COLOR = 0xFF3378D4;
      //打开时背景颜色
      private int openColor = OPEN_PAINT_COLOR;
      //关闭时背景颜色
      private int closeColor = CLOSE_PAINT_COLOR;
    
      private void init() {
          bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
          bgPaint.setStrokeCap(Cap.ROUND);
          bgPaint.setColor(CLOSE_PAINT_COLOR);
    
          circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
          circlePaint.setStrokeCap(Cap.ROUND);
          circlePaint.setStrokeJoin(Join.ROUND);
          circlePaint.setColor(Color.WHITE);
        }
    

    初始化完画笔之后,就可以绘制我们的背景了。要绘制就必须重写onDraw方法。

      @Override
      protected void onDraw(Canvas canvas) {
          //绘制背景
          drawBg(canvas);
          //绘制圆形滑块
          drawPoint(canvas);
      }
    
      private void drawBg(Canvas canvas) {
         
      }
    
      private void drawPoint(Canvas canvas) {
         
    }
    

    因为背景是圆角矩形,所以我们使用了

      //Rectf是一个矩形对象,我们的背景就绘制在这个矩形中
      //x,y代表在各自方向上圆角的半径(直接理解为矩形四个角的弧度有多大)
      canvas.drawRoundRect(Rectf rectf,float x,float y,Paint paint);
    

    圆形小空间我们使用了

      //这里用drawCircle也可以,看个人喜好。
      //此方法是在一个矩形中绘制内接圆,当这个矩形为正方形时,绘制的是园,否则是椭圆。
      canvas.drawOval(Rectf rectf,Paint paint)
    

    所以在drawBg() 和 drawPoint()方法中,这样实现:

      // 圆点半径
      private int mRadius;
      // 圆形滑块距离控件左端的偏移量(当我们改变此偏移量的时候,滑块便可以左右移动,初始为0在最左端)
      private int leftOffset = 0;
      // 空隙距离2dp
      private int intervalWidth = dip2px(2);
      // 图形背景绘制区域
      RectF bgRectf = new RectF();
      // 圆点按钮绘制区域
      RectF pointRectF = new RectF();
    
      private void drawBg(Canvas canvas) {
        canvas.drawRoundRect(bgRectf, dip2px(15), dip2px(15), bgPaint);
      }
    
      private void drawPoint(Canvas canvas) {
          pointRectF.set(intervalWidth + leftOffset, intervalWidth, intervalWidth + mRadius * 2 + leftOffset,
                mRadius * 2 + intervalWidth);
          canvas.drawOval(pointRectF, circlePaint);
      }
    
      //此方法是将dp值转化为px值,方便适配
      private int dip2px(float dpValue) {
        final float scale =   getContext().getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
      }
    

    很多同学一看到这立马炸毛,你这些变量都在哪初始化的值?别着急,这里我选择在onMeasure方法中初始化。

      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           setMeasuredDimension(getMeasuredSize(widthMeasureSpec,60),
                getMeasuredSize(heightMeasureSpec,25));
           //此方法便是初始化各种默认属性值
         initDefaultSize();
        }
    

    getMeasuredSize()方法是为了在布局文件中设置了wrap_content属性后,可以正常显示,属于一段模板代码,自定义View时经常要用到:

      private int getMeasuredSize(int measureSpecValue,int defaultValue) {
          int specMode = MeasureSpec.getMode(measureSpecValue);
          int specSize = MeasureSpec.getSize(measureSpecValue);
          int defaultSize = dip2px(defaultValue);
          if (specMode == MeasureSpec.EXACTLY) {
            defaultSize = specSize;
          } else if (specMode == MeasureSpec.AT_MOST) {
            defaultSize = Math.min(specSize, defaultSize);
          } 
          return defaultSize;
      }
    
      private void initDefaultSize() {
        // TODO Auto-generated method stub
          //半径为 (测量高度 /2) - 间隙 
        mRadius = getMeasuredHeight() / 2 - intervalWidth;
          //背景的矩形 四个值 左上右下 左-0 上-0 右-控件的测量宽 下-控件的测量高
        bgRectf.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
          //滑块可滑动的最大宽度 = 背景的宽度 -2* 间隙 - 圆形滑块的直径(这个可以看图理解)
        mMaxWidth = bgRectf.right -2* intervalWidth - mRadius * 2;
      }
    
    模型.png

    这里理解上图需要明确知道几个变量的意思:

    • intervalWidth ->圆形滑块与背景之间的间隙,默认2dp
    • mRadius->圆形滑块的半径,如图可知等于控件高度一般减去间隙
    • mMaxWidth ->圆形滑块距左端最大距离。这个稍微不同一些,如图所示,是从控件左端开始(也就是0)算起,这个mMaxWidth 最终要赋值给leftOffset,所以圆形滑块据相对控件左端最大的距离为leftOffset+intervalWidth,如drawPoint()方法中所写的那样。
    • mMinWidth ->圆形滑块距左端最小距离,为0,因为其也是赋值给leftOffset。
    • leftOffset ->真正控制圆形滑块位置的变量,这里我们都是从控件左端(0)开始算的,因为最终leftOffset要加上intervalWidth。

    如果懂了以上变量的意思,那我们就可以正式写滑动逻辑了,肯定是重写
    onTouchEvent()事件:

      // 手指按下时,起始X(这个x是距离屏幕左端的水平距离)
      private float preX;
      // 圆点在手指按下时,起始距离控件左端的偏移量
      private float preLeftOffSet;
    
      @Override
      public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //手指按下,没有滑动...
            preX = event.getRawX();
            //把间隙减掉,保证从最左端算起(0)
            preLeftOffSet = pointRectF.left - intervalWidth;
            break;
        case MotionEvent.ACTION_MOVE:
            //手指正在滑动...不断记录当前x轴坐标
            float culX = event.getRawX();
            //手指滑动距离(当前手指所在x轴坐标减去按下时x轴坐标)
            float dx = culX - preX;
            //当手指滑动5个像素时,我们才认为是真正滑动了
            if (Math.abs(dx) > 5) {
                //当前    
                leftOffset = (int) (dx + preLeftOffSet);
                //leftOffset = fixLeftOffset(leftOffset);
                invalidate();
            }
            break;
        case MotionEvent.ACTION_UP:
            //松开手指...
            break;
        }
        //事件被这个控件消费啦 不回传给父控件
        return true;
      }
    

    加上以上代码,我们的小滑块就可以左右拖动了,但是有些无法无天,他可以被拖动到控件之外,这显示不是我们想要的。所以必须要修正一下leftOffset。所以把注释的代码打开:
    fixLeftOffset(leftOffset) ->修正leftOffset,返回一个在mMinWidth到mMaxWidth之间的值

      private int fixLeftOffset(int leftOffset) {
        leftOffset = (int) (leftOffset > mMaxWidth ? mMaxWidth : leftOffset);
        leftOffset = (int) (leftOffset < mMinWidth ? mMinWidth : leftOffset);
        return leftOffset;
      }
    

    加上上述代码限制,我们的小滑块就踏不出我们的手掌心了。但现在问题又出现了,就是当松手时,我们希望滑块自动滑动到左端或是右端,而不是停在中间,这个该怎么事件呢,其实很简单,用ValueAnimator值动画就可以快速实现。

    控件的动画(滑块平移+背景渐变)

    1. 滑块平移动画
      首先,我们要知道,滑块是在手指松开时才产生动画,这里分四种情况。
    • 第一种:当手指松开时,滑块距控件左端的距离大于控件的一半,并且为close状态。这时,让滑块滑动到右端,状态置为open。


      one.gif
    • 第二种:当手指松开时,滑块距控件左端的距离小于控件的一半,并且为close状态。这时,滑块滑回左端,状态依然为close。


      two.gif
    • 第三种:当手指松开时,滑块距控件左端的距离小于控件的一半,并且为open状态。这时,让滑块滑动到左端,状态置为close。


      three.gif
    • 第四种:当手指松开时,滑块距控件左端的距离大于控件的一半,并且为open状态。这时,滑块滑回右端,状态依然为open。


      four.gif

    理解了上面四种情况,我们现在就可以编码实现啦~!

      // 是否打开
      private boolean checked = false;
    

    case MotionEvent.ACTION_UP添加以下代码:

      case MotionEvent.ACTION_UP:
           //拿到滑块的中心位置x轴坐标
           int pointCenterX = (int) pointRectF.centerX();
           //用滑块中心x轴坐标和背景(即控件)x轴坐标的一半作比较
           if (pointCenterX >= bgRectf.right / 2 && !checked) {
                changeState(checked);
           } else if (pointCenterX < bgRectf.right / 2 && checked) {
                changeState(checked);
           }
           //执行平移动画
           releaseShowAnim();
      break;
    

    这时可以顺便加上状态监听接口,方便外部回调,得知当前控件状态:

      public interface OnCheckedChangedListener {
          void onCheckedChange(boolean isCheck);
      }
    
      private OnCheckedChangedListener onCheckedChangedListener;
    
      public void setOnCheckedChangedListener(OnCheckedChangedListener onCheckedChangedListener) {
          this.onCheckedChangedListener = onCheckedChangedListener;
      }
      //变更当前状态
      private void changeState(boolean checked) {
          this.checked = !checked;
          //状态监听接口
          if (onCheckedChangedListener != null)
              onCheckedChangedListener.onCheckedChange(this.checked);
      }
    

    释放显示动画的代码:

      private void releaseShowAnim() {
        //值动画不难理解,下面这段代码的意思其实就是给定一个值,到另一个值。
        //在400毫秒的时间内,每隔一定时间,给你返回一个当前动画执行的进度。     
        //动画执行的进度,是一个百分数(0~1),0没执行呢,1执行完了。期间还能返回执行了多少,是一个确定值。
        //例如 1 ~ 100 执行100秒,执行进度30%(0.3),返回的是30(匀速运动前提下)
        //pointRectF.left - intervalWidth滑块距控件左端的距离,注意要把间隙减掉
        //如果为check状态,则滑动到最右端,否则滑到最左端。
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(pointRectF.left - intervalWidth,
                checked ? mMaxWidth : mMinWidth);
        //该动画执行400毫秒
        valueAnimator.setDuration(400);
        //定义该运动为先加速再减速 (还有很多)
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        //开启动画
        valueAnimator.start();
        //增加动画执行监听 这里就可以每次给你返回执行进度和执行值
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //当前动画执行了多少 也就是我们的偏移量
                float offset = (Float) animation.getAnimatedValue();
                //当前动画执行进度,这个值是用来以后改变背景颜色的~
                float fraction = animation.getAnimatedFraction();
                //赋值给我们的leftOffset吧
                leftOffset = (int) offset;
                //重新绘制
                invalidate();
            }
    
        });
      }
    

    好啦 ,加上如上代码,部署到机器上,运行,是不是已经有平移动画了呢?

    下面我们把点击事件也加上,当点击控件两端,我们也希望滑块做相应的滑动。这就要先判断当前手指是滑动还是点击。所以引入isScroll变量,记录手指是点击还是滑动。还需要有一个变量记录手指当前点击的位置距控件左端的距离clickLeftOffset,看代码:

      // 是否是滑动
      private boolean isScroll = false;
      // 手指点击位置 距控件左端的偏移量
      private float clickLeftOffset = 0;
    

    case MotionEvent.ACTION_DOWN添加以下代码:

       //getX()和getRawX()前者是获取手指点击位置距控件左端x轴坐标,后者是距屏幕左端 
       clickLeftOffset = event.getX();
       //在手指按下时 把isScroll置为false
       isScroll = false;
    

    case MotionEvent.ACTION_MOVE添加以下代码:

       if (Math.abs(dx) > 5) {
            //...
            isScroll = true;
       }
    

    case MotionEvent.ACTION_UP添加以下代码:

            if (isScroll) {//滑动
                int centerX = (int) pointRectF.centerX();
                if (centerX >= bgRectf.right / 2 && !checked) {
                    changeState(checked);
                } else if (centerX < bgRectf.right / 2 && checked) {
                    changeState(checked);
                }
            } else {//点击
                if (clickLeftOffset >= bgRectf.right / 2 && !checked) {
                    changeState(checked);
                } else if (clickLeftOffset < bgRectf.right / 2 && checked) {
                    changeState(checked);
                }
            }
    

    重新部署一下~是不是点击事件也生效了呢?

    1. 背景颜色渐变

    颜色渐变我采用了ArgbEvaluator,用法我会在后面介绍

    颜色渐变也分为两种情况:一种是在手指拖动的时候,另一种是在手指松开的时候(拖动到一半松开或者直接是点击)

    我们先来实现第一种,那么肯定要定位到ACTION_MOVE

       if (Math.abs(dx) > 5) {
            //...
            //通过当前偏移量  / mMaxWidth , 计算出滑动的百分比(0~1)
            float percent = leftOffset * 1.0f / mMaxWidth;
            //将百分比,颜色变化的区间(close时的背景颜色-open时的背景颜色)
            changeBgColor(percent, CLOSE_PAINT_COLOR, OPEN_PAINT_COLOR);
       }
    

    看changeBgColor方法:

      //颜色插值器
      ArgbEvaluator argbEvaluator= new ArgbEvaluator();
      
      private void changeBgColor(float fraction, int startColor, int endColor) {               
            bgPaint.setColor((int)argbEvaluator.evaluate(fraction, startColor, endColor));
      }
    

    argbEvaluator.evaluate(fraction,startColor,endColor)方法接收三个参数,第一个是百分比,后两个参数是颜色区间。他会根据百分比计算出一个当前处于区间范围内的一个值,返回给你。我们把这个值赋给背景的画笔,再重绘界面,这样我们的背景就会有一个渐变的效果。

    再看松开手指的执行渐变,这自然定位到我们的releaseShowAnim()方法。在其中找到动画监听,在监听里,之前所写的当前动画执行的百分比就派上用场了,看代码:

      //获取松开手指时,背景颜色
      final int startColor = bgPaint.getColor();
      valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float offset = (Float) animation.getAnimatedValue();
                float fraction = animation.getAnimatedFraction();
                if (checked)
                    //open状态,从当前颜色渐变到open时的颜色
                    changeBgColor(fraction, startColor, openColor);
                else
                    //close状态,从当前颜色渐变到close时的颜色
                    changeBgColor(fraction, startColor, closeColor);
                leftOffset = (int) offset;
                // Log.i(TAG, "------>leftOffset = " + leftOffset);
                // alpha = (int) (0x66 * (float) leftOffset / (float)
                // mMaxWidth);
                invalidate();
            }
    
        });
    

    赶紧加上试一试,背景已经如期望的那样渐变了吧!到此为止我们的自定义开关就接近尾声了,还有一些其他的功能,例如代码控制开关,控件不可用,当应用异常退出时保存View状态,改变颜色等等,都是一些很简单的小功能,希望小伙伴们自行实现,加深理解。

    代码没托管到github,写这个的主要目的是学习并且巩固,毕竟这样的轮子已经有很多了,会用的同时也要会写一写。好累 ,吃个饭~ 下篇自定义控件见!

    相关文章

      网友评论

        本文标题:自定义控件之---SlideView开关控件

        本文链接:https://www.haomeiwen.com/subject/jwqelttx.html