android自定义控件概述

引子:

  android SDK中会提供一些基础的控件以供开发。但是大多数情况下,这些基础的控件无法满足业务需求。本文主要说明自定义控件的分类,以及提供示例代码。

  本文只做入门级选手阅读,或者 加深印象 或 温故而知新,大佬大神敬请绕道。

android控件的3种方式:

1)派生控件 :

从SDK已有的控件为基础,改变其部分特征,形成符合需求的自定义控件。


具体做法举例:

public class MyTextView extends EditText(){...}
或者

public class MyListView extends ListView {...}


能够这样做,是因为SDK已有的控件其实已经提供了某些接口让开发者可以进行改造。


示例代码:这是一个继承了EditText,重写了某些函数,调用了EditText自己的API,形成了这种特殊效果,请看示例图。

 1 package com.example.administrator.hankstest0415.custom;
 2 
 3 import android.content.Context;
 4 import android.graphics.Rect;
 5 import android.graphics.drawable.Drawable;
 6 import android.text.Editable;
 7 import android.text.TextWatcher;
 8 import android.util.AttributeSet;
 9 import android.util.Log;
10 import android.view.MotionEvent;
11 import android.widget.EditText;
12 
13 import com.example.administrator.hankstest0415.R;
14 
15 /**17  * 这是一个带删除按钮的EditText,它能够在输入框中有内容时,显示最右边的删除按钮,点击该按钮可以直接清空内容
18  */
19 public class DelEditText extends EditText {
20 
21     private Drawable imgClear;
22     private Context mContext;
23 
24     public DelEditText(Context context, AttributeSet attrs) {
25         super(context, attrs);
26         this.mContext = context;
27         setDrawable();
28         init();
29     }
30 
31     private void init() {
32         imgClear = mContext.getResources().getDrawable(R.drawable.delete);
33         //添加watcher监听器,监听 文本被改变之后的事件
34         addTextChangedListener(new TextWatcher() {
35             //内容变化前
36             @Override
37             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
38 
39             }
40 
41             //内容正在改变
42             @Override
43             public void onTextChanged(CharSequence s, int start, int before, int count) {
44             }
45 
46             //在内容改变完之后
47             @Override
48             public void afterTextChanged(Editable editable) {
49                 Log.d("mytagX", "" + editable.toString());
50                 setDrawable();
51             }
52         });
53     }
54 
55     //绘制删除图片
56     //这里的setCompoundDrawablesWithIntrinsicBounds方法解释一下:
57     //按照原注释的意思,这个方法会在组件的上下左右,如果只需要在右侧显示,那就把其他3个参数设置为null,显示一个Drawable
58     private void setDrawable() {
59         if (length() < 1)//
60             setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
61         else
62             setCompoundDrawablesWithIntrinsicBounds(null, null, imgClear, null);
63     }
64 
65 
66     //当触摸范围在右侧时,触发删除方法,隐藏叉叉
67 
68     /**
69      * 继承父组件的触摸事件
70      *
71      * @param event
72      * @return
73      */
74     @Override
75     public boolean onTouchEvent(MotionEvent event) {
76         if (imgClear != null && event.getAction() == MotionEvent.ACTION_UP) {//如果触发的是 按下并释放的动作,也就是平时的点一下
77             int eventX = (int) event.getRawX();//就拿到当前点击的位置X,Y坐标
78             int eventY = (int) event.getRawY();
79             Log.d("mytagX", "" + eventX + " - " + eventY);
80             Rect rect = new Rect();//新建一个矩形
81             getGlobalVisibleRect(rect);//将当前View的绘制范围大小,设置到这个属性中. 比如说,这个View的绘制范围是 从 (0,0)到(100,200), 那么Rect的4个属性值就是0,0,100,200sa
82 
83             Log.d("onTouchEvent", "" + rect.left + " - " + rect.top + " - " + rect.right + " - " + rect.bottom);
84 
85             rect.left = rect.right - 100;//将rect的左 ,设置为它 右的值-100. 这是在控制触发事件的范围大小
86             if (rect.contains(eventX, eventY))//如果点击的位置,在Rect范围之内,那就触发清空事件
87             {
88                 setText("");
89                 Log.d("onTouchEvent", "点击了EditText并且触发了清空事件");
90             } else
91                 Log.d("onTouchEvent", "点击了EditText但是并没有点击到删除按钮的范围之内");
92 
93         }
94         return super.onTouchEvent(event);
95     }
96 
97 }


示例效果:

 (注:右边的这个图片,是调用EditText的api生成的)

2)自绘控件:

当SDK已有控件完全不能满足需求时,就需要我们直接继承所有控件的父类android.view.View来进行完全的自定义。

能够这么做的基础,就是 继承了View之后,可以重写其中的onDraw方法,使用参数提供的Canvas对象以及 自己创建的paint对象,进行绘图,并且可以调用postInvalidate产生动画效果。

 

示例代码:

  1 package com.example.administrator.hankstest0415.custom;
  2 
  3 import android.content.Context;
  4 import android.graphics.Canvas;
  5 import android.graphics.Paint;
  6 import android.graphics.RectF;
  7 import android.util.AttributeSet;
  8 import android.util.Log;
  9 import android.view.View;
 10 import android.widget.EditText;
 11 
 12 import com.example.administrator.hankstest0415.R;
 13 import com.example.administrator.hankstest0415.util.DensityUtils;
 14 
 15 import org.jetbrains.annotations.Nullable;
 16 
 17 public class PColumn extends View {
 18     int MAX = 100;//最大
 19     int corner = 40;
 20     int data = 0;//显示的数
 21     int tempData = 0;
 22     int textPadding = 20;
 23     Paint mPaint;
 24     int mColor;
 25 
 26     Context mContext;
 27 
 28     //首先,构造函数和 编译器自动生成的方式有所不同
 29     public PColumn(Context context) {
 30         super(context);
 31         mContext = context;
 32     }
 33 
 34     public PColumn(Context context, @Nullable AttributeSet attrs) {
 35         super(context, attrs);
 36         mContext = context;
 37         initPaint();
 38     }
 39 
 40     public PColumn(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
 41         super(context, attrs, defStyleAttr);
 42         mContext = context;
 43         initPaint();
 44     }
 45 
 46     private void initPaint() {
 47         mPaint = new Paint();
 48         mPaint.setAntiAlias(true);
 49         mColor = mContext.getResources().getColor(R.color.colorPrimary);
 50         mPaint.setColor(mColor);
 51         setData(80, 100);
 52     }
 53 
 54 
 55     private int defaultHeight = 400;
 56     private int defaultWidth = 180;
 57 
 58     /**
 59      * 重写onMeasure,设定控件最小宽高值。
 60      *
 61      * 因为当布局xml中对这个控件设置wrap_content,而 onMeasure方法并没有指定最小宽高值的话,该控件就会默认match_parent.
 62      *
 63      * @param widthMeasureSpec
 64      * @param heightMeasureSpec
 65      */
 66     @Override
 67     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 68         int width = measureDimension(defaultWidth, widthMeasureSpec);
 69         int height = measureDimension(defaultHeight, heightMeasureSpec);
 70         setMeasuredDimension(width, height);//重写onMeasure一定要调用setMeasuredDimension()。
 71     }
 72 
 73 
 74     public int measureDimension(int defaultSize, int measureSpec) {
 75         int result;
 76 
 77         int specMode = MeasureSpec.getMode(measureSpec);
 78         int specSize = MeasureSpec.getSize(measureSpec);
 79 
 80         if (specMode == MeasureSpec.EXACTLY) {//如果直接指定了宽度,比如100dp
 81             result = specSize;
 82         } else {
 83             result = defaultSize;   //UNSPECIFIED 设定一个默认值
 84             if (specMode == MeasureSpec.AT_MOST) {//如果设定宽度match_parent
 85                 result = Math.min(result, specSize);
 86             }
 87         }
 88         //如果既没有指定宽度,也没有设定match_parent,那么,就用之前设定好的默认值
 89         return result;
 90     }
 91 
 92     @Override
 93     protected void onDraw(Canvas canvas) {
 94         super.onDraw(canvas);
 95         if (data == 0) {
 96             mPaint.setTextSize(getWidth() / 2);
 97             RectF oval3 = new RectF(0, getHeight() - DensityUtils.pxTodip(mContext, 20), getWidth(), getHeight());// 设置个新的长方形
 98             //圆角长方形,醉了,drawRoundRect
 99             canvas.drawRoundRect(oval3, DensityUtils.pxTodip(mContext, corner), DensityUtils.pxTodip(mContext, corner), mPaint);
100 
101             canvas.drawText("0",
102                     getWidth() * 0.5f - mPaint.measureText("0") * 0.5f,
103                     getHeight() - DensityUtils.pxTodip(mContext, 20) - 2 * DensityUtils.pxTodip(mContext, textPadding),
104                     mPaint);
105             return;
106         }
107 
108         //防止数值很大的的时候,动画时间过长
109         int step = data / 100 + 1;
110 
111         if (tempData < data - step) {
112             tempData = tempData + step;
113         } else {
114             tempData = data;
115         }
116         //画圆角矩形
117         String S = tempData + "";
118         //一个字和两,三个字的字号相同
119         if (S.length() < 4) {
120             mPaint.setTextSize(getWidth() / 2);
121         } else {
122             mPaint.setTextSize(getWidth() / (S.length() - 1));
123         }
124 
125         float textH = mPaint.ascent() + mPaint.descent();
126         float MaxH = getHeight() - textH - 2 * DensityUtils.pxTodip(mContext, textPadding);
127         //圆角矩形的实际高度
128         float realH = MaxH / MAX * tempData;
129         RectF oval3 = new RectF(0, getHeight() - realH, getWidth(), getHeight());// 设置个新的长方形
130         canvas.drawRoundRect(oval3, DensityUtils.pxTodip(mContext, corner), DensityUtils.pxTodip(mContext, corner), mPaint);
131         //写数字
132         canvas.drawText(S,
133                 getWidth() * 0.5f - mPaint.measureText(S) * 0.5f,
134                 getHeight() - realH - 2 * DensityUtils.pxTodip(mContext, textPadding),
135                 mPaint);
136         if (tempData != data) {
137             postInvalidate();
138         }
139     }
140 
141     /**
142      * 如果只是自定义View,则onLayout方法不需要实现
143      * @param changed
144      * @param left
145      * @param top
146      * @param right
147      * @param bottom
148      */
149     @Override
150     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
151         super.onLayout(changed, left, top, right, bottom);
152     }
153 
154     @Override
155     public void draw(Canvas canvas) {
156         super.draw(canvas);
157     }
158 
159     public void setData(int data, int MAX) {
160         this.data = data;
161         tempData = 0;
162         this.MAX = MAX;
163         postInvalidate();//进行画面刷新
164     }
165 
166 
167 }

示例效果:

 

 另外,除了继承View之外,还可以继承SurfaceView. 两者的区别如下:

SurfaceView和View画图的区别:
1、SurfaceView更新图像不依赖主线程,直接用工作线程就行。View则是必须依赖主线程, 还有可能卡住主线程;
2、SurfaceView可以控制帧数,刷新频率。View的帧率则是系统默认的,无法控制。
3、SurfaceView消耗大,View消耗较小。

示例代码:

  2 
  3 import android.content.Context;
  4 import android.graphics.Canvas;
  5 import android.graphics.Color;
  6 import android.graphics.Paint;
  7 import android.util.AttributeSet;
  8 import android.util.Log;
  9 import android.view.SurfaceHolder;
 10 import android.view.SurfaceView;
 11 
 12 import java.util.Date;
 13 
 14 public class CircleClock extends SurfaceView implements SurfaceHolder.Callback, Runnable {
 15 
 16     private Paint mPaint, mPaint_face, mPaint_second, mPaint_minute, mPaint_hour;
 17     private static final String cloclColor = "#000000";
 18     // 子线程标志位
 19     private boolean mIsDrawing;//控制绘制过程的停和走
 20     private Canvas mCanvas;// 保存画布对象为全局变量
 21 
 22     private void initPaint() {
 23         mPaint = new Paint();
 24         mPaint.setColor(Color.parseColor(cloclColor));
 25         mPaint.setStrokeWidth(2);
 26         mPaint.setStyle(Paint.Style.STROKE);
 27         mPaint.setAntiAlias(true);
 28 
 29         mPaint_face = new Paint();
 30         mPaint_face.setColor(Color.parseColor(cloclColor));
 31         mPaint_face.setStrokeWidth(4);
 32         mPaint_face.setStyle(Paint.Style.STROKE);
 33         mPaint_face.setAntiAlias(true);
 34 
 35         mPaint_second = new Paint();
 36         mPaint_second.setColor(Color.parseColor(cloclColor));
 37         mPaint_second.setStrokeWidth(4);
 38         mPaint_second.setStyle(Paint.Style.STROKE);
 39         mPaint_second.setAntiAlias(true);
 40 
 41         mPaint_minute = new Paint();
 42         mPaint_minute.setColor(Color.parseColor(cloclColor));
 43         mPaint_minute.setStrokeWidth(5);
 44         mPaint_minute.setStyle(Paint.Style.STROKE);
 45         mPaint_minute.setAntiAlias(true);
 46 
 47         mPaint_hour = new Paint();
 48         mPaint_hour.setColor(Color.parseColor(cloclColor));
 49         mPaint_hour.setStrokeWidth(6);
 50         mPaint_hour.setStyle(Paint.Style.STROKE);
 51         mPaint_hour.setAntiAlias(true);
 52     }
 53 
 54     public CircleClock(Context context) {
 55         super(context);
 56         initPaint();
 57         initView();
 58     }
 59 
 60     public CircleClock(Context context, AttributeSet attrs) {
 61         super(context, attrs);
 62         initPaint();
 63         initView();
 64     }
 65 
 66     public CircleClock(Context context, AttributeSet attrs, int defStyleAttr) {
 67         super(context, attrs, defStyleAttr);
 68         initPaint();
 69         initView();
 70     }
 71 
 72 
 73     @Override
 74     public void surfaceCreated(SurfaceHolder holder) {
 75         // 一旦被创建成功,就启动动画
 76         reset();
 77     }
 78 
 79     @Override
 80     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
 81 
 82     }
 83 
 84     @Override
 85     public void surfaceDestroyed(SurfaceHolder holder) {
 86         mIsDrawing = false;
 87     }
 88 
 89     @Override
 90     public void run() {
 91         while (mIsDrawing) {
 92             drawClock();//无限循环绘制指针
 93             try {
 94                 Thread.sleep(500);//每隔1000MS绘制一次
 95             } catch (InterruptedException e) {
 96                 e.printStackTrace();
 97             }
 98         }
 99     }
100 
101     private SurfaceHolder holder;
102 
103     private void initView() {
104         holder = getHolder();//获得holder对象
105         holder.addCallback(this);//添加callback
106         setFocusable(true);
107         setFocusableInTouchMode(true);
108         setKeepScreenOn(true);
109     }
110 
111 
112     private int radiusTarget = 80;
113     private int currentRadius = 0;
114 
115     //详细的绘制过程
116 
117     /**
118      * 这个myDraw方法会无限循环调用
119      */
120     private void drawClock() {
121         try {
122             mCanvas = holder.lockCanvas();//
123             mCanvas.drawColor(Color.parseColor("#FFFFFF"));//绘制背景
124             drawClockFace();
125             drawPointer();
126 
127         } catch (Exception e) {
128 
129         } finally {
130             if (mCanvas != null) {
131                 holder.unlockCanvasAndPost(mCanvas);//释放,并且刷新surface
132             }
133         }
134     }
135 
136     private void drawPointer() {
137 
138         //这里逻辑会发生变化,因为我打算将当前系统时间的时分秒提取出来,然后计算出各自的角度,再将3个指针绘制出来
139         Date date = new Date();
140         int hour = date.getHours();
141         int minute = date.getMinutes();
142         int second = date.getSeconds();
143 
144         //先把秒钟指针画出来
145         // 如何把秒钟转化成角度
146         mCanvas.rotate(second * 6);// 表盘一共360度。 一共60秒,所以每走一秒,度数就走6度
147         mCanvas.drawLine(0, 0, radiusTarget * 7 / 10, 0, mPaint_second);//刻度的长度,设定为半径的1/10
148 
149         //再把分钟指针画出来
150 
151         //其实分钟数是一个小数,而不是int
152         //算出真正的分钟数
153         float realMinute = minute + second / 60.0f;
154         Log.d("drawPointer", "" + realMinute);
155         mCanvas.rotate(-second * 6);// 还得先把原来的角度转回去
156         mCanvas.rotate((realMinute * 6));//再旋转分钟的角度,表盘一共360度。 一共60分,所以每走一分,度数就走6度
157         mCanvas.drawLine(0, 0, radiusTarget * 6 / 10, 0, mPaint_minute);//
158 
159         hour = hour % 12;
160 
161         float realHour = hour + minute / 60.0f;
162         mCanvas.rotate(-realMinute * 6);// 还得先把原来的角度转回去
163         mCanvas.rotate((realHour * 30));//再旋转时钟的角度,表盘上一共12个小时,一共360度,所以每一个小时代表的是30度
164         mCanvas.drawLine(0, 0, radiusTarget * 5 / 10, 0, mPaint_hour);//
165     }
166 
167     /**
168      * 画出表盘
169      */
170     private void drawClockFace() {
171         //这些东西都是只需要绘制一次的
172         int w = getWidth();
173         int h = getHeight();
174         int cx = w / 2;
175         int cy = h / 2;
176         mCanvas.drawCircle(cx, cy, currentRadius, mPaint);
177         mCanvas.drawPoint(cx, cy, mPaint);
178 
179         mCanvas.translate(cx, cy);// 转移坐标轴中心,到原点处
180         mCanvas.rotate(-90);//让指针从12点位置开始走,因为原始的是从3点位置。中间差了90度,所以需要逆时针旋转坐标90度
181         for (int i = 1; i <= 60; i++) {//120次循环,绘制表盘
182             mCanvas.rotate(6);//每一次旋转3度,
183             if (i % 5 == 0) {//如果遇到整点,,1,2,3,4,5,6,7,8,9,10,11,12
184                 mCanvas.drawLine(radiusTarget * 8 / 10, 0, radiusTarget, 0, mPaint_face);//就用较粗的画笔画出较长的线条
185             } else
186                 mCanvas.drawLine(radiusTarget * 9 / 10, 0, radiusTarget, 0, mPaint);//否则,就用较细的画笔画出较短的线条
187         }
188     }
189 
190     public void reset() {
191         radiusTarget = getWidth() / 3;
192         currentRadius = radiusTarget;
193         mIsDrawing = true;
194         new Thread(this).start();
195     }
196 }


3)组合控件:

  

猜你喜欢

转载自www.cnblogs.com/hankzhouAndroid/p/8973621.html