自定義控件之---SlideView開關(guān)控件

1.演示:

別的不談,先看下效果:

slideview.gif

2.分析:

在做自定義控件之前,最重要的就是分析你要實現(xiàn)的控件的功能以及效果。將他們拆分成各個模塊,然后一一實現(xiàn)。這里我們分析一下這個SlideView。

  • (1) 由一個圓角矩形背景以及一個圓形滑塊組成。
  • (2) 圓形滑塊可以左右滑動,在滑動時,背景有一個漸變的效果。即圓形滑塊使用了平移動畫,背景使用了透明度動畫。
  • (3) 圓形滑塊沒有緊貼背景的矩形,有一定的間隙。

3.實現(xiàn):

剖析完控件之后,我們就可以按步驟一步步來實現(xiàn)了。

控件測量與繪制

首先建立一個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;
  //關(guān)閉時默認背景顏色
  public static final int CLOSE_PAINT_COLOR = 0x667f7f7f;
  //打開時默認背景顏色
  public static final int OPEN_PAINT_COLOR = 0xFF3378D4;
  //打開時背景顏色
  private int openColor = OPEN_PAINT_COLOR;
  //關(guān)閉時背景顏色
  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也可以,看個人喜好。
  //此方法是在一個矩形中繪制內(nèi)接圓,當(dāng)這個矩形為正方形時,繪制的是園,否則是橢圓。
  canvas.drawOval(Rectf rectf,Paint paint)

所以在drawBg() 和 drawPoint()方法中,這樣實現(xiàn):

  // 圓點半徑
  private int mRadius;
  // 圓形滑塊距離控件左端的偏移量(當(dāng)我們改變此偏移量的時候,滑塊便可以左右移動,初始為0在最左端)
  private int leftOffset = 0;
  // 空隙距離2dp
  private int intervalWidth = dip2px(2);
  // 圖形背景繪制區(qū)域
  RectF bgRectf = new RectF();
  // 圓點按鈕繪制區(qū)域
  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值轉(zhuǎn)化為px值,方便適配
  private int dip2px(float dpValue) {
    final float scale =   getContext().getResources().getDisplayMetrics().density;
    return (int) (dpValue * scale + 0.5f);
  }

很多同學(xué)一看到這立馬炸毛,你這些變量都在哪初始化的值?別著急,這里我選擇在onMeasure方法中初始化。

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

getMeasuredSize()方法是為了在布局文件中設(shè)置了wrap_content屬性后,可以正常顯示,屬于一段模板代碼,自定義View時經(jīng)常要用到:

  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,所以圓形滑塊據(jù)相對控件左端最大的距離為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:
        //手指正在滑動...不斷記錄當(dāng)前x軸坐標(biāo)
        float culX = event.getRawX();
        //手指滑動距離(當(dāng)前手指所在x軸坐標(biāo)減去按下時x軸坐標(biāo))
        float dx = culX - preX;
        //當(dāng)手指滑動5個像素時,我們才認為是真正滑動了
        if (Math.abs(dx) > 5) {
            //當(dāng)前    
            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;
  }

加上上述代碼限制,我們的小滑塊就踏不出我們的手掌心了。但現(xiàn)在問題又出現(xiàn)了,就是當(dāng)松手時,我們希望滑塊自動滑動到左端或是右端,而不是停在中間,這個該怎么事件呢,其實很簡單,用ValueAnimator值動畫就可以快速實現(xiàn)。

控件的動畫(滑塊平移+背景漸變)

  1. 滑塊平移動畫
    首先,我們要知道,滑塊是在手指松開時才產(chǎn)生動畫,這里分四種情況。
  • 第一種:當(dāng)手指松開時,滑塊距控件左端的距離大于控件的一半,并且為close狀態(tài)。這時,讓滑塊滑動到右端,狀態(tài)置為open。


    one.gif
  • 第二種:當(dāng)手指松開時,滑塊距控件左端的距離小于控件的一半,并且為close狀態(tài)。這時,滑塊滑回左端,狀態(tài)依然為close。


    two.gif
  • 第三種:當(dāng)手指松開時,滑塊距控件左端的距離小于控件的一半,并且為open狀態(tài)。這時,讓滑塊滑動到左端,狀態(tài)置為close。


    three.gif
  • 第四種:當(dāng)手指松開時,滑塊距控件左端的距離大于控件的一半,并且為open狀態(tài)。這時,滑塊滑回右端,狀態(tài)依然為open。


    four.gif

理解了上面四種情況,我們現(xiàn)在就可以編碼實現(xiàn)啦~!

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

case MotionEvent.ACTION_UP添加以下代碼:

  case MotionEvent.ACTION_UP:
       //拿到滑塊的中心位置x軸坐標(biāo)
       int pointCenterX = (int) pointRectF.centerX();
       //用滑塊中心x軸坐標(biāo)和背景(即控件)x軸坐標(biāo)的一半作比較
       if (pointCenterX >= bgRectf.right / 2 && !checked) {
            changeState(checked);
       } else if (pointCenterX < bgRectf.right / 2 && checked) {
            changeState(checked);
       }
       //執(zhí)行平移動畫
       releaseShowAnim();
  break;

這時可以順便加上狀態(tài)監(jiān)聽接口,方便外部回調(diào),得知當(dāng)前控件狀態(tài):

  public interface OnCheckedChangedListener {
      void onCheckedChange(boolean isCheck);
  }

  private OnCheckedChangedListener onCheckedChangedListener;

  public void setOnCheckedChangedListener(OnCheckedChangedListener onCheckedChangedListener) {
      this.onCheckedChangedListener = onCheckedChangedListener;
  }
  //變更當(dāng)前狀態(tài)
  private void changeState(boolean checked) {
      this.checked = !checked;
      //狀態(tài)監(jiān)聽接口
      if (onCheckedChangedListener != null)
          onCheckedChangedListener.onCheckedChange(this.checked);
  }

釋放顯示動畫的代碼:

  private void releaseShowAnim() {
    //值動畫不難理解,下面這段代碼的意思其實就是給定一個值,到另一個值。
    //在400毫秒的時間內(nèi),每隔一定時間,給你返回一個當(dāng)前動畫執(zhí)行的進度。     
    //動畫執(zhí)行的進度,是一個百分數(shù)(0~1),0沒執(zhí)行呢,1執(zhí)行完了。期間還能返回執(zhí)行了多少,是一個確定值。
    //例如 1 ~ 100 執(zhí)行100秒,執(zhí)行進度30%(0.3),返回的是30(勻速運動前提下)
    //pointRectF.left - intervalWidth滑塊距控件左端的距離,注意要把間隙減掉
    //如果為check狀態(tài),則滑動到最右端,否則滑到最左端。
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(pointRectF.left - intervalWidth,
            checked ? mMaxWidth : mMinWidth);
    //該動畫執(zhí)行400毫秒
    valueAnimator.setDuration(400);
    //定義該運動為先加速再減速 (還有很多)
    valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    //開啟動畫
    valueAnimator.start();
    //增加動畫執(zhí)行監(jiān)聽 這里就可以每次給你返回執(zhí)行進度和執(zhí)行值
    valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //當(dāng)前動畫執(zhí)行了多少 也就是我們的偏移量
            float offset = (Float) animation.getAnimatedValue();
            //當(dāng)前動畫執(zhí)行進度,這個值是用來以后改變背景顏色的~
            float fraction = animation.getAnimatedFraction();
            //賦值給我們的leftOffset吧
            leftOffset = (int) offset;
            //重新繪制
            invalidate();
        }

    });
  }

好啦 ,加上如上代碼,部署到機器上,運行,是不是已經(jīng)有平移動畫了呢?

下面我們把點擊事件也加上,當(dāng)點擊控件兩端,我們也希望滑塊做相應(yīng)的滑動。這就要先判斷當(dāng)前手指是滑動還是點擊。所以引入isScroll變量,記錄手指是點擊還是滑動。還需要有一個變量記錄手指當(dāng)前點擊的位置距控件左端的距離clickLeftOffset,看代碼:

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

case MotionEvent.ACTION_DOWN添加以下代碼:

   //getX()和getRawX()前者是獲取手指點擊位置距控件左端x軸坐標(biāo),后者是距屏幕左端 
   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,用法我會在后面介紹

顏色漸變也分為兩種情況:一種是在手指拖動的時候,另一種是在手指松開的時候(拖動到一半松開或者直接是點擊)

我們先來實現(xiàn)第一種,那么肯定要定位到ACTION_MOVE

   if (Math.abs(dx) > 5) {
        //...
        //通過當(dāng)前偏移量  / mMaxWidth , 計算出滑動的百分比(0~1)
        float percent = leftOffset * 1.0f / mMaxWidth;
        //將百分比,顏色變化的區(qū)間(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)方法接收三個參數(shù),第一個是百分比,后兩個參數(shù)是顏色區(qū)間。他會根據(jù)百分比計算出一個當(dāng)前處于區(qū)間范圍內(nèi)的一個值,返回給你。我們把這個值賦給背景的畫筆,再重繪界面,這樣我們的背景就會有一個漸變的效果。

再看松開手指的執(zhí)行漸變,這自然定位到我們的releaseShowAnim()方法。在其中找到動畫監(jiān)聽,在監(jiān)聽里,之前所寫的當(dāng)前動畫執(zhí)行的百分比就派上用場了,看代碼:

  //獲取松開手指時,背景顏色
  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狀態(tài),從當(dāng)前顏色漸變到open時的顏色
                changeBgColor(fraction, startColor, openColor);
            else
                //close狀態(tài),從當(dāng)前顏色漸變到close時的顏色
                changeBgColor(fraction, startColor, closeColor);
            leftOffset = (int) offset;
            // Log.i(TAG, "------>leftOffset = " + leftOffset);
            // alpha = (int) (0x66 * (float) leftOffset / (float)
            // mMaxWidth);
            invalidate();
        }

    });

趕緊加上試一試,背景已經(jīng)如期望的那樣漸變了吧!到此為止我們的自定義開關(guān)就接近尾聲了,還有一些其他的功能,例如代碼控制開關(guān),控件不可用,當(dāng)應(yīng)用異常退出時保存View狀態(tài),改變顏色等等,都是一些很簡單的小功能,希望小伙伴們自行實現(xiàn),加深理解。

代碼沒托管到github,寫這個的主要目的是學(xué)習(xí)并且鞏固,畢竟這樣的輪子已經(jīng)有很多了,會用的同時也要會寫一寫。好累 ,吃個飯~ 下篇自定義控件見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,844評論 4 61
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,374評論 25 708
  • 編輯: 王健 導(dǎo)讀:2017年人工智能已經(jīng)成為全球的創(chuàng)業(yè)熱點,為了幫助大家快速找到對應(yīng)的投資機構(gòu),獲得投資實現(xiàn)自己...
    Sting閱讀 730評論 0 1
  • // 1.截取字符串
    HJXu閱讀 879評論 0 0
  • 在音樂結(jié)束的角落, 我尋找她的身影, 感到了孤獨。 走在街上, 擦一擦被雨淋濕的臉, 靈魂慌亂里總有你在身旁。 這...
    夕木陽閱讀 205評論 0 0

友情鏈接更多精彩內(nèi)容