Android View(五)——自定义View

一.前言

自定义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须知

  1. 让View支持wrap_content(对于直接继承自View或者ViewGroup的控件,如果不在onMeasure中对wrap_content作特殊处理,那么wrap_content属性将失效)
  2. 支持padding和margin
    直接继承View的控件,如果不在draw方法中处理padding,padding属性是无法起作用的。直接继承自ViewGroup的控件需要在onMeasure和onLayout方法中考虑padding和子元素的margin对其造成的影响。
  3. 多线程应直接使用post方式
    View的内部本身提供了post系列的方法,完全可以替代Handler的作用,使用起来更加方便、直接。
  4. 避免内存泄漏
    主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。

启动或停止线程/ 动画的方式:
启动线程/动画:使用view.onAttachedToWindow(),因为该方法调用的时机是当包含View的Activity启动的时刻
停止线程/动画:使用view.onDetachedFromWindow(),因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻

  1. 处理好滑动冲突

  

扫描二维码关注公众号,回复: 12895535 查看本文章

四.自定义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自定义属性

  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

  1. 在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();
    }
  1. 在布局文件中使用自定义属性:
<?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();
    }


}

猜你喜欢

转载自blog.csdn.net/haazzz/article/details/114638919