自定义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 需要注意的点如下:
- 一定是先重写onMeasure确定子view的宽高和自己的宽高以后 才可以继续写onlayout 对这些子view进行布局噢~~
- viewgroup 的onMeasure其实就是遍历自己的view 对自己的每一个子view进行measure,绝大多数时候对子view的 measure都可以直接用 measureChild()这个方法来替代,简化我们的写法,如果你的viewgroup很复杂的话 无法就是自己写一遍measureChild 而不是调用measureChild 罢了。
- 计算出viewgroup自己的尺寸并且保存,保存的方法还是哪个setMeasuredDimension 不要忘记了
- 逼不得已要重写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);