对于Android的自定义控件是自己一直想研究总结的,所以未来会从基础开始,一点点来学习一些自定义控件的效果,这些知识并非完全自己来研究的,但是是自己学习成长的点滴记录,重在搞懂原理,言归正传~
这次要实现的滑动开关按钮的效果如下:
【说明】:之后所有的自定义效果的学习文章都是先上效果图之后,然后再一步步从无到有的去实现。
从效果图中可以发现,这是一个"很简单"的开关按钮控件,也就是平常使用的CheckBox的效果,但是又要比CheckBox控件要多出一个效果,就是该控件支持滑动,而且这个效果是从无到有一点点自定义出来的,也就是自己动手实现一个类似于CheckBox的效果,所以其实也不是很简单,麻雀虽小五脏俱全,通过这个例子来熟知自定义控件的整个过程,下面则一点点来实现它。
首先控件需要这两张图片素材:
自定义过CheckBox的都清楚,图片的高度是一样大小的:
新建一个工程,然后将这两个资源文件放入到工程中:
然后新建一个自定义View,如下:
其中需要重写构造方法:
其中只需要重写两个构造方法既可:
/** * 自定义滑动开关view */ public class MyToggleButton extends View { /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); } }
而从上面的代码注释中可以发现这两个构造方法是有区分的,有必要知道一下,第一个是类似于这种使用场景:
而第二个构造是在布局文件中定义该View,由系统去调用的,而不是我们去调的,也就是这样:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.togglebutton.MainActivity" > <com.example.togglebutton.MyToggleButton android:id="@+id/toggle_button" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </RelativeLayout>
此时,如果我们将第二个构造方法注释掉,就会报错:
/** * 自定义滑动开关view */ public class MyToggleButton extends View { /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ // public MyToggleButton(Context context, AttributeSet attrs) { // super(context, attrs); // } }
运行看效果:
所以对于这两个构造的含义就清楚了,将注释的代码还原。
接下来绘制View,在正式绘制之前,需要把View对象要能显示在屏幕上的几个步骤说明一下,相当于中心思想,抓住了中心思想代码写起来就能有的放矢,如下:
①、调用构造方法,创建对象。
②、测量View的大小,在绘制之前是需要先确定View的大小的,对应的方法是onMeasure(int , int)。
③、确定View的位置,View自身有一些建议权,决定权在父View手中,onLayout(),这个方法由于是由ViewGoup决定的,所以对于View一般没用,不会重写它。
④、绘制View的内容。对应的方法是onDraw(Canvas)。
下面则遵照上面的步骤一一去实现,第一步已经定义了,接着来实现第二步,重写onMeasure方法:
public class MyToggleButton extends View { /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
而它的大小应该是来背景图片来决定的,所以需要把图片加载进来:
public class MyToggleButton extends View { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
接着来实现onMeasure方法,先来查看一下它的父类的实现:
所以我们也依葫芦画瓢:
public class MyToggleButton extends View { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } }
接下来到第三部:确认View的位置,由于View本身决定不了,而是由它的父控件决定的,所以不用管这个方法,这里重写只用来观察方法:
public class MyToggleButton extends View { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } }
要显示在屏幕上还差最后一步了,重写onDraw()方法:
所以super可以直接删掉,下面来开始将图片绘制在画布上:
其中第四个参数中需要一个paint对象,所以先初始化一个:
public class MyToggleButton extends View { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { } }
下面来绘制图片:
【说明】:关于View的绘制这里不多讲,之后会不断去研究它的,这里先直接用。
这时先来运行看下初步的效果:
接下来,实现点击可以进行开关按钮的切换效果,先来思考一下怎么来实现:
所以这时的这个参数需要声明成一个变量动态去改变:
public class MyToggleButton extends View { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** 滑动按钮的左边距 **/ private float slideButtonLeft; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { // 先绘制背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再绘制滑动按钮 canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint); } }
接着给当前View增加点击事件,然后处理点击逻辑:
public class MyToggleButton extends View implements OnClickListener { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** 滑动按钮的左边距 **/ private float slideButtonLeft; /** 当前开关的状态,true为开,false为关 **/ private boolean currentToggleSate; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); setOnClickListener(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { // 先绘制背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再绘制滑动按钮 canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint); } @Override public void onClick(View v) { currentToggleSate = !currentToggleSate; flushState(); } /** * 刷新当前状态 */ private void flushState() { if (currentToggleSate) { slideButtonLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); } else { slideButtonLeft = 0; } invalidate(); } }
编译运行看效果:
接下来实现最后一个功能,也是相对而言最复杂的,也就是支持滑动切换,怎么做呢?当然是要监听它的touch事件喽:
这个滑动切换的第一步,就是这个SlideButton能够随着手指滑动,所以先来实现它:
public class MyToggleButton extends View implements OnClickListener { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** 滑动按钮的左边距 **/ private float slideButtonLeft; /** 当前开关的状态,true为开,false为关 **/ private boolean currentToggleSate; /** down 事件时的x值 **/ private int firstX; /** touch 事件时上一个x值 **/ private int lastX; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); setOnClickListener(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { // 先绘制背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再绘制滑动按钮 canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint); } @Override public void onClick(View v) { currentToggleSate = !currentToggleSate; flushState(); } /** * 刷新当前状态 */ private void flushState() { if (currentToggleSate) { slideButtonLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); } else { slideButtonLeft = 0; } invalidate(); } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = lastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int currentX = (int) event.getX(); // 算出移动的距离 int moveDistance = currentX - lastX; // 并把当前的x值缓存起来,但计算下次移动的距离 lastX = currentX; // 然后根据移动位置来动态改变slideButtonLeft slideButtonLeft = slideButtonLeft + moveDistance; break; case MotionEvent.ACTION_UP: break; } invalidate(); return true; } }
运行看下效果:
随着手指移动倒没啥问题了,但是发现移动时没有做位置限制,应该不允许滑出背景,所以接下来需要做一下判断,也就是在触摸刷新前需要判断一下:
public class MyToggleButton extends View implements OnClickListener { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** 滑动按钮的左边距 **/ private float slideButtonLeft; /** 当前开关的状态,true为开,false为关 **/ private boolean currentToggleSate; /** down 事件时的x值 **/ private int firstX; /** touch 事件时上一个x值 **/ private int lastX; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); setOnClickListener(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { // 先绘制背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再绘制滑动按钮 canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint); } @Override public void onClick(View v) { currentToggleSate = !currentToggleSate; flushState(); } /** * 刷新当前状态 */ private void flushState() { if (currentToggleSate) { slideButtonLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); } else { slideButtonLeft = 0; } flushView(); } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = lastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int currentX = (int) event.getX(); // 算出移动的距离 int moveDistance = currentX - lastX; // 并把当前的x值缓存起来,但计算下次移动的距离 lastX = currentX; // 然后根据移动位置来动态改变slideButtonLeft slideButtonLeft = slideButtonLeft + moveDistance; break; case MotionEvent.ACTION_UP: break; } flushView(); return true; } private void flushView() { // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft int maxLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); // 确保slideButtonLeft>=0 slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0; // 确保slideButtonLeft<=maxleft slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft; invalidate(); } }
其区域判断的核心就是:
再次运行:
从结果来看已经加入了区域限制,这时滑不出背景区域了,接下来还有一个问题需要处理一下,就是关于滑动事件与onClick冲突的问题,下面来看下:
首先给onClick()方法上加上一条log用来观察呆会的实验:
下面运行,对于onClick()事件的触发,就是称按下鼠标,最后松开鼠标这样就构成了一个点击事件,那如果按下鼠标不松手,然后进行滑动,最后再松开鼠标也会触发onClick()事件么,用实验来证明下:
可见系统的这种onClick()触发行为不是我们想要的,我们想要的是如果发生了滑动,则就不触发onClick()了,所以下面需要进行一个逻辑判断来避免这个问题:
public class MyToggleButton extends View implements OnClickListener { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** 滑动按钮的左边距 **/ private float slideButtonLeft; /** 当前开关的状态,true为开,false为关 **/ private boolean currentToggleSate; /** down 事件时的x值 **/ private int firstX; /** touch 事件时上一个x值 **/ private int lastX; /** 判断是否发生拖动,如果拖动了,则不响应onClick事件 **/ private boolean isDrag; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); setOnClickListener(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { // 先绘制背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再绘制滑动按钮 canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint); } @Override public void onClick(View v) { if (isDrag) { // 如果发生了拖动,则不响应点击事件了 return; } Log.d("cexo", "onClick()"); currentToggleSate = !currentToggleSate; flushState(); } /** * 刷新当前状态 */ private void flushState() { if (currentToggleSate) { slideButtonLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); } else { slideButtonLeft = 0; } flushView(); } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstX = lastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int currentX = (int) event.getX(); // 算出移动的距离 int moveDistance = currentX - lastX; // 并把当前的x值缓存起来,但计算下次移动的距离 lastX = currentX; // 然后根据移动位置来动态改变slideButtonLeft slideButtonLeft = slideButtonLeft + moveDistance; break; case MotionEvent.ACTION_UP: break; } flushView(); return true; } private void flushView() { // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft int maxLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); // 确保slideButtonLeft>=0 slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0; // 确保slideButtonLeft<=maxleft slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft; invalidate(); } }
那问题的关键来了,判断是否是拖动的界限是?其实可以这样来认为:如果从ACTION_DOWN到ACTION_MOVE这两点的位置超过了5px,则认为是滑动,所以判断代码如下:
下面再来运行看下是否对滑动和点击做了明显的区分:
从结果来看,当拖动时再松手,则就没有走onClick的逻辑了,而是停到了我们滑动的位置不动了,也就达到了我们的目的。
接下来就要处理滑动切换的效果了,那切换的界限在哪呢?
所以,根据上图的描述,开关的判断也很简单了,具体代码如下:
public class MyToggleButton extends View implements OnClickListener { /** 做为背景的图片 **/ private Bitmap backgroundBitmap; /** 可以滑动的图片 **/ private Bitmap slideButtonBitmap; private Paint paint; /** 滑动按钮的左边距 **/ private float slideButtonLeft; /** 当前开关的状态,true为开,false为关 **/ private boolean currentToggleSate; /** down 事件时的x值 **/ private int firstX; /** touch 事件时上一个x值 **/ private int lastX; /** 判断是否发生拖动,如果拖动了,则不响应onClick事件 **/ private boolean isDrag; /** * 在代码里面创建对象的时候,使用此构造方法 */ public MyToggleButton(Context context) { super(context); } /** * 在布局文件中声名的view,创建时由系统自动调用 */ public MyToggleButton(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background); slideButtonBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button); paint = new Paint(); paint.setAntiAlias(true); setOnClickListener(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置当前View的大小,以背景图为大小,单位都是像素 setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight()); } /** * 确定位置的时候调用此方法,自定义View的时候作用不大 */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { // 先绘制背景图 canvas.drawBitmap(backgroundBitmap, 0, 0, paint); // 再绘制滑动按钮 canvas.drawBitmap(slideButtonBitmap, slideButtonLeft, 0, paint); } @Override public void onClick(View v) { if (isDrag) { // 如果发生了拖动,则不响应点击事件了 return; } Log.d("cexo", "onClick()"); currentToggleSate = !currentToggleSate; flushState(); } /** * 刷新当前状态 */ private void flushState() { if (currentToggleSate) { slideButtonLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); } else { slideButtonLeft = 0; } flushView(); } @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isDrag = false; firstX = lastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int currentX = (int) event.getX(); // 判断是否发生拖动 if (Math.abs(currentX - firstX) > 5) { isDrag = true; } // 算出移动的距离 int moveDistance = currentX - lastX; // 并把当前的x值缓存起来,但计算下次移动的距离 lastX = currentX; // 然后根据移动位置来动态改变slideButtonLeft slideButtonLeft = slideButtonLeft + moveDistance; break; case MotionEvent.ACTION_UP: if (isDrag) { int maxLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); // 根据slideButtonLeft来判断当前应该是什么状态(开,关) if (slideButtonLeft > maxLeft / 2) { // 开状态 currentToggleSate = true; } else { currentToggleSate = false; } flushState(); } break; } flushView(); return true; } private void flushView() { // 对slideButtonLeft的值进行判断,确保滑动时只能在合理的范围内:0<=slideButtonLeft<=maxleft int maxLeft = backgroundBitmap.getWidth() - slideButtonBitmap.getWidth(); // 确保slideButtonLeft>=0 slideButtonLeft = slideButtonLeft > 0 ? slideButtonLeft : 0; // 确保slideButtonLeft<=maxleft slideButtonLeft = slideButtonLeft < maxLeft ? slideButtonLeft : maxLeft; invalidate(); } }
运行看下效果:
虽说这个例子很简单,但是实际上涉及了自定义一个View的一个大致过程,之后会不断对其进行研究。