Android進階之繪製-自定義View完全掌握(四)

  • 2020 年 1 月 20 日
  • 筆記

前面的案例中我們都是使用系統的一些控制項通過組合的方式來生成我們自定義的控制項,自定義控制項的實現還可以通過自定義類繼承View來完成。從該篇部落格開始,我們通過自定義類繼承View來實現一些我們自定義的控制項。 我們通過一個案例來學習,現在來實現這樣一個效果。

我們新建一個類MyToggleButton,讓它繼承View。 注意,一定要重寫帶兩個參數的構造方法,因為如果我們在布局文件使用該類,將會用這個構造方法實例該類,如果沒有就崩潰。 介紹一下一個控制項從創建到顯示過程中的主要方法。

  1. 執行構造方法實例化類
  2. 測量,通過measure方法,需要去重寫onMeasure方法 如果當前是一個ViewGroup,它還有義務去測量它的孩子 孩子只有建議權,就是說孩子可以建議控制項多高多寬,而最後是必須父類去決定寬高的
  3. 指定位置,通過layout方法,需要去重寫onLayout方法 指定控制項的位置,一般View不用重寫該方法,只有是ViewGroup的時候才需要去重寫它
  4. 繪製視圖,通過draw方法,需要去重寫onDraw方法 根據上面兩個方法的一些參數進行繪製

所以我們自定義View一般只需要重寫onMeasure(int,int)方法和onDraw(canvas)方法。 基本操作由三個方法完成:measure()方法、layou()方法、draw()方法,其內部又分別包含了onMeasure()方法、onLayout()方法、onDraw()方法。 貼出MyToggleButton類的程式碼。

package com.itcast.test0430_2;    import android.content.Context;  import android.graphics.Bitmap;  import android.graphics.BitmapFactory;  import android.graphics.Canvas;  import android.graphics.Paint;  import android.support.annotation.Nullable;  import android.util.AttributeSet;  import android.view.View;    import butterknife.BindBitmap;    /**   * Created by Administrator on 2019/4/30 0030.   */    public class MyToggleButton extends View {        private Bitmap backgroundBitmap;        private Bitmap slidingBitmap;        private int slidLeftMax;      private Paint paint;        /**       *  如果我們在布局文件使用該類,將會用這個構造方法實例該類,如果沒有就崩潰       * @param context       * @param attrs       */      public MyToggleButton(Context context, @Nullable AttributeSet attrs) {          super(context, attrs);          initView();      }        private void initView() {          paint = new Paint();          paint.setAntiAlias(true);//設置抗鋸齒          backgroundBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.switch_background);          slidingBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.switch_button);          slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();      }        /**       * 視圖的測量       * @param widthMeasureSpec       * @param heightMeasureSpec       */      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(backgroundBitmap.getWidth(),backgroundBitmap.getHeight());      }        /**       * 繪製       * @param canvas       */      @Override      protected void onDraw(Canvas canvas) {          canvas.drawBitmap(backgroundBitmap,0,0,paint);          canvas.drawBitmap(slidingBitmap,0,0,paint);      }  }

通過上面的講述,相信這些程式碼你們都理解。這樣一個自定義的View就繪製好了,然後我們在activity_main.xml文件中使用。

<?xml version="1.0" encoding="utf-8"?>  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:app="http://schemas.android.com/apk/res-auto"      xmlns:tools="http://schemas.android.com/tools"      android:layout_width="match_parent"      android:layout_height="match_parent"      tools:context="com.itcast.test0430_2.MainActivity">        <com.itcast.test0430_2.MyToggleButton          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:layout_centerInParent="true" />    </RelativeLayout>

運行項目,預覽效果。

這樣一個靜態的開關就被繪製上去了,現在我們要讓開關通過點擊能改變狀態。 我們先來分析一下,現在的狀態是處於關閉的狀態,如何讓它處於開啟狀態?我們在繪製第二張圖的時候是距離左邊距為0,而此時我們已經計算出了開啟狀態需要距離左邊的邊距,所以,我們只需這樣修改

canvas.drawBitmap(slidingBitmap,slidLeftMax,0,paint);

即可,我們重新運行項目,預覽效果。

這樣就使得開關處於開啟的狀態了。既然如此,那我們就可以通過動態地改變左邊距的值從而間接地控制開關狀態。 我們重新修改MyToggleButton類的程式碼。

package com.itcast.test0430_2;    import android.content.Context;  import android.graphics.Bitmap;  import android.graphics.BitmapFactory;  import android.graphics.Canvas;  import android.graphics.Paint;  import android.support.annotation.Nullable;  import android.util.AttributeSet;  import android.view.View;    import butterknife.BindBitmap;    /**   * Created by Administrator on 2019/4/30 0030.   */    public class MyToggleButton extends View implements View.OnClickListener {        private Bitmap backgroundBitmap;      private Bitmap slidingBitmap;      /**       * 距離左邊的最大距離       */      private int slidLeftMax;      private Paint paint;      private int slideLeft;        /**       * 如果我們在布局文件使用該類,將會用這個構造方法實例該類,如果沒有就崩潰       *       * @param context       * @param attrs       */      public MyToggleButton(Context context, @Nullable AttributeSet attrs) {          super(context, attrs);          initView();      }        private void initView() {          paint = new Paint();          paint.setAntiAlias(true);//設置抗鋸齒          backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);          slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);          slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();            setOnClickListener(this);      }        /**       * 視圖的測量       *       * @param widthMeasureSpec       * @param heightMeasureSpec       */      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());      }        /**       * 繪製       *       * @param canvas       */      @Override      protected void onDraw(Canvas canvas) {          canvas.drawBitmap(backgroundBitmap, 0, 0, paint);          canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);      }        private boolean isOpen = false;        @Override      public void onClick(View v) {          isOpen = !isOpen;            if (isOpen) {              slideLeft = slidLeftMax;          } else {              slideLeft = 0;          }          //強制繪製          invalidate();//這個方法會導致onDraw()方法執行      }  }

這樣我們就完成了點擊,運行項目,預覽效果。

但是,這離我們的目標還是有一點距離的,我們繼續來實現下一個需求,開關的滑動。 要想實現這樣的需求,我們就需要去重寫onTouchEvent()方法來監聽觸摸事件,然後獲得按下時的坐標,但是在event對象中,有getX()方法和getRawX()方法,那麼我們應該使用哪個方法呢?這兩個方法有什麼區別呢? 我貼出兩張圖。

相信看到圖就一目了然了吧。 我們對MyToggleButton類的程式碼進行修改。

package com.itcast.test0430_2;    import android.content.Context;  import android.graphics.Bitmap;  import android.graphics.BitmapFactory;  import android.graphics.Canvas;  import android.graphics.Paint;  import android.support.annotation.Nullable;  import android.util.AttributeSet;  import android.view.MotionEvent;  import android.view.View;    import butterknife.BindBitmap;    /**   * Created by Administrator on 2019/4/30 0030.   */    public class MyToggleButton extends View implements View.OnClickListener {        private Bitmap backgroundBitmap;      private Bitmap slidingBitmap;      /**       * 距離左邊的最大距離       */      private int slidLeftMax;      private Paint paint;      private int slideLeft;        /**       * 如果我們在布局文件使用該類,將會用這個構造方法實例該類,如果沒有就崩潰       *       * @param context       * @param attrs       */      public MyToggleButton(Context context, @Nullable AttributeSet attrs) {          super(context, attrs);          initView();      }        private void initView() {          paint = new Paint();          paint.setAntiAlias(true);//設置抗鋸齒          backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);          slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);          slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();            setOnClickListener(this);      }        /**       * 視圖的測量       *       * @param widthMeasureSpec       * @param heightMeasureSpec       */      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());      }        /**       * 繪製       *       * @param canvas       */      @Override      protected void onDraw(Canvas canvas) {          canvas.drawBitmap(backgroundBitmap, 0, 0, paint);          canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);      }        private boolean isOpen = false;        @Override      public void onClick(View v) {          isOpen = !isOpen;            if (isOpen) {              slideLeft = slidLeftMax;          } else {              slideLeft = 0;          }          //強制繪製          invalidate();//這個方法會導致onDraw()方法執行      }        private float startX;        @Override      public boolean onTouchEvent(MotionEvent event) {          switch (event.getAction()){              case MotionEvent.ACTION_DOWN:                  //1、記錄按下的坐標                  startX = event.getX();                  break;              case MotionEvent.ACTION_MOVE:                  //2、記錄結束值                  float endX = event.getX();                  //3、計算偏移量                  float distanceX = endX - startX;                    slideLeft += distanceX;                  //4、屏蔽非法值                    //5、刷新                  invalidate();                  //6、數據還原                  startX = event.getX();                    break;              case MotionEvent.ACTION_UP:                  break;          }          return super.onTouchEvent(event);      }  }

現在運行項目,預覽效果。

會發現,開關竟然被滑出去了,顯然這種現象是不被允許的,我們把第四步屏蔽非法值實現一下。

 if(slideLeft < 0){         slideLeft = 0;   }else if(slideLeft > slidLeftMax){        slideLeft = slidLeftMax;   }

現在運行預覽。

現在我們已經無法將開關滑出控制項外,但是,不知道你們有沒有發現,它可以滑動到一個比較尷尬的地方,就是既不是開啟狀態,也不是關閉狀態,而是處於兩者中間,那這種情況同樣也是不被允許的,所以,我們現在來解決一下這個問題。 重新修改MyToggleButton類的程式碼。

package com.itcast.test0430_2;    import android.content.Context;  import android.graphics.Bitmap;  import android.graphics.BitmapFactory;  import android.graphics.Canvas;  import android.graphics.Paint;  import android.support.annotation.Nullable;  import android.util.AttributeSet;  import android.view.MotionEvent;  import android.view.View;    import butterknife.BindBitmap;    /**   * Created by Administrator on 2019/4/30 0030.   */    public class MyToggleButton extends View implements View.OnClickListener {        private Bitmap backgroundBitmap;      private Bitmap slidingBitmap;      /**       * 距離左邊的最大距離       */      private int slidLeftMax;      private Paint paint;      private int slideLeft;        /**       * 如果我們在布局文件使用該類,將會用這個構造方法實例該類,如果沒有就崩潰       *       * @param context       * @param attrs       */      public MyToggleButton(Context context, @Nullable AttributeSet attrs) {          super(context, attrs);          initView();      }        private void initView() {          paint = new Paint();          paint.setAntiAlias(true);//設置抗鋸齒          backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);          slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);          slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();            setOnClickListener(this);      }        /**       * 視圖的測量       *       * @param widthMeasureSpec       * @param heightMeasureSpec       */      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());      }        /**       * 繪製       *       * @param canvas       */      @Override      protected void onDraw(Canvas canvas) {          canvas.drawBitmap(backgroundBitmap, 0, 0, paint);          canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);      }        private boolean isOpen = false;        @Override      public void onClick(View v) {          isOpen = !isOpen;            flushView();      }        private void flushView() {          if (isOpen) {              slideLeft = slidLeftMax;          } else {              slideLeft = 0;          }          //強制繪製          invalidate();//這個方法會導致onDraw()方法執行      }        private float startX;        @Override      public boolean onTouchEvent(MotionEvent event) {          super.onTouchEvent(event);          switch (event.getAction()){              case MotionEvent.ACTION_DOWN:                  //1、記錄按下的坐標                  startX = event.getX();                  break;              case MotionEvent.ACTION_MOVE:                  //2、記錄結束值                  float endX = event.getX();                  //3、計算偏移量                  float distanceX = endX - startX;                    slideLeft += distanceX;                    //4、屏蔽非法值                  if(slideLeft < 0){                      slideLeft = 0;                  }else if(slideLeft > slidLeftMax){                      slideLeft = slidLeftMax;                  }                    //5、刷新                  invalidate();                  //6、數據還原                  startX = event.getX();                    break;              case MotionEvent.ACTION_UP:                  if(slideLeft > slidLeftMax / 2){                      //顯示按鈕開                      isOpen = true;                  }else{                      isOpen = false;                  }                    flushView();                  break;          }          return true;      }  }

運行項目,預覽效果。

這個時候,雖然不會出現上次的尷尬情況,但是,這裡又有一個問題,就是我在滑動的時候,它總是往我滑動的反方向跑,我想讓它向右滑動,可它偏偏就要去左邊,這顯然也是不行的吧。這是因為我們的觸摸事件和點擊事件同時作用產生的問題。我們現在來解決這個問題。 再次修改MyToggleButton類的程式碼。

package com.itcast.test0430_2;    import android.content.Context;  import android.graphics.Bitmap;  import android.graphics.BitmapFactory;  import android.graphics.Canvas;  import android.graphics.Paint;  import android.support.annotation.Nullable;  import android.util.AttributeSet;  import android.view.MotionEvent;  import android.view.View;    import butterknife.BindBitmap;    /**   * Created by Administrator on 2019/4/30 0030.   */    public class MyToggleButton extends View implements View.OnClickListener {        private Bitmap backgroundBitmap;      private Bitmap slidingBitmap;      /**       * 距離左邊的最大距離       */      private int slidLeftMax;      private Paint paint;      private int slideLeft;        /**       * 如果我們在布局文件使用該類,將會用這個構造方法實例該類,如果沒有就崩潰       *       * @param context       * @param attrs       */      public MyToggleButton(Context context, @Nullable AttributeSet attrs) {          super(context, attrs);          initView();      }        private void initView() {          paint = new Paint();          paint.setAntiAlias(true);//設置抗鋸齒          backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);          slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);          slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();            setOnClickListener(this);      }        /**       * 視圖的測量       *       * @param widthMeasureSpec       * @param heightMeasureSpec       */      @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());      }        /**       * 繪製       *       * @param canvas       */      @Override      protected void onDraw(Canvas canvas) {          canvas.drawBitmap(backgroundBitmap, 0, 0, paint);          canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);      }        private boolean isOpen = false;      /**       * true:點擊事件生效,滑動事件不生效       * false:點擊事件不生效,滑動事件生效       */      private boolean isEnableClick = true;        @Override      public void onClick(View v) {          if (isEnableClick) {              isOpen = !isOpen;              flushView();          }      }        private void flushView() {          if (isOpen) {              slideLeft = slidLeftMax;          } else {              slideLeft = 0;          }          //強制繪製          invalidate();//這個方法會導致onDraw()方法執行      }        private float startX;      private float lastX;        @Override      public boolean onTouchEvent(MotionEvent event) {          super.onTouchEvent(event);          switch (event.getAction()) {              case MotionEvent.ACTION_DOWN:                  //1、記錄按下的坐標                  lastX = startX = event.getX();                  isEnableClick = true;                  break;              case MotionEvent.ACTION_MOVE:                  //2、記錄結束值                  float endX = event.getX();                  //3、計算偏移量                  float distanceX = endX - startX;                    slideLeft += distanceX;                    //4、屏蔽非法值                  if (slideLeft < 0) {                      slideLeft = 0;                  } else if (slideLeft > slidLeftMax) {                      slideLeft = slidLeftMax;                  }                    //5、刷新                  invalidate();                  //6、數據還原                  startX = event.getX();                    if (Math.abs(endX - lastX) > 5) {                      //滑動                      isEnableClick = false;                  }                    break;              case MotionEvent.ACTION_UP:                  if (!isEnableClick) {                      if (slideLeft > slidLeftMax / 2) {                          //顯示按鈕開                          isOpen = true;                      } else {                          isOpen = false;                      }                      flushView();                  }                  break;          }          return true;      }  }

這就是我們的最終版程式碼,也就完成了整個的案例。

點擊下載源碼

Tags: