序言
自定义View可以实现系统原生View无法满足的需求,并且可以根据开发中的不同需求实现自己的View。通过自定义View,还可以实现一些炫酷的效果,提升产品的用户体验。 一.自定义view的四个构造函数什么场景下触发
1. View(Context)
在java代码中 new的时候调用
2. View(Context mcontext, Attributeset attrs)
在xml声明时调用, attrs是从xml中解析出来的属性集合
3. View(Context mcontext, Attributeset attrs,int defStyleAttr)
仅当我们主动显式调用,例如:在第二个构造函数调用第三个构造函数;第三个参数是手动传入的style值,style配置默认的属性值。
4.View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
android 21时加上的;如果第三个为0,或者没有定义时,第四个参数才起作用。 如果同时使用这几种方式给 View 设置了属性那么 View 听谁的呢?
xml中直接定义 > xml 中 style 定义 >defStyleAttr > defStyleRes > Theme 中直接定义
自定义View的几种类型:
类型 | 定义 |
---|---|
自定义组合控件 | 多个组件组合成一个新的控件,方便多次复用 |
继承系统View控件 | 如继承TextView,在系统控件的基础功能进行拓展 |
继承系统的ViewGroup | 继承如LinearLayout等系统控件,在其基础上进行拓展 |
继承View/ViewGroup | 不复用系统控件,完全自定义 |
- 自定义组合控件:eg 将筛选功能用自定义组合控件封装成一个组合控件,并适当的将部分逻辑功能放在这里也是合适的。 需要我们在构造函数里将布局加载出来 :
csharp
private void initView() {
View view = View.inflate(getContext(), R.layout.filter_view, this);
}
- 继承系统view:eg:项目中使用较多的是带圆角、各种不同颜色填充颜色、边框颜色的TextView、LinearLayout...。步骤:a.先继承TextView ;b.获取xml自定义属性,c.在构造函数里设置TextView的背景样式,drawable继承GradientDrawable。
- 继承View、ViewGroup 不复用系统控件,完全自定义,这类相当复杂。这里我们展开看一下。
自定义View中的处理
实线用原生代码很好实现,画一条虚线就需要我们通过自定义View来实现,具体代码如下:
ini
package cn.sto.sxz.core.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import cn.sto.sxz.core.R;
/**
* author:justin
* time:2019/08/12
* desc:
*/
public class MyDashView extends View {
private static final String TAG = "DashView";
public static final int DEFAULT_DASH_WIDTH = 100;
public static final int DEFAULT_LINE_WIDTH = 100;
public static final int DEFAULT_LINE_HEIGHT = 10;
public static final int DEFAULT_LINE_COLOR = 0x9E9E9E;
/**
* 虚线的方向
*/
public static final int ORIENTATION_HORIZONTAL = 0;
public static final int ORIENTATION_VERTICAL = 1;
/**
* 默认为水平方向
*/
public static final int DEFAULT_DASH_ORIENTATION = ORIENTATION_HORIZONTAL;
/**
* 间距宽度
*/
private float dashWidth;
/**
* 线段高度
*/
private float lineHeight;
/**
* 线段宽度
*/
private float lineWidth;
/**
* 线段颜色
*/
private int lineColor;
private int dashOrientation;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int widthSize;
private int heightSize;
public MyDashView(Context context) {
this(context, null);
}
public MyDashView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DashView);
dashWidth = typedArray.getDimension(R.styleable.DashView_dashView_dashWidth, DEFAULT_DASH_WIDTH);
lineHeight = typedArray.getDimension(R.styleable.DashView_dashView_lineHeight, DEFAULT_LINE_HEIGHT);
lineWidth = typedArray.getDimension(R.styleable.DashView_dashView_lineWidth, DEFAULT_LINE_WIDTH);
lineColor = typedArray.getColor(R.styleable.DashView_dashView_lineColor, DEFAULT_LINE_COLOR);
dashOrientation = typedArray.getInteger(R.styleable.DashView_dashView_dashOrientation, DEFAULT_DASH_ORIENTATION);
mPaint.setColor(lineColor);
mPaint.setStrokeWidth(lineHeight);
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
widthSize = MeasureSpec.getSize(widthMeasureSpec);
heightSize = MeasureSpec.getSize(heightMeasureSpec);
int deafaultWidth = 100;
int defaultHeight = 100;
int mode = MeasureSpec.getMode(widthMeasureSpec);
Log.d(TAG, "onMeasure: " + widthSize + "----" + heightSize);
Log.d(TAG, "dashOrientation: " + dashOrientation);
if (dashOrientation == ORIENTATION_HORIZONTAL) {
//不管在布局文件中虚线高度设置为多少,虚线的高度统一设置为实体线段的高度
if (mode == MeasureSpec.AT_MOST) {
setMeasuredDimension(Math.min(deafaultWidth, widthSize), (int) lineHeight);
} else {
setMeasuredDimension(Math.min(heightSize, defaultHeight), (int) lineHeight);
}
} else {
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST){
setMeasuredDimension(Math.min(defaultHeight,MeasureSpec.getSize(heightMeasureSpec)), heightSize);
}else {
setMeasuredDimension((int) lineHeight, heightSize);
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
switch (dashOrientation) {
case ORIENTATION_VERTICAL:
drawVerticalLine(canvas);
break;
default:
drawHorizontalLine(canvas);
}
}
/**
* 画水平方向虚线
*
* @param canvas
*/
public void drawHorizontalLine(Canvas canvas) {
int paddingLeft = getPaddingLeft();
// paddingLeft =0;
int paddingRight = getPaddingRight();
float totalWidth = paddingLeft;
canvas.save();
float[] pts = {paddingLeft, 0, lineWidth + paddingLeft, 0};
//在画线之前需要先把画布向下平移办个线段高度的位置,目的就是为了防止线段只画出一半的高度
//因为画线段的起点位置在线段左下角
canvas.translate(0, lineHeight / 2);
while (totalWidth <= widthSize - paddingRight) {
canvas.drawLines(pts, mPaint);
canvas.translate(lineWidth + dashWidth, 0);
totalWidth += lineWidth + dashWidth;
}
canvas.restore();
}
/**
* 画竖直方向虚线
*
* @param canvas
*/
public void drawVerticalLine(Canvas canvas) {
int paddingTop = 0, paddingBottom = 0;
paddingBottom =getPaddingBottom();
float totalWidth = paddingTop;
paddingTop =getPaddingTop();
canvas.save();
float[] pts = {0, paddingTop, 0, lineWidth+paddingTop};
//在画线之前需要先把画布向右平移半个线段高度的位置,目的就是为了防止线段只画出一半的高度
//因为画线段的起点位置在线段左下角
canvas.translate(lineHeight / 2, 0);
while (totalWidth <= heightSize-paddingBottom) {
canvas.drawLines(pts, mPaint);
canvas.translate(0, lineWidth + dashWidth);
totalWidth += lineWidth + dashWidth;
}
canvas.restore();
}
}
总结:首先我们在构造函数里获取自定义View的属性,然后在onMeasure里处理 wrap_content,如若不处理设置的wrap_content 不会生效相当于 match_parent,最后在onDraw()里处理padding属性。
自定义ViewGroup
自定义ViewGroup不仅需要处理 padding,wrap_content也需要处理 margin;我们用具体代码看一下处理的逻辑:
布局文件
js
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.bellnet.customview1.CustomViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_light"
android:padding="10dp">
<com.bellnet.customview1.CustomView
android:layout_width="250dp"
android:layout_height="wrap_content"
app:backgroundColor="@android:color/holo_blue_bright"/>
<com.bellnet.customview1.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundColor="@android:color/holo_green_dark"/>
</com.bellnet.customview1.CustomViewGroup>
</RelativeLayout>
我们来看一下效果
ini
public class CustomViewGroup extends ViewGroup {
private int paddingLeft;
private int paddingRight;
private int paddingTop;
private int paddingBottom;
private int viewsWidth;//所有子View的宽度之和(在该例子中仅代表宽度最大的那个子View的宽度)
private int viewsHeight;//所有子View的高度之和
private int viewGroupWidth = 0;//ViewGroup算上padding之后的宽度
private int viewGroupHeight = 0;//ViewGroup算上padding之后的高度
private int marginLeft
private int marginTop;
private int marginRight;
private int marginBottom;
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
viewsWidth = 0;
viewsHeight = 0;
marginLeft = 0;
marginTop = 0;
marginRight = 0;
marginBottom = 0;
paddingLeft = getPaddingLeft();
paddingTop = getPaddingTop();
paddingRight = getPaddingRight();
paddingBottom = getPaddingBottom();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
viewsHeight += childView.getMeasuredHeight();
viewsWidth = Math.max(viewsWidth, childView.getMeasuredWidth());
marginLeft = Math.max(0,lp.leftMargin);//在本例中找出最大的左边距
marginTop += lp.topMargin;//在本例中求出所有的上边距之和
marginRight = Math.max(0,lp.rightMargin);//在本例中找出最大的右边距
marginBottom += lp.bottomMargin;//在本例中求出所有的下边距之和
}
/* 用于处理ViewGroup的wrap_content情况 */
viewGroupWidth = paddingLeft + viewsWidth + paddingRight + marginLeft + marginRight;
viewGroupHeight = paddingTop + viewsHeight + paddingBottom + marginTop + marginBottom;
setMeasuredDimension(measureWidth(widthMeasureSpec, viewGroupWidth), measureHeight
(heightMeasureSpec, viewGroupHeight));
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
if (b) {
int childCount = getChildCount();
int mTop = paddingTop;
for (int j = 0; j < childCount; j++) {
View childView = getChildAt(j);
MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams();
int mLeft = paddingLeft + lp.leftMargin;
mTop += lp.topMargin;
// 测量所有的子View
childView.layout(mLeft, mTop, mLeft + childView.getMeasuredWidth(), mTop + childView.getMeasuredHeight());
mTop += (childView.getMeasuredHeight() + lp.bottomMargin);
}
}
}
private int measureWidth(int measureSpec, int viewGroupWidth) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = specSize;
break;
case MeasureSpec.AT_MOST:
/* 将剩余宽度和所有子View + padding的值进行比较,取小的作为ViewGroup的宽度 */
result = Math.min(viewGroupWidth, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
private int measureHeight(int measureSpec, int viewGroupHeight) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = specSize;
break;
case MeasureSpec.AT_MOST:
/* 将剩余高度和所有子View + padding的值进行比较,取小的作为ViewGroup的高度 */
result = Math.min(viewGroupHeight, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
}
总结:
- 自定义View中处理padding,只需在onDraw()中处理,同时别忘记在在onMeasure()中处理wrap_content;
- 自定义ViewGroup处理padding,一般只需要在onLayout()中,给子View布局时算上padding值即可(自定View还需要处理onDraw场景较少)
- 自定义View无需处理margin,自定义ViewGroup处理margin时,需要在onMeasure()根据margin计算ViewGroup的宽高,同时在onLayout布局子View别忘记根据margin来布局。