注意1:
1、关键部分:
Drawing the layout is a two pass process: a measure pass and a layout pass.
所以一个view执行OnDraw时最关键的是measure和layout。其实这很好理解的,一个view需要绘制出来,那么必须知道他要占多大的空间也就是measure,还得知道在哪里绘制,也就是把view放在哪里即layout。把这两部分掌握好也就可以随意自定义view了。至于viewGroup中如何绘制就参考官方文档,其实就是一个分发绘制,直到child是一个view自己进行绘制。
2、重写一个view一般情况下只需要重写onDraw()方法。那么什么时候需要重写onMeasure()、onLayout()、onDraw() 方法呢,这个问题只要把这几个方法的功能弄清楚就应该知道怎么做了。
①、如果需要绘制View的图像,那么需要重写onDraw()方法。(这也是最常用的重写方式。)
②、如果需要改变view的大小,那么需要重写onMeasure()方法。
③、如果需要改变View的(在父控件的)位置,那么需要重写onLayout()方法。
④、根据上面三种不同的需要你可以组合出多种重写方案。
3、按类型划分,自定义View的实现方式可分为三种:自绘控件、组合控件、以及继承控件。
注意2:
View的三种测量模式
ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:
1、EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;
2、AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;
3、UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。
【备注】:
上面的每一行都有一个一般,意思上述不是绝对的,对于childView的mode的设置还会和ViewGroup的测量mode有一定的关系;当然了,这是第一篇自定义ViewGroup,而且绝大部分情况都是上面的规则,所以为了通俗易懂,暂不深入讨论其他内容。
注意3:
三种类型的自定义View
(1)自绘控件:
1、概念:自绘控件的意思就是,这个View上所展现的内容全部都是自己绘制出来的。
2、自绘控件的步骤:
1、绘制View :
绘制View主要是onDraw()方法中完成。通过参数Canvas来处理,相关的绘制主要有drawRect、drawLine、drawPath等等。
Canvas绘制的常用方法:
drawColor() 填充颜色
drawLine() 绘制线
drawLines() 绘制线条
drawOval() 绘制圆
drawPaint()
drawPath() 绘制路径
drawPicture() 绘制图片
drawPoint() 绘制点
drawPoints() 绘制点
drawRGB() 填充颜色
drawRect() 绘制矩形
drawText() 绘制文本
drawTextOnPath() 在路径上绘制文本
2、刷新View :(刷新view的方法这里主要有:)
invalidate(int l,int t,int r,int b)
刷新局部,四个参数分别为左、上、右、下
invalidate()
整个view刷新。执行invalidate类的方法将会设置view为无效,最终重新调用onDraw()方法。
invalidate()是用来刷新View的,必须是在UI线程中进行工作。在修改某个view的显示时,调用invalidate()才能看到重新绘制的界面。invalidate()的调用是把之前的旧的view从主UI线程队列中pop掉。
invalidate(Rect dirty)
刷新一个矩形区域
3、控制事件:(例如处理以下几个事件)
onSaveInstanceState() 处理屏幕切换的现场保存事件
onRestoreInstanceState() 屏幕切换的现场还原事件
onKeyDown() 处理按键事件
onMeasure() 当控件的父元素正要放置该控件时调用
(2)组合控件:
概念:
组合控件的意思就是,不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
(3)继承控件
概念:
继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。
案例一:
自定义ImageView控件:自定义ImageView控件继承ImageView
备注:
短句:使用短句生成构造方法 ViewConstructor
自定义ImageView控件类
public class SmartImageView extends ImageView {
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 1:
//自定义ImageView类对象调用此方法
setImageBitmap((Bitmap) msg.obj);
break;
case 2:
setImageResource(msg.arg1);
break;
}
}
};
public SmartImageView(Context context) {
this(context, null);
}
public SmartImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SmartImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setSmartImageView(final String url, final int resId){
new Thread(){
@Override
public void run() {
super.run();
Message msg = Message.obtain();
byte[] data = OkHttpUtils.getByteArrayByUrl(url);
if(data != null){
Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);
msg.what = 1;
msg.obj = bitmap;
}else{
msg.what = 2;
msg.arg1 = resId;
}
handler.sendMessage(msg);
}
}.start();
}
}
xml:
<com.sign.days28selfview01.SmartImageView
android:id="@+id/ivPic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
android:src="@mipmap/ic_launcher"
android:contentDescription="@string/app_name"/>
MainActivity:
public void download(View view) {
//调用自定义ImageView控件类的设置图片方法
ivPic.setSmartImageView(url,R.mipmap.ic_launcher);
}
案例二:自定义TextView控件
自定义TextView控件类
public class Rular extends TextView {
public Rular(Context context) {
this(context, null);
}
public Rular(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public Rular(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 设置Ruler中的文字位置。相当于属性中的android:gravity
setGravity(Gravity.BOTTOM);
}
/**
* 重写该方法,实现自己“绘制”控件的样式
* 该方法无须手动调用, 系统在绘制该控件时会自动调用该方法绘制该控件
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Paint(画笔
Paint p = new Paint();
// 设置画笔颜色
p.setColor(Color.YELLOW);
// getWidth()为获取控件的宽度
int w = getWidth()/10;
int top = 1;
for (int i = 0;i<10;i++){
// canvas的drawRect(float left,float top,float right,float bottom,Paint paint)
// 参数分别为左上右下(顺时针)(左上右下离控件的x,y轴的距离),画笔对象
// Canvas(画布)
canvas.drawRect(i,top,w+i*w,top+w,p);
}
// for循环语句相当于
// canvas.drawRect(0,top,getWidth(),top+w,p);
}
}
布局文件:
MainActivity添加控件时会自动找到控件对应的类,并自动调用其中方法对控件进行相应设置
<com.sign.days28selfview02ruler.Rular
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#0cf"
android:text="0cm" />
<com.sign.days28selfview02ruler.Rular
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#acc"
android:text="1cm" />
<com.sign.days28selfview02ruler.Rular
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#f0d"
android:text="2cm" />
<com.sign.days28selfview02ruler.Rular
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#ae0"
android:text="3cm" />
<com.sign.days28selfview02ruler.Rular
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#0d0"
android:text="4cm" />
案例三:
组合控件:
将progressbar与button组合在一起,点击button按钮后,发送一个消息,在主线程中更新progressBar进度并判断是否到100%若不是继续发送消息并根据进度设置Button上文字,若是则不再发送消息并设置Button文字为下载完成
(实际上是ProgressBar在下面,Button在上面,看到的ProgressBar进度实际上是ProgressBar的边框在更新)
备注:在Android的layout样式定义中,可以使用xml文件方便的实现,有时候为了模块的复用,使用include标签可以达到此目的。
在activity_main中:
<include layout="@layout/my_layout"></include>
引入my_layout布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical"
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.sign.days28selfview03progressbutton.MainActivity">
<ProgressBar
android:id="@+id/pbShow"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="25dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/my_back"/>
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="#0fc"
android:textSize="16sp"
android:onClick="start"
android:text="start"/>
</LinearLayout>
此处注意:
progressbar的progressDrawable属性为一个xml文件
还不太理解??????
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<!--<clip
android:drawable="@drawable/btn_back_pre"
android:gravity="left">
</clip>-->
<clip
android:clipOrientation="horizontal"
android:gravity="left"
android:drawable="@drawable/pb_back">
</clip>
</item>
</layer-list>
此处再注意:
第二个clip节点的drawable为名为pb_back的xml文件
**备注:**Android中常常使用shape来定义控件的一些显示属性
参考自:
http://blog.youkuaiyun.com/by317966834/article/details/8773518
http://blog.youkuaiyun.com/harvic880925/article/details/41850723
pd_back.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--设置渐变
startColor centerColor endColor 起始、中间、结束颜色
type 渐变模式 linear 线性渐变 radial 径向渐变 需要指定半径-->
<gradient
android:centerColor="#0cd"
android:centerX="0"
android:centerY="0"
android:endColor="#f00"
android:gradientRadius="50"
android:startColor="#0f0"
android:type="radial" />
<!--设置填充-->
<solid android:color="@android:color/holo_blue_bright" />
<!--设置dashGap和dashWidth则边框显示为虚线,dashGap为破折线空隙,dashWidth为破折线长度
dashGap为0dp时为实线 dashWidth需要指定-->
<!--width为边框的宽度-->
<stroke
android:width="5dp"
android:color="@android:color/holo_orange_light"
android:dashGap="2dp"
android:dashWidth="0dp" />
<!--设置矩形的圆角半径-->
<corners android:radius="5dp" />
</shape>
案例四:
自定义ProgressView继承View
效果:一个逐渐转动变大的扇形
在values目录下添加attrs.xml文件
<resources>
<!--attrs.xml文件 该文件是定义属性名和格式的地方,
declare-styleable是给自定义控件添加自定义属性用的
需要用<declare-styleable name="ToolBar"></declare-styleable>包围所有属性。
其中name为该属性集的名字,主要用途是标识该属性集-->
<declare-styleable name="MyProgressView">
<attr name="sweepStep" format="integer"/>
<attr name="padding" format="integer"/>
<attr name="startAngle" format="integer"/>
<attr name="circleColor" format="color|reference"/>
<attr name="sweepColor" format="color|reference"/>
</declare-styleable>
</resources>
自定义ProgressView控件
public class MyProgressView extends View {
private static final int DEFAULT_WIDTH = 100;
private static final int DEFAULT_HEIGHT = 100;
private int sweepAngle = 0;//扇形扫过的角度
private int sweepStep = 5;//扇形添加的角度
private int padding = 5;//设置扇形与圆之间距离
private int startAngle = -90;//设置扇形开始的角度
private int circleColor = Color.GREEN;//圆的背景颜色
private int sweepColor = Color.BLUE;//扇形的颜色
public MyProgressView(Context context) {
this(context, null);
}
public MyProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyProgressView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
/**
* TypedArray实例是个属性的容器,context.obtainStyledAttributes()方法返回得到
* context.obtainStyledAttributes(AttributeSet set,@StyleableRes int[] attrs)
* set :节点的属性集合
* attrs :包含attr节点的StyleableRes节点name标识
*/
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyProgressView);
if(array != null){
// 分别获取MyProgress节点中的各个属性
sweepStep = array.getInteger(R.styleable.MyProgressView_sweepStep,sweepStep);
startAngle = array.getInteger(R.styleable.MyProgressView_startAngle,startAngle);
padding = array.getInteger(R.styleable.MyProgressView_padding,padding);
sweepColor = array.getInteger(R.styleable.MyProgressView_sweepColor,sweepColor);
circleColor = array.getInteger(R.styleable.MyProgressView_circleColor,circleColor);
// array复用 作用:在TypedArray后调用recycle主要是为了缓存。当recycle被调用后,这就说明这个对象从现在可以被重用了
array.recycle();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint p = new Paint();
// 消除锯齿
p.setAntiAlias(true);
// 画圆
// 设置圆的颜色
p.setColor(circleColor);
// 前两个参数分别确定圆心x、y坐标,第三个参数为圆的半径,第四个参数为画笔
canvas.drawCircle(getWidth()/2,getWidth()/2,getWidth()/2,p);
// 画扇形
// 重新设置画笔颜色为扇形颜色
p.setColor(sweepColor);
/**
* public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
oval :指定圆弧的外轮廓矩形区域。
startAngle: 圆弧起始角度,单位为度。0度为水平向左
sweepAngle: 圆弧扫过的角度,顺时针方向,单位为度,从右中间开始为零度。
useCenter: 如果为True时,在绘制圆弧时将圆心包括在内,通常用来绘制扇形。
paint: 画笔
*/
canvas.drawArc(new RectF(padding,padding,getWidth()-padding,getWidth()-padding),startAngle,sweepAngle,true,p);
// 每画完一个扇形累加已扫过的角度
sweepAngle += sweepStep;
// 当已扫过的角度为360时,将已扫过的角度置空
sweepAngle= sweepAngle >= 360 ? 0:sweepAngle;
// 刷新整个界面(每次画完一个扇形就刷新整个界面,看起来就像扇形在更新进度)
invalidate();
}
/**
* 设置控件自身的宽和高
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthMode){
case MeasureSpec.AT_MOST:
widthSize = DEFAULT_WIDTH;
heightSize = DEFAULT_HEIGHT;
break;
case MeasureSpec.EXACTLY:
widthSize = heightSize = Math.min(widthSize,heightSize);
break;
case MeasureSpec.UNSPECIFIED:
break;
}
// 这个方法决定了当前View的大小
setMeasuredDimension(widthSize,heightSize);
}
}
activity_main文件:
备注: xmlns:app=”http://schemas.android.com/apk/res-auto”
android中xml中有些控件的属性里面有 “app:..” ,此处的app:是什么意思?和一般的android:有什么区别?
答:这两个是声明的不同的命名空间,android的是系统的,app是自定义的。Android自定义控件的属性,在xml中使用自己自定义的attr的时候,其中有一步就是要自定义一个xml的命名空间后然后再给自定义属性赋值。
参考自:http://zhidao.baidu.com/link?url=628WYzFtOkZXw2aAau3vV8yZhBWN6OApXGFxn5IBTmy7OUUekmOeaQxpWpc9RLr-ecsYG-QzpA1Tz7ncbrEApC5RyYwRIFRvBzXOteg0aky
<!--注意:此处使用的为wrap_content,在MyProgressView中会走到case MeasureSpec.AT_MOST
设置宽和高为100,但其为100px,所以与下一个控件的大小100dp并不相等-->
<com.sign.days28selfviw04progress.MyProgressView
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<com.sign.days28selfviw04progress.MyProgressView
android:layout_width="100dp"
android:layout_height="100dp"
app:circleColor="#0f0"
app:sweepColor="#0cc"
app:padding="26"
app:startAngle="0"
app:sweepStep="5"/>