自定义 View 三步骤
自定义View三步骤,即:onMeasure()(测量),onLayout()(布局),onDraw()(绘制)。
onMeasure()
首先我们需要弄清楚,自定义 View 为什么需要重新测量。正常情况下,我们直接在 XML 布局文件中定义好 View 的宽高,然后让自定义 View 在此宽高的区域内显示即可。但是为了更好地兼容不同尺寸的屏幕,Android 系统提供了 wrap_content 和 match_parent 属性来规范控件的显示规则。它们分别代表自适应大小和填充父视图的大小,但是这两个属性并没有指定具体的大小,因此我们需要在 onMeasure 方法中过滤出这两种情况,真正的测量出自定义 View 应该显示的宽高大小。
/**
* 测量
* @param widthMeasureSpec 包含测量模式和宽度信息
* @param heightMeasureSpec 包含测量模式和高度信息
* int型数据,采用二进制,占32个bit。其中前2个bit为测量模式。后30个bit为测量数据(尺寸大小)。
* 这里测量出的尺寸大小,并不是View的最终大小,而是父View提供的参考大小。
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e("TAG","onMeasure()");
}
MeasureSpec:
- 测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。只是测量宽高,不一定等于实际宽高。
- MeasureSpec代表一个32位int值(避免过多的对象内存分配),高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。并提供了打包和解包方法。
SpecMode | 说明 |
UNSPECIFIED | 父容器没有对当前 View 有任何限制,当前 View 可以取任意尺寸,比如 ListView 中的 item。这种情况一般用于系统内部,表示一种测量的状态。 |
EXACTLY | 父容器已检测出View所需要的精确大小,就是SpecSize所指定的值。它对应于LayoutParams中的Match_parent和具体数值这两种模式。 |
AT_MOST | 父容器指定SpecSize,View不能大于这个值。它对应于LayoutParams中的wrap_content。 |
MeasureSpec和LayoutParams的对应关系:
- 在测量时,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,再根据MeasureSpec来确定View测量后宽高。(需要注意的是,决定MeasureSpec的有两点。即LayoutParams和父容器约束)
- 对于顶级View(DecorView)和普通View,MeasureSpec的转换过程略有不同。除了自身的LayoutParams这点,前者由窗口的尺寸,后者由父容器的MeasureSpec来约束决定。MeasureSpec一定确定,onMeasure中就可以确定View的测量宽高。
当继承 View 或 ViewGroup 时,如果没有复写 onMeasure 方法时,默认使用父类也就是 View 中的实现,View 中的 onMeasure 默认实现如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// setMeasuredDimension 是一个非常重要的方法,这个方法传入的值直接决定 View 的宽高,也就是说如果调用 setMeasuredDimension(100,200),最终 View 就显示宽 100 * 高 200 的矩形范围。
// getDefaultSize 返回的是默认大小,默认为父视图的剩余可用空间。
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
查看 setMeasuredDimension 方法。其它现有控件的 onMeasure 方法的 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 经过一系列计算,最后也是调用到 setMeasuredDimension 方法。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
一种情况是:在 XML 中指定的是 wrap_content,但是实际使用的宽高值却是父视图的剩余可用空间,从 getDefaultSize 方法中可以看出是整个屏幕的宽高。解决方法只要复写 onMeasure,过滤出 wrap_content 的情况,并主动调用 setMeasuredDimension 方法设置正确的宽高即可:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 判断是 wrap_content 的测量模式
if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode){
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
// int size = measuredWidth > measuredHeight ? measuredHeight : measuredWidth;
// 将宽高设置为传入宽高的最小值
int size = Math.min(measuredWidth, measuredHeight);
// 设置 View 实际大小
setMeasuredDimension(size,size);
}
}
ViewGroup 中的 onMeasure
如果自定义的控件是一个容器,onMeasure 方法会更加复杂一些。因为 ViewGroup 在测量自己的宽高之前,需要先确定其内部子 View 的所占大小,然后才能确定自己的大小。比如 LinearLayout 的宽高为 wrap_content 表示由子控件的大小决定,那 LinearLayout 的最终宽度由其内部最大的子 View 宽度决定。
onLayout()
/**
* 布局
* @param changed 当前View的大小和位置改变了
* @param left 左部位置(相对于父视图)
* @param top 顶部位置(相对于父视图)
* @param right 右部位置(相对于父视图)
* @param bottom 底部位置(相对于父视图)
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.e("TAG","onLayout()");
// 一般在自定义ViewGroup时使用,来定义子View的位置。
}
这里扩展一些View位置相关知识点:
- View的位置参数:
由View的四个属性决定;left(左上角横坐标),right(右下角横坐标),top(左上角纵坐标),bottom(右下角纵坐标)。是一种相对坐标,相对父容器。
四个参数对应View源码中的mLeft等四个成员变量,通过getLeft()等方法来获取。 - View的宽高和坐标的关系:
width=right-left;
height=bottom-top; - 从Android3.0开始,新增额外的四个参数:
x,y,translationX,translationY。前两者是View左上角坐标,后两者是View左上角相对于父容器的偏移量,并且默认值0。和四个基本位置参数一样,也提供了get/set方法。 - 换算关系如下;
x=left+translationX;
y=top+translationY;
注意;在View平移过程中,top和left表示的是原始左上角的位置信息,值并不会改变。发生改变的是;x,y,translationX,translationY这四个参数。
它是一个抽象方法,也就是说每一个自定义 ViewGroup 都必须主动实现如何排布子 View,具体就是遍历每一个子 View,调用 child.(l, t, r, b) 方法来为每个子 View 设置具体的布局位置。四个参数分别代表左上右下的坐标位置,一个简易的 FlowLayout 实现如下:
在大多数 App 的搜索界面经常会使用 FlowLayout 来展示历史搜索记录或者热门搜索项。
FlowLayout 的每一行上的 item 个数不一定,当每行的 item 累计宽度超过可用总宽度,则需要重启一行摆放 item 项。因此我们需要在 onMeasure 方法中主动的分行计算出 FlowLayout 的最终高度,如下所示:
public class FlowLayout extends ViewGroup {
//存放容器中所有的View
private List<List<View>> mAllViews = new ArrayList<List<View>>();
//存放每一行最高View的高度
private List<Integer> mPerLineMaxHeight = new ArrayList<>();
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
super.generateLayoutParams(p);
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
/**
* 测量控件的宽和高
*
* onMeasure 方法的主要目的有 2 个:
* 1.调用 measureChild 方法递归测量子 View;
* 2.通过叠加每一行的高度,计算出最终 FlowLayout 的最终高度 totalHeight。
*/
@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);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获得容器中子View的个数
int childCount = getChildCount();
//记录每一行View的总宽度
int totalLineWidth = 0;
//记录每一行最高View的高度
int perLineMaxHeight = 0;
//记录当前ViewGroup的总高度
int totalHeight = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
//对子View进行测量
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
//获得子View的测量宽度
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
//获得子View的测量高度
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (totalLineWidth + childWidth > widthSize) {
//统计总高度
totalHeight += perLineMaxHeight;
//开启新的一行
totalLineWidth = childWidth;
perLineMaxHeight = childHeight;
} else {
//记录每一行的总宽度
totalLineWidth += childWidth;
//比较每一行最高的View
perLineMaxHeight = Math.max(perLineMaxHeight, childHeight);
}
//当该View已是最后一个View时,将该行最大高度添加到totalHeight中
if (i == childCount - 1) {
totalHeight += perLineMaxHeight;
}
}
//如果高度的测量模式是EXACTLY,则高度用测量值,否则用计算出来的总高度(这时高度的设置为wrap_content)
heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight;
setMeasuredDimension(widthSize, heightSize);
}
//摆放控件
//1.表示该ViewGroup的大小或者位置是否发生变化
//2.3.4.5.控件的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mPerLineMaxHeight.clear();
//存放每一行的子View
List<View> lineViews = new ArrayList<>();
//记录每一行已存放View的总宽度
int totalLineWidth = 0;
//记录每一行最高View的高度
int lineMaxHeight = 0;
/*************遍历所有View,将View添加到List<List<View>>集合中***************/
//获得子View的总个数
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (totalLineWidth + childWidth > getWidth()) {
mAllViews.add(lineViews);
mPerLineMaxHeight.add(lineMaxHeight);
//开启新的一行
totalLineWidth = 0;
lineMaxHeight = 0;
lineViews = new ArrayList<>();
}
totalLineWidth += childWidth;
lineViews.add(childView);
lineMaxHeight = Math.max(lineMaxHeight, childHeight);
}
//单独处理最后一行
mAllViews.add(lineViews);
mPerLineMaxHeight.add(lineMaxHeight);
/************遍历集合中的所有View并显示出来*****************/
//表示一个View和父容器左边的距离
int mLeft = 0;
//表示View和父容器顶部的距离
int mTop = 0;
for (int i = 0; i < mAllViews.size(); i++) {
//获得每一行的所有View
lineViews = mAllViews.get(i);
lineMaxHeight = mPerLineMaxHeight.get(i);
for (int j = 0; j < lineViews.size(); j++) {
View childView = lineViews.get(j);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
int leftChild = mLeft + lp.leftMargin;
int topChild = mTop + lp.topMargin;
int rightChild = leftChild + childView.getMeasuredWidth();
int bottomChild = topChild + childView.getMeasuredHeight();
//四个参数分别表示View的左上角和右下角
childView.layout(leftChild, topChild, rightChild, bottomChild);
mLeft += lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin;
}
mLeft = 0;
mTop += lineMaxHeight;
}
}
}
这样一个自定义布局就定义好了,接下来可以 根据需要添加相应样式的子 View。
onDraw()
onDraw 方法接收一个 Canvas 类型的参数。Canvas 可以理解为一个画布,在这块画布上可以绘制各种类型的 UI 元素。
/**
* 绘制
* @param canvas 画布
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e("TAG","onDraw()");
}
系统提供了一系列 Canvas 操作方法,如下:
void drawRect(RectF rect,Paint paint): // 绘制矩形区域
void drawOval(RectF oval,Paint paint): // 绘制椭圆
void drawCircle(float cx,float cy,float radius,Paint paint): // 绘制圆形
void drawArc(RectF oval,float startAngle,float sweepAngle,boolean useCenter,Paint paint): // 绘制弧形
void drawPath(Path path,Paint paint): // 绘制 Path 路径
void drawLine(float startX,float startY,float stopX,float stopY,Paint paint): // 绘制连线
void drawPoint(float x,float y,Paint paint): // 绘制点
Paint
Canvas 中每一个绘制操作都需要传入一个 Paint 对象。Paint 就相当于一个画笔,因为 Canvas(画布)本身只是呈现的一个载体,真正绘制出来的效果则取决于Paint(画笔)。可以通过设置画笔的各种属性,来实现不同绘制效果。
setStyle(Style style): // 设置绘制模式
setColor(int color) : // 设置颜色
setAlpha(int a): // 设置透明度
setShader(Shader shader): // 设置 Paint 的填充效果
setStrokeWidth(float width): // 设置线条宽度
setTextSize(float textSize): // 设置文字大小
setAntiAlias(boolean aa): // 设置抗锯齿开关
setDither(boolean dither): // 设置防抖动开关
例如 canvas.drawCircle(centerX, centerY, r, paint); 是在坐标 centerX 和 centerY 处绘制一个半径为 r 的圆,但具体圆是什么样子的则由 paint 来决定。
示例:绘制一个简易的圆形进度条控件。
public class PieImageView extends View {
private static final int MAX_PROGRESS = 100;
private Paint mArcPaint;
private RectF mBound;
private Paint mCirclePaint;
private int mProgress = 0;
public PieImageView(Context context) {
this(context, null, 0);
}
public PieImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PieImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void setProgress(@IntRange(from = 0, to = MAX_PROGRESS) int mProgress) {
this.mProgress = mProgress;
ViewCompat.postInvalidateOnAnimation(this);
}
private void init() {
mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mArcPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mArcPaint.setStrokeWidth(dpToPixel(0.1f, getContext()));
mArcPaint.setColor(Color.RED);
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(dpToPixel(2, getContext()));
mCirclePaint.setColor(Color.argb(120, 0xff, 0xff, 0xff));
mBound = new RectF();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 判断是wrap_content的测量模式。
// 如果没做处理,将 PieImageView 的宽高设置为 wrap_content(也就是自适应),PieImageView 不会正常显示。它会占满屏幕空间。
if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode) {
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
// 将宽高设置为传入宽高的最小值
int size = measuredWidth > measuredHeight ? measuredHeight : measuredWidth;
// 调用setMeasuredDimension设置View实际大小
setMeasuredDimension(size, size);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int min = Math.min(w, h);
int max = w + h - min;
int r = Math.min(w, h) / 3;
mBound.set((max >> 1) - r, (min >> 1) - r, (max >> 1) + r, (min >> 1) + r);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mProgress != MAX_PROGRESS && mProgress != 0) {
float mAngle = mProgress * 360f / MAX_PROGRESS;
canvas.drawArc(mBound, 270, mAngle, true, mArcPaint);
canvas.drawCircle(mBound.centerX(), mBound.centerY(), mBound.height() / 2, mCirclePaint);
}
}
private float scale = 0;
private int dpToPixel(float dp, Context context) {
if (scale == 0) {
scale = context.getResources().getDisplayMetrics().density;
}
return (int) (dp * scale);
}
}
public class PieImageActivity extends AppCompatActivity {
PieImageView pieImageView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pie_image);
// pieImageView = findViewById(R.id.pieImageView);
// pieImageView.setProgress(45);
}
}
示例
效果图:
完整代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
android:gravity="center"
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"
android:background="@android:color/darker_gray"
tools:context="com.example.xwxwaa.myapplication.MainActivity">
<!--记录两个问题-->
<!--1.这里的父布局是LinearLayout-->
<!--如果换成RelativeLayout,效果还有问题。-->
<!--2.MyCustomViewGroup的宽高如果是wrap_content-->
<!--则子View的宽高设置成match_parent无效。-->
<com.example.xwxwaa.myapplication.MyCustomViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:layout_marginLeft="2dp"
android:layout_marginTop="2dp"
android:paddingRight="5dp"
android:paddingBottom="5dp">
<TextView
android:layout_marginLeft="2dp"
android:layout_marginTop="2dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自定义View"
android:background="@color/colorAccent"/>
<!--app为命名空间,为了使用自定义属性-->
<com.example.xwxwaa.myapplication.MyCustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:layout_marginTop="2dp"
android:paddingRight="5dp"
android:paddingBottom="5dp"
app:default_size="100dp"
app:default_color="@color/colorPrimaryDark"
/>
</com.example.xwxwaa.myapplication.MyCustomViewGroup>
</LinearLayout>
public class MyCustomView extends View{
private int defaultSize;
private int defaultColor;
private Paint paint ;
/**
* 需要两个构造参数
* @param mContext
*/
public MyCustomView(Context mContext){
super(mContext);
init();
}
public MyCustomView(Context mContext, AttributeSet attributeSet){
super(mContext,attributeSet);
// 通过它,取出在xml中,由命名空间定义的属性值
// 第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
// 即属性集合的标签,在R文件中名称为R.styleable+name
TypedArray a = mContext.obtainStyledAttributes(attributeSet, R.styleable.MyCustomView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
defaultSize = a.getDimensionPixelSize(R.styleable.MyCustomView_default_size, 100);
defaultColor = a.getColor(R.styleable.MyCustomView_default_color,Color.BLUE);
//最后将TypedArray对象回收
a.recycle();
init();
}
private void init(){
// 初始化Paint
paint = new Paint();
paint.setColor(defaultColor);
paint.setStyle(Paint.Style.STROKE);//设置圆为空心
paint.setStrokeWidth(3.0f);//设置线宽
}
/**
* 测量
* @param widthMeasureSpec 包含测量模式和宽度信息
* @param heightMeasureSpec 包含测量模式和高度信息
* int型数据占32个bit。其中前2个bit为测量模式。后30个bit为测量数据(尺寸大小)。
* 这里测量出的尺寸大小,并不是View的最终大小,而是父View提供的参考大小。
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 定义宽高尺寸
int width = getSize(widthMeasureSpec);
int height = getSize(heightMeasureSpec);
// 实现一个正方形,取小值
int sideLength =Math.min(width,height);
// 设置View宽高
setMeasuredDimension(sideLength,sideLength);
}
private int getSize(int measureSpec){
int mySize = defaultSize;
// 可通过下面的方法,来获取测量模式和尺寸大小。
// 注意这里的specSize值单位是px,而我们xml中一般为dp。
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode){
case MeasureSpec.UNSPECIFIED:
// 父容器不对View有任何限制,这种情况一般用于系统内部,表示一种测量的状态。
// 一般也不需要我们处理。。可以看ScrollView或列表相关组件。
Log.e("TAG","测量模式;MeasureSpec.UNSPECIFIED");
break;
case MeasureSpec.EXACTLY:
// 父容器已检测出View所需要的精确大小,就是SpecSize所指定的值。
// 当xml中,宽或高指定为match_parent或具体数值,会走这里。
mySize = specSize;
Log.e("TAG","测量模式;MeasureSpec.EXACTLY");
break;
case MeasureSpec.AT_MOST:
// View的尺寸大小,不能大于父View指定的SpecSize。
// 当xml中,宽或高指定为wrap_content时,会走这里。
mySize = specSize/2;
Log.e("TAG","测量模式;MeasureSpec.AT_MOST");
break;
default:
break;
}
return mySize;
}
/**
* 绘制
* @param canvas 画布
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 接下来绘制一个正圆。
// 需要知道,圆的半径,和圆点坐标。
int r = getMeasuredWidth() ;
int centerX ;
int centerY ;
int paddingL = getPaddingLeft();
int paddingT = getPaddingTop();
int paddingR = getPaddingRight();
int paddingB = getPaddingBottom();
// 计算View减去padding后的可用宽高
int canUsedWidth = r - paddingL - paddingR;
int canUsedHeight = r - paddingT - paddingB;
// 圆心坐标
centerX = canUsedWidth / 2 + paddingL;
centerY = canUsedHeight / 2 + paddingT;
// 取两者最小值作为圆的直径
int minSize = Math.min(canUsedWidth, canUsedHeight);
// 绘制一个圆
canvas.drawColor(Color.WHITE);//设置画布颜色
canvas.drawCircle(centerX,centerY,minSize / 2,paint);
}
/**
* 布局
* @param changed 当前View的大小和位置改变了
* @param left 左部位置(相对于父视图)
* @param top 顶部位置(相对于父视图)
* @param right 右部位置(相对于父视图)
* @param bottom 底部位置(相对于父视图)
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// 一般在自定义ViewGroup时使用,来定义子View的位置。
}
}
public class MyCustomViewGroup extends ViewGroup{
// 内边距
private int paddingL ;
private int paddingT ;
private int paddingR ;
private int paddingB ;
// 外边距
private int marginL;
private int marginT;
private int marginR;
private int marginB;
public MyCustomViewGroup (Context mContext){
super(mContext);
}
public MyCustomViewGroup(Context mContext, AttributeSet attributeSet){
super(mContext,attributeSet);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 宽高的测量模式和尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 获取内边距
paddingL = getPaddingLeft();
paddingT = getPaddingTop();
paddingR = getPaddingRight();
paddingB = getPaddingBottom();
// 初始化外边距,因为测量不止一次。
marginL = 0;
marginT = 0;
marginR = 0;
marginB = 0;
// 测量所有子View的宽高。它会触发每个子View的onMeasure()。
// measureChildren(widthMeasureSpec,heightMeasureSpec);
// measureChild是对单个View进行测量。
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
marginL = Math.max(0,lp.leftMargin);//在本例中找出最大的左边距
marginT += lp.topMargin;//在本例中求出所有的上边距之和
marginR = Math.max(0,lp.rightMargin);//在本例中找出最大的右边距
marginB += lp.bottomMargin;//在本例中求出所有的下边距之和
}
if (childCount == 0){
// 没有子View
setMeasuredDimension(0,0);
}else {
// 最大宽度,加上内外边距
int viewGroupWidth = paddingL + getChildMaxWidth() + paddingR +marginL+marginR;
// 高度之和,加上内外边距
int viewGroupHeight = paddingT + getChildTotalHeight() + paddingB+marginT+marginB;
// 选小值
int resultWidth = Math.min(viewGroupWidth, widthSize);
int resultHeight = Math.min(viewGroupHeight, heightSize);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
// 如果父布局宽高都是wrap_content,只会走这个方法。
// 宽高都是包裹内容,用于处理ViewGroup的wrap_content情况
setMeasuredDimension(resultWidth,resultHeight);
}else if (widthMode == MeasureSpec.AT_MOST){
// 宽度是包裹内容
setMeasuredDimension(resultWidth,heightSize);
}else if (heightMode == MeasureSpec.AT_MOST){
// 高度是包裹内容
setMeasuredDimension(widthSize,resultHeight);
}
// 这里如果没进上面的条件判断中,super.onMeasure()会调用setMeasuredDimension()的,默认占满剩余可用空间。
}
}
/**
* 获取所有子View的最大宽度
* @return
*/
private int getChildMaxWidth(){
int count = getChildCount();
int maxWidth = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getMeasuredWidth() > maxWidth){
maxWidth = child.getMeasuredWidth();
}
}
return maxWidth;
}
/**
* 获取所有子View的高度之和
* @return
*/
private int getChildTotalHeight(){
int count = getChildCount();
int totalHeight = 0;
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
totalHeight += view.getMeasuredHeight();
}
return totalHeight;
}
@Override
protected void onLayout(boolean c, int l, int t, int r, int b) {
int count = getChildCount();
int coordHeight = paddingT;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
int coordWidth = paddingL+ lp.leftMargin;
coordHeight += lp.topMargin;
child.layout(coordWidth,coordHeight,coordWidth+width,coordHeight+height);
coordHeight+=height+lp.bottomMargin;
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定义属性-->
<declare-styleable name="MyCustomView">
<!--dimension是一个包含单位(dp、sp、px等)的尺寸,可用于定义视图的宽度、字号等。-->
<attr name="default_size" format="dimension" />
<attr name="default_color" format="color" />
</declare-styleable>
</resources>
优化
比如重复绘制,还有大图长图优化。
加载长图大图优化:
- 压缩图片
- 沿着对角线缩放
- 加载屏幕能够看见的区域
- 复用上一个 bitmap 区域的内存
- 处理滑动
对覆盖区域的 View ,一定要避免不要重复绘制。比如竞技棋牌类型的 APP 。打斗地主的时候,很多扑克都是覆盖的,那么就不能每张图片进行绘制,一定要先计算显示的区域,把不需要的截取,然后在绘制。
备注
参考资料:
Android 开发艺术探索