自定义View的实现方式有以下几种: 组合控件,继承控件,自绘控件
详细可分为:自定义组合控件,继承系统View控件,继承系统ViewGroup,自绘View控件,自会ViewGroup控件
组合控件就是将多个控件组合成一个新的控件,可以重复使用。
1.编写布局文件
2.实现构造方法
3.初始化UI
4.提供对外的方法
5.在布局当中引用该控件
6.activity中使用
示例:中间是title的文字,左边是返回按钮,右边是一个添加按钮
//因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。
//关于各个构造方法的介绍可以参考前面的内容
public class YFHeaderView extends RelativeLayout {
public YFHeaderView(Context context) {
super(context);
}
public YFHeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//初始化UI,可根据业务需求设置默认值。
private void initView(Context context) {
LayoutInflater.from(context).inflate(R.layout.view_header, this, true);
img_left = (ImageView) findViewById(R.id.header_left_img);
img_right = (ImageView) findViewById(R.id.header_right_img);
text_center = (TextView) findViewById(R.id.header_center_text);
layout_root = (RelativeLayout) findViewById(R.id.header_root_layout);
layout_root.setBackgroundColor(Color.BLACK);
text_center.setTextColor(Color.WHITE);
}
//设置标题文字的方法
private void setTitle(String title) {
if (!TextUtils.isEmpty(title)) {
text_center.setText(title);
}
}
//对左边按钮设置事件的方法
private void setLeftListener(OnClickListener onClickListener) {
img_left.setOnClickListener(onClickListener);
}
//对右边按钮设置事件的方法
private void setRightListener(OnClickListener onClickListener) {
img_right.setOnClickListener(onClickListener);
}
}
通过继承系统控件(View子类控件或ViewGroup子类控件)来完成自定义View,一般是希望在原有系统控件基础上做一些修饰性的修改,而不会做大幅度的改动,如在TextView的文字下方添加下划线,在LinearLayout布局中加一个蒙板等。这种方式往往都会复用系统控件的onMeasure和onLayout方法,而只需要重写onDraw方法,在其中绘制一些需要的内容。下面会分别继承View类控件和ViewGroup类控件来举例说明。
如下示例为在TextView文字下方显示红色下划线,其基本步骤如下:
(1)继承View控件,并重写onDraw方法
@SuppressLint("AppCompatCustomView")
public class UnderlineTextView extends TextView{
public UnderlineTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(5);
int width = getWidth();
int height = getBaseline();
canvas.drawLine(0,height,width,height,paint);
}
}
(2)在布局文件中调用
就像使用一个普通TextView一样使用UnderlineTextView。
如下示例演示,在layout布局上添加一个浅红色的半透明蒙板,这种需求在工作中也是非常常见的。
(1)继承ViewGroup类系统控件
public class ForegroundLinearLayout extends LinearLayout{
public ForegroundLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.drawColor(Color.parseColor("#50FF0000"));
}
}
(2)在布局文件中调用
对ForegroundLinearLayout的使用,就和使用其父类LinearLayout一样。
效果:在宽为全屏宽度,高为200dp的布局范围内,绘制完子其子控件TextView后,在上面覆盖了一层浅红色的半透明蒙板。
从上面两个例子可见,继承系统原有的控件来实现自定义View,步骤非常简单,比组合控件简单多了。但是这一节需要对Canvas,paint,Path等绘制方面的知识有一定的了解,且还需要对ViewGroup的中内容的绘制顺序有一定的了解,才能在原生控件的基础上做出想要的效果来。
这三种方法中,自绘控件是最复杂的,因为所有的绘制逻辑和流程都需要自己完成。采用自绘控件这种方式时,如果自定义View为最终的叶子控件,那么需要直接继承View;而不过自定义View为容器类控件,则需要直接继承ViewGroup。这里依然针对直接继承View和ViewGroup分别举例进行说明。
直接继承View会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑。自绘叶子View控件时,最主要工作就是绘制出丰富的内容,这一过程是在重写的onDraw方法中实现的。由于是叶子view,它没有子控件了,所以重写onLayout没有意义。onMeasure的方法可以根据自己的需要来决定是否需要重写,很多情况下,不重写该方法并不影响正常的绘制。
我们用自定义View来绘制一个正方形。
1.首先定义构造方法,以及做一些初始化操作
public class RectView extends View{
//定义画笔
private Paint mPaint = new Paint();
/**
* 实现构造方法
* @param context
*/
public RectView(Context context) {
super(context);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(Color.BLUE);
}
}
2.重写draw方法,绘制正方形,注意对padding属性进行设置
/**
* 重写draw方法
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取各个编剧的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//获取绘制的View的宽度
int width = getWidth()-paddingLeft-paddingRight;
//获取绘制的View的高度
int height = getHeight()-paddingTop-paddingBottom;
//绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint);
}
3.重写onMeasure
方法
在View的源码当中并没有对AT_MOST
和EXACTLY
两个模式做出区分,也就是说View在wrap_content
和match_parent
两个模式下是完全相同的,都会是match_parent
,显然这与我们平时用的View不同,所以我们要重写onMeasure
方法。
/**
* 重写onMeasure方法
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//处理wrap_contentde情况
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300);
}
}
整个过程大致如下,直接继承View时需要有几点注意:
1、在onDraw当中对padding属性进行处理。
2、在onMeasure过程中对wrap_content属性进行处理。
3、至少要有一个构造方法。
自定义ViewGroup的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子View的测量参数负责。
自绘ViewGroup控件,需要直接继承ViewGroup,在该系列第一篇文章中将绘制流程的时候就讲过,onLayout是ViewGroup中的抽象方法,其直接继承者必须实现该方法。所以这里,onLayout方法必须要实现的,如果这里面的方法体为空,那该控件的子view就无法显示了。要想准确测量,onMeasure方法也是要重写的。
实现一个类似于Viewpager的可左右滑动的布局。
1.继承ViewGroup类,定义构造方法,进行一些初始化操作
2.重写onMeasure
方法
3.接下来重写`onLayout`方法,对各个子View设置位置。
4.事件分发处理,从onInterceptTouchEvent开始
5.当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。
6.在XML代码当中引入自定义View
示例:
public class HorizontaiView extends ViewGroup {
private int lastX;
private int lastY;
private int currentIndex = 0;
private int childWidth = 0;
private Scroller scroller;
private VelocityTracker tracker;
/**
* 1.创建View类,实现构造函数
* 实现构造方法
* @param context
*/
public HorizontaiView(Context context) {
super(context);
init(context);
}
public HorizontaiView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
scroller = new Scroller(context);
tracker = VelocityTracker.obtain();
}
/**
* 2、根据自定义View的绘制流程,重写`onMeasure`方法,注意对wrap_content的处理
* 重写onMeasure方法
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@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);
//测量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//如果没有子View,则View大小为0,0
if (getChildCount() == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
int childHeight = childOne.getMeasuredHeight();
//View的宽度=单个子View宽度*子View个数,View的高度=子View高度
setMeasuredDimension(getChildCount() * childWidth, childHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childWidth = childOne.getMeasuredWidth();
//View的宽度=单个子View宽度*子View个数,View的高度=xml当中设置的高度
setMeasuredDimension(getChildCount() * childWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
View childOne = getChildAt(0);
int childHeight = childOne.getMeasuredHeight();
//View的宽度=xml当中设置的宽度,View的高度=子View高度
setMeasuredDimension(widthSize, childHeight);
}
}
/**
* 3、接下来重写`onLayout`方法,对各个子View设置位置。
* 设置子View位置
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int left = 0;
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
childWidth = child.getMeasuredWidth();
child.layout(left, 0, left + childWidth, child.getMeasuredHeight());
left += childWidth;
}
}
}
/**
* 4、因为我们定义的是ViewGroup,从onInterceptTouchEvent开始。
* 重写onInterceptTouchEvent,对横向滑动事件进行拦截
* @param event
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercrpt = false;
//记录当前点击的坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//当X轴移动的绝对值大于Y轴移动的绝对值时,表示用户进行了横向滑动,对事件进行拦截
if (Math.abs(deltaX) > Math.abs(delatY)) {
intercrpt = true;
}
break;
}
lastX = x;
lastY = y;
//intercrpt = true表示对事件进行拦截
return intercrpt;
}
/**
* 5、当ViewGroup拦截下用户的横向滑动事件以后,后续的Touch事件将交付给`onTouchEvent`进行处理。
* 重写onTouchEvent方法
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
tracker.addMovement(event);
//获取事件坐标(x,y)
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - lastX;
int delatY = y - lastY;
//scrollBy方法将对我们当前View的位置进行偏移
scrollBy(-deltaX, 0);
break;
//当产生ACTION_UP事件时,也就是我们抬起手指
case MotionEvent.ACTION_UP:
//getScrollX()为在X轴方向发生的便宜,childWidth * currentIndex表示当前View在滑动开始之前的X坐标
//distance存储的就是此次滑动的距离
int distance = getScrollX() - childWidth * currentIndex;
//当本次滑动距离>View宽度的1/2时,切换View
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++;
} else {
currentIndex--;
}
} else {
//获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
tracker.computeCurrentVelocity(1000);
float xV = tracker.getXVelocity();
//当X轴加速度>50时,也就是产生了快速滑动,也会切换View
if (Math.abs(xV) > 50) {
if (xV < 0) {
currentIndex++;
} else {
currentIndex--;
}
}
}
//对currentIndex做出限制其范围为【0,getChildCount() - 1】
currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
//滑动到下一个View
smoothScrollTo(currentIndex * childWidth, 0);
tracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
private void smoothScrollTo(int destX, int destY) {
//startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
//invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
invalidate();
}
//重写computeScroll方法
@Override
public void computeScroll() {
super.computeScroll();
//当scroller.computeScrollOffset()=true时表示滑动没有结束
if (scroller.computeScrollOffset()) {
//调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//没有滑动结束,继续刷新View
postInvalidate();
}
}
}
Android系统的控件以android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。
Android自定义属性可分为以下几步:
1.首先在values目录下创建attrs.xml
2.自定义View类:在java代码中进行设置
public class MyTextView extends View {
private static final String TAG = MyTextView.class.getSimpleName();
//在View的构造方法中通过TypedArray获取
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
String text = ta.getString(R.styleable.test_testAttr);
int textAttr = ta.getInteger(R.styleable.test_text, -1);
Log.e(TAG, "text = " + text + " , textAttr = " + textAttr);
ta.recycle();
}
}
3.布局文件中使用
...
示例:
要完成一些酷炫的自定义View,还需要好好地掌握Canvas,Paint,Path等工具的使用,以及View的绘制流程原理
https://www.cnblogs.com/andy-songwei/p/10979161.html
https://www.jianshu.com/p/705a6cb6bfee
https://www.jianshu.com/p/af266ff378c6