自定义 View 三步骤
自定义View三步骤,即:onMeasure()(测量),onLayout()(布局),onDraw()(绘制)。
onMeasure()
首先我们需要弄清楚,自定义 View 为什么需要重新测量。正常情况下,我们直接在 XML 布局文件中定义好 View 的宽高,然后让自定义 View 在此宽高的区域内显示即可。但是为了更好地兼容不同尺寸的屏幕,Android 系统提供了 wrap_content 和 match_parent 属性来规范控件的显示规则。它们分别代表自适应大小和填充父视图的大小,但是这两个属性并没有指定具体的大小,因此我们需要在 onMeasure 方法中过滤出这两种情况,真正的测量出自定义 View 应该显示的宽高大小。
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@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 默认实现如下:
1 2 3 4 5 6
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
|
查看 setMeasuredDimension 方法。其它现有控件的 onMeasure 方法的 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 经过一系列计算,最后也是调用到 setMeasuredDimension 方法。
1 2 3 4 5 6 7 8 9 10 11 12
| 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 方法设置正确的宽高即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode){ int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
int size = Math.min(measuredWidth, measuredHeight); setMeasuredDimension(size,size); } }
|
ViewGroup 中的 onMeasure
如果自定义的控件是一个容器,onMeasure 方法会更加复杂一些。因为 ViewGroup 在测量自己的宽高之前,需要先确定其内部子 View 的所占大小,然后才能确定自己的大小。比如 LinearLayout 的宽高为 wrap_content 表示由子控件的大小决定,那 LinearLayout 的最终宽度由其内部最大的子 View 宽度决定。
onLayout()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
@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()");
}
|
这里扩展一些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 的最终高度,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| public class FlowLayout extends ViewGroup {
private List<List<View>> mAllViews = new ArrayList<List<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); }
@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);
int childCount = getChildCount(); int totalLineWidth = 0; int perLineMaxHeight = 0; int totalHeight = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); measureChild(childView, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (totalLineWidth + childWidth > widthSize) { totalHeight += perLineMaxHeight; totalLineWidth = childWidth; perLineMaxHeight = childHeight; } else { totalLineWidth += childWidth; perLineMaxHeight = Math.max(perLineMaxHeight, childHeight); } if (i == childCount - 1) { totalHeight += perLineMaxHeight; } } heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight; setMeasuredDimension(widthSize, heightSize); }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear(); mPerLineMaxHeight.clear();
List<View> lineViews = new ArrayList<>(); int totalLineWidth = 0;
int lineMaxHeight = 0;
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); int mLeft = 0; int mTop = 0;
for (int i = 0; i < mAllViews.size(); i++) { 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(); childView.layout(leftChild, topChild, rightChild, bottomChild); mLeft += lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin; } mLeft = 0; mTop += lineMaxHeight; }
} }
|
这样一个自定义布局就定义好了,接下来可以 根据需要添加相应样式的子 View。
onDraw()
onDraw 方法接收一个 Canvas 类型的参数。Canvas 可以理解为一个画布,在这块画布上可以绘制各种类型的 UI 元素。
1 2 3 4 5 6 7 8 9
|
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Log.e("TAG","onDraw()"); }
|
系统提供了一系列 Canvas 操作方法,如下:
1 2 3 4 5 6 7
| 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): 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(画笔)。可以通过设置画笔的各种属性,来实现不同绘制效果。
1 2 3 4 5 6 7 8
| setStyle(Style style): setColor(int color) : setAlpha(int a): setShader(Shader shader): setStrokeWidth(float width): setTextSize(float textSize): setAntiAlias(boolean aa): setDither(boolean dither):
|
例如 canvas.drawCircle(centerX, centerY, r, paint); 是在坐标 centerX 和 centerY 处绘制一个半径为 r 的圆,但具体圆是什么样子的则由 paint 来决定。
示例:绘制一个简易的圆形进度条控件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| 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); 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(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); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class PieImageActivity extends AppCompatActivity {
PieImageView pieImageView;
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_pie_image);
} }
|
示例
效果图:
完整代码:
activity_main.xml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?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">
<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"/>
<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>
|
MyCustomView.java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| public class MyCustomView extends View{
private int defaultSize; private int defaultColor; private Paint paint ;
public MyCustomView(Context mContext){ super(mContext); init(); }
public MyCustomView(Context mContext, AttributeSet attributeSet){ super(mContext,attributeSet); TypedArray a = mContext.obtainStyledAttributes(attributeSet, R.styleable.MyCustomView);
defaultSize = a.getDimensionPixelSize(R.styleable.MyCustomView_default_size, 100); defaultColor = a.getColor(R.styleable.MyCustomView_default_color,Color.BLUE);
a.recycle();
init(); } private void init(){ paint = new Paint(); paint.setColor(defaultColor); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(3.0f); }
@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);
setMeasuredDimension(sideLength,sideLength); }
private int getSize(int measureSpec){ int mySize = defaultSize;
int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode){ case MeasureSpec.UNSPECIFIED: Log.e("TAG","测量模式;MeasureSpec.UNSPECIFIED"); break; case MeasureSpec.EXACTLY: mySize = specSize; Log.e("TAG","测量模式;MeasureSpec.EXACTLY"); break; case MeasureSpec.AT_MOST: mySize = specSize/2; Log.e("TAG","测量模式;MeasureSpec.AT_MOST"); break; default: break; } return mySize; }
@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();
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); }
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom);
} }
|
MyCustomViewGroup.java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
| 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;
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){ 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){ setMeasuredDimension(resultWidth,resultHeight); }else if (widthMode == MeasureSpec.AT_MOST){ setMeasuredDimension(resultWidth,heightSize); }else if (heightMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSize,resultHeight); } } }
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; }
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); } }
|
attrs.xml1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyCustomView"> <attr name="default_size" format="dimension" /> <attr name="default_color" format="color" /> </declare-styleable> </resources>
|
优化
比如重复绘制,还有大图长图优化。
加载长图大图优化:
- 压缩图片
- 沿着对角线缩放
- 加载屏幕能够看见的区域
- 复用上一个 bitmap 区域的内存
- 处理滑动
对覆盖区域的 View ,一定要避免不要重复绘制。比如竞技棋牌类型的 APP 。打斗地主的时候,很多扑克都是覆盖的,那么就不能每张图片进行绘制,一定要先计算显示的区域,把不需要的截取,然后在绘制。
备注
参考资料:
Android 开发艺术探索
拉钩教育-Android 工程师进阶 34 讲
传送门:GitHub
欢迎关注微信公众号:非也缘也