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;      }  }

这就是我们的最终版代码,也就完成了整个的案例。

点击下载源码