最近闲置在家,回顾以前做过的东西,发现自己一下子凌乱了,写过的或者用过的东西,再次敲出来,竟然有些难度,这才发现,虽然到最后都实现了需求,但是自己却有些不知所云。想想也对,只是单纯的为了实现某一功能,为了追求时间追求效率,能找到现成的,就用,很少真正的去探索原理和实现思路。所以,决定也借鉴各位大神的方法,借用Blog来梳理一些东西,便于温故而知新,同时也希望能帮助到需要该功能的同行。
闲话不多说,接下来要梳理的第一个就是近期项目用到的一个在加载WebView时的进度条(嗯嗯,怎么感觉有点自娱自乐的样子哈),不过就效果来看,别的地方貌似也可以用的到。同时呢,也要感谢http://blog.csdn.net/hanhailong726188/article/details/47363911的文章,给提供了方向,在此感谢。在使用过程中,发现里面有一些地方不是很完善,我在这里就着他的源码做了优化和补充,整个下来,对自定义的控件的实现和动画的理解又加深了一些。
源代码:DoubleBallProgress
要优化的地方:适配Android3.0,即API-11以前的版本。该进度条中的动画使用的是Property Animation,我们知道属性动画是Android 3.0以后才有的特新,因此,3.0以前的版本,就要进行适配,不能把3.0以前的就那么抛弃了不是。先上效果图:
Android2.2效果图:
Android4.4效果图:
实现方式是:引用了一个开源的nineoldandroids.jar来兼容3.0以前的版本,至于使用,很简单,请自行百度(或者点击查阅源码),我就不再这里嘚啵嘚啵了。
接下来,就是一些补充,添加了自定义的属性,可以直接在layout的布局文件中设置,补充了大部分的color颜色定义,下面直接上代码(具体的实现请下载源码自行查看,或者直接查阅原作者的文章)。
首先,要自定义attrs.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="DoubleBallProgress"> <attr name="one_ball_color" format="color" /> <attr name="two_ball_color" format="color" /> <attr name="max_radius" format="dimension" /> <attr name="min_radius" format="dimension" /> <!--两个ball之间的运行轨迹的间距--> <attr name="distance" format="dimension" /> <attr name="duration" format="integer" /> </declare-styleable> </resources>当这个文件写好以后,就可以在布局文件中,来定义各个属性了:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/rl" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.hqian.doubleballprogress.MainActivity"> <com.example.hqian.doubleballprogress.view.DoubleBallProgress android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" custom:two_ball_color="#00FF00" custom:one_ball_color="#FF8C00" custom:duration="1500" /> </RelativeLayout>需要提的一点是:要使用自定义属性,那么就要导入自己的xmlns,因为我使用的IDE是android studio,是这样子引用的xmlns:custom="http://schemas.android.com/apk/res-auto" 在Eclipse中好像是这样子引用的xmlns:custom="http://schemas.android.com/apk/res/packagename,使用Eclipse的同学可以自己试试。
当在布局中定义好了自己的布局以后,接下来就需要在自定义的View的构造方法中获取我们定义的属性了以及相关的实现了:
import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; import com.example.hqian.doubleballprogress.R; import com.nineoldandroids.animation.AnimatorSet; import com.nineoldandroids.animation.ObjectAnimator; import com.nineoldandroids.animation.ValueAnimator; /** * Created by heqian on 2016/8/16. */ public class DoubleBallProgress extends View { private final static String TAG = "DoubleBallProgress"; //默认小球最小半径 private final static int DEFAULT_MIN_RADIUS = 5; //默认小球最大半径 private final static int DEFAULT_MAX_RADIUS = 15; //默认两个小球运行轨迹距离 private final static int DEFAULT_DISTANCE = 20; //默认第一个小球颜色 private final static int DEFAULT_ONE_BALL_COLOR = Color.parseColor("#40df73"); //默认第二个小球颜色 private final static int DEFAULT_TWO_BALL_COLOR = Color.parseColor("#ffdf3e"); //默认动画执行时间 private final static int DEFAULT_ANIMATOR_DURATION = 1000; private Paint mPaint; private float minRadius = 0; private float maxRadius = 0; private int oneBallColor = 0; private int twoBallColor = 0; private int distance = 0; private long animatorDuration = 1000; private Ball mOneBall; private Ball mTwoBall; private float mCenterX; private float mCenterY; private AnimatorSet animatorSet; private boolean onMeasureFlag; public DoubleBallProgress(Context context) { this(context, null); } public DoubleBallProgress(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DoubleBallProgress(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.DoubleBallProgress); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics dm = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(dm); // 或者可以使用 getResources().getDisplayMetrics() minRadius = typedArray.getDimensionPixelSize(R.styleable.DoubleBallProgress_min_radius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MIN_RADIUS, dm)); maxRadius = typedArray.getDimensionPixelSize(R.styleable.DoubleBallProgress_max_radius, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_MAX_RADIUS, dm)); distance = typedArray.getDimensionPixelSize(R.styleable.DoubleBallProgress_distance, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_DISTANCE, dm)); oneBallColor = typedArray.getColor(R.styleable.DoubleBallProgress_one_ball_color, DEFAULT_ONE_BALL_COLOR); twoBallColor = typedArray.getColor(R.styleable.DoubleBallProgress_two_ball_color, DEFAULT_TWO_BALL_COLOR); animatorDuration = typedArray.getInteger(R.styleable.DoubleBallProgress_duration, DEFAULT_ANIMATOR_DURATION); mOneBall = new Ball(); mTwoBall = new Ball(); mOneBall.setColor(oneBallColor); mTwoBall.setColor(twoBallColor); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); initAnimator(); } /** * 进行一些宽和高的测量 * * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!onMeasureFlag) { Log.d(TAG, "onMeasure: widthMeasureSpec = " + widthMeasureSpec + ",heightMeasureSpec = " + heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSoecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSoecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(50,50); } else if (widthSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(50,heightSoecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST){ setMeasuredDimension(widthSoecSize,50); } mCenterX = getWidth() / 2; mCenterY = getHeight() / 2; onMeasureFlag = true; } } /** * 在布局发生变化时的回调函数,间接回去调用onMeasure,onLayout函数重新布局 */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); Log.d(TAG, "onSizeChanged: w = " + w + ",h = " + h + ",oldw = " + oldw + ",oldh = " + oldh); mCenterX = w / 2; mCenterY = h / 2; } @Override protected void onDraw(Canvas canvas) { //画两个小球,半径小的先画,半径大的后画 if (mOneBall.getRadius() > mTwoBall.getRadius()) { mPaint.setColor(mTwoBall.getColor()); canvas.drawCircle(mTwoBall.getCenterX(), mCenterY, mTwoBall.getRadius(), mPaint); mPaint.setColor(mOneBall.getColor()); canvas.drawCircle(mOneBall.getCenterX(), mCenterY, mOneBall.getRadius(), mPaint); } else { mPaint.setColor(mOneBall.getColor()); canvas.drawCircle(mOneBall.getCenterX(), mCenterY, mOneBall.getRadius(), mPaint); mPaint.setColor(mTwoBall.getColor()); canvas.drawCircle(mTwoBall.getCenterX(), mCenterY, mTwoBall.getRadius(), mPaint); } } /** * 配置属性动画 */ private void initAnimator() { Log.d(TAG, "initAnimator was executed"); float centerRadius = (minRadius + maxRadius) * 0.5f; //第一个小球缩放动画,通过改变小球的半径,半径变化规律:中s间大小->最大->中间大小->最小->中间大小 ObjectAnimator oneScaleAnimator = ObjectAnimator.ofFloat(mOneBall, "radius", centerRadius, maxRadius, centerRadius, minRadius, centerRadius); oneScaleAnimator.setRepeatCount(ValueAnimator.INFINITE); //第二个小球缩放动画,变化规律:中间大小->最小->中间大小->最大->中间大小 ObjectAnimator twoScaleAnimator = ObjectAnimator.ofFloat(mTwoBall, "radius", centerRadius, minRadius, centerRadius, maxRadius, centerRadius); twoScaleAnimator.setRepeatCount(ValueAnimator.INFINITE); //第一个小球位移动画,通过改变小球的圆心的位置,并且进行重绘 ValueAnimator oneCenterAnimator = ValueAnimator.ofFloat(-1, 0, 1, 0, -1); oneCenterAnimator.setRepeatCount(ValueAnimator.INFINITE); oneCenterAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); float x = mCenterX + (distance) * value; mOneBall.setCenterX(x); //不停的刷新view,让view不停的重绘 invalidate(); } }); //第二个小球位移动画 ValueAnimator twoCenterAnimator = ValueAnimator.ofFloat(1, 0, -1, 0, 1); twoCenterAnimator.setRepeatCount(ValueAnimator.INFINITE); twoCenterAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); float x = mCenterX + (distance) * value; mTwoBall.setCenterX(x); } }); animatorSet = new AnimatorSet(); animatorSet.playTogether(oneScaleAnimator, oneCenterAnimator, twoScaleAnimator, twoCenterAnimator); animatorSet.setDuration(animatorDuration); // 时间插值器,表示加速度,此处定义的是开始最快,结尾最慢 animatorSet.setInterpolator(new DecelerateInterpolator()); } /** * 加载到window上的时候就开始动画 */ @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); startAnimator(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopAnimator(); } /** * 在该View隐藏的时候关闭动画,在可见的时候开启动画 * * @param v */ @Override protected void onVisibilityChanged(View changedView, int v) { super.onVisibilityChanged(changedView, v); if (v == GONE || v == INVISIBLE) { stopAnimator(); } else { startAnimator(); } } /** * ball */ public class Ball { private float radius;//半径 private float centerX;//圆心 private int color;//颜色 public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } public float getCenterX() { return centerX; } public void setCenterX(float centerX) { this.centerX = centerX; } public int getColor() { return color; } public void setColor(int color) { this.color = color; } } /** * 设置第一个球的颜色 * * @param color */ public void setOneBallColor(int color) { mOneBall.setColor(color); } /** * 设置第二个球的颜色 * * @param color */ public void setmTwoBallColor(int color) { mTwoBall.setColor(color); } /** * 设置球的最大半径 * * @param maxRadius */ public void setMaxRadius(float maxRadius) { this.maxRadius = maxRadius; initAnimator(); } /** * 设置球的最小半径 * * @param minRadius */ public void setMinRadius(float minRadius) { this.minRadius = minRadius; initAnimator(); } /** * 设置两个球旋转的最大范围距离 * * @param distance */ public void setDistance(int distance) { this.distance = distance; } public void setDuration(long duration) { this.animatorDuration = duration; if (animatorSet != null) { animatorSet.setDuration(duration); } } /** * 开始动画 */ public void startAnimator() { if (getVisibility() != VISIBLE) return; if (animatorSet.isRunning()) return; if (animatorSet != null) { animatorSet.start(); } } /** * 结束停止动画 */ public void stopAnimator() { if (animatorSet != null) { animatorSet.end(); } } }在initAnimator方法中还是实现了动画的效果,该动画使用了属性动画,它是API-11 即android 3.0 以上版本新加的特性,区别与View动画,它的作用对象进行了扩展,可以对任意对象做动画,甚至于还可以没有对象:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。因此,我觉得属性动画是3.0以后伟大的改变,它几乎是无所不能的,只要对象有这个属性,它都能实现动画效果。但是因为是从API-11才有的新特性,所以就要对API-11以前的做适配,就像开头写的,用nimeoldandroids来兼容以前的版本
属性动画中有ValueAnimator,ObjectAnimator和Animator的概念,通过它们可以实现许多炫丽的动画效果,具体的使用方法,可以参照initAnimator方法来理解。
在onMeasure方法中,来获取中心点的位置,因为该方法可能会被多次调用,因此,定义了一个flag来标识,当获取到位置以后,就不执行该下面的代码,防止它多次进行测量。在一个View中,主要的工作流程是measure,layout,draw这三大流程,即测量,布局和绘制。measure确定View的测量宽/高,因为这里只是定义一个View,而非ViewGroup,那么layout就不需要去重写了。Measure的过程分两种情况,如果只是一个View的时候,那么通过Measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程,还对遍历去调用所有子元素的measure方法,各个子元素在递归去执行这个方法。View的Measure过程由measure方法来完成,因为它是一个final类型的方法,因此只要看onMeasure的实现即可,所以就直接在onMeasure方法中来计算了中心点的位置。
在onDraw方法中,来绘制自己的图像。它是被View的draw调用的。draw的绘制过程遵循如下几步:
1.绘制背景background.draw(canvas);
2.绘制自己(onDraw);
3.绘制children(dispatchDraw);
4.绘制装饰(onDrawScrollBars)。
View的绘制过程通过dispatchDraw方法开实现,它会遍历所有的子元素的draw方法,一步步的往下面绘制,这些都可以通过draw方法的源码看出来。(源码就不贴出来了,相信都不咋看的。)
最后,需要说的就是,在自定义View中如果有动画或者线程,需要及时停止,防止内存泄漏
比如该View中重写onDetachedFeomWindow()方法,在该方法中停止动画,该方法会在包含该View的Activity退出或者View被remove掉的时候调用,与该方法对应的是onAttachedToWindow()方法;同时在View可见状态改变的时候,重写onVisibilityChanged方法,开启或者停止动画
@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); stopAnimator(); } @Override protected void onVisibilityChanged(View changedView, int v) { super.onVisibilityChanged(changedView, v); if (v == GONE || v == INVISIBLE) { stopAnimator(); } else { startAnimator(); } }
戳此获取源码:DoubleBallProgress
