底部黑边是录制时不小心录上的,可以忽略。
既然是自定义View我们就按标准的流程来,第一步,自定义属性
自定义属性
建立属性文件
在Android项目的res->values目录下新建一个attrs.xml文件,文件内容如下:
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="PendulumView"><attr name="globeNum" format="integer"/><attr name="globeColor" format="color"/><attr name="globeRadius" format="dimension"/><attr name="swingRadius" format="dimension"/> </declare-styleable></resources>其中declare-styleable的name属性用于在代码中引用该属性文件。name属性,一般情况下写的都是我们自定义View的类名,较为直观。
public PendulumView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);//使用TypedArray读取自定义的属性值TypedArray ta = context.getResources().obtainAttributes(attrs, R.styleable.PendulumView);int count = ta.getIndexCount();for (int i = 0; i < count; i++) {int attr = ta.getIndex(i);switch (attr) {case R.styleable.PendulumView_globeNum:mGlobeNum = ta.getInt(attr, 5);break;case R.styleable.PendulumView_globeRadius:mGlobeRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));break;case R.styleable.PendulumView_globeColor:mGlobeColor = ta.getColor(attr, Color.BLUE);break;case R.styleable.PendulumView_swingRadius:mSwingRadius = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 16, getResources().getDisplayMetrics()));break;}}ta.recycle(); //避免下次读取时出现问题mPaint = new Paint();mPaint.setColor(mGlobeColor);}重写OnMeasure()方法
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);//高度为小球半径+摆动半径int height = mGlobeRadius + mSwingRadius;//宽度为2*摆动半径+(小球数量-1)*小球直径int width = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1) + mSwingRadius;//如果测量模式为EXACTLY,则直接使用推荐值,如不为EXACTLY(一般处理wrap_content情况),使用自己计算的宽高setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize : width, (heightMode == MeasureSpec.EXACTLY) ? heightSize : height);}其中
重写onDraw()方法
@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//绘制除左右两个小球外的其他小球for (int i = 0; i < mGlobeNum - 2; i++) {canvas.drawCircle(mSwingRadius + (i + 1) * 2 * mGlobeRadius, mSwingRadius, mGlobeRadius, mPaint);}if (mLeftPoint == null || mRightPoint == null) {//初始化最左右两小球坐标mLeftPoint = new Point(mSwingRadius, mSwingRadius);mRightPoint = new Point(mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1), mSwingRadius);//开启摆动动画startPendulumAnimation();}//绘制左右两小球canvas.drawCircle(mLeftPoint.x, mLeftPoint.y, mGlobeRadius, mPaint);canvas.drawCircle(mRightPoint.x, mRightPoint.y, mGlobeRadius, mPaint);}onDraw()方法是自定义View的关键所在,在该方法体内绘制View的显示效果。代码首先绘制了除去最左边最右边小球以外的其他小球,然后对左右两小球的坐标值进行判断,如果是第一次绘制,坐标值均为空,则初始化两小球坐标,并且开启动画。最后通过mLeftPoint,mRightPoint的x,y值,绘制左右两个小球。
public void startPendulumAnimation() {//使用属性动画final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {@Overridepublic Object evaluate(float fraction, Object startValue, Object endValue) {//参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值double angle = Math.toRadians(90 * fraction);int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));Point point = new Point(x, y);return point;}}, new Point(), new Point());anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {Point point = (Point) animation.getAnimatedValue();//获得当前的fraction值float fraction = anim.getAnimatedFraction();//判断是否是fraction先减小后增大,即是否处于即将向上摆动状态//在每次即将向上摆动时切换小球if (lastSlope && fraction > mLastFraction) {isNext = !isNext;}//通过不断改动左右小球的x,y坐标值实现动画效果//利用isNext来判断应该是左边小球动,还是右边小球动if (isNext) {//当左边小球摆动时,右边小球置于初始位置mRightPoint.x = mSwingRadius + mGlobeRadius * 2 * (mGlobeNum - 1);mRightPoint.y = mSwingRadius;mLeftPoint.x = mSwingRadius - point.x;mLeftPoint.y = mGlobeRadius + point.y;} else {//当右边小球摆动时,左边小球置于初始位置mLeftPoint.x = mSwingRadius;mRightPoint.y = mSwingRadius;mRightPoint.x = mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + point.x;mRightPoint.y = mGlobeRadius + point.y;}invalidate();lastSlope = fraction < mLastFraction;mLastFraction = fraction;}});//设置永久循环播放anim.setRepeatCount(ValueAnimator.INFINITE);//设置循环模式为倒序播放anim.setRepeatMode(ValueAnimator.REVERSE);anim.setDuration(200);//设置补间器,控制动画的变化速率anim.setInterpolator(new DecelerateInterpolator());anim.start();}其中使用ValueAnimator.ofObject方法是为了可以对Point对象进行操作,更为形象具体。还有就是通过ofObject方法使用了自定义的TypeEvaluator对象,由此得到了fraction值,该值是一个从0-1变化的小数。所以该方法的后两个参数startValue(new Point()),endValue(new Point())并没有实际意义,也可以直接不写,此处写上主要是为了便于理解。同样道理也可以直接使用ValueAnimator.ofFloat(0f, 1f)方法获取到一个从0-1变化的小数。
final ValueAnimator anim = ValueAnimator.ofObject(new TypeEvaluator() {@Overridepublic Object evaluate(float fraction, Object startValue, Object endValue) {//参数fraction用于表示动画的完成度,我们根据它来计算当前的动画值double angle = Math.toRadians(90 * fraction);int x = (int) ((mSwingRadius - mGlobeRadius) * Math.sin(angle));int y = (int) ((mSwingRadius - mGlobeRadius) * Math.cos(angle));Point point = new Point(x, y);return point;}}, new Point(), new Point());通过fraction,我们计算得到小球摆动时的角度变化值,0-90度
mSwingRadius-mGlobeRadius表示的值是图中绿色直线的长度,摆动的路线,小球圆心的路线是一个以(mSwingRadius-mGlobeRadius)为半径的弧线,变化的X值为(mSwingRadius-mGlobeRadius)*sin(angle),变化的y值为(mSwingRadius-mGlobeRadius)*cos(angle)
对应的小球实际的圆心坐标为(mSwingRadius-x,mGlobeRadius+y)
右边小球运动路线与左边类似,仅仅是方向不同。右边小球实际的圆心坐标(mSwingRadius + (mGlobeNum - 1) * mGlobeRadius * 2 + x,mGlobeRadius+y)
可见左右两边小球的纵坐标是相同的,仅横坐标不同。
float fraction = anim.getAnimatedFraction();//判断是否是fraction先减小后增大,即是否处于即将向上摆动状态//在每次即将向上摆动时切换小球if (lastSlope && fraction > mLastFraction) {isNext = !isNext;}//记录上一次fraction是否不断减小lastSlope = fraction < mLastFraction;//记录上一次的fractionmLastFraction = fraction;这两段代码用于计算何时切换运动的小球,本动画设置了循环播放,且循环模式为倒序播放,所以动画的一个周期即为小球抛起加上小球落下的过程。在该过程中fraction的值先有0变为1,再由1变为0。那么何时是动画新一轮周期的开始呢?就是在小球即将抛起的时候,在这个时候切换运动的小球,即可实现左边小球落下后右边小球抛起,右边小球落下后左边小球抛起的动画效果。
anim.setDuration(200);//设置补间器,控制动画的变化速率anim.setInterpolator(new DecelerateInterpolator());anim.start();设置动画的持续时间为200毫秒,读者可以通过更改该值而达到修改小球摆动速度的目的。