下篇链接:http://www.jianshu.com/p/94e1e267b3b3
题目命名是不是很简单粗暴←_←
咳咳,进入正题,关于本项目什么的,在GitHub都写得清清楚楚了,我们就不废话,直接进入主题。
微信朋友圈在我认识的版本中,有两个(废话orz),一个是IOS,一个是Android,(再次废话)。
其中IOS因为得天独厚的UI实现优势,可以轻松地做出各种看起来顺眼而且又很有逼格的动画,这可苦了Android了,相较之下,Android为了实现几个动画就必须得多写N行代码,就比如朋友圈的下拉刷新。
朋友圈的下拉刷新在两个系统里有一个很明显的区别,在于刷新的那个icon,在android中,刷新的Icon永远都处于headerview中,而且是在headerview的底部,无法突破headerview的限制,而在ios版本中,icon不受listview控制,这两者似乎是分离的。因此在ios中,刷新的icon是可以随着listview的下拉而被一起拉下来。
上文说起来也许有点不清楚,大家可以找找两个系统的手机一起刷一次,留意一下刷新Icon的动作,就知道怎么回事了。
那么作为一枚高逼格(苦逼)的android程序猿,我们当然要挑战ios的刷新啦是不是。
于是,就有了我们的这个系列的第一篇(说好的不废话呢)
话不多说,预览图送上:(请忽略穹妹)
开工之前,我们先分析一下实现的方案
因为不制造重复的轮子这个名言,同时根据这篇文章(https://github.com/desmond1121/Android-Ptr-Comparison ) 的分析,我就选用了android-Ultra-Pull-To-Refresh这个库来进行扩展。 (库git:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh ) 这个库的优点在于其强大的扩展性和可定制性,所以选它无可厚非。
库选择完毕,接下来就是思考了。
首先,我们的刷新icon要突破listview限制,那么这个icon绝对不可以是listview的一部分,那么我暂时想到以下两个方案:
- icon使用imageview,在布局文件中单独存在而不是作为listview的一部分
- icon使用imageview,使用WindowManager动态添加一个
为了方便(偷懒),我采用了第一个方案。于是我们的布局文件就出来了: 我知道直接复制xml代码是又长又臭的,所以在下截了个图:
可以看到,我们的布局十分简洁,从上到下是listview->imageview->actionbar,为什么我要这么放呢,这就关乎到布局文件的绘制顺序问题了,
绘制(Drawing)是从布局的根结点开始的,布局层次的绘制顺序为声明的顺序,例如,父view的绘制先于它的子view,而子view的绘制顺序也是按照声明的顺序。
简单的说,在视觉上,就是先画上面的,再画下面的。
所以我们的布局就这么写:
- 先画出listview
- 再画出我们的icon(让其在Listviews上方)
- 最后画出actionbar(让其可以盖住icon和listview)
写到这里,我们大概就知道实现的方案:
- 在listview下拉的时候,将距离回调中控制我们的icon距离顶部的距离(topMargin),同时listview也下拉,两者互不干扰
- 当拉到了刷新距离的时候,松手,listview回弹,icon因为设置了margin,所以会保持刷新距离那个位置,此时播放动画(不断地旋转),同时执行刷新操作
- 在刷新完成后,因为我们的listview已经回弹,此时没有任何位移信息可以使用,所以我们需要用一个线程来手动做一个插值器,动态更新icon的margin,使之回到最顶部隐藏在actionbar下方。
上面的方案看起来很复杂,事实上也确实有点复杂,但幸运的是,下拉框架已经实现了最麻烦的接口,得益于PtrUIHandler和PtrHandler这两个回调,我们起码节省了70%的时间。
接下来我们先初步实现header。 我们的header没啥功能,它只有一个作用,就是下拉后的overscroll那一部分的颜色,所以它的布局也是十分的简单:
我们初步定义高度为300dp,因为在我的测试中,即使我从顶部拉到底部,我们的header还是没有显示完(得益于阻尼参数),所以300dp足够了
布局完成后,我们撸出我们的代码:
public class FriendCirclePtrHeader extends RelativeLayout {
private static final String TAG = "FriendCirclePtrHeader";
private ImageView mRotateIcon;
private View rootView;
private boolean isAutoRefresh;
private RotateAnimation rotateAnimation;
private SmoothChangeThread mSmoothChangeThread;
//当前状态
private PullStatus mPullStatus;
public FriendCirclePtrHeader(Context context) {
this(context, null);
}
public FriendCirclePtrHeader(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FriendCirclePtrHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public FriendCirclePtrHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView(context);
}
private void initView(Context context) {
rootView = LayoutInflater.from(context).inflate(R.layout.widget_ptr_header, this, false);
addView(rootView);
rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
rotateAnimation.setDuration(600);
rotateAnimation.setInterpolator(new LinearInterpolator());
rotateAnimation.setRepeatCount(Animation.INFINITE);
}
复制代码
我们直接inflate一个view出来,然后添加到我们的header中,同时初始化一些anima
接下来就是最主要的实现部分:
//=============================================================ptr:
private PtrUIHandler mPtrUIHandler = new PtrUIHandler() {
/**回到初始位置*/
@Override
public void onUIReset(PtrFrameLayout frame) {
mPullStatus = PullStatus.NORMAL;
if (mRotateIcon.getAnimation() != null) {
mRotateIcon.clearAnimation();
}
}
/**离开初始位置*/
@Override
public void onUIRefreshPrepare(PtrFrameLayout frame) {
}
/**开始刷新动画*/
@Override
public void onUIRefreshBegin(PtrFrameLayout frame) {
mPullStatus = PullStatus.REFRESHING;
if (mRotateIcon != null) {
if (mRotateIcon.getAnimation() != null) {
mRotateIcon.clearAnimation();
}
mRotateIcon.startAnimation(rotateAnimation);
}
}
/**刷新完成*/
@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {
mPullStatus = PullStatus.NORMAL;
if (mSmoothChangeThread==null){
mSmoothChangeThread=SmoothChangeThread.CreateLinearInterpolator(mRotateIcon,frame.getOffsetToRefresh
(),0,300,75);
mSmoothChangeThread.setOnSmoothResultChangeListener(new SmoothChangeThread.OnSmoothResultChangeListener() {
@Override
public void onSmoothResultChange(int result) {
updateRotateAnima(result);
mRotateIcon.setRotation(-(result << 1));
}
});
}else {
mSmoothChangeThread.stop();
}
mRotateIcon.post(mSmoothChangeThread);
}
/**位移更新重载*/
@Override
public void onUIPositionChange(PtrFrameLayout frame, boolean isUnderTouch, byte status, PtrIndicator ptrIndicator) {
final int mOffsetToRefresh = frame.getOffsetToRefresh();
final int currentPos = ptrIndicator.getCurrentPosY();
final int lastPos = ptrIndicator.getLastPosY();
if (currentPos < mOffsetToRefresh) {
//未到达刷新线
if (status == PtrFrameLayout.PTR_STATUS_PREPARE && mRotateIcon != null) {
updateRotateAnima(currentPos);
mRotateIcon.setRotation(-(currentPos << 1));
}
}
else if (currentPos > mOffsetToRefresh) {
//到达或超过刷新线
if (isUnderTouch && status == PtrFrameLayout.PTR_STATUS_PREPARE && mRotateIcon != null) {
updateRotateAnima(mOffsetToRefresh);
mRotateIcon.setRotation(-(currentPos << 1));
}
}
}
};
private void updateRotateAnima(int marginTop) {
Log.d(TAG, "curMargin=========" + marginTop);
if (mRotateIcon == null) return;
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mRotateIcon.getLayoutParams();
params.topMargin = marginTop;
mRotateIcon.setLayoutParams(params);
}
复制代码
ptruihandler是框架暴露给我们用来控制UI下拉时的回调,相关信息都已经在注释中写明了。
这里我们主要关注这个回调: onUIRefreshComplete 这个回调是当刷新完成后,外部执行ptrframe.refreshComplete()时会执行,但我们的listview已经回弹了,也就是说没有任何位移信息供我们更新topMargin,如果没有位移,我们直接 updateRotateAnima(0)的话,在画面上展示出来的就是我们的icon一下子就消失了,而没有一个过渡的动画,因此我们通过一个线程来执行这个动作
/**
* @desc 平滑滚动线程,用于递归调用自己来实现某个视图的平滑滚动
* */
public class SmoothChangeThread implements Runnable {
//需要操控的视图
private View v = null;
//原Y坐标
private int fromY = 0;
//目标Y坐标
private int toY = 0;
//动画执行时间(毫秒)
private long durtion = 0;
//帧率
private int fps = 60;
//间隔时间(毫秒),间隔时间 = 1000 / 帧率
private int interval = 0;
//启动时间,-1 表示尚未启动
private long startTime = -1;
//减速插值器
private static Interpolator mInterpolator = null;
private OnSmoothResultChangeListener mListener;
public static SmoothChangeThread CreateLinearInterpolator(View v, int fromY, int toY, long durtion, int fps){
mInterpolator=new LinearInterpolator();
return new SmoothChangeThread(v,fromY,toY,durtion,fps);
}
public static SmoothChangeThread CreateDecelerateInterpolator(View v, int fromY, int toY, long durtion, int fps){
mInterpolator=new DecelerateInterpolator();
return new SmoothChangeThread(v,fromY,toY,durtion,fps);
}
public static SmoothChangeThread CreateAccelerateDecelerateInterpolator(View v, int fromY, int toY, long durtion, int fps){
mInterpolator=new AccelerateDecelerateInterpolator();
return new SmoothChangeThread(v,fromY,toY,durtion,fps);
}
/**
*
* @param v view
* @param fromY 原始数据
* @param toY 目标数据
* @param durtion 持续时间
* @param fps 帧数
*/
private SmoothChangeThread(View v, int fromY, int toY, long durtion, int fps) {
this.v = v;
this.fromY = fromY;
this.toY = toY;
this.durtion = durtion;
this.fps = fps;
this.interval = 1000 / this.fps;
}
@Override
public void run() {
//先判断是否是第一次启动,是第一次启动就记录下启动的时间戳,该值仅此一次赋值
if (startTime == -1) {
startTime = System.currentTimeMillis();
}
//得到当前这个瞬间的时间戳
long currentTime = System.currentTimeMillis();
//放大倍数,为了扩大除法计算的浮点精度
int enlargement = 1000;
//算出当前这个瞬间运行到整个动画时间的百分之多少
float rate = (currentTime - startTime) * enlargement / durtion;
//这个比率不可能在 0 - 1 之间,放大了之后即是 0 - 1000 之间
rate = Math.max(Math.min(rate, 1000),0);
//将动画的进度通过插值器得出响应的比率,乘以起始与目标坐标得出当前这个瞬间,视图应该滚动的距离。
int changeDistance = Math.round((fromY - toY) * mInterpolator.getInterpolation(rate / enlargement));
int currentY = fromY - changeDistance;
if (mListener!=null){
mListener.onSmoothResultChange(currentY);
}
if (currentY != toY) {
v.postDelayed(this, this.interval);
}
else {
return;
}
}
public void stop() {
v.removeCallbacks(this);
startTime=-1;
}
public OnSmoothResultChangeListener getOnSmoothResultChangeListener() {
return mListener;
}
public void setOnSmoothResultChangeListener(OnSmoothResultChangeListener listener) {
mListener = listener;
}
public interface OnSmoothResultChangeListener{
void onSmoothResultChange(int result);
}
}
复制代码
这个java源文件是在网上找的自定义插值器,我经过修改后,通过接口回调把计算结果抛出去,并且使用静态工厂提供不同类型的插值器效果,我们就可以通过这个接口来动态更新我们的margin了(ps:这个工具类还可以用在很多地方呢)
文章至此,我们的header基本定制完成,完整代码可以查看github,下一步要实现的就是对ptrframe的封装,让其变成我们的ptrlistview。
华丽的分割线
这几天收到了一些评论,大致如下:
- 为何不用recylerview
- 为何不用valueanimator代替线程
现在回答如下:
- 因为目前说实话,大多数项目一直都是用着listview,而且牵扯比较深了,所以这里就用listview,其次,其实在下很喜欢recylerview的说。。。。。另外,框架支持添加任意view,所以喜欢的话可以换成recylerview。
- 当时拼命想着如何去更新这个margin,于是脑里面蹦出了一个“线程计算啊笨蛋”,于是就干了。看了评论才忽然发现。。。。为何我不用valueanimator啊,我笨啊!!!现在在git更新了。两种方法-V-
更新代码如下:
/**刷新完成*/
@Override
public void onUIRefreshComplete(PtrFrameLayout frame) {
mPullState = PullState.NORMAL;
if (mRotateIcon==null)return;
/**采取通用插值器线程实现*/
/* if (mSmoothChangeThread == null) {
mSmoothChangeThread = SmoothChangeThread.CreateLinearInterpolator(mRotateIcon,
frame.getOffsetToRefresh(), 0, 300, 75);
mSmoothChangeThread.setOnSmoothResultChangeListener(
new SmoothChangeThread.OnSmoothResultChangeListener() {
@Override
public void onSmoothResultChange(int result) {
updateRotateAnima(result);
mRotateIcon.setRotation(-(result << 1));
}
});
}
else {
mSmoothChangeThread.stop();
}
mRotateIcon.post(mSmoothChangeThread);*/
/**采取valueAnimator*/
if (mValueAnimator==null){
mValueAnimator=ValueAnimator.ofInt(frame.getOffsetToRefresh(),0);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int result= (int) animation.getAnimatedValue();
updateRotateAnima(result);
mRotateIcon.setRotation(-(result << 1));
}
});
mValueAnimator.setDuration(300);
}
mValueAnimator.start();
}
复制代码
两个方法都保留了