Android中的自绘View的那些事儿(七)之 实例分享
前面的一系列文章中,我们介绍了如何自定义View和自定义View中如何自绘的一些相关知识,相信掌握这些知识后便可以绘制出大多实际需求中的炫酷控件了。不过,前面的文章中都是为了突出当前文章的知识点而粗略地用代码展示的一些小的Demo。今天我们就通过一个真实的实例来真正了解实际开发过程中是如何自定义、自绘制一个View的。本实例主要是实现一个Loading旋转的控件。下面我们来通过代码分析关键步骤。
代码
public class LoadingView extends View {
private final static int DEF_WIDTH_PX = 100; // 整个控件默认宽的像素
private final static int DEF_HEIGHT_PX = 100; // 整个控件默认高的像素
private final static int DEF_RING_PAINT_STROKE_PX = 2; // 圆环画笔默认宽大小的像素
private final static int DEF_RING_RADIUS_PX = 45; // 圆环默认半径的像素
private final static int DEF_HEAD_CIRCLE_RADIUS_PX = 2; // 圆环头部实心圆默认半径的像素
private final static int CIRCLE_ANGLE_MIN = 0; // 圆的角度最小值
private final static int CIRCLE_ANGLE_MAX = 360; // 圆的角度最大值
public final static int DRAW_INTERVAL = 16; // 动画绘制间隔16毫秒
private final static int ROTATE_DURATION = 1000; // 动画旋转时间
private final static int START_POINT_TOP = -90; // 默认是从3点钟方向开始,所以再让他逆时针旋转90°,从0点开始
private final static int[] RING_COLOR = {0x00ffffff, 0xffffffff}; // 圆环颜色
private final static int HEAD_CIRCLE_COLOR = 0xffffffff; // 圆环上实心圆颜色
private float mDefWidth; // 控件默认宽度
private float mDefHeight; // 控件默认高度
private int mCenterX; // 中心坐标X
private int mCenterY; // 中心坐标Y
private float mMobileAngle; // 旋转度数
private float mSizeProportion; // 大小比例
private boolean mContinueDraw; // 动画是否要继续绘制
private Paint mRingPaint; // 圆环的画笔
private float mRingRadius; // 圆环的半径
private SweepGradient mRingSweepGradient; // 圆环的颜色渐变渲染器
private Matrix mRingMatrix; // 旋转矩阵
private Paint mHeadCirclePaint; // 圆环上实心圆的画笔
private float mHeadCircleRadius; // 圆环上实心圆的半径
private Handler mHandler;
private AnimatorSet mAnimatorSet;
public LoadingView(Context context) {
this(context, null, 0);
}
public LoadingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mHandler = new Handler();
mRingMatrix = new Matrix();
mRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRingPaint.setStyle(Paint.Style.STROKE);
mHeadCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHeadCirclePaint.setColor(HEAD_CIRCLE_COLOR);
mHeadCirclePaint.setStyle(Paint.Style.FILL);
mDefWidth = dp2px(DEF_WIDTH_PX);
mDefHeight = dp2px(DEF_HEIGHT_PX);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopAnimation();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == VISIBLE) {
startAnimation();
} else {
stopAnimation();
}
}
/**
* 结束动画
*/
private void stopAnimation() {
if (mAnimatorSet != null) {
if (mContinueDraw) {
mAnimatorSet.cancel();
}
mAnimatorSet = null;
}
}
/**
* 开始动画
*/
private void startAnimation() {
stopAnimation();
mAnimatorSet = new AnimatorSet();
mAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mContinueDraw = true;
invalidate();
}
@Override
public void onAnimationEnd(Animator animation) {
mContinueDraw = false;
}
});
ValueAnimator animationToInit = getAnimationToRotate();
mAnimatorSet.play(animationToInit);
mAnimatorSet.start();
}
private ValueAnimator getAnimationToRotate() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(CIRCLE_ANGLE_MIN, CIRCLE_ANGLE_MAX); // 旋转从0度到360度
valueAnimator.setDuration(ROTATE_DURATION); // 每次动画的时间
valueAnimator.setRepeatCount(Integer.MAX_VALUE); // int的最大值,代表无限重复动画
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mMobileAngle = (float)animation.getAnimatedValue();
}
});
return valueAnimator;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int)mDefWidth, (int)mDefHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int)mDefWidth, heightSpaceSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpaceSize, (int)mDefHeight);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mCenterX = getWidth() / 2;
mCenterY = getHeight() / 2;
if (mCenterX == 0) {
return;
}
mRingSweepGradient = new SweepGradient(mCenterX, mCenterY, RING_COLOR, null);
if (getWidth() >= getHeight()) {
mSizeProportion = getHeight() / mDefHeight;
} else {
mSizeProportion = getWidth() / mDefWidth;
}
mRingPaint.setStrokeWidth(dp2px(DEF_RING_PAINT_STROKE_PX * mSizeProportion));
mRingRadius = dp2px(DEF_RING_RADIUS_PX * mSizeProportion);
mHeadCircleRadius = dp2px(DEF_HEAD_CIRCLE_RADIUS_PX * mSizeProportion);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawRing(canvas);
drawHeadCircle(canvas);
drawInterval();
}
/**
* 绘制圆环
* @param canvas
*/
private void drawRing(Canvas canvas) {
mRingMatrix.setRotate((START_POINT_TOP + mMobileAngle), mCenterX, mCenterY);
mRingSweepGradient.setLocalMatrix(mRingMatrix);
mRingPaint.setShader(mRingSweepGradient);
canvas.drawCircle(mCenterX, mCenterY, mRingRadius, mRingPaint);
}
/**
* 绘制圆环头部的实心圆
* @param canvas
*/
private void drawHeadCircle(Canvas canvas) {
float[] positions = getCirclePositions();
canvas.drawCircle(positions[0], positions[1], mHeadCircleRadius, mHeadCirclePaint);
}
/**
* 绘制间隔
*/
private void drawInterval() {
if (!mContinueDraw) {
return;
}
mHandler.postDelayed(new Runnable() {
public void run() {
invalidate();
}
}, DRAW_INTERVAL);
}
/**
* 获得圆环上的坐标
* @return
*/
private float[] getCirclePositions() {
double headCircleRadian = Math.PI / (CIRCLE_ANGLE_MAX / 2f) * mMobileAngle; // 角度转弧度
float circleX = (float) (mCenterX + mRingRadius * Math.sin(headCircleRadian));
float circleY = (float) (mCenterY - mRingRadius * Math.cos(headCircleRadian)); // 手机屏幕中,y轴是从上到下,所这里是减
float[] positions = {circleX, circleY};
return positions;
}
/**
* dp转px
* @param dp
* @return
*/
private float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<zyx.drawdemo.LoadingView
android:layout_marginTop="80dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"/>
<zyx.drawdemo.LoadingView
android:layout_marginTop="250dp"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
运行效果
重点说明
一、onMeasure方法
onMeasure方法的作用是实现了控件在XML配置中layout_width或layout_height为wrap_content时,会默认使用mDefWidth或mDefHeight两个变量来决定控件在宽或高。这里是100dp。
二、onLayout方法
1.因为要获得控件的宽和高来决定圆心,所以要onLayout方法调用getWidth()和getHeight()来获得。
2.mSizeProportion变量是大小比例的意思,用于当在XML配置中layout_width或layout_height为实现dp值时,会按照默认100dp与当前尺寸的比例来决定:mRingPaint、mRingRadius 和 mHeadCircleRadius 这三个变量的大小。
三、onDraw方法
1.onDraw方法内部调用了3个私有方法:drawRing、drawHeadCircle和 drawInterval。
2.drawRing方法负责绘制旋转的圆环。方法内有Matrix对象,用于旋转,变量mMobileAngle决定当前旋转的角度。
3.drawHeadCircle方法负责绘制圆环头部的实心圆。通过getCirclePositions方法获得当前旋转角度对应在圆环上的坐标点来决定实心圆的圆心。
4.drawInterval方法是动画中使用,内部做了是否经继续绘制和若要继续绘制会延时 DRAW_INTERVAL 毫秒再次绘制。
5.DRAW_INTERVAL的值是16,意思就是每16毫秒进行一次刷新绘制。为什么是16?其实很简单,因为常规屏幕刷新率是60,人眼在1秒内能看到最舒服的刷新频率是60,那么1000毫秒/60的结果就是16.66666。所以一般地凡是做动画,绘制刷新的间隔值都是设为16毫秒的。小于16的话效果无区别,但会影响性能。
四、构造函数
构造函数中有init方法,里头做了一些变量初始化的处理,其中会发现有两个Paint对象。没错,是两个,因为我们要绘制的东西有两个,一个是圆环,另一个是圆环头上的实心圆。为了提高绘制性能,而不必在每次绘制都去设置相关的大小、颜色之类的参数,我们一般会单独定义Paint对象。
五、动画
1.onDetachedFromWindow、onAttachedToWindow和 onWindowVisibilityChanged三个方法最适合像当前控件一样,在显示时、初始化时便执行开始动画,在隐藏时、销毁时便执行停止动画。
2.当前控件主是要旋转,所以属性动画主要针对mMobileAngle变量做值变化处理。
3.在本实例中,因为只是简单的旋转动画,所以如果不使用属性动画,也可以使用另一种办法。就是在每次onDraw后对mMobileAngle进行递增,以至到360后再清空为0。具体实现这里省略。
源码下载
上一篇: .NET Framework 4.5的C#中的对话框消息
下一篇: 1.1 将序列分解为多个变量