文章目录
一.前言
自定义View的实现需要我们对View的层次结构,事件分发机制和View的工作原理等知识有较好的掌握,具体的可参看如下的这些博客。
Android View(一)——View的基础知识
Android View(二)——View的事件分发机制
Android View(三)——View的滑动冲突
Android View(四)——View的工作原理
二.自定义View的分类
1.继承View重写onDraw方法
主要用来实现一些不规则的效果,这种效果不方便用布局组合的方式达到,通常需要静态或者动态的显示一些不规则的图形,这种需要绘制的需要自己支持wrap_content,并支持padding。
2.继承ViewGroup派生出特殊的Layout
这种方式主要用于实现自定义布局,当某种效果看起来很像几种Veiw组合在一起的时候,可以采取这种方式来实现。这种方式复杂些,需要合适的处理ViewGroup的测量布局这两个过程,并同时处理子元素的测量和布局过程。
3.继承特定的View(比如TextView)
这种方法比较常见,一般用于扩展已有的View功能,比如TextView,这种方法比较容易实现,这种方法不需要自己支持wrap_content和padding。
4.继承特定的ViewGroup(比如LinearLayout)
这种方发也比较常见,当某种效果看起来很想几种View组合在一起的时候,可以采用这种方法实现,这种方法不需要自己处理测量和布局两个过程,一般来说方法2介意实现的效果方法4也可以实现,区别在于方法2更接近低层。
我们需要去找到一种代价最小,最高效的方法去实现。
三.自定义View须知
- 让View支持wrap_content(对于直接继承自View或者ViewGroup的控件,如果不在onMeasure中对wrap_content作特殊处理,那么wrap_content属性将失效)
- 支持padding和margin
直接继承View的控件,如果不在draw方法中处理padding,padding属性是无法起作用的。直接继承自ViewGroup的控件需要在onMeasure和onLayout方法中考虑padding和子元素的margin对其造成的影响。 - 多线程应直接使用post方式
View的内部本身提供了post系列的方法,完全可以替代Handler的作用,使用起来更加方便、直接。 - 避免内存泄漏
主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。
启动或停止线程/ 动画的方式:
启动线程/动画:使用view.onAttachedToWindow(),因为该方法调用的时机是当包含View的Activity启动的时刻
停止线程/动画:使用view.onDetachedFromWindow(),因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻
- 处理好滑动冲突
四.自定义View示例
1.继承View重写onDraw方法
自定义View实现圆的绘制,它会在自己的中心点以宽/高的最小值为直径绘制一个实心圆
public class CircleView extends View {
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
int mColor = Color.RED;
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width,height) / 2;
canvas.drawCircle(width / 2, height / 2,radius,mPaint ); //画圆
}
}
布局代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#ffffff">
<com.example.four_view_workingprincipletest.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"
android:id="@+id/circleView1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
运行效果图:
- 问题1:此时,当我们把layout_width改为wrap_content,效果和上面完全一样。
这是上面提到的让View支持wrap_content问题,具体的解决在Android View(四)——View的工作原理这里给出 - 问题2:当在布局文件中加上android:padding="20dp"不起作用
针对padding问题,需要我们在绘制时考虑一下padding,修改onDraw方法如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width,height) / 2;
canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2,radius,mPaint );
}
改完这两个问题之后的运行效果图如下:左边为layout_width改为wrap_content,右边为加上android:padding=“20dp”
|
|
1.1自定义属性
- 在valuse目录下创建自定义属性的XML,比如attrs.xml。文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
在上面的XML中声明了一个自定义属性集合“CircleView”,在这个集合里可以有很多的自定义属性,这里只定义一个格式为“color”的属性circle_color
- 在View的构造方法中(两个参数的构造方法)解析自定义属性并做相应处理:
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//加载自定义属性集合CircleView
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
/*解析CircleView属性集合中的circle_color属性
如果在使用时没有指定这个属性,就会选择红色作为默认的颜色值
*/
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
//释放资源
a.recycle();
init();
}
- 在布局文件中使用自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#ffffff">
<com.example.four_view_workingprincipletest.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000"
android:id="@+id/circleView1"
android:layout_margin="20dp"
android:padding="20dp"
app:circle_color="@color/purple_700"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
运行结果:
1.2完整代码
public class CircleView extends View {
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mColor ;
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width,height) / 2;
canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2,radius,mPaint );
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int mWidth = 200;
int mHeight = 200;
if(widthSpecMode == MeasureSpec.AT_MOST &&
heightSpecMode == MeasureSpec.AT_MOST){
//在wrap_content时设置此宽高
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, mHeight);
}
}
}
2.继承ViewGroup派生特殊的Layout
一个类似ViewPager的控件,内部元素可以进行水平滑动并且子元素的内部还可以进行竖直滑动。
主要进行这几方面的工作:
- 重写onMeasure(主要是为了实现wrap_content效果)
- onLayout重写(必须要重写的方法,因为他本身为一个抽象方法)
- 重写onInterceptTouchEvent(解决滑动冲突)
- 重写onTouchEvent(实现滑动功能)
public class HorizontalEx2 extends ViewGroup {
//分别记录上次滑动的坐标
private int lastX,lastY;
//分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept,mLastYIntercept;
private int childIndex;
private Scroller mScroller; //用于弹性滑动
private VelocityTracker mVelocityTracker; //速度追踪
public HorizontalEx2(Context context) {
super(context);
init(context);
}
public HorizontalEx2(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public HorizontalEx2(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mScroller = new Scroller(context);
mVelocityTracker = VelocityTracker.obtain();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
if (childCount == 0) {
//如果没有子元素
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//宽/高是否采取了wrap_content
height = getChildAt(0).getMeasuredHeight();
width = childCount * getChildAt(0).getMeasuredWidth();
setMeasuredDimension(width, height);
} else if (widthMode == MeasureSpec.AT_MOST) {
//宽是否采取了wrap_content,是的话 宽是所有子元素之和
width = childCount * getChildAt(0).getMeasuredWidth();
setMeasuredDimension(width, height);
} else if(heightMode == MeasureSpec.AT_MOST){
//高是否采取wrap_content, 是的话 高是第一个元素的高
height = getChildAt(0).getMeasuredHeight();
setMeasuredDimension(width, height);
}else{
setMeasuredDimension(width, height);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftOffset = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if(child.getVisibility() != View.GONE){
child.layout(l + leftOffset, t, r + leftOffset, b);
leftOffset += child.getMeasuredWidth();
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int)ev.getX(); //相对当前View左上角的x和y坐标
int y = (int)ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:{
intercepted = false;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x-mLastXIntercept;
int deltaY = y-mLastYIntercept;
intercepted = Math.abs(deltaX) > Math.abs(deltaY);
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:
break;
}
lastX = x;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
//private boolean isFirstTouch = true;
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX(); //相对当前View左上角的x和y坐标
int y = (int)event.getY();
mVelocityTracker.addMovement(event);
ViewConfiguration configuration = ViewConfiguration.get(getContext());
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
/*
if(isFirstTouch){
isFirstTouch = false; //在开始滑动时更新lastX,lastY
lastX = x;
lastY = y;
}
*/
final int deltaX = x-lastX;
scrollBy(-deltaX,0);
break;
case MotionEvent.ACTION_UP:
//isFirstTouch = true;
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
float mVelocityX = mVelocityTracker.getXVelocity(); //水平速度
if(Math.abs(mVelocityX) > configuration.getScaledMinimumFlingVelocity()){
childIndex = mVelocityX < 0 ? childIndex+1 : childIndex-1;
}else{
childIndex = (scrollX + getChildAt(0).getWidth() / 2) / getChildAt(0).getWidth();
}
childIndex = Math.min(getChildCount() - 1, Math.max(0, childIndex));
smoothScrollBy(childIndex*getChildAt(0).getWidth()-scrollX,0);
mVelocityTracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), getScrollY(), dx, dy,500);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVelocityTracker.recycle();
}
}