Android View/ViewGroup

1,028 阅读36分钟

自定义View的三种方式:继承布局,继承原生控件,继承View

继承布局

效果图

在layout文件夹中创建布局title_view.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="?actionBarSize"
    android:id="@+id/title_view"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <ImageView
        android:id="@+id/back"
        android:src="@drawable/ic_action_arrow_left"
        android:padding="16dp"
        android:adjustViewBounds="true"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
    <TextView
        android:id="@+id/title"
        tools:text="Title"
        android:textColor="@android:color/black"
        android:textSize="24sp"
        android:gravity="center"
        app:layout_constraintLeft_toRightOf="@id/back"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
    <ImageView
        android:id="@+id/menu"
        tools:src="@mipmap/ic_launcher"
        android:padding="8dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="match_parent" />
</android.support.constraint.ConstraintLayout>

在values文件夹中新建attrs.xml,在其中声明自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomTitleView">
        <attr name="backgroundColor" format="color"/>
        <attr name="title" format="string"/>
        <attr name="menuSrc" format="reference"/>
    </declare-styleable>
</resources>

本例中自定义了背景颜色,标题,菜单资源三个属性,format是指该属性的取值类型,format取值一共有string,color,demension,integer,enum,reference,float,boolean,fraction,flag这几种,其中reference是指引用资源文件。

新建CustomTitleView文件,并重写三个构造方法

在public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr)方法中绑定布局,并将其他两个构造方法修改成调用public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr)方法。这样就实现了每个构造方法都会绑定我们刚才写的布局。当然这里也可以在每个构造方法中都写一遍绑定布局。

public CustomTitleView(Context context) {
    this(context,null);
}

public CustomTitleView(Context context, AttributeSet attrs) {
    this(context, attrs,0);
}

public CustomTitleView(final Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //绑定布局
    LayoutInflater.from(context).inflate(R.layout.title_view,this);
}

找到控件并获取属性

public class CustomTitleView extends ConstraintLayout{
    private ConstraintLayout clTitleView;
    private ImageView ivBack;
    private TextView tvTitle;
    private ImageView ivMenu;
    //背景色
    private int backgroundColor;
    //标题
    private String title;
    //菜单图片资源
    private int menuSrc;
    public CustomTitleView(Context context) {
        this(context,null);
    }

    public CustomTitleView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomTitleView(final Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //绑定布局
        LayoutInflater.from(context).inflate(R.layout.title_view,this);
        //找到控件
        clTitleView = findViewById(R.id.title_view);
        ivBack = findViewById(R.id.back);
        tvTitle = findViewById(R.id.title);
        ivMenu = findViewById(R.id.menu);

        //获取属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomTitleView,defStyleAttr,0);
        //获取背景色属性,默认透明
        backgroundColor = typedArray.getColor(R.styleable.CustomTitleView_backgroundColor, Color.TRANSPARENT);
        //获取标题属性
        title = typedArray.getString(R.styleable.CustomTitleView_title);
        //获取菜单图片资源属性,未设置菜单图片资源则默认为-1,后面通过判断此值是否为-1决定是否设置图片
        menuSrc = typedArray.getResourceId(R.styleable.CustomTitleView_menuSrc,-1);
        //TypedArray使用完后需手动回收
        typedArray.recycle();

        //设置属性
        clTitleView.setBackgroundColor(backgroundColor);
        tvTitle.setText(title);
        if(menuSrc!=-1){
            ivMenu.setImageResource(menuSrc);
        }
        //back图标点击事件,点击关闭activity
        ivBack.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                ((Activity)getContext()).finish();
            }
        });
    }
}

使用TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomTitleView,defStyleAttr,0);获取所有属性

再使用typedArray的getColor,getString,getResourceId方法分别获取format为color,string,reference的自定义属性。这些方法中有的需要传入两个参数,第二个参数就是没有设置此属性时的默认值。

在绑定布局后找到控件,然后为控件设置属性。

typedArray使用完之后需要手动调用typedArray.recycle()回收掉。

在布局中使用CustomTitleView

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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.sample.studycustomview.CustomTitleView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:backgroundColor="@color/colorPrimary"
        app:title="Title"
        app:menuSrc="@mipmap/ic_launcher"/>
</android.support.constraint.ConstraintLayout>

继承原生控件

效果图

在values文件夹下的attrs.xml中,声明自定义属性

<declare-styleable name="CustomProgressBar">
    <attr name="circleColor" format="color"/>
    <attr name="circleWidth" format="dimension"/>
    <attr name="startAngle" format="integer"/>
    <attr name="textSize" format="dimension"/>
    <attr name="textColor" format="color"/>
</declare-styleable>

新建CustomProgressbar,继承ProgressBar,重写三个构造方法,并获取自定义的属性

public class CustomProgressBar extends ProgressBar{
    private Paint mPaint;
    private int mCircleColor;//圆的颜色
    private int mCircleWidth;//圆的粗细
    private int mStartAngle;//起始角度
    private int mTextSize;//文字大小
    private int mTextColor;//文字颜色
    public CustomProgressBar(Context context) {
        this(context,null,0);
    }

    public CustomProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomProgressBar,defStyleAttr,0);
        //获取圆的颜色,默认黑色
        mCircleColor = typedArray.getColor(R.styleable.CustomProgressBar_circleColor,Color.BLACK);
        //获取圆的粗细,默认5dp
        mCircleWidth = (int) typedArray.getDimension(R.styleable.CustomProgressBar_circleWidth,FormatUtil.dp2px(context,5));
        //获取圆的起始角度,默认0度
        mStartAngle = typedArray.getInteger(R.styleable.CustomProgressBar_startAngle,0);
        //获取文字大小,默认18sp
        mTextSize = (int) typedArray.getDimension(R.styleable.CustomProgressBar_textSize,FormatUtil.sp2px(getContext(),18));
        //获取文字颜色,默认黑色
        mTextColor = typedArray.getColor(R.styleable.CustomProgressBar_textColor,Color.BLACK);
        typedArray.recycle();
        mPaint = new Paint();
    }
}

重写onMeasure,计算宽高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //默认宽度60dp,默认高度60dp
    if(widthMode == MeasureSpec.AT_MOST){
        width = getPaddingLeft() + getPaddingRight() + FormatUtil.dp2px(getContext(),60);
    }
    if(heightMode == MeasureSpec.AT_MOST){
        height = getPaddingTop() + getPaddingBottom() + FormatUtil.dp2px(getContext(),60);
    }
    setMeasuredDimension(width,height);
}

重写onDraw,画圆弧和文字

public class CustomProgressBar extends ProgressBar{
    private Paint mPaint;
    private int mCircleColor;//圆的颜色
    private int mCircleWidth;//圆的粗细
    private int mStartAngle;//起始角度
    private int mTextSize;//文字大小
    private int mTextColor;//文字颜色
    private RectF mRectF;//限制弧线的矩形
    private Rect mBounds;//测量文字的边缘
    public CustomProgressBar(Context context) {
        this(context,null,0);
    }

    public CustomProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomProgressBar,defStyleAttr,0);
        //获取圆的颜色,默认黑色
        mCircleColor = typedArray.getColor(R.styleable.CustomProgressBar_circleColor,Color.BLACK);
        //获取圆的粗细,默认5dp
        mCircleWidth = (int) typedArray.getDimension(R.styleable.CustomProgressBar_circleWidth,FormatUtil.dp2px(context,5));
        //获取圆的起始角度,默认0度
        mStartAngle = typedArray.getInteger(R.styleable.CustomProgressBar_startAngle,0);
        //获取文字大小,默认18sp
        mTextSize = (int) typedArray.getDimension(R.styleable.CustomProgressBar_textSize,FormatUtil.sp2px(getContext(),18));
        //获取文字颜色,默认黑色
        mTextColor = typedArray.getColor(R.styleable.CustomProgressBar_textColor,Color.BLACK);
        typedArray.recycle();
        mRectF = new RectF();
        mBounds = new Rect();
        mPaint = new Paint();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //默认宽度60dp,默认高度60dp
        if(widthMode == MeasureSpec.AT_MOST){
            width = getPaddingLeft() + getPaddingRight() + FormatUtil.dp2px(getContext(),60);
        }
        if(heightMode == MeasureSpec.AT_MOST){
            height = getPaddingTop() + getPaddingBottom() + FormatUtil.dp2px(getContext(),60);
        }
        setMeasuredDimension(width,height);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        //1.画圆弧
        mPaint.setAntiAlias(true);
        //设置只画边框模式
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mCircleColor);
        mPaint.setStrokeWidth(mCircleWidth);
        //限制圆弧的左、上、右、下坐标
        mRectF.set(getPaddingLeft(),getPaddingTop(),getWidth() - getPaddingRight(),getHeight() - getPaddingBottom());
        //画圆弧,传入RectF,开始角度,扫过角度,是否连接中心,画笔
        canvas.drawArc(mRectF,mStartAngle,getProgress()*1.0f/getMax()*360,false,mPaint);
        //2.画文字
        String strProgress = getProgress()+"%";
        mPaint.setTextSize(mTextSize);
        mPaint.setColor(mTextColor);
        mPaint.setStrokeWidth(FormatUtil.dp2px(getContext(),1));
        //设置填充模式
        mPaint.setStyle(Paint.Style.FILL);
        //获取文字边缘
        mPaint.getTextBounds(strProgress,0,strProgress.length(),mBounds);
        //画文字,传入文字内容,文字左下角坐标,画笔
        canvas.drawText(strProgress
                ,(getWidth() - getPaddingLeft() - getPaddingRight() - mBounds.width())/2+getPaddingLeft()
                ,(getHeight() - getPaddingTop() - getPaddingBottom() - mBounds.height())/2+getPaddingTop()+mBounds.height(),mPaint);
    }
}

在布局中使用CustomProgressBar

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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.sample.studycustomview.CustomProgressBar
        android:progress="60"
        android:max="100"
        android:padding="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:circleColor="@color/colorPrimary"
        app:circleWidth="3dp"
        app:startAngle="90"
        app:textSize="15sp"
        app:textColor="@color/colorPrimary"/>
</android.support.constraint.ConstraintLayout>

附上FormatUtil工具类,主要是为了dp、sp、px互相转换

public class FormatUtil {
    private FormatUtil()
    {
        /* cannot be instantiated */
        throw new UnsupportedOperationException("cannot be instantiated");
    }

    /**
     * Value of dp to value of px.
     *
     * @param dpValue The value of dp.
     * @return value of px
     */
    public static int dp2px(Context context,final float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    /**
     * Value of px to value of dp.
     *
     * @param pxValue The value of px.
     * @return value of dp
     */
    public static int px2dp(Context context,final float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    /**
     * Value of sp to value of px.
     *
     * @param spValue The value of sp.
     * @return value of px
     */
    public static int sp2px(Context context,final float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    /**
     * Value of px to value of sp.
     *
     * @param pxValue The value of px.
     * @return value of sp
     */
    public static int px2sp(Context context,final float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }
}

继承View

效果图

在values文件夹下的attrs.xml中,声明自定义属性

<declare-styleable name="CustomAnimNumberView">
    <attr name="number" format="string"/>
    <attr name="numberColor" format="color"/>
    <attr name="numberSize" format="dimension"/>
    <attr name="animDuration" format="integer"/>
</declare-styleable>

新建CustomAnimNumberView,继承View,重写三个构造方法,并获取自定义的属性,其中用到的FormatUtil和上例中一样:

public class CustomAnimNumberView extends View {
    private Paint paint;
    private int number;//
    private int numberColor;//文字颜色
    private int numberSize;//文字大小
    private int animDuration;//动画时长
    
    public CustomAnimNumberView(Context context) {
        this(context,null);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomAnimNumberView,defStyleAttr,0);
        number = typedArray.getInt(R.styleable.CustomAnimNumberView_number,0);
        numberColor = typedArray.getColor(R.styleable.CustomAnimNumberView_numberColor, Color.BLACK);
        numberSize = typedArray.getDimensionPixelSize(R.styleable.CustomAnimNumberView_numberSize,FormatUtil.sp2px(context,18));
        animDuration = typedArray.getInt(R.styleable.CustomAnimNumberView_animDuration,1000);
        typedArray.recycle();
        paint = new Paint();
    }
}

重写onMeasure,计算宽高

public class CustomAnimNumberView extends View {
    private Paint paint;
    private int number;//
    private int numberColor;//文字颜色
    private int numberSize;//文字大小
    private int animDuration;//动画时长
    private Rect bounds;//文字边缘
    public CustomAnimNumberView(Context context) {
        this(context,null);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomAnimNumberView,defStyleAttr,0);
        number = typedArray.getInt(R.styleable.CustomAnimNumberView_number,0);
        numberColor = typedArray.getColor(R.styleable.CustomAnimNumberView_numberColor, Color.BLACK);
        numberSize = typedArray.getDimensionPixelSize(R.styleable.CustomAnimNumberView_numberSize,FormatUtil.sp2px(context,18));
        animDuration = typedArray.getInt(R.styleable.CustomAnimNumberView_animDuration,1000);
        typedArray.recycle();
        paint = new Paint();
        paint.setTextSize(numberSize);
        paint.setColor(numberColor);
        bounds = new Rect();
        paint.getTextBounds(String.valueOf(number),0,String.valueOf(number).length(),bounds);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if(widthMode == MeasureSpec.AT_MOST){
            width = getPaddingLeft() + getPaddingRight() + bounds.width();
        }
        if(heightMode == MeasureSpec.AT_MOST){
            height = getPaddingTop() + getPaddingBottom() + bounds.height();
        }
        setMeasuredDimension(width,height);
    }
}

重写onDraw,画动画的数字

public class CustomAnimNumberView extends View {
    private Paint paint;
    private int number;//
    private int numberColor;//文字颜色
    private int numberSize;//文字大小
    private int animDuration;//动画时长
    private ValueAnimator animation;//动画
    private Rect bounds;//文字边缘
    public CustomAnimNumberView(Context context) {
        this(context,null);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CustomAnimNumberView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomAnimNumberView,defStyleAttr,0);
        number = typedArray.getInt(R.styleable.CustomAnimNumberView_number,0);
        numberColor = typedArray.getColor(R.styleable.CustomAnimNumberView_numberColor, Color.BLACK);
        numberSize = typedArray.getDimensionPixelSize(R.styleable.CustomAnimNumberView_numberSize,FormatUtil.sp2px(context,18));
        animDuration = typedArray.getInt(R.styleable.CustomAnimNumberView_animDuration,1000);
        typedArray.recycle();
        paint = new Paint();
        paint.setTextSize(numberSize);
        paint.setColor(numberColor);
        bounds = new Rect();
        paint.getTextBounds(String.valueOf(number),0,String.valueOf(number).length(),bounds);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //如果宽高为固定dp 或 match_parent 直接使用以上获得的width和height即可,如果是wrap_content 需要单独处理
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if(widthMode == MeasureSpec.AT_MOST){
            width = getPaddingLeft() + getPaddingRight() + bounds.width();
        }
        if(heightMode == MeasureSpec.AT_MOST){
            height = getPaddingTop() + getPaddingBottom() + bounds.height();
        }
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //传入左下角
        paint.getTextBounds(String.valueOf(number),0,String.valueOf(number).length(),bounds);
        canvas.drawText(String.valueOf(number),getPaddingLeft(),getPaddingTop()+bounds.height(),paint);
        if(animation == null){
            animation = ValueAnimator.ofInt(0,number);
            animation.setDuration(animDuration);
            animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    number = (int) animation.getAnimatedValue();
                    postInvalidate();
                }
            });
            animation.start();
        }
    }
}

在布局中使用CustomAnimNumberView

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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.sample.studycustomview.CustomAnimNumberView
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        app:number="100"
        app:numberColor="@color/colorPrimary"
        app:numberSize="22sp"
        app:animDuration="2000"/>
</android.support.constraint.ConstraintLayout>

View的滑动方式

a.layout(left,top,right,bottom):通过修改View四个方向的属性值来修改View的坐标,从而滑动View

b.offsetLeftAndRight() offsetTopAndBottom():指定偏移量滑动view

c.LayoutParams,改变布局参数:layoutParams中保存了view的布局参数,可以通过修改布局参数的方式滑动view

d.通过动画来移动view:注意安卓的平移动画不能改变view的位置参数,属性动画可以

e.scrollTo/scrollBy:注意移动的是view的内容,scrollBy(50,50)你会看到屏幕上的内容向屏幕的左上角移动了,这是参考对象不同导致的,你可以看作是它移动的是手机屏幕,手机屏幕向右下角移动,那么屏幕上的内容就像左上角移动了

f.scroller:scroller需要配置computeScroll方法实现view的滑动,scroller本身并不会滑动view,它的作用可以看作一个插值器,它会计算当前时间点view应该滑动到的距离,然后view不断的重绘,不断的调用computeScroll方法,这个方法是个空方法,所以我们重写这个方法,在这个方法中不断的从scroller中获取当前view的位置,调用scrollTo方法实现滑动的效果

View的事件分发机制

点击事件产生后,首先传递给Activity的dispatchTouchEvent方法,通过PhoneWindow传递给DecorView,然后再传递给根ViewGroup,进入ViewGroup的dispatchTouchEvent方法,执行onInterceptTouchEvent方法判断是否拦截,再不拦截的情况下,此时会遍历ViewGroup的子元素,进入子View的dispatchToucnEvent方法,如果子view设置了onTouchListener,就执行onTouch方法,并根据onTouch的返回值为true还是false来决定是否执行onTouchEvent方法,如果是false则继续执行onTouchEvent,在onTouchEvent的Action Up事件中判断,如果设置了onClickListener ,就执行onClick方法。

View的加载流程

View随着Activity的创建而加载,startActivity启动一个Activity时,在ActivityThread的handleLaunchActivity方法中会执行Activity的onCreate方法,这个时候会调用setContentView加载布局创建出DecorView并将我们的layout加载到DecorView中,当执行到handleResumeActivity时,Activity的onResume方法被调用,然后WindowManager会将DecorView设置给ViewRootImpl,这样,DecorView就被加载到Window中了,此时界面还没有显示出来,还需要经过View的measure,layout和draw方法,才能完成View的工作流程。我们需要知道View的绘制是由ViewRoot来负责的,每一个DecorView都有一个与之关联的ViewRoot,这种关联关系是由WindowManager维护的,将DecorView和ViewRoot关联之后,ViewRootImpl的requestLayout会被调用以完成初步布局,通过scheduleTraversals方法向主线程发送消息请求遍历,最终调用ViewRootImpl的performTraversals方法,这个方法会执行View的measure layout 和draw流程

View的measure layout 和 draw流程

View绘制流程的入口在ViewRootImpl的performTraversals方法,在方法中首先调用performMeasure方法,传入一个childWidthMeasureSpec和childHeightMeasureSpec参数,这两个参数代表的是DecorView的MeasureSpec值,这个MeasureSpec值由窗口的尺寸和DecorView的LayoutParams决定,最终调用View的measure方法进入测量流程

measure:

View的measure过程由ViewGroup传递而来,在调用View.measure方法之前,会首先根据View自身的LayoutParams和父布局的MeasureSpec确定子view的MeasureSpec,然后将view宽高对应的measureSpec传递到measure方法中,那么子view的MeasureSpec获取规则是怎样的?分几种情况进行说明

1.父布局是EXACTLY模式:

a.子view宽或高是个确定值,那么子view的size就是这个确定值,mode是EXACTLY

b.子view宽或高设置为match_parent,那么子view的size就是占满父容器剩余空间,模式就是EXACTLY

c.子view宽或高设置为wrap_content,那么子view的size就是占满父容器剩余空间,不能超过父容器大小,模式就是AT_MOST

2.父布局是AT_MOST模式:

a.子view宽或高是个确定值,那么子view的size就是这个确定值,mode是EXACTLY

b.子view宽或高设置为match_parent,那么子view的size就是占满父容器剩余空间,不能超过父容器大小,模式就是AT_MOST

c.子view宽或高设置为wrap_content,那么子view的size就是占满父容器剩余空间,不能超过父容器大小,模式就是AT_MOST

3.父布局是UNSPECIFIED模式:

a.子view宽或高是个确定值,那么子view的size就是这个确定值,mode是EXACTLY

b.子view宽或高设置为match_parent,那么子view的size就是0,模式就是UNSPECIFIED

c.子view宽或高设置为wrap_content,那么子view的size就是0,模式就是UNSPECIFIED

获取到宽高的MeasureSpec后,传入view的measure方法中来确定view的宽高,这个时候还要分情况

1.当MeasureSpec的mode是UNSPECIFIED,此时view的宽或者高要看view有没有设置背景,如果没有设置背景,就返回设置的minWidth或minHeight,这两个值如果没有设置默认就是0,如果view设置了背景,就取minWidth或minHeight和背景这个drawable固有宽或者高中的最大值返回

2.当MeasureSpec的mode是AT_MOST和EXACTLY,此时view的宽高都返回从MeasureSpec中获取到的size值,这个值的确定见上边的分析。因此如果要通过继承view实现自定义view,一定要重写onMeasure方法对wrap_conten属性做处理,否则,他的match_parent和wrap_content属性效果就是一样的

layout: layout方法的作用是用来确定view本身的位置,onLayout方法用来确定所有子元素的位置,当ViewGroup的位置确定之后,它在onLayout中会遍历所有的子元素并调用其layout方法,在子元素的layout方法中onLayout方法又会被调用。layout方法的流程是,首先通过setFrame方法确定view四个顶点的位置,然后view在父容器中的位置也就确定了,接着会调用onLayout方法,确定子元素的位置,onLayout是个空方法,需要继承者去实现。

getMeasuredHeight和getHeight方法有什么区别?getMeasuredHeight(测量高度)形成于view的measure过程,getHeight(最终高度)形成于layout过程,在有些情况下,view需要measure多次才能确定测量宽高,在前几次的测量过程中,得出的测量宽高有可能和最终宽高不一致,但是最终来说,还是会相同,有一种情况会导致两者值不一样,如下,此代码会导致view的最终宽高比测量宽高大100px

public void layout(int l,int t,int r, int b){
    super.layout(l,t,r+100,b+100);
}

draw:

View的绘制过程遵循如下几步: a.绘制背景 background.draw(canvas) b.绘制自己(onDraw) c.绘制children(dispatchDraw) d.绘制装饰(onDrawScrollBars)

View绘制过程的传递是通过dispatchDraw来实现的,它会遍历所有的子元素的draw方法,如此draw事件就一层一层的传递下去了

ps:view有一个特殊的方法setWillNotDraw,如果一个view不需要绘制内容,即不需要重写onDraw方法绘制,可以开启这个标记,系统会进行相应的优化。默认情况下,View没有开启这个标记,默认认为需要实现onDraw方法绘制,当我们继承ViewGroup实现自定义控件,并且明确知道不需要具备绘制功能时,可以开启这个标记,如果我们重写了onDraw,那么要显示的关闭这个标记

子view宽高可以超过父view?能

1.android:clipChildren = "false" 这个属性要设置在父 view 上。代表其中的子View 可以超出屏幕。

2.子view 要有具体的大小,一定要比父view 大 才能超出。比如 父view 高度 100px 子view 设置高度150px。子view 比父view大,这样超出的属性才有意义。(高度可以在代码中动态赋值,但不能用wrap_content / match_partent)。

3.对父布局还有要求,要求使用linearLayout(反正我用RelativeLayout 是不行)。你如果必须用其他布局可以在需要超出的view上面套一个linearLayout 外面再套其他的布局。

4.最外面的布局如果设置的padding 不能超出

自定义view需要注意的几点

1.让view支持wrap_content属性,在onMeasure方法中针对AT_MOST模式做专门处理,否则wrap_content会和match_parent效果一样(继承ViewGroup也同样要在onMeasure中做这个判断处理)

if(widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(200,200); // wrap_content情况下要设置一个默认值,200只是举个例子,最终的值需要计算得到刚好包裹内容的宽高值
}else if(widthMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(200,heightMeasureSpec );
}else if(heightMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(heightMeasureSpec ,200);
}

2.让view支持padding(onDraw的时候,宽高减去padding值,margin由父布局控制,不需要view考虑),自定义ViewGroup需要考虑自身的padding和子view的margin造成的影响

3.在view中尽量不要使用handler,使用view本身的post方法

4.在onDetachedFromWindow中及时停止线程或动画

5.view带有滑动嵌套情形时,处理好滑动冲突

viewgroup 的测量布局流程

1.view 在onMeasure()方法中进行自我测量和保存,也就是说对于view来说一定在onMeasure方法中计算出自己的尺寸并且保存下来

2.viewgroup实际上最终也是循环从上到小来调用子view的measure方法,注意子view的measure其实最终调用的是子view的onMeasure方法。

所以我们理解这个过程为:

viewgroup循环遍历调用所有子view的onmeasure方法,利用onmeasure方法计算出来的大小,来确定这些子view最终可以占用的大小和所处的布局的位置。

3.measure方法是一个final方法,可以理解为做测量工作准备工作的,既然是final方法所以我们无法重写它,不需要过多 关注他,因为measure最终要调用onmeasure ,这个onmeasure我们是可以重写的。

4.父view调用子view的layout方法的时候会把之前measure阶段确定的位置和大小都传递给子view。

5.对于自定义view/viewgroup来说 我们几乎只需要关注下面三种需求:

  • 对于已有的android自带的view,我们只需要重写他的onMeasure方法即可。修改一下这个尺寸即可完成需求。
  • 对于android系统没有的,属于我们自定义的view,比上面那个要复杂一点,要完全重写onMeasure方法。
  • 第三种最复杂,需要重写onmeasure和onlayout2个方法,来完成一个复杂viewgroup的测量和布局。

6.onMeasure方法的特殊说明:

7.如何理解父view对子view的限制?

onMeasure的两个参数既然是父view对子view的限制,那么这个限制的值到底是哪来的呢?

实际上,父view对子view的限制绝大多数就来自于我们开发者所设置的layout开头的这些属性

比方说我们给一个imageview设置了他的layout_width和layout_height 这2个属性,那这2个属性其实就是我们开发者 所期望的宽高属性,但是要注意了, 设置的这2个属性是给父view看的,实际上对于绝大多数的layout开头的属性这些属性都是设置给父view看的

为什么要给父view看?因为父view要知道这些属性以后才知道要对子view的测量加以什么限制?

到底是不限制(UNSPECIFIED)?还是限制个最大值(AT_MOST),让子view不超过这个值?还是直接限制死,我让你是多少就得是多少(EXACTLY)。

自定义一个BannerImageView 修改onMeasure方法

所谓bannerImageview,就是很多电商其实都会放广告图,这个广告图的宽高比都是可变的,我们在日常开发过程中 也会经常接触到这种需求:imageview的宽高比 在高保真中都标注出来,但是考虑到很多手机的屏幕宽度或者高度都不确定 所以我们通常都要手动来计算出这个imageview高度或者宽度,然后动态改变width或者height的值。这种方法可用但是很麻烦 这里给出一个自定义的imageview,通过设置一个ratio的属性即可动态的设置iv的高度。很是方便

看下效果

public class BannerImageView extends ImageView {

    //宽高比
    float ratio;

    public BannerImageView(Context context) {
        super(context);
    }

    public BannerImageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerImageView);
        ratio = typedArray.getFloat(R.styleable.BannerImageView_ratio, 1.0f);
        typedArray.recycle();
    }

    public BannerImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //人家自己的测量还是要自己走一遍的,因为这个方法内部会调用setMeasuredDimension方法来保存测量结果了
        //只有保存了以后 我们才能取得这个测量结果 否则你下面是取不到的
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //取测量结果
        int mWidth = getMeasuredWidth();

        int mHeight = (int) (mWidth * ratio);

        //保存了以后,父view就可以拿到这个测量的宽高了。不保存是拿不到的噢。
        setMeasuredDimension(mWidth, mHeight);
    }
}

自定义view,完全自己写onMeasure方法

首先明确一个结论:

对于完全自定义的view,完全自己写的onMeasure方法来说,你保存的宽高必须要符合父view的限制,否则会发生bug, 保存父view对子view的限制的方法也很简单直接调用resolveSize方法即可。

所以对于完全自定义的view onMeasure方法也不难写了,

  • 先算自己想要的宽高,比如你画了个圆,那么宽高就肯定是半径的两倍大小, 要是圆下面还有字, 那么高度肯定除了半径的两倍还要有字体的大小。对吧。很简单。这个纯看你自定义view是啥样的

  • 算完自己想要的宽高以后 直接拿resolveSize 方法处理一下 即可。

  • 最后setMeasuredDimension 保存。

public class LoadingView extends View {

    //圆形的半径
    int radius;

    //圆形外部矩形rect的起点
    int left = 10, top = 30;


    Paint mPaint = new Paint();

    public LoadingView(Context context) {
        super(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
        radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        int width = left + radius * 2;
        int height = top + radius * 2;

        //一定要用resolveSize方法来格式化一下你的view宽高噢,否则遇到某些layout的时候一定会出现奇怪的bug的。
        //因为不用这个 你就完全没有父view的感受了 最后强调一遍
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        RectF oval = new RectF(left, top,
                left + radius * 2, top + radius * 2);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(oval, mPaint);
        //先画圆弧
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        canvas.drawArc(oval, -90, 360, false, mPaint);
    }
}
<LinearLayout
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="#000000"
    android:orientation="horizontal">

    <com.example.a16040657.customviewtest.LoadingView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/dly"
        app:radius="200"></com.example.a16040657.customviewtest.LoadingView>

    <com.example.a16040657.customviewtest.LoadingView
        android:layout_marginLeft="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/dly"
        app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
</LinearLayout>

自定义一个viewgroup

自定义一个viewgroup 需要注意的点如下:

  1. 一定是先重写onMeasure确定子view的宽高和自己的宽高以后 才可以继续写onlayout 对这些子view进行布局噢~~
  2. viewgroup 的onMeasure其实就是遍历自己的view 对自己的每一个子view进行measure,绝大多数时候对子view的 measure都可以直接用 measureChild()这个方法来替代,简化我们的写法,如果你的viewgroup很复杂的话 无法就是自己写一遍measureChild 而不是调用measureChild 罢了。
  3. 计算出viewgroup自己的尺寸并且保存,保存的方法还是哪个setMeasuredDimension 不要忘记了
  4. 逼不得已要重写measureChild方法的时候,其实也不难无非就是对父view的测量和子view的测量 做一个取舍关系而已, 你看懂了基础的measureChild方法,以后就肯定会写自己的复杂的measureChild方法了。

下面是一个极简的例子,一个很简单的flowlayout的实现,没有对margin padding做处理,也假设了每一个tag的高度 是固定的,可以说是极为简单了,但是麻雀虽小 五脏俱全,足够你们好好理解自定义viewgroup的关键点了。

/**
 * 写一个简单的flowlayout 从左到右的简单layout,如果宽度不够放 就直接另起一行layout
 * 这个类似的开源控件有很多,有很多写的出色的,我这里只仅仅实现一个初级的flowlayout
 * 也是最简单的,目的是为了理解自定义viewgroup的关键核心点。
 * <p>
 * 比方说这里并没有对padding或者margin做特殊处理,你们自己写viewgroup的时候 记得把这些属性的处理都加上
 * 否则一旦有人用了这些属性 发现没有生效就比较难看了。。。。。。
 */
public class SimpleFlowLayout extends ViewGroup {
    public SimpleFlowLayout(Context context) {
        super(context);
    }

    public SimpleFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SimpleFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * layout的算法 其实就是 不够放剩下一行 那另外放一行 这个过程一定要自己写一遍才能体会,
     * 个人有个人的写法,说不定你的写法比开源的项目还要好
     * 其实也没什么夸张的,无法就是前面onMeasure结束以后 你可以拿到所有子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 childTop = 0;
        int childLeft = 0;
        int childRight = 0;
        int childBottom = 0;

        //已使用 width
        int usedWidth = 0;


        //customlayout 自己可使用的宽度
        int layoutWidth = getMeasuredWidth();
        Log.v("wuyue", "layoutWidth==" + layoutWidth);
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            //取得这个子view要求的宽度和高度
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //如果宽度不够了 就另外启动一行
            if (layoutWidth - usedWidth < childWidth) {
                childLeft = 0;
                usedWidth = 0;
                childTop += childHeight;
                childRight = childWidth;
                childBottom = childTop + childHeight;
                childView.layout(0, childTop, childRight, childBottom);
                usedWidth = usedWidth + childWidth;
                childLeft = childWidth;
                continue;
            }
            childRight = childLeft + childWidth;
            childBottom = childTop + childHeight;
            childView.layout(childLeft, childTop, childRight, childBottom);
            childLeft = childLeft + childWidth;
            usedWidth = usedWidth + childWidth;

        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //先取出SimpleFlowLayout的父view 对SimpleFlowLayout 的测量限制 这一步很重要噢。
        //你只有知道自己的宽高 才能限制你子view的宽高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);


        int usedWidth = 0;      //已使用的宽度
        int remaining = 0;      //剩余可用宽度
        int totalHeight = 0;    //总高度
        int lineHeight = 0;     //当前行高

        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            LayoutParams lp = childView.getLayoutParams();

            //先测量子view
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            //然后计算一下宽度里面 还有多少是可用的 也就是剩余可用宽度
            remaining = widthSize - usedWidth;

            //如果一行不够放了,也就是说这个子view测量的宽度 大于 这一行 剩下的宽度的时候 我们就要另外启一行了
            if (childView.getMeasuredWidth() > remaining) {
                //另外启动一行的时候,使用过的宽度 当然要设置为0
                usedWidth = 0;
                //另外启动一行了 我们的总高度也要加一下,不然高度就不对了
                totalHeight = totalHeight + lineHeight;
            }

            //已使用 width 进行 累加
            usedWidth = usedWidth + childView.getMeasuredWidth();
            //当前 view 的高度
            lineHeight = childView.getMeasuredHeight();
        }

        //如果SimpleFlowLayout 的高度 为wrap cotent的时候 才用我们叠加的高度,否则,我们当然用父view对如果SimpleFlowLayout 限制的高度
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = totalHeight;
        }
        setMeasuredDimension(widthSize, heightSize);
    }
}

Android属性动画

属性动画之ViewPropertyAnimator

ImageView.animate().translationY(500);

通常一些简单的android 原生的view动画 我们都优先考虑这种方法,因为真的很方便啊。

public ViewPropertyAnimator animate() {
        if (mAnimator == null) {
            mAnimator = new ViewPropertyAnimator(this);
        }
        return mAnimator;
    }

看下这个函数返回就知道了。

ObjectAnimator

ViewPropertyAnimator虽然好用,但是自定义view很难使用这个,且支持的属性有限。很多情况我们要自己支持一些属性,就得用到ObjectAnimator

总体来说 分几步:

  • 动画执行过程中要改变的属性 必须有gettter和setter方法
  • ObjectAnimator.ofXXX() 创建 ObjectAnimator 对象
  • 最后start执行动画即可

public class LoadingView extends View {

    Paint mPaint = new Paint();

    public float getProgress() {
        return progress;
    }

    public void setProgress(float progress) {
        this.progress = progress;
        //setter方法是肯定会被ObjectAnimator调用的,调用完以后 我们要主动invalidate方法
        //onDraw方法才会主动执行,否则,只改变一个属性的值而不重绘 肯定是没效果的。
        //这也就是为什么属性动画 不是直接更改属性的值,而要调用属性的setter方法,因为直接
        //更改属性的值 invalidate没地方调用了,动画自然没效果了。
        invalidate();
    }

    /**
     * 进度条
     */
    float progress = 0;

    public LoadingView(Context context) {
        super(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //这里没有用0,0  width,hegiht 这2个点来定位这个矩形 是因为我们的圆形边有宽度,所以要
        //稍微窄一点 不然的话 边界处会有丢失的部分 很难看
        RectF oval = new RectF(10, 10,
                getWidth()-10, getHeight()-10);
        //先画圆弧
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(20);
        canvas.drawArc(oval, -90, 360 * progress/100, false, mPaint);
        //再画文字
        mPaint.reset();
        mPaint.setColor(Color.BLUE);
        mPaint.setTextSize(80);
        mPaint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText((int)progress + "%", oval.centerX(), oval.centerY(), mPaint);

    }
}
  // 创建 ObjectAnimator 对象
ObjectAnimator animator = ObjectAnimator.ofFloat(loadingView, "progress", 0, 85);
animator.setDuration(3000);
animator.start();

ofXXX有很多方法,可以满足我们任何自定义view属性的要求。

Interpolator 插值器

这个其实挺好理解的,打个比方 一个人从起跑点0m处跑到终点100m处。可以有很多种跑法

  • 一直加速跑 跑到终点
  • 跑到一半减速再加速到终点
  • 跑到一半停下来休息一下 再跑到终点
  • .....等等 有无限种跑法 ,全看你自己想怎么跑。甚至都可以跑过终点跑到150m处再跑回去

Interpolator 也是这样,Interpolator 就是设置你动画执行过程的,以不同的速度模型来将你的动画执行完毕

PathInterpolator这个较为特殊,尤其是配合 贝塞尔曲线使用的时候 会有很多酷炫的特效

  Path interpolatorPath = new Path();
// 先以「动画完成度 : 时间完成度 = 1 : 1」的速度匀速运行 25% 50的百分之25 就是12.5
interpolatorPath.lineTo(0.25f, 0.25f);
// 然后瞬间跳跃到 100% 的动画完成度  在这里其实也就是从50 直接跳跃到100 也就是圆形直接画满
interpolatorPath.moveTo(0.25f, 2.0f);
// 再匀速倒车,返回到目标点     画满以后 再回到 目标值
interpolatorPath.lineTo(1, 1);

// 创建 ObjectAnimator 对象
ObjectAnimator animator = ObjectAnimator.ofFloat(loadingView, "progress", 0, 50);
animator.setDuration(3000);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
    PathInterpolator pathInterpolator=new PathInterpolator(interpolatorPath);
    animator.setInterpolator(pathInterpolator);
}
animator.start();

ofXXX方法无法满足我咋办?Evaluator来帮你

比如说我自定义了某个view,这个view要完成我想要的动画需要改变的属性是一个自定义对象那咋办呢?自定义Evaluator呗

public class LoadingView extends View {

    Paint mPaint = new Paint();

    public CustomProperty getCustomProperty() {
        return customProperty;
    }

    public void setCustomProperty(CustomProperty customProperty) {
        this.customProperty = customProperty;
        invalidate();
    }

    /**
     * 进度条

     */
    CustomProperty customProperty = new CustomProperty(0,0);

    public LoadingView(Context context) {
        super(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF oval = new RectF(30, 30,
                getWidth()-30, getHeight()-30);
        //先画圆弧
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(customProperty.getStrokeWidth());
        canvas.drawArc(oval, -90, 360 * customProperty.getProgress()/100, false, mPaint);
    }
}
public class CustomProperty {
    float progress;

    public CustomProperty(float progress, int strokeWidth) {
        this.progress = progress;
        this.strokeWidth = strokeWidth;
    }

    public float getProgress() {
        return progress;
    }

    public void setProgress(float progress) {
        this.progress = progress;
    }

    public int getStrokeWidth() {
        return strokeWidth;
    }

    public void setStrokeWidth(int strokeWidth) {
        this.strokeWidth = strokeWidth;
    }

    @Override
    public String toString() {
        return "CustomProperty{" +
                "progress=" + progress +
                ", strokeWidth=" + strokeWidth +
                '}';
    }

    int strokeWidth;
}

最重要的自定义Evaluator

class CustomPropertyEvaluator implements TypeEvaluator<CustomProperty>
{
    CustomProperty customProperty=new CustomProperty(0,0);

    @Override
    public CustomProperty evaluate(float fraction, CustomProperty startValue, CustomProperty endValue) {

        float progress=startValue.progress+ fraction*endValue.getProgress();
        int strokeWidth=(int)(startValue.strokeWidth+fraction*endValue.getStrokeWidth());
        customProperty.setProgress(progress);
        customProperty.setStrokeWidth(strokeWidth);
        return customProperty;
    }
}

最后调用动画

 // 创建 ObjectAnimator 对象
ObjectAnimator animator = ObjectAnimator.ofObject(loadingView, "customProperty", new CustomPropertyEvaluator(),new CustomProperty(0,0), new CustomProperty(75,30));
animator.setDuration(3000);
animator.start();

PropertyValuesHolder 组合动画

组合动画无非就是动画执行的顺序集合,大概也就是分三种,先说前两种

一起执行和顺序执行

  //ofPropertyValuesHolder 代表一起执行动画的集合,holder1 holder2 holder3 可以一起执行 共享一个插值器 interpolator
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 1);
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 1);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 1);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(loadingView, holder1, holder2, holder3);
animator.start();

AnimatorSet animatorSet=new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofPropertyValuesHolder(loadingView, holder1, holder2, holder3);
ObjectAnimator animator2 = ObjectAnimator.ofPropertyValuesHolder(loadingView, holder1, holder2);
//如果需要动画依次播放:
animatorSet.playSequentially(animator1,animator2);
//也可以指定顺序
animatorSet.play(animator1).before(animator2);
animatorSet.start();

最后一种关键帧动画着重说一下,还记得前面插值器的介绍吗?有一个path插值器的,我们写了个demo带有回弹效果的, 利用关键帧动画的写法 可以不用那么复杂的插值器即可完成

//从0开始
Keyframe keyframe1 = Keyframe.ofFloat(0, 0);
//时间走到一半 我们应该圆圈画完
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
//时间走完的时候 我们的圆圈应该回到一半的位置
Keyframe keyframe3 = Keyframe.ofFloat(1, 50);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("progress", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(loadingView, holder);
animator.setDuration(3000);
animator.start();

自定义view 范围裁切 三维变换 以及绘制顺序

裁切

裁切的本质就是不管你想绘制什么,最终canvas只会在你规定的区域里绘制你想要的东西,换句话说 裁切也相当于在你绘制好的自定义view中只显示出来你裁切的那一块,其余部分不展示

这个布局预览器看到的灰色边框就是自定义view的大小,明显的能看出来我们实际绘制的内容距离我们自定义view的距离。 这是查验裁切效果最好的方法。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);

        canvas.save();
        canvas.clipRect(20,20,260,260);
        canvas.drawBitmap(bitmap,0,0,mPaint);
        canvas.restore();

        canvas.save();
        canvas.clipRect(20,300,260,460);
        canvas.drawBitmap(bitmap,0,0,mPaint);
        canvas.restore();
    }

二维变换

先看一段简单的translate的代码:

 @Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
    canvas.translate(200,100);
    canvas.drawBitmap(bitmap,0,0,mPaint);
}

再看旋转

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
//        canvas.translate(200,100);
        canvas.save();
        //注意旋转的角度 是以顺时针为正,逆时针为负
        canvas.rotate(45,200,200);
        canvas.drawBitmap(bitmap,0,0,mPaint);
        canvas.restore();

        //绘制这个中心点只是让你明白 是以哪个点为中心 进行旋转,
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setColor(Color.BLUE);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(10);
        canvas.drawPoint(200,200,mPaint);
    }

放大缩小

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);

        //等比例缩放 这里寻找中心点的方法 直接用bitmap实际的宽高来做,比之前的写死位置的更加直观
        canvas.save();
        canvas.scale(3.3f, 3.3f, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
        canvas.drawBitmap(bitmap, 0, 0, mPaint);
        canvas.restore();

    }

canvas变换的顺序

所有canvas的变化顺序 全是反着来的,这点要注意一下。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);

        int centerX=bitmap.getWidth()/2;
        int centerY=bitmap.getHeight()/2;


        // 目标效果  想 先移动 x轴 100个像素  再旋转90度  绘制

        //所以代码层面 必须 先rotate 90 再 translate
        canvas.save();
        canvas.rotate(90,183,275);
        canvas.translate(100,0);
        //注意旋转的角度 是以顺时针为正,逆时针为负
        canvas.drawBitmap(bitmap,0,0,mPaint);
        canvas.restore();

    }

三维变换

要理解好三维变化,首先要理解好android的三维坐标系,注意这个和view的canvans的二维坐标系是不一样的

再看下 三维坐标系的旋转方向

此外,最重要的一点就是 android的三维坐标系所对应的类为camera,这个camera 可不是拍照的那个camera,引入的时候要注意了,最后特别强调。

camera所有的rotate都是以view的原点为中心 也就是(0,0,0) 这个点。 且不支持设置旋转的轴心。

要想实现类似旋转轴心的效果,我们只能先把canvas挪到原点 然后进行 旋转,然后canvas 再挪回到我们想绘制的位置即可

注意canvas的混合变换是倒序的,这点千万不要忘记了

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Point point1 = new Point(200, 200);
        canvas.save();
        camera.save();
        camera.rotateX(30);
        camera.applyToCanvas(canvas);
        camera.restore();
        canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
        canvas.restore();
    }

然而这样的效果 显然我们不满意,按照之前的说法 我们应该canvas的坐标 先translate到原点,然后camera映射 再然后 translate 到我们目标绘制点,效果就能好很多。 注意这个translate的过程如果使用matrix则可以控制顺序 如果用原生的canvas的话 只能倒序,不要忘记这点

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //这个是我们想要绘制bitmap的 起点
        Point point1 = new Point(200, 200);

        //计算出我们bitmap的宽高
        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();

        //计算出 我们bitmap 绘制结束以后的中心点
        int center1X = point1.x + bitmapWidth / 2;
        int center1Y = point1.y + bitmapHeight / 2;

        camera.save();
        matrix.reset();
        camera.rotateX(30);
        camera.getMatrix(matrix);
        camera.restore();
        //先translate到0,0这个点进行 rotateX
        matrix.preTranslate(-center1X, -center1Y);
        //rotateX 结束以后 再translate 到我们目标位置 进行绘制
        matrix.postTranslate(center1X, center1Y);
        canvas.save();
        canvas.concat(matrix);
        canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
        canvas.restore();
    }

绘制顺序

自定义view的绘制顺序很好理解,基本原则就是 后面绘制的会盖住前面绘制的

draw()方法是用来调度 绘制顺序的,主要绘制方法有

按绘制的先后顺序来:

先调用drawBackground方法,注意这个方法是私有的 我们无法重写这个方法噢。

然后 onDraw()方法,这个不用多说了。

再然后

dispatchDraw()方法,注意这个方法一般viewgroup才使用,纯正的view这个方法是几乎用不到的。

换句话说 对于viewgroup来说,总是先ondraw绘制完自己以后 再调用dispatchDraw()来绘制子view

所以有时候我们extends某些viewgroup的时候如果仅仅是在ondraw方法里面重写我们想要的效果,

结果往往看不到,因为ondraw方法走完以后 dispatchDraw() 绘制子view 把我们绘制的内容覆盖掉了

所以谨记viewgroup 自定义的时候 到底是在ondraw还是dispatchDraw 中重写 要考虑清楚了。

最后 在 ViewGroup 的子类中重写除 dispatchDraw() 以外的绘制方法时,可能需要调用 setWillNotDraw(false); 在重写的方法有多个选择时,优先选择 onDraw()。

还有个在 onDrawForeground()这个方法是最后调用的,用来绘制滑动条和前景的。这个用的不多大家可以参考下。 有些蒙版效果 要用这个方法实现。

自定义view-渐变色,着色器

颜色渐变Gradient

  Shader shader = new LinearGradient(100, 100, 500, 500, Color.parseColor("#007500"),
                    Color.parseColor("#ff3333"), Shader.TileMode.REPEAT);
            paint.setShader(shader);
            canvas.drawRect(100, 100, 500, 500, paint);

看下效果:

下面修改一下代码看看三种TileMode的不同。

 //只有你Gradient的两点在 你绘制范围之内的时候才能见到三种不同 Shader.TileMode的效果
Shader shader = new LinearGradient(200, 200, 400, 400, Color.parseColor("#007500"),
        Color.parseColor("#ff3333"), Shader.TileMode.REPEAT);
paint.setShader(shader);
canvas.drawRect(100, 100, 500, 500, paint);


Shader shader2 = new LinearGradient(200, 700, 400, 900, Color.parseColor("#007500"),
        Color.parseColor("#ff3333"), Shader.TileMode.CLAMP);
paint.setShader(shader2);
canvas.drawRect(100, 600, 500, 1000, paint);


Shader shader3 = new LinearGradient(200, 1200, 400, 1300, Color.parseColor("#007500"),
        Color.parseColor("#ff3333"), Shader.TileMode.MIRROR);
paint.setShader(shader3);
canvas.drawRect(100, 1100, 500, 1500, paint);

BitmapShader着色器

要精确理解shader 其实也要熟悉坐标系,一切的一切都和坐标和绘制区域息息相关,效果理解是其次,主要要精确理解坐标和 绘制区域。首先我在xxhdpi的目录下放了一张美女图片

然后我们写一段代码在展示这张图片到我的手机里,我的手机当然也是xxhdpi的手机,这样图片不会缩放,我们理解起来容易

 Paint mPaint = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);

        canvas.drawBitmap(bitmap, 100, 50, mPaint);

        canvas.drawBitmap(bitmap, 100, 610, mPaint);


//        Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
//        mPaint.setShader(shader);
//
//        canvas.drawCircle(500, 500, 500, mPaint);
    }

然后再来理解shader

 Paint mPaint = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
        //绘制
        canvas.drawBitmap(bitmap, 0, 0, mPaint);


        Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mPaint.setShader(shader);

        canvas.drawCircle(900, 900, 100, mPaint);
    }

好,现在我们说 只想看到丫丫的脸,不想看其他的,哪我们既然知道丫丫的bitmap绘制区域是在view的左上角, 宽高我们也知道 是366和550,所以大概估算一下, 我们圆心的位置 180,250,半径就是150左右

 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
        //绘制
      //  canvas.drawBitmap(bitmap, 0, 0, mPaint);


        Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        mPaint.setShader(shader);

        canvas.drawCircle(180, 250, 150, mPaint);

然后我们再想看看tilemode的效果怎么办?其实这里的tilemode效果和上面讲的渐变的tilemode是一样的, 超出原本的bitmap绘制区域 才能看到效果噢,所以我们把半径放大就可以看到tilemode的效果了

 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
//绘制
//  canvas.drawBitmap(bitmap, 0, 0, mPaint);


Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint.setShader(shader);

canvas.drawCircle(180, 250, 450, mPaint);

ComposeShader混合着色器

这个稍微有点难度和复杂,主要是理解方式要找准。找准理解方式,其实就简单多了。

 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
//绘制
//  canvas.drawBitmap(bitmap, 0, 0, mPaint);


Shader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
mPaint.setShader(shader);

canvas.drawCircle(183, 275, 183, mPaint);

然后绘制一个圆形 注意坐标是怎么算出来的啊,其实就是算这个矩形的中心点。不说了,很显然最终效果应该是在屏幕中间有个圆形的头像

  Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
        Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        //这个android logo的原图是 144*144大小 注意了
        Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher_round);
        Shader shader2 = new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        Shader composeShader = new ComposeShader(shader, shader2, PorterDuff.Mode.SRC_OVER);

        mPaint.setShader(composeShader);

        canvas.drawCircle(102, 72, 72, mPaint);

然后看下这个混合着色器的效果:

改变不一样的参数 有不一样的效果。

//这个参数就是蒙版抠图效果了
Shader composeShader = new ComposeShader(shader, shader2, PorterDuff.Mode.DST_IN);

PorterDuff.Mode 参数很多,具体的可以查看官方文档的大图,好好理解是什么效果,这里只给一种效果 让大家明白如何使用。其实最后还是要理解坐标系。

ColorFilter 颜色变换

这个好理解,其实改变画笔的颜色矩阵,从而有不一样的效果。比如阳光色啊之类的。具体的有 LightingColorFilter PorterDuffColorFilter ColorMatrixColorFilter等。 取个最简单的LightingColorFilter 模拟下光照效果 看看

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);
//        Shader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

//        //这个android logo的原图是 144*144大小 注意了
//        Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher_round);
//        Shader shader2 = new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//
//        //这个参数就是蒙版抠图效果了
//        Shader composeShader = new ComposeShader(shader, shader2, PorterDuff.Mode.DST_IN);


        ColorFilter lightingColorFilter = new LightingColorFilter(0x00ffff, 0x000000);
        mPaint.setColorFilter(lightingColorFilter);

        canvas.drawBitmap(bitmap,0,0,mPaint);

MaskFilter

 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.dly);

        //BlurMaskFilter.Blur参数值大家可以自行试试 总共四种
        mPaint.setMaskFilter(new BlurMaskFilter(50, BlurMaskFilter.Blur.INNER));

        canvas.drawBitmap(bitmap,0,0,mPaint);