Android简易实战教程--第二十七话《自定义View入门案例之开关按钮详细分析》
转载此博客请注明出处点击打开链接 http://blog.csdn.net/qq_32059827/article/details/52444145
对于自定义view,可能是一个比较大的瓶颈期。笔者也是如此,就像毛主席说的,抓住主要矛盾,一切都不难。一些大神也声称过自定义view并不难。PS笔者比较实在,是真还没到感觉自定义view不难的水平!
这一篇文章,将对一个案例做详细的代码分析;即使说是入门的案例,但是掌握起来也不是那么容易;笔者尽量把所有代码标注注释帮助理解,同时,我觉得对初学者入门可能还要一段时间。
首先简单介绍view的绘制过程以及一些简单的理论知识:
View的绘制流程(相对性)
1. mearsue: 测量,final,控制控件的大小
2. layout: 布局,用来控制自己的布局位置
3. draw: 绘制,用来控制控件的显示样式
mearsure --> layout ---> draw。这是系统的绘制调用过程。但是对于开发者而言,暴露的方法是下边这三个方法:1. onMearsure: 2. onLayout:3. onDraw:
View的行为:
1. click,longClick,安卓系统没有这些方法,只有ontauch方法,都是封装好的
1. dispatchTouchEvent():touch分发,android希望用来处理是否分发touch事件
2. onInterceptTouchEvent():touch拦截,android希望处理是否拦截touch事件
1. 是否拦截孩子touch
3. onTouchEvent(): touch处理, anroid希望开发人员封装触摸行为给用户提供交换
4. setOnTouchListener():暴露给开发者,实现监听的回调
View的刷新调用顺序: invalidate() ---> draw() ---> onDraw()
好了,废话了一大堆。也该来点代码 ”解解渴“。先看一下自定义View类是咋个回事:
package com.itydl.Switch; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; public class SwitchView extends View { private Bitmap mSwitchBackground; Bitmap mSwitchSlide; private boolean isOpened = true;// 滑块的状态,默认开启 /** 为行为打标记 **/ private final static int STATE_NONE = 0;// 默认状态值,空状态 private final static int STATE_DOWN = 1;// 按下状态值 private final static int STATE_MOVE = 2;// 移动状态值 private final static int STATE_UP = 3;// 点击松开状态值 private float curentX; private int currentState = STATE_NONE;// 当前状态标记,默认空状态 private OnSwitchClickListener mListener; public SwitchView(Context context) { this(context, null); // 构造方法,new的时候调用 } public SwitchView(Context context, AttributeSet attrs) { super(context, attrs); //构造方法,写布局的时候调用 } /** * 设置背景的图片 * * @param resId * 设置背景图片的资源id */ public void setSwitchBackground(int resId) { // 背景图片 mSwitchBackground = BitmapFactory.decodeResource(getResources(), resId); } /** * 设置滑块的图片 * * @param resId * 设置滑块图片的资源id */ public void setSwitchSlide(int resId) { // 滑块 mSwitchSlide = BitmapFactory.decodeResource(getResources(), resId); } /** * 当前控件测量自己的宽高时,调用此方法 */ // 要不要绘制控件的大小? @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 用于设置自己的大小。在onMearsure里面调用setMeasuredDimension(width,height): // 用来设置(测量)自己的大小 if (mSwitchBackground != null) { int width = mSwitchBackground.getWidth();// 获取背景图片的大小,宽度 int height = mSwitchBackground.getHeight();// 获取背景图片的大小,高度 setMeasuredDimension(width, height);// 设置背景大小。设置【控件自己的宽高】正好等于背景图片的宽高 } else { // 系统默认设置的大小(其实是填充父组件大小) super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } /** * 当前控件开始绘制时,回调此方法 * 绘制开关的状态 */ // 要不要设置控件的样子?绘制 @Override protected void onDraw(Canvas canvas) { // 父类空实现,注销下边代码 // super.onDraw(canvas); // 绘制背景的显示 if (mSwitchBackground != null) { int left = 0;//bitmap相对于控件是贴在一起的,所以值为0 int top = 0; /** * bitmap The bitmap to be drawn 要绘制谁,绘制背景图片 表示相对控件左上角坐标位置: * position of the left side of the bitmap being drawn ,绘制的背景图片,相对于控件的相对宽度 * position of the top side of the bitmap being drawn ,绘制的背景图片,相对于控件的相对高度 * paint used to draw the bitmap (may be null) 画笔为null */ //绘制背景的显示,背景图片绘制出来。 canvas.drawBitmap(mSwitchBackground, left, top, null);//在控件上画图片 } if (mSwitchSlide == null) { return; } // 获取控件滑块的中点坐标 int slideWidth = mSwitchSlide.getWidth();// 滑块的宽度 // 获取背景的宽度坐标 int switchBackWidth = mSwitchBackground.getWidth();// 背景宽度 switch (currentState) {//onDraw()不知道具体什么状态,因此用使用状态标记 case STATE_DOWN: case STATE_MOVE://移动状态和按下状态是一样的 // 是否是按下 if (!isOpened) { // 滑块处于关闭状态 // 判断左右,设置状态 if (curentX < slideWidth / 2f) {//点击左侧 // 点击的滑块左侧,不变化。但是要有一次绘制当前状态位置,绘制在左侧 canvas.drawBitmap(mSwitchSlide, 0, 0, null); } else { // 点击滑块的右侧,滑块的中线和按下鼠标的x坐标对齐。计算滑块左侧的坐标值 float left = curentX - slideWidth / 2f; // 获取最大的left值(滑块往右滑动,有临界值,否则会滑出背景) float maxLeft = switchBackWidth - slideWidth; // 判断是否越界 if (left > maxLeft) {//如果越界 left = maxLeft;// 设置滑块左侧坐标为最大位置 } // 绘制这个位置 canvas.drawBitmap(mSwitchSlide, left, 0, null); } } else { // 滑块处于打开状态 // 获取滑块的中线坐标 float middleX = switchBackWidth - slideWidth / 2f; if (curentX > middleX) { // 点击的位置在滑块的中线右侧,无反应,设置默认 canvas.drawBitmap(mSwitchSlide, switchBackWidth - slideWidth, 0, null); } else { // 点击的位置在滑块的中线左侧,滑块中线等于当前点击的x值位置 // 当前滑块左边的坐标 float left = curentX - slideWidth / 2f; if (left < 0) {// 点击左边不要越界 left = 0; } // 点击位置没有越界,滑块中线等于当前点击的x值位置 canvas.drawBitmap(mSwitchSlide, left, 0, null); } } break; case STATE_UP: if (!isOpened) { // 关闭状态 canvas.drawBitmap(mSwitchSlide, 0, 0, null); // System.out.println("关闭"); } else { canvas.drawBitmap(mSwitchSlide, switchBackWidth - slideWidth, 0, null); // System.out.println("打开"); } break; case STATE_NONE:// 设置默认状态 // 绘制滑块,判断状态标记,来设置滑块的默认状态位置 if (!isOpened) {// 关闭 // 先写死为(0,0) canvas.drawBitmap(mSwitchSlide, 0, 0, null); } else { // 打开状态 canvas.drawBitmap(mSwitchSlide, switchBackWidth - slideWidth, 0, null); } break; default: break; } } /** * 用户触摸调用 */ @Override public boolean onTouchEvent(MotionEvent event) { // 重写onTouchEvent方法就养成写出三大状态的习惯 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 状态改变,行为标记改变 currentState = STATE_DOWN; // 按下鼠标的相对横坐标: curentX = event.getX(); //获取到最新坐标,需要绘制一下最新坐标。但是在onTouchEvent方法中是获取不到canvas对象的。因此使用invalidate(); invalidate();// View的刷新调用顺序: invalidate() ---> draw() ---> onDraw() break; case MotionEvent.ACTION_MOVE:// 绘制过程和点击绘制过程一样,滑块中点跟着当前鼠标x坐标变化 currentState = STATE_MOVE; curentX = event.getX(); invalidate();// View的刷新调用顺序: invalidate() ---> draw() ---> onDraw().---->主线程调用 //postInvalidate();//触发刷新----->子线程调用的 break; case MotionEvent.ACTION_UP: currentState = STATE_UP; curentX = event.getX(); // 根据滑动松开位置情况,设置跳转到打开还是关闭状态标记 // 获取背景中点 int switchBackWidth = mSwitchBackground.getWidth(); if (curentX < switchBackWidth / 2f && isOpened) {// 节省资源,本来关闭的,不再触发关闭打印。&& isOpened表示是否当前是打开状态。 // 状态值设置为关闭状态 isOpened = false; if (mListener != null) { //调用接口方法,实际调用实现类方法 mListener.onSwitchChanged(isOpened); } } else if (curentX >= switchBackWidth / 2f && !isOpened) {// && !isOpened表示当前是否是关闭状态 isOpened = true; if (mListener != null) { //调用接口方法,实际调用实现类方法 mListener.onSwitchChanged(isOpened); } } invalidate();// View的刷新调用顺序: invalidate() ---> draw() ---> onDraw() break; default: break; } // 消费触摸事件,不使用父类的 return true; } /** * 调用此方法,设置开关状态的点击事件 * * @param listener */ public void setOnSwitchClickListener(OnSwitchClickListener listener) { this.mListener = listener; } /** * 开关状态监听 * * @author lenovo * */ public interface OnSwitchClickListener { /** * 接口回调,实现此方法,根据开关打开关闭做相应的事件。参数:打开true;关闭false */ void onSwitchChanged(boolean isOpend); } }
自定义了view,就引入这个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" > <com.itydl.Switch.SwitchView android:id="@+id/swit_switchview" android:layout_width="wrap_content" android:layout_height="wrap_content" > </com.itydl.Switch.SwitchView> </RelativeLayout>
既然设置了回调,就看看manactivity代码:
package com.itydl.Switch; import android.app.Activity; import android.os.Bundle; import android.widget.Toast; import com.itydl.Switch.SwitchView.OnSwitchClickListener; public class MainActivity extends Activity { private SwitchView mSwitchView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initView(); } private void initView() { // TODO Auto-generated method stub setContentView(R.layout.activity_main); //获取自定义view的实例 mSwitchView = (SwitchView) findViewById(R.id.swit_switchview); //设置背景图片,传递资源到自定义view mSwitchView.setSwitchBackground(R.drawable.switch_background); //设置滑块资源图片,把滑块的资源图片id传递到自定义view mSwitchView.setSwitchSlide(R.drawable.slide_button_background); //根据开关打开关闭,设置点击事件 mSwitchView.setOnSwitchClickListener(new OnSwitchClickListener() { @Override public void onSwitchChanged(boolean isOpend) { // TODO Auto-generated method stub Toast.makeText(getApplicationContext(), isOpend? "打开":"关闭", 0).show(); } }); } }运行结果如下:
欢迎关注本博客点击打开链接 http://blog.csdn.net/qq_32059827,每天花上5分钟,阅读一篇有趣的安卓小文哦