自定义控件实现中,主要包含:属性的获取,初始化,量测,布局和绘制。
这一章节,主要用于实现理解量测过程。
一、模拟ImageView实现量测过程
1,自定义基础控件实现
public class MImageView extends View {
/**
* 图片文件
*/
private Bitmap bitmap;
public MImageView(Context context) {
super(context);
}
public MImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制内容
canvas.drawBitmap(bitmap, 0, 0, null);
}
/**
* 设置展示图片
*
* @param bitmap
*/
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
(1)布局文件中引入
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.future.measuredemo.MainActivity">
<com.future.measuredemo.view.MImageView
android:id="@+id/content_miv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<ImageView
android:id="@+id/normal_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/content_miv"
android:src="@mipmap/eddie1" />
</RelativeLayout>
(2)设置所需要资源文件
public class MainActivity extends AppCompatActivity {
/**
* 控件
*/
private MImageView imageView;
/**
* 展示内容
*/
private Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
/**
* 初始化布局
*/
private void initView() {
imageView = findViewById(R.id.content_miv);
}
/**
* 初始化数据
*/
private void initData() {
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.eddie2);
imageView.setBitmap(bitmap);
}
}
此时使用默认量测效果。想修改量测,最简单的方法,就是设置固定值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 设置测量尺寸【固定值】
setMeasuredDimension(500, 500);
}
展示效果如图所示:上面的是设置定值显示,下面的使用ImageView控件展示内容。
因为设置定值,图片超出定值的部分并没有显示。
4,修改量测方式
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 计算宽度
*/
int width = 0;
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY) {
width = sizeWidth;
} else {
width = bitmap.getWidth();
}
/**
* 计算高度
*/
int height = 0;
//获取数据拆解
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
height = sizeHeight;
} else {
height = bitmap.getHeight() ;
}
setMeasuredDimension(width, height);
}
此时,对控件设置padding值并不起作用。继续优化显示方案量测显示方案:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 计算宽度
*/
int width = 0;
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY) {
width = sizeWidth;
} else {
// width = bitmap.getWidth();
width = bitmap.getWidth() + getPaddingLeft() + getPaddingRight();
if (modeWidth == MeasureSpec.AT_MOST) {
width = Math.min(width, sizeWidth);
}
}
/**
* 计算高度
*/
int height = 0;
//获取数据拆解
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
height = sizeHeight;
} else {
// height = bitmap.getHeight() ;
height = bitmap.getHeight() + getPaddingTop() + getPaddingBottom();
if (modeHeight == MeasureSpec.AT_MOST) {
height = Math.min(height, sizeHeight);
}
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制内容
// canvas.drawBitmap(bitmap, 0, 0, null);
canvas.drawBitmap(bitmap, getPaddingLeft(), getPaddingTop(), null);
}
展示效果:
当绘制函数不优化时,周边的padding值有加入运算,实际展示效果却不对。自定义控件的三个主要方法,其实是相互关联的。
有了padding值,就会想到margin值。margin值主要交于父控件计算。此时添加margin值,也是可以正确显示的,因为margin值交于自定义控件父控件RelativeLayout处理。
二、模拟图片加文字合成控件实现量测
为增强理解整个量测过程,将TextView和ImageView合成一个控件。
public class ImageTextView extends View {
/**
* 图片内容
*/
private Bitmap bitmap;
/**
* 绘制文本笔
*/
private TextPaint textPaint;
/**
* 文本内容
*/
private String textStr;
/**
* 文字大小
*/
private float textSize;
/**
* 枚举标识
*/
private enum Direction {
WIDTH, HEIGHT
}
public ImageTextView(Context context) {
super(context);
}
public ImageTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
calculationParameters();
init();
}
public ImageTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 计算参数
*/
private void calculationParameters() {
int screenWidth = MainApplication.getApplication().getWidth();
textSize = screenWidth / 10f;
}
/**
* 初始化
*/
private void init() {
if (null == bitmap) {
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.eddie4);
}
if (null == textStr || textStr.trim().length() == 0) {
textStr = "阳光正能量偶像";
}
//设置笔形参数
textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG
| Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
textPaint.setColor(Color.CYAN);
textPaint.setTextSize(textSize);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasureSize(widthMeasureSpec, Direction.WIDTH)
, getMeasureSize(heightMeasureSpec, Direction.HEIGHT));
}
/***
* 量测布局
* @param measureSpec
* @param direction
* @return
*/
private int getMeasureSize(int measureSpec, Direction direction) {
int result = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
result = size;
break;
default:
if (direction == Direction.WIDTH) {
float textWidth = textPaint.measureText(textStr);
result = (textWidth >= bitmap.getWidth()) ? (int) textWidth : bitmap.getWidth();
} else if (direction == Direction.HEIGHT) {
result = (int) (textPaint.descent() - textPaint.ascent() * 2 + bitmap.getHeight());
}
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
break;
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
canvas.drawBitmap(bitmap, getWidth() / 2 - bitmap.getWidth() / 2,
getHeight() / 2 - bitmap.getHeight() / 2, null);
canvas.drawText(textStr, getWidth() / 2, bitmap.getHeight()
+ getHeight() / 2 - bitmap.getHeight() / 2 - textPaint.ascent(), textPaint);
}
}
展示效果:
其实还是比较懒的啦,就是在上面Demo稍微修改就展示了。View的量测到此就基本结束了。
三、ViewGroup量测实现过程
仅仅只是完成View的量测是不够的,毕竟自定义控件内部可能嵌套很多种的形式。ViewGroup的量测也是非常重要的。不废话,直接来一个小栗子看看。
public class ViewGroupLayout extends ViewGroup {
public ViewGroupLayout(Context context) {
super(context);
}
public ViewGroupLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
//遍历量测子布局
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
// 获取父容器内边距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
if (getChildCount() > 0) {
// 那么遍历子元素并对其进行定位布局
/* for (int j = 0; j < getChildCount(); j++) {
View child = getChildAt(j);
child.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
}*/
/**
* 所有布局均从[0,0]开始,不符合布局方式
* 在开始点处有初始数据积累
*/
int tempHeight = 0;
for (int j = 0; j < getChildCount(); j++) {
View child = getChildAt(j);
// child.layout(0, tempHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + tempHeight);
child.layout(parentPaddingLeft, parentPaddingTop + tempHeight,
child.getMeasuredWidth() + parentPaddingLeft,
child.getMeasuredHeight() + tempHeight + parentPaddingTop);
tempHeight += child.getMeasuredHeight();
}
}
}
}
注意小细节:
在最开始时,Layout直接都从[0,0]开始,所有的子View都叠在了左上角。修改布局方式,整体展示基本正常。
但是这里,与量测有个毛关系啊!!
当然不是了,当前布局中,使用了系统自带量测方法。
整个量测其实是从顶部往下开始计算的,最开始的布局是填充父窗体,也就是整个屏幕。
ViewGroup的量测主要使用了measureChildren()方法。说曹操曹操到:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 获取子元素的布局参数
final LayoutParams lp = child.getLayoutParams();
/*
* 将父容器的测量规格已经上下和左右的边距还有子元素本身的布局参数传入getChildMeasureSpec方法计算最终测量规格
*/
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 调用子元素的measure传入计算好的测量规格
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
要看看,那就尽量看清楚些吧。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父容器的测量模式和尺寸大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 这个尺寸应该减去内边距的值
int size = Math.max(0, specSize - padding);
// 声明临时变量存值
int resultSize = 0;
int resultMode = 0;
/*
* 根据模式判断
*/
switch (specMode) {
case MeasureSpec.EXACTLY: // 父容器尺寸大小是一个确定的值
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension;
// 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 因为父容器的大小是被确定的所以子元素大小也是可以被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 但是子元素的大小包裹了其内容后不能超过父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST: // 父容器尺寸大小拥有一个限制值
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension;
// 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 因为父容器的大小是受到限制值的限制所以子元素的大小也应该受到父容器的限制
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 那么就将父容器的大小作为结果
resultSize = size;
// 但是子元素的大小包裹了其内容后不能超过父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension;
// 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 返回封装后的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
似乎就能大功告成了,最后发现添加margin值,不顶用了?!哎哟,XXX,想骂人,但是没用啊。
之前说过,margin值由父类控制计算。这一次,逃不掉了呗~~~
自定义参数类型,为御用做准备:
public static class ViewGroupLayoutParams extends MarginLayoutParams {
public ViewGroupLayoutParams(MarginLayoutParams source) {
super(source);
}
public ViewGroupLayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
public ViewGroupLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public ViewGroupLayoutParams(int width, int height) {
super(width, height);
}
}
@Override
protected ViewGroupLayoutParams generateDefaultLayoutParams() {
return new ViewGroupLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
return new ViewGroupLayoutParams(p);
}
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ViewGroupLayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
return p instanceof ViewGroupLayoutParams;
}
重写量测方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentDesireWidth = 0;
int parentDesireHeight = 0;
if (getChildCount() > 0) {
if (getChildCount() > 0) {
// 那么遍历子元素并对其进行测量
for (int i = 0; i < getChildCount(); i++) {
// 获取子元素
View child = getChildAt(i);
// 获取子元素的布局参数
ViewGroupLayoutParams clp = (ViewGroupLayoutParams) child.getLayoutParams();
// 测量子元素并考虑外边距
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 计算父容器的期望值
parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
// 考虑父容器的内边距
parentDesireWidth += getPaddingLeft() + getPaddingRight();
parentDesireHeight += getPaddingTop() + getPaddingBottom();
// 尝试比较建议最小值和期望值的大小并取大值
parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
}
// 设置最终测量值
setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));
}
}
他的老兄弟就不干了,你都更新刷新装备了,我得升级啊!onLayout()也就在这边嗷嗷叫了。。。。。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 获取一个子元素
View child = getChildAt(i);
ViewGroupLayoutParams clp = (ViewGroupLayoutParams) child.getLayoutParams();
// 通知子元素进行布局
// 此时考虑父容器内边距和子元素外边距的影响
child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin,
child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin,
child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
}
}
说句实在话,这最后的一些,只是借助网络的力量实现了,在消化上还需要功夫。
此处,Layout的使用也进入顶配了。后续的文章会细化Layout基础的理解部分。
展示效果:
【一如既往的源码地址】
谁不是一边不想活了,一边努力活着!
努力活着才有希望。
才能看见美丽的风景,
才能完成梦想,
才能等到期盼已久的自由。
爱你的人还在赶来的路上,
你要好好的活下去。
请你继续热爱生活,
一边哭泣,一边咬牙继续。
那些磨砺的沙,
总有一天会让你变成珍珠。
没有一种痛苦是专门为你准备的。
夜黑透了,接下来就是黎明。