1.为什么要自定义View?
- UI的奇葩设计。
- 多界面的组件复用。
2.知识点
2.1 MeasureSpec
MeasureSpec是一个32为的整数值,前两位表示测量的模式Spec
Mode,后30位表示该模式下的规格大小SpecSize。
MeasureSpec核心代码如下:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
* will automatically get a size of 0. Older apps expect this.
*
* @hide internal use only for compatibility with system widgets and older apps
*/
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Returns a String representation of the specified measure
* specification.
*
* @param measureSpec the measure specification to convert to a String
* @return a String with the following format: "MeasureSpec: MODE SIZE"
*/
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
- 主要方法
方法 | 用法 |
---|---|
makeMeasureSpec | 重置MeasureSpec约束 |
getMode | 获取MeasureSpec的模式specMode |
getSize | 获取Measure的模式specSize |
- 三种模式
方法(音标) | 用法 |
---|---|
UNSPECTIFIED(ʌn'spɛsɪfaɪd) | 未指明尺寸 |
EXACTLY(ɪɡ'zæktli) | 精确的尺寸 |
AT_MOST | 父视图允许的最大尺寸 |
2.2 Measure
Measure操作用来计算View的实际大小,用于确定当前View或ViewGroup的实际宽高。
常用方法如下:
- onMeasure的源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
由onMeasure的源码可以看出,该方法的目的,就是为了确定view的具体宽高;
- 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);
}
用于设置View的宽和高,在每一次view尺寸运算完成使用,可以理解为,onMeasure中最后一个必调的方法;
- setMeasuredDimensionRaw的源码
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
设置每一行的宽高,很少使用,略;
- getMeasuredWidth的源码
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
获取view的宽度;
- getLayoutParams()的源码
@ViewDebug.ExportedProperty(deepExport = true, prefix = "layout_")
public ViewGroup.LayoutParams getLayoutParams() {
return mLayoutParams;
}
获取View的Layoutparams(备注:layout_开头的所有参数都放在了里面);
- measureChildren的源码(ViewGroup)
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);
}
}
}
遍历所有的子View,测量全部View的宽高。一般与getMeasuredWidth()或getMeasuredHeight()配合使用;
- measureChild的源码(ViewGroup)
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
用于测量具体子view的宽和高;一般与getMeasuredWidth()或getMeasuredHeight()配合使用;
-
getChildAt(i)(ViewGroup)
获取子View -
getChildCount()(ViewGroup)
获取子View的数量 -
备注:layout_width 三种设置值的方式,以及对应的模式流程
赋值方式 | 模式流程 |
---|---|
wrap_content | AT_MOST |
match_parent | AT_MOST --> EXACTLY |
100dp | EXACTLY |
2.3 Layout(ViewGroup)
Layout的过程是用于确定View在父布局中的位置。由父布局获取参数,然后将参数传递给子View的layout方法中,将view放在具体的位置;
在onLayout中没有太复杂的逻辑需要处理,相应的参数都可以在onMeasure中获得。建议使用LayoutParams进行传值,若子view的值固定,可以使用makeMeasureSpec进行重置约束,通过setMeasuredDimension或onMeasure进行设置子view的约束。
实例代码如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(l, t, r, b);//这里不能直接使用onLayout的参数,具体的参数值可以通过,全局变量或LayoutParams进行传递;
}
}
2.4 Draw(多用于View)
onDraw操作用于绘制view的具体界面,可以绘制常见的图形、文字等,通过对view的操作,也可以实现具体的动画,如MD动画的实现。
同时,由于ViewGroup大多为容器,用户承载view,很少会使用onDraw,当然,也可以使用Draw绘制背景等。
核心知识点
- Paint 画笔
Paint方法用于在Canvas上绘制内容,可以设置Paint的宽度、颜色、笔触、以及对图片进行滤镜处理等。
详细的Paint效果,请查看Android 画笔Paint
常用的Paint代码:
private void initPaint() {
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);//设置画笔样式
mPaint.setStrokeWidth(10);// 设置画笔宽度
mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));//设置画笔颜色
mPaint.setAntiAlias(true);//设置抗锯齿
mPaint.setTextSize(60);//设置文字尺寸
}
设置
笔触类型 | 描述 |
---|---|
FILL_AND_STROKE | 填充内部且描边 |
STROKE | 描边 |
FILL | 填充内部 |
-
Canvas 画布
绘制文字
/**
* @param text 文本
* @param x 水平方向起点
* @param y 竖直方向的文字底部
* @param paint 画笔
*/
canvas.drawText("画圆:", 50, 1000, mPaint);
绘制矩形
/**
* RectF : 屏幕左上角是坐标原点,4个参数:left,top,right,bottom
* left:屏幕左边到矩形左边的距离
* top:屏幕顶部到矩形顶边的距离
* right:屏幕左边到矩形右边的距离
* bottom:屏幕顶边到矩形底边的距离
*
* @desc 绘制一个宽50的矩形
*/
canvas.drawRect(new RectF(50,50,100,100),mPaint);
** 将整个画布绘制成画笔的颜色**
/**
* 将整个画布绘制成画笔的颜色
*/
canvas.drawPaint(mPaint);
** 绘制一条水平直线**
/**
* @param startX X轴的起点
* @param startY Y轴的起点
* @param stopX X轴的终点
* @param stopY Y轴的终点
* @param paint
*/
canvas.drawLine(10,10,100,10,mPaint);
** 绘制一条水平直线**
/**
* @param startX X轴的起点
* @param startY Y轴的起点
* @param stopX X轴的终点
* @param stopY Y轴的终点
* @param paint
*/
canvas.drawLine(10,10,100,10,mPaint);
绘制圆弧或扇形
/**
* oval:圆弧所在的RectF对象。
* startAngle:圆弧的起始角度。
* sweepAngle:圆弧的结束角度。
* useCenter:是否显示半径连线,true表示显示圆弧与圆心的半径连线,false 表示不显示。
* paint:绘制时所使用的画笔。
*/
//绘制扇形
RectF oval1 = new RectF(50, 50, 300, 300);
canvas.drawArc(oval1,90,90,true,mPaint);
//绘制圆弧
mPaint.setStyle(Paint.Style.STROKE);
RectF oval2 = new RectF(150, 150, 600, 600);
canvas.drawArc(oval2,90,90,false,mPaint);
绘制圆角矩形
/**
* RectF : 屏幕左上角是坐标原点,4个参数:left,top,right,bottom
* left:屏幕左边到矩形左边的距离
* top:屏幕顶部到矩形顶边的距离
* right:屏幕左边到矩形右边的距离
* bottom:屏幕顶边到矩形底边的距离
* @param rx 圆角X轴半径
* @param ry 圆角Y轴半径
* @desc 绘制一个宽50的矩形
*/
canvas.drawRoundRect(new RectF(50,50,100,100),mPaint);
绘制图片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
/**
* @param bitmap The bitmap to be drawn
* @param left 左上角x坐标
* @param top 左上角y坐标
* @param paint 画笔
*/
canvas.drawBitmap(bitmap, 360, 1300, mPaint);
绘制点
/**
* x,y坐标
*/
//画一个点
canvas.drawPoint(500, 1200, p);
//画多个点
canvas.drawPoints(new float[]{600, 1200, 650, 1250, 700, 1200}, mPaint);
绘制三角形
Path path = new Path();
path.moveTo(500, 750);// 此点为多边形的起点
path.lineTo(400, 850);
path.lineTo(600, 850);
path.close(); // 使这些点构成封闭的多边形
canvas.drawPath(path, mPaint);
绘制贝塞尔曲线
Path path2 = new Path();
path2.moveTo(500, 1050);//设置Path的起点
//设置贝塞尔曲线的控制点坐标和终点坐标
path2.quadTo(600, 950, 700, 1050);
path2.quadTo(800, 1150, 900, 1050);
canvas.drawPath(path2, mPaint);
备注:画线的时候,一定要将画笔设置为Paint.Style.STROKE
2.5 自定义xml中的属性
1. 创建attrs.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlowLayout">
<attr name="space" format="dimension"></attr>
</declare-styleable>
</resources>
declare-styleable 设置自定义view的名称,attr中设置view中需要使用的具体属性,以及相应属性的具体格式;
所有属性的格式如下:
格式 | 描述 | 使用 |
---|---|---|
reference | 资源ID | @drawable/图片ID |
color | 颜色值 | #ffffff |
boolean | 布尔值 | false or true |
dimension | 尺寸dp值 | 100dp or 100dip |
float | 浮点值 | 1.0 |
integer | 整型值 | 100 |
string | 字符串 | "str" |
fraction | 百分数 | 100% |
enum | 枚举类型 | 使用enum标签设置 |
flag | 位或运算 | 类事枚举,使用flag标签设置 |
2. 在View中引用
获取自定义属性值
void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
space = ta.getDimensionPixelSize(R.styleable.FlowLayout_space, 0);
ta.recycle();
}
init方法应该放在以下的构造方法中。(备注:默认都会调用以下构造方法,无论是否进行findViewById)
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
Log.e(TAG, "FlowLayout(Context context, AttributeSet attrs)");
init(context, attrs);
Log.e(TAG,space+"");
}
3. 在布局文件中使用
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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="study.zxh.com.viewdemo.MainActivity">
<study.zxh.com.viewdemo.FlowLayout
android:id="@+id/fl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:space="10dp" />
</android.support.constraint.ConstraintLayout>
在跟标签引入
xmlns:app="http://schemas.android.com/apk/res-auto"
在自定义的布局中使用
app:space="10dp"
2.6 LayoutParam
- 自定义LayoutParams
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
private static class LayoutParams extends ViewGroup.LayoutParams {
private int left;
private int top;
private int right;
private int bottom;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super(source);
this.left = source.left;
this.top = source.top;
this.right = source.right;
this.bottom = source.bottom;
}
}
- 引用步骤
- 实现静态内部类LayoutParams,并继承自ViewGroup.LayoutParams或其子类;
- 重写checkLayoutParams用于检测其类型。
- 重写generateLayoutParams实现自定义LayoutParams的构造;
- 重写generateDefaultLayoutParams()传入默认的LayoutParams,一般传入ViewGroup.LayoutParams。
- 使用时,直接通过view.getLayoutParams(),获取子view的LayoutParams使用;
3.自定义View之实现MD按钮动画
4.自定义ViewGroup之流式布局
实现的最终效果
实现的最终效果最终代码
/**
* Created by zhangxuehui on 2017/6/16.
* 实现动态添加文字标签
*/
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
private int space = 0;//文字间的间距,以及四周的边距;
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
Log.e(TAG, space + "");
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//初始化自定义的属性
void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
space = ta.getDimensionPixelSize(R.styleable.FlowLayout_space, 0);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int curWidth = 0;
int curHeight = 0;
int maxWidth = 0;
int maxHeight = 0;
int pos = 0;//用于判读最后一行是否有数据
for (int i = 0; i < this.getChildCount(); i++) {
View child = this.getChildAt(i);
//测量child的宽高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.left = curWidth + space;
lp.top = maxHeight + space;
lp.right = lp.left + child.getMeasuredWidth();
lp.bottom = lp.top + child.getMeasuredHeight();
curWidth = lp.right;
curHeight = Math.max(child.getMeasuredHeight(), curHeight);
if (curWidth > widthSize - space) {//翻页,需要去除右边距,否则,最右侧标签可能会贴紧屏幕;
//翻页后,初始化相应的参数
maxWidth = Math.max(maxWidth, curWidth);
pos = i;
curHeight = 0;
curWidth = 0;
maxHeight = lp.bottom;
//重新设置超出屏幕的view
lp.left = curWidth + space;
lp.top = maxHeight + space;
lp.right = lp.left + child.getMeasuredWidth();
lp.bottom = lp.top + child.getMeasuredHeight();
//取得最新的参数
curWidth = lp.right;
curHeight = Math.max(child.getMeasuredHeight(), curHeight);
}
Log.e(TAG, lp.toString());
}
if (getChildCount() > pos) {
maxHeight += curHeight + space * 2;
}
widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);//重建约束
heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);//重建约束
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, MeasureSpec.EXACTLY), resolveSizeAndState(maxHeight, heightMeasureSpec, MeasureSpec.EXACTLY));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < this.getChildCount(); i++) {
View child = this.getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(lp.left, lp.top, lp.right, lp.bottom);
}
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
private static class LayoutParams extends ViewGroup.LayoutParams {
private int left;
private int top;
private int right;
private int bottom;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(LayoutParams source) {
super(source);
this.left = source.left;
this.top = source.top;
this.right = source.right;
this.bottom = source.bottom;
}
public void clear() {
this.left = 0;
this.top = 0;
this.right = 0;
this.bottom = 0;
}
@Override
public String toString() {
return "LayoutParams{" +
"left=" + left +
", top=" + top +
", right=" + right +
", bottom=" + bottom +
'}';
}
}
}
5.组合view之微信聊天界面
最终效果
实现思想
首先整个聊天界面通过系统的ListView实现,通过组合控件实现聊天气泡和输入框,全部代码可以划分为三个组合控件,WeChatMsgList、WeChatMsgInput、WeChatBubble
最终代码
- WeChatMsgList
- WeChatMsgInput
- WeChatBubble
网友评论