Android自定义View的分类

前言

上一篇文章我们知道,自定义View共分为五种,分别是自定义组合View、继承系统View(TextView等)、直接继承View、继承系统ViewGroup控件(LinearLayout等)、直接继承ViewGroup。

一.自定义组合View

自定义组合View就是将多个控件组合成一个新的控件,主要是为了复用。常见的组合控件有标题栏、loading加载框等。

举例说明

1. 编写布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/header_root_layout"
    android:layout_height="45dp"
    android:background="#827192">
 
    <ImageView
        android:id="@+id/iv_back"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentLeft="true"
        android:paddingLeft="12dp"
        android:paddingRight="12dp"
        android:src="@drawable/back"
        android:scaleType="fitCenter"/>
 
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:lines="1"
        android:maxLines="20"
        android:ellipsize="end"
        android:text="title"
        android:textStyle="bold"
        android:textColor="#ffffff"/>
    
    <ImageView
        android:id="@+id/iv_home"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_alignParentRight="true"
        android:src="@drawable/home"
        android:scaleType="fitCenter"
        android:paddingRight="12dp"
        android:paddingLeft="12dp"/>
 
</RelativeLayout>

很简单的布局,左边是一个返回按钮,中间是标题,右边是home按钮

2. 实现构造方法

//因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。
//关于各个构造方法的介绍可以参考前面的内容
public class CommonHeadView extends RelativeLayout {
 
    public CommonHeadView(Context context) {
        super(context);
    }
 
    public CommonHeadView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
 
    public CommonHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

3. 提供对外的方法

    //设置标题文字的方法
    private void setTitle(String title) {
        if (!TextUtils.isEmpty(title)) {
            tv_title.setText(title);
        }
    }
    //对左边按钮设置事件的方法
    private void setLeftListener(OnClickListener onClickListener) {
        iv_back.setOnClickListener(onClickListener);
    }
 
    //对右边按钮设置事件的方法
    private void setRightListener(OnClickListener onClickListener) {
        img_right.setOnClickListener(onClickListener);
    }

4. 在布局当中引用该控件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <com.example.demo.view.CommonHeadView 
        android:layout_width="match_parent"
        android:layout_height="45dp">
    </com.example.example.view.CommonHeadView>
 
</LinearLayout>

到此基本的功能已经实现了,除了这些基础的功能外,我们还可以做一些功能扩展,比如可以在布局时设置我的View显示的元素,因为可能有些需求并不需要右边的按钮。这时就需要用到自定义属性来解决了。

前面已经简单介绍过自定义属性的相关知识,我们之间看代码
4.1.首先在values目录下创建attrs.xml

内容如下

<resources>
    <declare-styleable name="CommonHeadViewStyle">
       <attr name="title_text_clolor" format="color" />
        <attr name="title_text" format="string" />
        <attr name="left_img" format="integer" />
        <attr name="center_text" format="integer" />
        <attr name="right_text" format="integer" />
    </declare-styleable>
</resources>

这里我们定义了三个属性,文字内容、颜色以及要显示的元素。

4.2.在java代码中进行设置

      private void initAttrs(Context context, AttributeSet attrs) {
       TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeaderBar);
        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
        showLeft = mTypedArray.getInt(R.styleable.HeaderBar_left_img, 1);
        showCenter = mTypedArray.getInt(R.styleable.HeaderBar_center_text, 1);
        showRight = mTypedArray.getInt(R.styleable.HeaderBar_right_text, 1);
        text_center.setTextColor(mTypedArray.getColor(R.styleable.HeaderBar_title_text_clolor, Color.WHITE));
        mTypedArray.recycle();
        showView();
    }
    
    private void showView() {
        img_left.setVisibility(showLeft == 1 ? VISIBLE : GONE);
        text_center.setVisibility(showCenter == 1 ? VISIBLE : GONE);
        tv_next.setVisibility(showRight == 1 ? VISIBLE : GONE);
        }
    }

4.3 在布局文件中进行设置

    <com.example.demo.view.CommonHeadView
        android:layout_width="match_parent"
        android:layout_height="80dp"
        app:title_text="标题"
        app:title_text_clolor="#FF03DAC5"
        app:left_img="1"
        app:center_text="1"
        app:right_text="1"
       tools:ignore="MissingConstraints">
    </com.example.demo.view.CommonHeadView>

OK,到这里整个View基本定义完成。整个CommonHeadView 的代码如下

public class CommonHeadView extends RelativeLayout {

    private ImageView img_left;
    private TextView text_center;
    private TextView tv_next;
    private RelativeLayout layout_root;
    private Context context;
    String element;

    private int showLeft;
    private int showCenter;
    private int showRight;

    public CommonHeadView(Context context) {
        super(context);
        this.context = context;
        initView(context);
    }

    public CommonHeadView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initView(context);
        initAttrs(context, attrs);
    }

    public CommonHeadView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initView(context);
        initAttrs(context, attrs);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.CommonHeadViewStyle);
        String title = mTypedArray.getString(R.styleable.CommonHeadViewStyle_title_text);
        if (!TextUtils.isEmpty(title)) {
            text_center.setText(title);
        }
        showLeft = mTypedArray.getInt(R.styleable.CommonHeadViewStyle_left_img, 1);
        showCenter = mTypedArray.getInt(R.styleable.CommonHeadViewStyle_center_text, 1);
        showRight = mTypedArray.getInt(R.styleable.CommonHeadViewStyle_right_text, 1);
        text_center.setTextColor(mTypedArray.getColor(R.styleable.CommonHeadViewStyle_title_text_clolor, Color.WHITE));
        mTypedArray.recycle();
        showView();

    }

    private void showView() {
        img_left.setVisibility(showLeft == 1 ? VISIBLE : GONE);
        text_center.setVisibility(showCenter == 1 ? VISIBLE : GONE);
        tv_next.setVisibility(showRight == 1 ? VISIBLE : GONE);
    }

    private void initView(final Context context) {
        LayoutInflater.from(context).inflate(R.layout.base_title, this, true);
        img_left = (ImageView) findViewById(R.id.iv_back);
        tv_next = (TextView) findViewById(R.id.tv_next);
        text_center = (TextView) findViewById(R.id.tv_title);
        layout_root = (RelativeLayout) findViewById(R.id.layout_titlebar);
        layout_root.setBackgroundColor(Color.BLACK);
        img_left.setOnClickListener(view -> Toast.makeText(context, element + "", Toast.LENGTH_SHORT).show());
    }

    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) {
        tv_next.setOnClickListener(onClickListener);
    }
}

二.继承系统控件

继承系统的控件可以分为继承View子类(如TextVIew等)和继承ViewGroup子类(如LinearLayout等),根据业务需求的不同,实现的方式也会有比较大的差异。这里介绍一个比较简单的,继承子View的实现方式。

业务需求:实现一个带边框圆角的TextView。

因为这种实现方式会复用系统的逻辑,大多数情况下我们希望复用系统的onMeaseur和onLayout流程,所以我们只需要重写onDraw方法 。实现非常简单,话不多说,直接上代码。

1.代码

public class MyTextView extends AppCompatTextView {


    Paint mPaint;
    RectF mRect;
    int mColor;
    float mSize;
    float mDensity;
    int mThickness = 2;
    DisplayMetrics mMetric = new DisplayMetrics();

    public MyTextView(Context context) {
        super(context);
        init(context, null);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    void init(Context context, AttributeSet attrs){
        mPaint = new Paint();
        mRect = new RectF();
        if(attrs != null){
            //获取自定义属性(注意属性的命名方式)
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
            mColor = a.getColor(R.styleable.MyTextView_rect_color, Color.BLUE);
            mSize = a.getDimension(R.styleable.MyTextView_round_size, 5);
            a.recycle();
        }

    }

    @Override
    protected void onDraw(Canvas canvas) {

        //获取当前手机设备的像素密度
        getDisplay().getMetrics(mMetric);
        mDensity = mMetric.density;
        //计算边框厚度值
        float thickness = mThickness*mDensity;

        //获取控件的宽高
        int w = getWidth();
        int h = getHeight();

        //设置抗锯齿,否则锯齿会很明显
        mPaint.setAntiAlias(true);

        //绘制外圆角矩形
        mPaint.setColor(mColor);
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = w;
        mRect.bottom = h;
        canvas.drawRoundRect(mRect, mSize* mDensity,mSize* mDensity,mPaint);

        //绘制内圆角矩形(白色)
        mPaint.setColor(Color.WHITE);
        mRect.left = thickness;
        mRect.top = thickness;
        mRect.right = w-thickness;
        mRect.bottom = h-thickness;
        //计算圆角值
        float r = (mSize-1>0 ? mSize-1 : 0)* mDensity;
        canvas.drawRoundRect(mRect, r, r, mPaint);

        //绘制文本文字,还走原来的逻辑
        super.onDraw(canvas);

    }
}

2.attrs文件定义了一个颜色属性、一个圆角值属性

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="MyTextView">
        <attr name="rect_color" format="color"/>
        <attr name="round_size" format="dimension"/>
    </declare-styleable>

</resources>

3、在布局文件中引用自定义控件

<com.example.demo.view.MyTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        app:rect_color="@android:color/holo_red_light"
        app:round_size="2dp"
        android:textSize="20sp"
        android:text="Hello World!"/>

4.效果图
在这里插入图片描述
一个简单的自定义TextView的控件就实现了。

三. 直接继承View

直接继承View会比上一种实现方复杂一些,这种方法的使用情景下,完全不需要复用系统控件的逻辑,除了要重写onDraw外还需要对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);
    }

之前我们讲到过View的measure过程,再看一下源码对这一步的处理

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
 
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

在View的源码当中并没有对AT_MOST和EXACTLY两个模式做出区分,也就是说View在wrap_content和match_parent两个模式下是完全相同的,都会是match_parent,显然这与我们平时用的View不同,所以我们要重写onMeasure方法。

3.重写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的代码如下:

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);
 
    }
 
    /**
     * 重写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);
 
        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);
        }
    }
 
    /**
     * 重写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);
    }
}

整个过程大致如下,直接继承View时需要有几点注意:
1、在onDraw当中对padding属性进行处理。
2、在onMeasure过程中对wrap_content属性进行处理。
3、至少要有一个构造方法。

四.继承ViewGroup

自定义ViewGroup的过程相对复杂一些,因为除了要对自身的大小和位置进行测量之外,还需要对子View的测量参数负责。

需求实例
实现一个类似于Viewpager的可左右滑动的布局。
代码比较多,我们结合注释分析。

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;
            }
        }
    }
}

到这里我们的View布局就已经基本结束了。但是要实现Viewpager的效果,还需要添加对事件的处理。

    /**
     * 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();
        }
    }

这部分代码比较多,为了方便阅读,在代码当中进行了注释。
之后就是在XML代码当中引入自定义View

<com.example.demo.view.HorizontaiView
        android:id="@+id/test_layout"
        android:layout_width="match_parent"
        android:layout_height="400dp">
        <ListView
            android:id="@+id/list1"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
 
        </ListView>
 
        <ListView
            android:id="@+id/list2"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
 
        </ListView>
 
        <ListView
            android:id="@+id/list3"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
 
        </ListView>
 
    </com.example.demo.view.HorizontaiView>

总结

本篇文章对常用的自定义View的方式进行了总结,并简单分析了View的绘制流程。对各种实现方式写了简单的实现。
参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值