Android自定义View笔记
Posted 炎之铠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android自定义View笔记相关的知识,希望对你有一定的参考价值。
自定义View的分类
自定义View的有好几种分类,以我目前的阅历我把它分成4种:
- 特定的View的子类:android的API已经为我们提供了不少可以使用的View,如TextView、ImageView、Button等等,但是有时候我们需要在这些基础的View上扩展一些功能,例如在Button里绑定一个TextWatch监测若干个EditText的输入情况时,就是继承Button类,在它的子类进行扩展了。这种自定义View实现难度低,不需要自己支持wrap_content和padding等属性,非常常见。
- 特定的ViewGroup子类:Android的API也为我们提供了不少可以使用的ViewGroup,如LinearLayout、RelativeLayout等等,但是有时候我们想把实现同一个需求若干个View组合起来,就可以用这种方式的自定义View来打包了。这种自定义View的实现难度低,也不需要自己处理ViewGroup对每个子View的测量和布局,非常常见。
- View的子类:View是一个很基础的父类,有一个空的onDraw()方法,继承它首先就是要实现这个方法,在里面利用Canvas画出自己想要的内容,不然View是不会显示任何东西的,使用这种自定义View主要用于实现一些非常规的图形效果,例如一些动态变化的View等等。这种自定义View的实现难度比较高,除了需要自己重写onDraw(),还要自己支持wrap_content和padding等属性,不过这种View也很常见。
- ViewGroup的子类:ViewGroup是用于实现View的组合布局的基础类,直接继承ViewGroup的子类主要是用于实现一些非常规的布局,即不同于官方API给出的LinearLayout等这些的布局。这种这种自定义View的实现难度高,需要处理好ViewGroup和它子View的测量和布局,比较少见。
进入自定义View
下面是4种自定义View所需的步骤,有一些是必须的,有一些事根据实际需求选择的。
下面我们来一个一个的来学习,其中重写onDraw()和重写onMeasure()是通过写自定义View的例子学习,重写自身和子类的onMesure()和onLayout()是通过写自定义ViewGroup来学习。
自定义属性
想要实现自定义的功能,我们有时候就需要一些自己定义的属性,怎么让这些属性可以通过在xml上设置呢?只需要在res/value文件夹里新建一个attrs.xml(名字随便,建立位置对就行):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="Color" format="color"/>
<attr name="inVelocityX" format="integer"/>
<attr name="inVelocityY" format="integer"/>
<attr name="Text" format="string"/>
<attr name="TextColor" format="color"/>
<declare-styleable name="BallView">
<attr name="color"/>
<attr name="inVelocityX" />
<attr name="inVelocityY" />
<attr name="Text" />
<attr name="TextColor"/>
</declare-styleable>
</resources>
BallView就是我demo里面的自定义View名字,在declare-styleable外面声明一些自定义属性和属性的类型format,在里面申明BallView需要哪些属性(当然也可以直接在declare-styleable里面声明属性的format,这样就不需要在外面声明了,但是这样的话这些属性也不能被另一个自定义View重用)。
关于属性的format有很多种,reference,color,boolean等等,想看全部可以参考这里。
在attrs.xml声明了属性之后,就可以在View的xml里用了,不过首先要在根ViewGroup里声明变量空间:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<scut.com.learncustomview.BallView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
cust:color="#ff0000"
cust:Text="我是一个球"
cust:TextColor="#ffffff"
cust:TextSize= "34"
cust:inVelocityX="6"
cust:inVelocityY="6"/>
</RelativeLayout>
然后我们就要在自定义View里面获取这些属性了,自定义View的构造函数有4个,自定义View必须重写至少一个构造函数:
public BallView(Context context)
super(context);
public BallView(Context context, AttributeSet attrs)
super(context, attrs);
public BallView(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
//API21之后才使用
public BallView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
super(context, attrs, defStyleAttr, defStyleRes);
4个构造函数中:如果View是在Java代码里面new的,则调用第一个构造函数;如果是在xml里声明的,则调用第二个构造函数,我们所需要的自定义属性也就是从这个AttributeSet参数传进来的;第三第四个构造函数不会自动调用,一般是在第二个构造主动调用(例如View有style属性的时候)。
//球的x,y方向速度
private int velocityX = 0,velocityY = 0;
//球的颜色
private int color;
//球里面的文字
private String text;
//文字的颜色
private int textColor;
public BallView(Context context, AttributeSet attrs)
super(context, attrs);
//获取自定义属性数组
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.BallView, 0, 0);
int n = a.getIndexCount();
for (int i = 0;i < n;i++)
int attr = a.getIndex(i);
switch (attr)
case R.styleable.BallView_inVelocityX:
velocityX = a.getInt(attr,0);
break;
case R.styleable.BallView_inVelocityY:
velocityY = a.getInt(attr,0);
break;
case R.styleable.BallView_color:
color = a.getColor(attr,Color.BLUE);
break;
case R.styleable.BallView_Text:
text = a.getString(attr);
break;
case R.styleable.BallView_TextColor:
textColor = a.getColor(attr,Color.RED);
break;
可以看到输出:
System.out: text:球
System.out: textColor:-1
System.out: velocityX:3
System.out: velocityY:3
System.out: color:-65536
重写onMeasure()
关于重写onMeasure()的解释,我觉得用BallView不合适,于是就另外开了个TestMeasureView进行测试:
下面是没有重写onMeasure()来支持wrap_content的例子:
public class TestMeasureView extends View
private Paint paint;
public TestMeasureView(Context context)
super(context);
public TestMeasureView(Context context, AttributeSet attrs)
super(context, attrs);
public TestMeasureView(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
@Override
protected void onDraw(Canvas canvas)
super.onDraw(canvas);
canvas.drawColor(Color.BLUE);
在xml上使用这个View:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<scut.com.learncustomview.TestMeasureView
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
得出的结果是这样的:
这就是为什么View的之类要自己支持wrap_parent的原因了,如果不重写wrap_parent就被当成match_parent。具体原因可以看一下View的Measure过程,这个是必须了解的,下面的图(从链接里面盗的)是关键。
了解Measure过程之后我们发现我们现在这个TestMeasureView的长宽参数是由父View的测量模式(RelativeLayout的EXACTLY)和自身的参数(wrap_content)决定的(AT_MOST),所以我们就可以重写onMeasure()让View支持wrap_content了,下面网上流传很广的方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);
int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);
int width = wSpeSize;
int height = hSpeSize;
if (wSpeMode == MeasureSpec.AT_MOST)
//在这里实现计算需要wrap_content时需要的宽度,这里我直接当作赋值处理了
width =200;
if (hSpeMode == MeasureSpec.AT_MOST)
//在这里实现计算需要wrap_content时需要的高度,这里我直接当作赋值处理了
height = 200;
//传入处理后的宽高
setMeasuredDimension(width,height);
结果是成功的:
网上的很多都是这样做,通过判断测量模式是否AT_MOST来判断View的参数是否是wrap_content,然而,通过上面的表我们发现View的AT_MOST模式对应的不只是wrap_content,还有当父View是AT_MOST模式的时候的match_parent,如果我们这样做的话,父View是AT_MOST的时候这个自定义View的match_parent不就失效了吗。
测试一下,我们把TestMeasureView长宽参数设置为match_parent,然后在外面再包一个模式为AT_MOST的父View(把父View的宽高都设为wrap_content,这样就确保了模式是AT_MOST,UNSPECIFIED因为不会出现在这里可以忽略):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<scut.com.learncustomview.TestMeasureView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</RelativeLayout>
运行一下,结果果然是match_parent失效:
所以说看到的东西要思考一下,才能真正地转化为自己的,然后这个怎么解决呢,很简单,直接在onMeasure里面判断参数是否wrap_content就好:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);
int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);
int width = wSpeSize;
int height = hSpeSize;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT)
//在这里实现计算需要wrap_content时需要的宽
width =200;
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT)
//在这里实现计算需要wrap_content时需要的高
height =200;
//传入处理后的宽高
setMeasuredDimension(width,height);
然后我把参数设回wrap_content(xml就不贴代码了),结果是正确的:
但是这种方法有一个缺陷,就是可能会将UNSPECIFIED的情况也覆盖掉,但是UNSPECIFIED一般只出现在系统内部的View,不会出现在自定义View,而且当它出现的时候也可以加个判断按情况解决。
重写onDraw()
这里就是利用onDraw()给出的Canvas画出各种东西了,具体可以参考我之前的笔记。这里是BallView的onMeasure()方法和onDraw(),通过以下代码,可以实现在wrap_content的时候根据字的内容长度画出相应的圆,然后可以根据给出的速度移动,遇到“墙会碰撞”。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
int width = wSpeSize ;
int height = hSpeSize;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT)
//在这里实现计算需要wrap_content时需要的宽高
width = bounds.width();
else if(getLayoutParams().width != ViewGroup.LayoutParams.MATCH_PARENT)
width = getLayoutParams().width;
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT)
//在这里实现计算需要wrap_content时需要的宽高
height =bounds.height();
else if(getLayoutParams().height != ViewGroup.LayoutParams.MATCH_PARENT)
height = getLayoutParams().height;
//计算半径
radius = Math.max(width,height)/2;
//传入处理后的宽高
setMeasuredDimension((int) (radius*2+1), (int) (radius*2+1));
@Override
protected void onDraw(Canvas canvas)
super.onDraw(canvas);
canvas.drawCircle(getWidth()/2,getHeight()/2,radius,paintFill);
//让字体处于球中间
canvas.drawText(text,getWidth()/2,getHeight()/2+bounds.height()/2,paintText);
checkCrashScreen();
offsetLeftAndRight(velocityX);
offsetTopAndBottom(velocityY);
postInvalidateDelayed(10);
//检测碰撞,有碰撞就反弹
private void checkCrashScreen()
if ((getLeft() <= 0 && velocityX < 0))
velocityX = -velocityX ;
if (getRight() >= screenWidth && velocityX > 0)
velocityX = -velocityX ;
if ((getTop() <= 0 && velocityY < 0))
velocityY = -velocityY ;
if (getBottom() >= screenHeight -sbHeight && velocityY > 0)
velocityY = -velocityY ;
最后结果:
重写自身和子类的onMesure()和onLayout()
上面是以自定义View为例子,这次就以一个自定义ViewGroup做为例子,做一个很简单的可以按照斜向下依次排列View的ViewGroup,类似于LinearLayout。要做一个新的ViewGroup,首先就是要重写它的onMesure()方法,让它可以按照需求测量子View和自身的宽高,还可以在这里支持wrap_content。
onMesure()和onLayout()是干什么的呢?为什么需要重写的是它们?因为View的绘制过程大概是Measure(测量)→Layout(定位)→Draw(绘图)三个过程,至于具体是怎样的呢?可以看工匠若水的这篇文章,看不懂没关系,可以看图。。。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
// 计算出所有的childView的宽和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
int cCount = getChildCount();
int width = 0;
int height = 0;
//处理WRAP_CONTENT情况,把所有子View的宽高加起来作为自己的宽高
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT)
for (int i = 0; i < cCount; i++)
View childView = getChildAt(i);
width += childView.getMeasuredWidth();
else
width = sizeWidth;
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT)
for (int i = 0; i < cCount; i++)
View childView = getChildAt(i);
height += childView.getMeasuredHeight();
else
height =sizeHeight;
//传入处理后的宽高
setMeasuredDimension(width,height);
还有通过重写onLayout()把子View一个个排序斜向放好:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
int cCount = getChildCount();
int sPointX = 0;
int sPointY = 0;
int cWidth = 0;
int cHeight = 0;
//遍历子View,根据它们的宽高定位
for (int i = 0; i < cCount; i++)
View childView = getChildAt(i);
//这里使用getMeasuredXXX()方法是因为还没layout完,使用getWidth()和getHeight()获取会得不到正确的宽高
cWidth = childView.getMeasuredWidth();
cHeight = childView.getMeasuredHeight();
//定位
childView.layout(sPointX,sPointY,sPointX + cWidth,sPointY + cHeight);
sPointX += cWidth;
sPointY += cHeight;
结果:
参数为WRAP_CONTENT的时候,成功地显示了:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cust="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<scut.com.learncustomview.InclinedLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#000fff">
<TextView
android:layout_width="50dp"
android:layout_height="50dp"
android:text="1"
android:background="#fff000"/>
<TextView
android:layout_width="20dp"
android:layout_height="50dp"
android:text="2"
android:background="#00ff00"/>
<TextView
android:layout_width="50dp"
android:layout_height="30dp"
android:text="3"
android:background="#ff0000"/>
</scut.com.learncustomview.InclinedLayout>
</RelativeLayout>
还有match_parent的时候:
这样斜向下排列的ViewGroup就完成了,这些只是最简单的一个demo,用于我们熟悉自定义View的步骤,掌握了这些,复杂的自定义View也可以一步一步地完成了。
发布项目到JCenter
在github上能看到很多人家做好的自定义View,使用人家造好的轮子是一件很方便的事情,而且很多都不用自己手动下载,只有在AndroidStudio里的Gradle添加依赖项complie ‘XXXX’就行了,这是怎样弄的呢?就是作者把项目上传到JCenter或者Maven了,要怎么做才能上传呢?
读取上面这两篇博文,按上面的的步骤做
http://blog.csdn.net/lmj623565791/article/details/51148825
http://blog.csdn.net/u012375207/article/details/56840217
我有几点补充:
1. 注册邮箱不能是qq邮箱,163邮箱(不知道是不是所有中国的都不行),别注册错了企业版,企业版右上角会有个30天倒计时的,点进去再点 Cancel Enterprise (trial)就可以换成个人版了,不用重新注册。
2. 要新建一个module把想要上传的内容装起来,不然可能会出错: Could not get unknown property ‘main’ for SourceSet container.
3. 注意要翻墙。。。
4. compile成功后引用是用封装上传的时候的包名,就像我这个上传的Module里面的包是scut.com.ballviewdemo需要import的时候也是import这个。
5. 最后是我这个项目上传之后的结果:
compile 'com.yanzhikaijky:LearnCustomView:1.0.1'
参考资料:
(除了上面引用的)
《Android开发艺术探索》——任玉刚
以上是关于Android自定义View笔记的主要内容,如果未能解决你的问题,请参考以下文章
Android自定义View(三深入解析控件测量onMeasure)