自定义控件(视图)2期笔记07:自定义控件之 自定义属性(开关按钮案例的优化)

1.先前,我们编好的开关按钮的项目工程,如下:

2. 下面我们要使用自定义的属性优化这个开关按钮,如下:

(1)第1步,我们在res/values文件夹下,新建一个attrs.xml文件,如下:

其中attrs.xml,如下:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <resources>
 3      <!-- 声明一个属性集的名称 -->
 4     <declare-styleable name="MyToggleBtn">
 5 
 6         <!-- 声明一个属性name是my_background  类型为引用类型  引用资源ID-->
 7         <attr name="my_background" format="reference"></attr>
 8         
 9         <!-- 声明一个属性name是my_slide_btn  类型为引用类型  引用资源ID -->
10         <attr name="my_slide_btn" format="reference"></attr>
11         
12         <!-- 声明一个属性name是curr_state  类型为布尔值 -->
13         <attr name="curr_state" format="boolean"></attr>
14     </declare-styleable>
15     
16 </resources>



(2)第2步,在布局文件activity_main.xml文件中使用上面设置的属性,如下:

(3)接下来就是来到MyToggleButton代码中,获得上面自定义的属性my_background、my_slide_btn和curr_state,如下:

  1 package com.himi.togglebtn;
  2 
  3 import android.content.Context;
  4 import android.content.res.TypedArray;
  5 import android.graphics.Bitmap;
  6 import android.graphics.BitmapFactory;
  7 import android.graphics.Canvas;
  8 import android.graphics.Paint;
  9 import android.util.AttributeSet;
 10 import android.view.MotionEvent;
 11 import android.view.View;
 12 import android.view.View.OnClickListener;
 13 
 14 public class MyToggleButton extends View implements OnClickListener {
 15 
 16     //作为背景的图片
 17     private Bitmap backgroundBitmap;
 18     //滑动的开关图片
 19     private Bitmap slidebtn;
 20     private Paint paint;
 21     
 22     //滑动按钮的左边界
 23     private float slidebtn_left;
 24     
 25     /**
 26      * 当前开关的状态
 27      * true :为开
 28      * false:为关
 29      */
 30     private boolean currState = false;
 31     /**
 32      * 背景图的资源ID
 33      */
 34     private int backgroundId;
 35     /**
 36      * 滑动图片的资源ID
 37      */
 38     private int slideBtnId;
 39     
 40 
 41     /**
 42      * 我们在代码里面创建对象的时候,使用此构造方法
 43      * @param context
 44      */
 45     public MyToggleButton(Context context) {
 46         super(context);
 47         // TODO 自动生成的构造函数存根
 48     }
 49     
 50     /**
 51      * 在布局文件xml中声明的View,创建时候由系统自动调用此构造方法。
 52      * 倘若我们(使用全路径)在xml布局文件中,声明使用这个自定义的View,但是我们没有这个构造方法,就会报错(系统不能找到这个构造)
 53      * @param context
 54      * @param attrs
 55      */
 56     public MyToggleButton(Context context, AttributeSet attrs) {
 57         super(context, attrs);
 58             
 59         //获得自定义的属性,这里获得在xml文件中定义的属性值,然后进行相应的设置
 60         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn);
 61         
 62         int N = ta.getIndexCount();
 63         for(int i=0; i<N; i++) {
 64             /*
 65              * 获得某个属性的ID值
 66              */
 67             int itemId = ta.getIndex(i);
 68             switch (itemId) {
 69             case R.styleable.MyToggleBtn_curr_state:
 70                 currState = ta.getBoolean(itemId, false);
 71                 
 72                 break;
 73 
 74             case R.styleable.MyToggleBtn_my_background:
 75                 backgroundId = ta.getResourceId(itemId, -1);
 76                 if(backgroundId == -1) {
 77                     throw new RuntimeException("请设置背景图片");
 78                 }
 79                 backgroundBitmap = BitmapFactory.decodeResource(getResources(), backgroundId);
 80                 break;
 81             case R.styleable.MyToggleBtn_my_slide_btn:
 82                 slideBtnId = ta.getResourceId(itemId, -1);
 83                 if(slideBtnId == -1) {
 84                     throw new RuntimeException("请设置滑动按钮图片");
 85                 }
 86                 slidebtn = BitmapFactory.decodeResource(getResources(), slideBtnId);
 87                 break;
 88             }
 89         }
 90         
 91 
 92         
 93         initView();
 94     }
 95 
 96     
 97     /**
 98      * 这个构造方法比上面的构造方法都了一个参数 defStyle,这个参数View默认的样式,这里可以重新这个构造,设置defStyle
 99      * 改变生成自定义的View的样式style
100      * @param context
101      * @param attrs
102      * @param defStyle
103      */
104     public MyToggleButton(Context context, AttributeSet attrs, int defStyle) {
105         super(context, attrs, defStyle);
106         // TODO 自动生成的构造函数存根
107     }
108 
109     
110     //初始化
111     private void initView() {
112         //初始化图片
113         //backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
114         //slidebtn = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button);
115         
116         //初始化画笔
117         paint = new Paint();
118         paint.setAntiAlias(true);//打开抗锯齿
119         
120         //添加Onclick事件监听
121         setOnClickListener(this);
122         //刷新当前状态,因为上面构造方法中设置了currState,所以这里必须设置一下flushState(),每次改变了currState,自然要刷新,才能生效
123         flushState();
124     }
125     
126     /*
127      * View对象显示在屏幕上,有几个重要步骤:
128      * 1. 构造方法 创建  对象.
129      * 2. 测量View的大小.  onMeasure(int, int):系统调用的方法,获知View的大小
130      * 3. 确定View的位置,View自身有一些建议权,View位置决定权在父View手中. onLayout(): ViewGroup调用
131      * 4. 绘制View的内容           onDraw(canvas)
132      * 
133      */
134     
135     
136     
137     /**
138      * 
139      * 测量尺寸时候的回调方法
140      */
141     @Override
142     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
143         
144         //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
145         /**
146          * 设置当前View的大小
147          * width :当前View的宽度
148          * height:当前view的高度(单位:像素)
149          */
150         setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
151     }
152     
153     
154     /**
155      * 自定义的View,作用不大
156      * 确定位置的时候,系统调用的方法(我们不用关心),这里我们就不改写这个方法
157      */
158     @Override
159     protected void onLayout(boolean changed, int left, int top, int right,
160             int bottom) {
161         // TODO 自动生成的方法存根
162         super.onLayout(changed, left, top, right, bottom);
163     }
164     
165     /**
166      * 绘制当前View的内容
167      */
168     @Override
169     protected void onDraw(Canvas canvas) {
170         // TODO 自动生成的方法存根
171         //super.onDraw(canvas);
172         
173         //绘制背景图
174         /*
175          * backgroundBitmap:要绘制的图片
176          * left 图片的左边界
177          * top 图片的上边界
178          * paint 绘制图片要使用的画笔
179          */
180         canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
181         //绘制可滑动的按钮
182         canvas.drawBitmap(slidebtn, slidebtn_left, 0, paint);
183     }
184     
185     /**
186      * 判断是否发生拖到
187      * 如果拖动了,就不再响应Onclick事件
188      * true:发生拖动
189      * false:没有发生拖动
190      */
191     private boolean isDrag = false;
192 
193     /**
194      * onClick事件在view.onTouchEvent中被解析
195      * 系统对Onclick事件的解析,过于简陋,只要有down事件和up事件,系统即认为发生了click事件
196      */
197     public void onClick(View v) {
198         /*
199          * 如果没有拖动,才执行改变状态的动作
200          */
201         if(!isDrag) {
202             currState = ! currState;
203             flushState();//刷新当前开关状态
204         }
205     }
206 
207     /**
208      * 刷新当前开关视图
209      */
210     private void flushState() {
211         if(currState) {
212             slidebtn_left = backgroundBitmap.getWidth()-slidebtn.getWidth();
213         }else {
214             slidebtn_left =0;
215         }
216         
217         flushView();    
218     }
219     
220     public void flushView() {
221         /**
222          * 对slidebtn_left的值进行判断
223          * 0 <= slidebtn_left <= backgroundwidth-slidebtnwidth(这样才能保证滑动的开关不会滑动越界)
224          *             
225          */
226         int maxLeft = backgroundBitmap.getWidth()-slidebtn.getWidth();//slidebtn左边界最大值
227         //确保slidebtn_left >= 0
228         slidebtn_left =(slidebtn_left>0)?slidebtn_left:0;
229         //确保slidebtn_left <=maxLeft
230         slidebtn_left = (slidebtn_left<maxLeft)?slidebtn_left:maxLeft;
231         
232         //告诉系统我需要刷新当前视图,只要当前视图可见状态,就会调用onDraw方法重新绘制,达到刷新视图的效果
233         invalidate();
234     }
235     
236     /**
237      * down事件时的x值
238      */
239     private int firstX;
240     /**
241      * touch事件的上一个x值
242      */
243     private int lastX;
244     
245     @Override
246     public boolean onTouchEvent(MotionEvent event) {
247         super.onTouchEvent(event);
248         switch(event.getAction()) {
249         case MotionEvent.ACTION_DOWN:
250             firstX = lastX = (int) event.getX();
251             isDrag = false;
252             break;
253         case MotionEvent.ACTION_MOVE:
254             //判断是否发生拖动
255             if(Math.abs(event.getX()-lastX)>5) {
256                 isDrag = true;
257             }
258             
259             //计算手指在屏幕上移动的距离
260             int dis = (int) (event.getX()-lastX);
261             //将本次的位置设置给lastX
262             lastX = (int) event.getX();
263             //根据手指移动的距离,改变slidebtn_left的值
264             slidebtn_left = slidebtn_left+dis;
265             break;
266         case MotionEvent.ACTION_UP:
267             
268             //在发生拖动的情况下,根据最后的位置,判断当前的开关的状态
269             if(isDrag){
270                 int maxLeft = backgroundBitmap.getWidth()-slidebtn.getWidth();//slidebtn左边界最大值    
271                 /**
272                  * 根据slidebtn_left判断,当前应该是什么状态
273                  * 
274                  */
275                 if(slidebtn_left>maxLeft/2) {//应为打开状态
276                     currState = true;
277                 }else {
278                     currState = false;
279                 }
280                 flushState();
281             }
282             
283             break;
284         
285         }
286         flushView();
287         return  true;
288     }
289  }

运行效果如下:

3. 上面的是标准的自定义属性的使用,还有不太正规,但是很方便的使用自定义属性的方法,如下:

(1)在上面的"开关按钮"工程的activity_main.xml文件中,添加如下:

(2)然后在MyToggleButton.java使用在xml文件中定义的testAttrs,如下:

  1 package com.himi.togglebtn;
  2 
  3 import android.content.Context;
  4 import android.content.res.TypedArray;
  5 import android.graphics.Bitmap;
  6 import android.graphics.BitmapFactory;
  7 import android.graphics.Canvas;
  8 import android.graphics.Paint;
  9 import android.util.AttributeSet;
 10 import android.view.MotionEvent;
 11 import android.view.View;
 12 import android.view.View.OnClickListener;
 13 
 14 public class MyToggleButton extends View implements OnClickListener {
 15 
 16     //作为背景的图片
 17     private Bitmap backgroundBitmap;
 18     //滑动的开关图片
 19     private Bitmap slidebtn;
 20     private Paint paint;
 21     
 22     //滑动按钮的左边界
 23     private float slidebtn_left;
 24     
 25     /**
 26      * 当前开关的状态
 27      * true :为开
 28      * false:为关
 29      */
 30     private boolean currState = false;
 31     /**
 32      * 背景图的资源ID
 33      */
 34     private int backgroundId;
 35     /**
 36      * 滑动图片的资源ID
 37      */
 38     private int slideBtnId;
 39     
 40 
 41     /**
 42      * 我们在代码里面创建对象的时候,使用此构造方法
 43      * @param context
 44      */
 45     public MyToggleButton(Context context) {
 46         super(context);
 47         // TODO 自动生成的构造函数存根
 48     }
 49     
 50     /**
 51      * 在布局文件xml中声明的View,创建时候由系统自动调用此构造方法。
 52      * 倘若我们(使用全路径)在xml布局文件中,声明使用这个自定义的View,但是我们没有这个构造方法,就会报错(系统不能找到这个构造)
 53      * @param context
 54      * @param attrs
 55      */
 56     public MyToggleButton(Context context, AttributeSet attrs) {
 57         super(context, attrs);
 58         
 59         //无命名空间测试,测试我们在activity_main.xml文件中定义的属性-----testAttrs="hello"
 60         String testAttrs = attrs.getAttributeValue(null, "testAttrs");
 61         System.out.println("testAttrs==:"+testAttrs);
 62         
 63         //获得自定义的属性
 64         TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyToggleBtn);
 65         
 66         int N = ta.getIndexCount();
 67         for(int i=0; i<N; i++) {
 68             /*
 69              * 获得某个属性的ID值
 70              */
 71             int itemId = ta.getIndex(i);
 72             switch (itemId) {
 73             case R.styleable.MyToggleBtn_curr_state:
 74                 currState = ta.getBoolean(itemId, false);
 75                 
 76                 break;
 77 
 78             case R.styleable.MyToggleBtn_my_background:
 79                 backgroundId = ta.getResourceId(itemId, -1);
 80                 if(backgroundId == -1) {
 81                     throw new RuntimeException("请设置背景图片");
 82                 }
 83                 backgroundBitmap = BitmapFactory.decodeResource(getResources(), backgroundId);
 84                 break;
 85             case R.styleable.MyToggleBtn_my_slide_btn:
 86                 slideBtnId = ta.getResourceId(itemId, -1);
 87                 if(slideBtnId == -1) {
 88                     throw new RuntimeException("请设置滑动按钮图片");
 89                 }
 90                 slidebtn = BitmapFactory.decodeResource(getResources(), slideBtnId);
 91                 break;
 92             }
 93         }
 94         
 95 
 96         
 97         initView();
 98     }
 99 
100     
101     /**
102      * 这个构造方法比上面的构造方法都了一个参数 defStyle,这个参数View默认的样式,这里可以重新这个构造,设置defStyle
103      * 改变生成自定义的View的样式style
104      * @param context
105      * @param attrs
106      * @param defStyle
107      */
108     public MyToggleButton(Context context, AttributeSet attrs, int defStyle) {
109         super(context, attrs, defStyle);
110         // TODO 自动生成的构造函数存根
111     }
112 
113     
114     //初始化
115     private void initView() {
116         //初始化图片
117         //backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
118         //slidebtn = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button);
119         
120         //初始化画笔
121         paint = new Paint();
122         paint.setAntiAlias(true);//打开抗锯齿
123         
124         //添加Onclick事件监听
125         setOnClickListener(this);
126         //刷新当前状态
127         flushState();
128     }
129     
130     /*
131      * View对象显示在屏幕上,有几个重要步骤:
132      * 1. 构造方法 创建  对象.
133      * 2. 测量View的大小.  onMeasure(int, int):系统调用的方法,获知View的大小
134      * 3. 确定View的位置,View自身有一些建议权,View位置决定权在父View手中. onLayout(): ViewGroup调用
135      * 4. 绘制View的内容           onDraw(canvas)
136      * 
137      */
138     
139     
140     
141     /**
142      * 
143      * 测量尺寸时候的回调方法
144      */
145     @Override
146     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
147         
148         //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
149         /**
150          * 设置当前View的大小
151          * width :当前View的宽度
152          * height:当前view的高度(单位:像素)
153          */
154         setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
155     }
156     
157     
158     /**
159      * 自定义的View,作用不大
160      * 确定位置的时候,系统调用的方法(我们不用关心),这里我们就不改写这个方法
161      */
162     @Override
163     protected void onLayout(boolean changed, int left, int top, int right,
164             int bottom) {
165         // TODO 自动生成的方法存根
166         super.onLayout(changed, left, top, right, bottom);
167     }
168     
169     /**
170      * 绘制当前View的内容
171      */
172     @Override
173     protected void onDraw(Canvas canvas) {
174         // TODO 自动生成的方法存根
175         //super.onDraw(canvas);
176         
177         //绘制背景图
178         /*
179          * backgroundBitmap:要绘制的图片
180          * left 图片的左边界
181          * top 图片的上边界
182          * paint 绘制图片要使用的画笔
183          */
184         canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
185         //绘制可滑动的按钮
186         canvas.drawBitmap(slidebtn, slidebtn_left, 0, paint);
187     }
188     
189     /**
190      * 判断是否发生拖到
191      * 如果拖动了,就不再响应Onclick事件
192      * true:发生拖动
193      * false:没有发生拖动
194      */
195     private boolean isDrag = false;
196 
197     /**
198      * onClick事件在view.onTouchEvent中被解析
199      * 系统对Onclick事件的解析,过于简陋,只要有down事件和up事件,系统即认为发生了click事件
200      */
201     public void onClick(View v) {
202         /*
203          * 如果没有拖动,才执行改变状态的动作
204          */
205         if(!isDrag) {
206             currState = ! currState;
207             flushState();//刷新当前开关状态
208         }
209     }
210 
211     /**
212      * 刷新当前开关视图
213      */
214     private void flushState() {
215         if(currState) {
216             slidebtn_left = backgroundBitmap.getWidth()-slidebtn.getWidth();
217         }else {
218             slidebtn_left =0;
219         }
220         
221         flushView();    
222     }
223     
224     public void flushView() {
225         /**
226          * 对slidebtn_left的值进行判断
227          * 0 <= slidebtn_left <= backgroundwidth-slidebtnwidth(这样才能保证滑动的开关不会滑动越界)
228          *             
229          */
230         int maxLeft = backgroundBitmap.getWidth()-slidebtn.getWidth();//slidebtn左边界最大值
231         //确保slidebtn_left >= 0
232         slidebtn_left =(slidebtn_left>0)?slidebtn_left:0;
233         //确保slidebtn_left <=maxLeft
234         slidebtn_left = (slidebtn_left<maxLeft)?slidebtn_left:maxLeft;
235         
236         //告诉系统我需要刷新当前视图,只要当前视图可见状态,就会调用onDraw方法重新绘制,达到刷新视图的效果
237         invalidate();
238     }
239     
240     /**
241      * down事件时的x值
242      */
243     private int firstX;
244     /**
245      * touch事件的上一个x值
246      */
247     private int lastX;
248     
249     @Override
250     public boolean onTouchEvent(MotionEvent event) {
251         super.onTouchEvent(event);
252         switch(event.getAction()) {
253         case MotionEvent.ACTION_DOWN:
254             firstX = lastX = (int) event.getX();
255             isDrag = false;
256             break;
257         case MotionEvent.ACTION_MOVE:
258             //判断是否发生拖动
259             if(Math.abs(event.getX()-lastX)>5) {
260                 isDrag = true;
261             }
262             
263             //计算手指在屏幕上移动的距离
264             int dis = (int) (event.getX()-lastX);
265             //将本次的位置设置给lastX
266             lastX = (int) event.getX();
267             //根据手指移动的距离,改变slidebtn_left的值
268             slidebtn_left = slidebtn_left+dis;
269             break;
270         case MotionEvent.ACTION_UP:
271             
272             //在发生拖动的情况下,根据最后的位置,判断当前的开关的状态
273             if(isDrag){
274                 int maxLeft = backgroundBitmap.getWidth()-slidebtn.getWidth();//slidebtn左边界最大值    
275                 /**
276                  * 根据slidebtn_left判断,当前应该是什么状态
277                  * 
278                  */
279                 if(slidebtn_left>maxLeft/2) {//应为打开状态
280                     currState = true;
281                 }else {
282                     currState = false;
283                 }
284                 flushState();
285             }
286             
287             break;
288         
289         }
290         flushView();
291         return  true;
292     }
293 
294 
295 }

测试logcat输出为:

原文地址:https://www.cnblogs.com/hebao0514/p/4847204.html