1、前言
在上篇博客中我主要介绍了自定义属性的定义和获取,还有如何在布局文件添加我们的自定义控件。这几乎是自定义控件中必不可少的两步,而onMeasure()、onDraw()方法如果是在我们讲的TopBar这样的只需修改几个属性的控件中使用是可以不做的。onLayout()就更不必说了,它是来设置子View的位置的。所以这篇博客我会仔细讲解这几个方法。
2、onMeasure解析
我们在TopBar中继承的是RelativeLayout,所以已经复写好了View类中的onMeasure方法,所以我们在用的时候不会出现宽高的问题,但是如果继承的是View或ViewGroup的话,就必须复写onMeasure()方法了,我们来看看View中的onMeasure()的代码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。
所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法,再看看RelativeLayout的代码,这个比较长,就用动图演示一下,大家有兴趣可以自己去看源代码:
大家也不用管代码的功能是什么,只要知道RelativeLayout已经帮我们考虑了很多情况,我们不用太考虑宽高的测量。
对于onMeasure(),首先我们要理解的是widthMeasureSpec, heightMeasureSpec这两个参数,onMeasure()由包含这个View的具体的ViewGroup调用,因此值也是从这个ViewGroup中传入的。
子View的这两个参数,由ViewGroup中的layout_width,layout_height和padding以及View自身的layout_margin共同决定。权值weight也是尤其需要考虑的因素,有它的存在情况可能会稍微复杂点。
了解了这两个参数的来源,还要知道这两个值的作用。拿heightMeasureSpec来说,这个值由高32位和低16位组成,高32位保存的值叫specMode,可以通过MeasureSpec.getMode()获取;低16位为specSize,同样可以由MeasureSpec.getSize()获取。
那么specMode和specSize的作用有是什么呢?要想知道这一点,我们需要知道所有的View的onMeasure()的最后一行都会调用的setMeasureDimension()方法的作用——这个函数调用中传进去的值是View最终的视图大小。也就是说onMeasure()中之前所作的所有工作都是为了最后这一句话服务的。
我们知道在ViewGroup中,给View分配的空间大小并不是确定的,有可能随着具体的变化而变化,而这个变化的条件就是传到specMode中决定的,specMode一共有三种可能:
- MeasureSpec.EXACTLY:父视图希望子视图的大小应该是specSize中指定的。一般是设置了明确的值或者是MATCH_PARENT。
- MeasureSpec.AT_MOST:子视图的大小最多是specSize中指定的值,也就是说不建议子视图的大小超过specSize中给定的值。表示子布局限制在一个最大值内,一般为WARP_CONTENT。
- MeasureSpec.UNSPECIFIED:我们可以随意指定视图的大小,表示子布局想要多大就多大,很少使用。
我们写个例子,打印一下log,这样能让大家更能理解,specMode如何取值,specSize如何计算:
public class CustomView extends View {
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
Log.d("TAG", "---widthSize = " + widthSize + "");
Log.d("TAG", "---heightSize = " + heightSize + "");
if(widthMode == MeasureSpec.AT_MOST){
Log.d("TAG", "---AT_MOST---");
}
if(widthMode == MeasureSpec.EXACTLY){
Log.d("TAG", "---EXACTLY---");
}
if(widthMode == MeasureSpec.UNSPECIFIED){
Log.d("TAG", "---UNSPECIFIED---");
}
if(heightMode == MeasureSpec.AT_MOST){
Log.d("TAG", "---AT_MOST---");
}
if(heightMode == MeasureSpec.EXACTLY){
Log.d("TAG", "---EXACTLY---");
}
if(heightMode == MeasureSpec.UNSPECIFIED){
Log.d("TAG", "---UNSPECIFIED---");
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="200dp"
android:layout_height="300dp"
android:layout_marginTop="30dp"
android:background="@android:color/darker_gray"
android:paddingTop="20dp">
<com.ht.animator.CustomView
android:layout_width="150dp"
android:layout_height="match_parent"
android:layout_marginTop="15dp"
android:background="@android:color/holo_red_light"
android:paddingTop="10dp" />
</LinearLayout>
</LinearLayout>
log的输出为:
可以看到specMode的值的确如我介绍的那样,layout_height和layout_width的值为match_parent或精确的值则对应的specMode为EXACTLY,wrap_content没有测试但就是AT_MOST,大家可以自己试试。这个值与父视图的layout_width是没有关系的。
有点要注意的就是,xml中用的单位是dp,log中得到的单位是px,我所使用的虚拟机的dpi为360,所以屏幕密度为2.0,只需要进行简单的换算即得px = 2.0 * dp。
widthSize = 2.0 * layout_width = 300;
heightSize = layout_height(LinearLayout) * 2.0 - paddingTop(LinearLayout) * 2.0 - layout_marginTop * 2.0 = 600 - 40 - 30 = 530。
影响heightSize的因素为:父视图的layout_height和paddingTop以及自身的layout_marginTop。但是我们不要忘记有weight时的影响。
3、TopBar测量
这篇博客的例子同样是自定义TopBar,不过这次我们就不继承RelativeLayout了,这意味着我们必须重写它的onMeasure()。不过在写onMeasure()方法之前,我们要先了解测量都需要用到什么样的参数。我觉得首先肯定是各个控件的宽高,然后我们有可能为我们的控件在父布局中设置margin,父布局中也会设置padding,所以我们要计算的应该就是这几个参数的值。对于我们这个例子,我们只需要ViewGroup能够支持margin即可,那么我们直接使用系统的MarginLayoutParams。
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
{
return new MarginLayoutParams(getContext(), attrs);
}
重写父类的该方法,返回MarginLayoutParams的实例,这样就为我们的ViewGroup指定了其LayoutParams为MarginLayoutParams。我们就可以直接使用margin了。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
int cCount = getChildCount();
int cWidth = 0;
int cHeight = 0;
MarginLayoutParams cParams = null;
for (int i = 0; i < cCount; i++) {
View childView = getChildAt(i);
cWidth = childView.getMeasuredWidth();
cHeight = childView.getMeasuredHeight();
cParams = (MarginLayoutParams) childView.getLayoutParams();
width += cWidth + cParams.leftMargin + cParams.rightMargin;
height = Math.max(height, cHeight + cParams.topMargin + cParams.bottomMargin);
}
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? sizeWidth
: width, (heightMode == MeasureSpec.EXACTLY) ? sizeHeight
: height);
}
首先获取该ViewGroup父容器为其设置的计算模式和尺寸,大多情况下,只要不是wrap_content,父容器都能正确的计算其尺寸。所以我们自己需要计算如果设置为wrap_content时的宽和高,那就是通过其childView的宽和高来进行计算。
通过ViewGroup的measureChildren()方法为其所有的孩子设置宽和高,此行执行完成后,childView的宽和高都已经正确的计算过了。
根据childView的宽和高,以及margin,计算ViewGroup在wrap_content时的宽和高。
最后,如果宽高属性值为wrap_content,则设置为我们计算的值,否则为其父容器传入的宽和高。
因为我们要完成的是TopBar,所以我们都是设置为match_parent,这种写法,仅供演示。
4、onLayout
onLayout方法是ViewGroup中子View的布局方法,用于放置子View的位置。放置子View很简单,只需在重写onLayout方法,然后获取子View的实例,调用子View的layout方法实现布局。在实际开发中,一般要配合onMeasure测量方法一起使用。
onLayout方法:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
该方法在ViewGroup中定义是抽象函数,继承该类必须实现onLayout方法,而ViewGroup的onMeasure并非必须重写的。View的放置都是根据一个矩形空间放置的,onLayout传下来的l,t,r,b分别是放置父控件的矩形可用空间(除去margin和padding的空间)的左上角的left、top以及右下角right、bottom值。
layout:
public void layout(int l, int t, int r, int b);
该方法是View的放置方法,在View类实现。调用该方法需要传入放置View的矩形空间左上角left、top值和右下角right、bottom值。这四个值是相对于父控件而言的。例如传入的是(10, 10, 100, 100),则该View在距离父控件的左上角位置(10, 10)处显示,显示的大小是宽高是90(参数r,b是相对左上角的),这有点像绝对布局。我们确定了子View的位置也就是l,t,r,b四个值后就用这个方法把子View放到那去。
平常开发所用到RelativeLayout、LinearLayout、FrameLayout…这些都是继承ViewGroup的布局。这些布局的实现都是通过都实现ViewGroup的onLayout方法,只是实现方法不一样而已。
在自定义View中,onLayout配合onMeasure方法一起使用,可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法,动态获取子View和子View的测量大小,然后进行layout布局。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int cCount = getChildCount();
int cWidth = 0;
int cHeight = 0;
MarginLayoutParams cParams = null;
int cl = 0, ct = 0, cr = 0, cb = 0;
for (int i = 0; i < cCount; i++) {
View childView = getChildAt(i);
cWidth = childView.getMeasuredWidth();
cHeight = childView.getMeasuredHeight();
cParams = (MarginLayoutParams) childView.getLayoutParams();
switch (i) {
case 0:
cl = cParams.leftMargin;
ct = cParams.topMargin;
break;
case 1:
cl = getWidth() - cWidth - cParams.rightMargin;
ct = cParams.topMargin;
break;
}
cr = cl + cWidth;
cb = cHeight + ct;
childView.layout(cl, ct, cr, cb);
}
}
因为还要讲解onDraw方法,所以这个例子我只使用了两个Button,打算在ViewGroup的中间去主动绘制文字。
这个逻辑是很清晰的,我们要实现的TopBar,一个Button在最左边,另一个则在最右边,知道位置自然很好获取。
写到这里,布局已经可以出现一些东西了,让我们运行看看结果是不是如我们所想:
可以看到我们定义的两个Button已经到指定的地方了,最后就是绘制我们的文字。
5、onDraw
onDraw方法其实反而是最常用的自定义时的方法,因为就像我在上篇博客所说的,系统提供的控件已经能满足大部分效果,我们大多数是不满意它的样子,而要改变它的样子自然要重写onDraw方法。
使用onDraw方法,首先要对Canvas和Paint这两个类有所料解,它们一个是画布,一个是画笔,它们的使用是多种多样的,这里我就不提啦,有兴趣的朋友可以去查阅相关资料。
我们要绘制的是比较简单的,实现的效果就是在ViewGroup的中间绘制文字,我们需要的就是中间的那一块矩形。
private Rect mBound;
private Paint mPaint;
public CustomTopBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
setBackgroundColor(0xFFF59563);
mPaint = new Paint();
mPaint.setTextSize(titleTextSize);
mBound = new Rect();
layout();
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.getTextBounds(title, 0, title.length(), mBound);
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
mPaint.setColor(titleTextColor);
canvas.drawText(title, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
}
mBound就是我们为文字设置的矩形,我们将测量好的控件绘制颜色,在中间绘制文字相信大家都能看懂。
<?xml version="1.0" encoding="utf-8"?>
<com.ht.animator.CustomTopBar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lht="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
lht:leftBackground="@android:color/holo_blue_bright"
lht:leftTextColor="#FFFFFF"
lht:leftText="Back"
lht:rightBackground="@android:color/holo_blue_bright"
lht:rightTextColor="#FFFFFF"
lht:rightText="More"
lht:title="自定义标题"
lht:titleTextColor="#000000"
lht:titleTextSize="20sp">
</com.ht.animator.CustomTopBar>
都设置好了,就看看我们运行的结果吧。
6、用户交互
一个精美的布局是我们自定义控件所要达到的基础,但是我们可不能满足与此,我们还需要给它添加用户交互,否则就算界面再漂亮也只是空壳子而已。
我们的这个例子没有onTouch方法的处理,不过有两个Button,我们可以为它们的点击事件设置回调方法,让大家看看如何使用回调。
我们不可能每次要修改点击事件就去文件中修改代码,应该在调用这个控件的时候为里面的Button添加事件,我们可以写个像onClick的接口回调。
private topbarClickListener listener;
public interface topbarClickListener {
public void leftClick();
public void rightClick();
}
public void setOnTopbarClickListener(topbarClickListener listener) {
this.listener = listener;
}
Button的setOnClickListener是触发点击事件,真正实现点击之后内容的是new OnClickListener()这个匿名内部类,所以我们仿照它写,定义了topbarClickListener这个接口,里面的方法分别实现左右Button的点击逻辑,然后就是写个对外的方法setOnTopbarClickListener()。这样我们就实现了我们的回调方法啦。
leftButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.leftClick();
}
});
rightButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
listener.rightClick();
}
});
设置好就在我们的Activity中调用:
public class TopBarActivity extends AppCompatActivity {
private boolean flag = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test);
final CustomTopBar topbar = (CustomTopBar) findViewById(R.id.topbar);
topbar.setOnTopbarClickListener(new CustomTopBar.topbarClickListener() {
@Override
public void leftClick() {
Toast.makeText(TopBarActivity.this, "LEFT", Toast.LENGTH_SHORT).show();
}
@Override
public void rightClick() {
Toast.makeText(TopBarActivity.this, "RIGHT", Toast.LENGTH_SHORT).show();
}
});
}
}
这样我们就可以在外面也可以对自定义控件中的View做操作啦,更改功能也不需要去修改自定义控件中代码,提升了代码复用。
同样我们也可以在控件中提供方法,去修改那些子View的属性,更方便我们对自定义控件的操控,这里写个设置可见的例子。
public void setVisible(boolean flag) {
if (flag) {
leftButton.setVisibility(View.VISIBLE);
} else {
leftButton.setVisibility(View.GONE);
}
}
private boolean flag = true;
...
@Override
public void rightClick() {
flag = !flag;
topbar.setVisible(flag);
}
以上就是我这篇博客的全部内容啦。
结束语:本文仅用来学习记录,参考查阅。