这一次我们将会实现一个完整纯粹的自定义控件,而不是像之前的组合控件一样,拿系统的控件来实现;计划分为三部分:自定义控件的基本部分,自定义控件的触摸事件的处理和自定义控件的自定义属性;
下面就开始第一部分的编写,本次以一个定义的开关按钮为例,下面就开始吧:
先看看效果,一个点击开关按钮,实现点击切换开关状态:
为了能够讲解清晰,还是来一些基本的介绍。
首先需要明确的就是自定义控件还是继承自View这个类,Google在View这个类里面提供了相当多的方法供我们使用,使用这些方法我们可以实现相当多的效果和功能,在这里需要用到几个主要的方法;
自定义控件的步骤、用到的主要方法:
1、首先需要定义一个类,继承自View;对于继承View的类,会需要实现至少一个构造方法;实际上这里一共有三个构造方法:
**public View (Context context)**是在java代码创建视图的时候被调用(使用new的方式),如果是从xml填充的视图,就不会调用这个
**public View (Context context, AttributeSet attrs)**这个是在xml创建但是没有指定style的时候被调用
**public View (Context context, AttributeSet attrs, int defStyle)**这个是在第二个基础上添加style的时候被调用的
所以对于这里来说,如果不使用style, 我们重点关注第二个构造方法即可
2、对于任何一个控件来说,它需要显示在我们的界面上,那么肯定需要定义它的大小;
在这里Google提供了一个方法:**protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);我们去看这个方法的内部,实际上是调用了protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);**这个方法,其中第一个参数是view的宽,第二个参数是view的高,这样我们就可以设置view的宽高了,但是要注意,这样设置的单位都是像素
3、对于一个需要显示的控件来说,我们往往还需要确定它的位置:
这就要求重写onLayout方法;但是实际上这个方法在自定义view的时候使用的不多,原因是因为对于位置来说,控件只有建议权而没有决定权,决定权一般在父控件那里。
4、对于一个控件,需要显示,我们当然需要将它绘制出来,这里就需要重写onDraw方法,来将这个控件绘制出来
5、当控件状态改变的时候,我们很可能需要刷新view的显示状态,这时候就需要调用invalidate()方法,这个方法实际上会重新调用onDraw方法来重绘控件
6、在定义控件的过程中,如果需要对view设置点击事件,可以直接使用setOnClickListener方法,而不需要写view.setOnClickListener;
**7、在布局文件中将这个自定义控件定义出来,注意名字要使用全类名;**而且,由于是继承自view控件,所以在xml文件中如果是view本身的属性都可以直接使用,比如:android:layout_width等等
这里比较关键的地方就在于这个onDraw方法,我们一起来看一下:
<div>
<embed id="ZeroClipboardMovie_1" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_1">
</embed>
</div>
<span class="tracking-ad" data-mod="popu_167">[](https://code.csdn.net/snippets/505895)</span><span class="tracking-ad" data-mod="popu_170">[](https://code.csdn.net/snippets/505895/fork)</span></div> </div>
- <span class="comment">/**</span>
- <span class="comment"> * 画view的方法,绘制当前view的内容</span>
- <span class="comment"> */</span>
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onDraw(Canvas canvas) {
- <span class="comment">// super.onDraw(canvas);</span>
-
-
- Paint paint = <span class="keyword">new</span> Paint();
- <span class="comment">// 打开抗锯齿</span>
- paint.setAntiAlias(<span class="keyword">true</span>);
-
-
- <span class="comment">// 画背景</span>
- canvas.drawBitmap(backgroundBitmap, <span class="number"></span>, <span class="number"></span>, paint);
- <span class="comment">// 画滑块</span>
- canvas.drawBitmap(slideButton, slideBtn_left, <span class="number"></span>, paint);
- }
</div>
**onDraw**方法传入的参数是一个**Canvas**画布对象,这个实际上跟Java中的差不太多,我们要在画布上画画也需要一个画笔,我们这里也将其初始化出来**Paint paint = new Paint()**,同时设置了一个抗锯齿效果**paint.setAntiAlias(true)**,然后调用**drawBitmap**的方法,先后绘制了开关的背景和开关的滑块,分别入下图:
 
这里要注意的一点就是,**drawBitmap(Bitmap bitmap, float left, float top, Paint paint)**方法中间的两个float类型的参数,分别代表绘制图形的左上角的x和y的坐标(原点设置在左上角),所以这里如果我们个绘制坐标都传入0,0,那么开关会处在一个关的状态,这里,我们对于滑块使用了一个变量**slideBtn_left**来设置其位置,**那么对于关闭状态,slideBtn_left的值就应该为0,对于开启状态,slideBtn_left的值就应该是backgroundBitmap(背景)的宽度减去slideButton(滑块)的宽度**;
那么这样一来,机制就比较清楚了,我们只需要在控件上设置一个点击事件,同时设置一个boolean变量代表开关的状态,当点击的时候,切换这个boolean类型的变量为true或者false,同时变化**slideButton**的值为****或者**backgroundBitmap.getWidth()-slideButton.getWidth()**,然后再调用**invalidate()**方法刷新控件,就可以实现基本的开关功能了
下面来看具体的代码,注解比较详细:
自定义控件的类MyToggleButton.java,继承自View:
<div class="dp-highlighter bg_java">
<div class="bar">
<div class="tools">
**[java]** [view plain](http://blog.csdn.net/cyp331203/article/details/40736027#)<span class="tracking-ad" data-mod="popu_168"><span class="tracking-ad" data-mod="popu_168"> [copy](http://blog.csdn.net/cyp331203/article/details/40736027#)</span></span>
<div>
<embed id="ZeroClipboardMovie_2" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_2">
</embed>
</div>
<span class="tracking-ad" data-mod="popu_167">[](https://code.csdn.net/snippets/505895)</span><span class="tracking-ad" data-mod="popu_170">[](https://code.csdn.net/snippets/505895/fork)</span></div> </div>
- <span class="keyword">package</span> com.example.togglebutton.ui;
-
- <span class="keyword">import</span> com.example.togglebutton.R;
-
- <span class="keyword">import</span> android.content.Context;
- <span class="keyword">import</span> android.graphics.Bitmap;
- <span class="keyword">import</span> android.graphics.BitmapFactory;
- <span class="keyword">import</span> android.graphics.Canvas;
- <span class="keyword">import</span> android.graphics.Paint;
- <span class="keyword">import</span> android.util.AttributeSet;
- <span class="keyword">import</span> android.view.View;
-
- <span class="comment">/*</span>
- <span class="comment"> * 自定义view的几个步骤:</span>
- <span class="comment"> * 1、首先需要写一个类来继承自View</span>
- <span class="comment"> * 2、需要得到view的对象,那么需要重写构造方法,其中一参的构造方法用于new,二参的构造方法用于xml布局文件使用,三参的构造方法可以传入一个样式</span>
- <span class="comment"> * 3、需要设置view的大小,那么需要重写onMeasure方法</span>
- <span class="comment"> * 4、需要设置view的位置,那么需要重写onLayout方法,但是这个方法在自定义view的时候用的不多,原因主要在于view的位置主要是由父控件来决定</span>
- <span class="comment"> * 5、需要绘制出所需要显示的view,那么需要重写onDraw方法</span>
- <span class="comment"> * 6、当控件状态改变的时候,需要重绘view,那么调用invalidate();方法,这个方法实际上会重新调用onDraw方法</span>
- <span class="comment"> * 7、在这其中,如果需要对view设置点击事件,可以直接调用setOnClickListener方法</span>
- <span class="comment"> */</span>
-
- <span class="keyword">public</span> <span class="keyword">class</span> MyToggleButton <span class="keyword">extends</span> View {
-
- <span class="comment">/**</span>
- <span class="comment"> * 开关按钮的背景</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> Bitmap backgroundBitmap;
- <span class="comment">/**</span>
- <span class="comment"> * 开关按钮的滑动部分</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> Bitmap slideButton;
- <span class="comment">/**</span>
- <span class="comment"> * 滑动按钮的左边界</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">float</span> slideBtn_left;
- <span class="comment">/**</span>
- <span class="comment"> * 当前开关的状态</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">boolean</span> currentState = <span class="keyword">false</span>;
-
- <span class="comment">/**</span>
- <span class="comment"> * 在代码里面创建对象的时候,使用此构造方法</span>
- <span class="comment"> * </span>
- <span class="comment"> * @param context</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> MyToggleButton(Context context) {
- <span class="keyword">super</span>(context);
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * 在布局文件中声明的view,创建时由系统自动调用</span>
- <span class="comment"> * </span>
- <span class="comment"> * @param context</span>
- <span class="comment"> * @param attrs</span>
- <span class="comment"> */</span>
- <span class="keyword">public</span> MyToggleButton(Context context, AttributeSet attrs) {
- <span class="keyword">super</span>(context, attrs);
- initView();
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * 测量尺寸时的回调方法</span>
- <span class="comment"> */</span>
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onMeasure(<span class="keyword">int</span> widthMeasureSpec, <span class="keyword">int</span> heightMeasureSpec) {
- <span class="comment">// super.onMeasure(widthMeasureSpec, heightMeasureSpec);</span>
- <span class="comment">// 设置当前view的大小 width:view的宽,单位都是像素值 heigth:view的高,单位都是像素值</span>
- setMeasuredDimension(backgroundBitmap.getWidth(),
- backgroundBitmap.getHeight());
- }
-
- <span class="comment">// 这个方法对于自定义view的时候帮助不大,因为view的位置一般由父组件来决定的</span>
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onLayout(<span class="keyword">boolean</span> changed, <span class="keyword">int</span> left, <span class="keyword">int</span> top, <span class="keyword">int</span> right,
- <span class="keyword">int</span> bottom) {
- <span class="keyword">super</span>.onLayout(changed, left, top, right, bottom);
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * 画view的方法,绘制当前view的内容</span>
- <span class="comment"> */</span>
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onDraw(Canvas canvas) {
- <span class="comment">// super.onDraw(canvas);</span>
-
- Paint paint = <span class="keyword">new</span> Paint();
- <span class="comment">// 打开抗锯齿</span>
- paint.setAntiAlias(<span class="keyword">true</span>);
-
- <span class="comment">// 画背景</span>
- canvas.drawBitmap(backgroundBitmap, <span class="number"></span>, <span class="number"></span>, paint);
- <span class="comment">// 画滑块</span>
- canvas.drawBitmap(slideButton, slideBtn_left, <span class="number"></span>, paint);
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * 初始化view</span>
- <span class="comment"> */</span>
- <span class="keyword">private</span> <span class="keyword">void</span> initView() {
- backgroundBitmap = BitmapFactory.decodeResource(getResources(),
- R.drawable.switch_background);
- slideButton = BitmapFactory.decodeResource(getResources(),
- R.drawable.slide_button);
-
- <span class="comment">/*</span>
- <span class="comment"> * 点击事件</span>
- <span class="comment"> */</span>
- setOnClickListener(<span class="keyword">new</span> OnClickListener() {
-
- <span class="annotation">@Override</span>
- <span class="keyword">public</span> <span class="keyword">void</span> onClick(View v) {
-
- currentState = !currentState;
- flushState();
- flushView();
- }
- });
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * 刷新视图</span>
- <span class="comment"> */</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> flushView() {
- <span class="comment">// 刷新当前view会导致ondraw方法的执行</span>
- invalidate();
- }
-
- <span class="comment">/**</span>
- <span class="comment"> * 刷新当前的状态</span>
- <span class="comment"> */</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> flushState() {
- <span class="keyword">if</span> (currentState) {
- slideBtn_left = backgroundBitmap.getWidth()
- – slideButton.getWidth();
- } <span class="keyword">else</span> {
- slideBtn_left = <span class="number"></span>;
- }
- }
-
- }
</div>
在布局文件中将其定义出来:
<div class="dp-highlighter bg_html">
<div class="bar">
<div class="tools">
**[html]** [view plain](http://blog.csdn.net/cyp331203/article/details/40736027#)<span class="tracking-ad" data-mod="popu_168"><span class="tracking-ad" data-mod="popu_168"> [copy](http://blog.csdn.net/cyp331203/article/details/40736027#)</span></span>
<div>
<embed id="ZeroClipboardMovie_3" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_3">
</embed>
</div>
<span class="tracking-ad" data-mod="popu_167">[](https://code.csdn.net/snippets/505895)</span><span class="tracking-ad" data-mod="popu_170">[](https://code.csdn.net/snippets/505895/fork)</span></div> </div>
- <span class="tag"><</span><span class="tag-name">RelativeLayout</span> <span class="attribute">xmlns:android</span>=<span class="attribute-value">“http://schemas.android.com/apk/res/android”</span>
- <span class="attribute">xmlns:tools</span>=<span class="attribute-value">“http://schemas.android.com/tools”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“match_parent”</span>
- <span class="attribute">tools:context</span>=<span class="attribute-value">“<span class="katex math inline">{relativePackage}.</span>{activityClass}”</span> <span class="tag">></span>
-
-
- <span class="tag"><</span><span class="tag-name">com.example.togglebutton.ui.MyToggleButton</span>
- <span class="attribute">android:id</span>=<span class="attribute-value">“@+id/my_toggle_btn”</span>
- <span class="attribute">android:layout_width</span>=<span class="attribute-value">“wrap_content”</span>
- <span class="attribute">android:layout_height</span>=<span class="attribute-value">“wrap_content”</span>
- <span class="attribute">android:layout_centerInParent</span>=<span class="attribute-value">“true”</span> <span class="tag">/></span>
-
-
- <span class="tag"></</span><span class="tag-name">RelativeLayout</span><span class="tag">></span>
</div>
在这里由于没有写任何点击触发业务的逻辑,只是一个单纯的控件,所以在MainActivity里面没有加入多的代码:
<div class="dp-highlighter bg_java">
<div class="bar">
<div class="tools">
**[java]** [view plain](http://blog.csdn.net/cyp331203/article/details/40736027#)<span class="tracking-ad" data-mod="popu_168"><span class="tracking-ad" data-mod="popu_168"> [copy](http://blog.csdn.net/cyp331203/article/details/40736027#)</span></span>
<div>
<embed id="ZeroClipboardMovie_4" src="http://static.blog.csdn.net/scripts/ZeroClipboard/ZeroClipboard.swf" type="application/x-shockwave-flash" width="18" height="18" align="middle" name="ZeroClipboardMovie_4">
</embed>
</div>
<span class="tracking-ad" data-mod="popu_167">[](https://code.csdn.net/snippets/505895)</span><span class="tracking-ad" data-mod="popu_170">[](https://code.csdn.net/snippets/505895/fork)</span></div> </div>
- <span class="keyword">package</span> com.example.togglebutton;
-
- <span class="keyword">import</span> android.app.Activity;
- <span class="keyword">import</span> android.os.Bundle;
-
- <span class="keyword">public</span> <span class="keyword">class</span> MainActivity <span class="keyword">extends</span> Activity {
-
- <span class="annotation">@Override</span>
- <span class="keyword">protected</span> <span class="keyword">void</span> onCreate(Bundle savedInstanceState) {
- <span class="keyword">super</span>.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- }
- }
</div>
至此一个自定义的开关按钮就完成了,后面两篇将会介绍如何在上面实现 **[点击拖动开关的效果](http://blog.csdn.net/cyp331203/article/details/40779335)** 和如何实现自定义属性,谢谢支持!
💬 评论