前言
思乡心切啊!还有三天就可以回家过年了!
“业余时间”是自己写的一个APP,最初学习Android的时候因为很闲,所以听从师父的建议,利用业余时间把公司的项目重写了一遍(大部分主要功能),之后开始写自己的APP,以为了快速提高技术。这个APP很简陋,只是一些图片和文字的列表,还有设置功能和毫无卵用的启动密码保护功能,虽然现在来看里面的代码写的惨不忍睹,但是不可否认的是,自己当时确实也通过写这个APP学到了不少东西,如:列表的下拉刷新与加载更多的逻辑,Android5.0的新控件,图片列表在未知图片宽高时的加载优化等等,甚至为了模仿探探和网易云音乐一个很好看的功能,学会了反编译和抓包。这也是为什么最近想开始重写这个APP的原因,就像任玉刚说的,我们不可能通过公司的项目学到Android的每个知识点和新技术,那么业余时间的学习就显得很重要。况且公司的项目大多数情况下也不是我们想怎么改就怎么改的,leader会让你把okhttp改为Retrofit?会让你把SharedPreferences改为GreenDao3?会让你把MVC改为MVP?有太多的新技术不是一个公司的项目就全能用到的。
头部标识
一个APP怎么可能没有列表呢,既然有列表那么就来个个性化的下拉刷新提示与加载提示吧!最终效果图如下
刷新头部时钟的关键代码:@Override时钟的原图是从网上找的,在onSizeChanged方法中计算始终各个部位相对于时钟大小的比例,在onDraw中画出时钟就可以了。为了响应下拉的距离,对外暴露一个实时设置角度的方法:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
size = Math.min(getWidth(), getHeight());
int factor = 524;//默认624
sharpRadius = size * 30.0f / (factor * 2.0f);
otherRadius = size * 18.0f / (factor * 2.0f);
outsideCircleWidth = size * 25.0f / factor;
outsideDistance = size * 95.0f / factor;
hourLength = size * 180.0f / factor;
minuteLength = size * 110.0f / factor;
needleWidth = size * 35.0f / (factor * 2.0f);
centerCircleRadius = needleWidth;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画背景和外圆
canvas.drawColor(Color.TRANSPARENT);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(outsideCircleWidth);
canvas.drawCircle(size / 2, size / 2, (size - outsideCircleWidth) / 2, paint);
//画时间圆点
paint.setStyle(Paint.Style.FILL);
for (int i = 0; i < 12; i++) {
if (i % 3 == 0) {
canvas.drawCircle(size / 2, outsideDistance, sharpRadius, paint);
} else {
canvas.drawCircle(size / 2, outsideDistance, otherRadius, paint);
}
canvas.rotate(30, size / 2, size / 2);
}
paint.setStrokeWidth(needleWidth);
//画时针
canvas.drawLine(size / 2, size / 2, getX(minuteLength, angle), getY(minuteLength, angle), paint);
//画分针
int hourAngle = (angle % 30) * 12 - 90;
canvas.drawLine(size / 2, size / 2, getX(hourLength, hourAngle), getY(hourLength, hourAngle), paint);
//画中心圆点
canvas.drawCircle(size / 2, size / 2, centerCircleRadius, paint);
}
private float getX(float radius, int angle) {
return (float) (size / 2 + radius * Math.cos(angle * 3.14 / 180));
}
private float getY(float radius, int angle) {
return (float) (size / 2 + radius * Math.sin(angle * 3.14 / 180));
}public void setClockAngle(int angle) {这里的angle参数实际上就是刷新头部的高度。正在刷新的动画用ValueAnimator轻松实现:
angle /= 4;
this.angle = angle - 90;
invalidate();
}public void startClockAnim() {这里起始的角度就是手指松开时时钟对应的角度,动画数值为时针的角度,通过时针角度可以算出分针角度,这里利用setIntValues(int... values)方法重新设置起始角度,而非每次新创建ValueAnimator。OK,刷新头部的刷新标识就实现了。
if (animator == null) {
animator = ValueAnimator.ofInt(angle, angle + 360);
animator.setRepeatMode(ValueAnimator.RESTART);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setDuration(10000).setInterpolator(new LinearInterpolator());
animator.start();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
angle = (int) animation.getAnimatedValue();
invalidate();
}
});
} else {
animator.setIntValues(angle, angle + 360);
animator.start();
}
}
public void stopClockAnim() {
if (animator != null) {
animator.cancel();
}
}
尾部标识
尾部标识动画的关键在于角度的计算,这是之前的项目的一个动画,当时是射击师给的图用帧动画实现的,后来发现图片的适配有问题就改用自定义View实现了,当时在写的时候角度算了好久就是不对,后来想到之前一个IOS同事的说过的话,动画的实现得一步一步来,所以当我把它拆解为半圆的角度渐增动画和半圆的旋转动画时发现,其实还是很简单的:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
rectF.set(outsizeWidth / 2, outsizeWidth / 2, getWidth() - outsizeWidth / 2, getHeight() - outsizeWidth / 2);
float space = outsizeWidth + insideWidth * 3 / 2;
rectFInside.set(space, space, getWidth() - space, getHeight() - space);
circleAnimate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
angle = angle == 0 ? 1 : angle;
canvas.drawArc(rectF, rotateAngle - angle / 2, angle, false, paint);
canvas.drawArc(rectFInside, rotateAngle + 180 - angle / 2, angle, false, paintInside);
}
private void circleAnimate() {
if (animator == null) {
animator = ValueAnimator.ofInt(0, maxAngle);
animator.setDuration(duration);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
angle = (int) animation.getAnimatedValue();
invalidate();
}
});
animatorRotate = ValueAnimator.ofInt(0, 360);
animatorRotate.setDuration(duration);
animatorRotate.setRepeatMode(ValueAnimator.RESTART);
animatorRotate.setRepeatCount(ValueAnimator.INFINITE);
animatorRotate.setInterpolator(new LinearInterpolator());
animatorRotate.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
rotateAngle = (int) animation.getAnimatedValue();
}
});
}
if (!animator.isRunning()) {
animator.start();
animatorRotate.start();
}
}
End
刷新标识和加载已经实现,接下来只需要放在刷新头部和加载尾部的布局中即可,业余时间项目中的列表通过对RecyclerView进一步封装来实现。是的,网上能找出很多这样的封装控件,不过,还是那句话:那是别人的*!RecyclerView的进一步封装放在下一篇博客吧!
新年快乐!鸡年大吉吧!