项目地址:
下篇链接: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(); }复制代码
两个方法都保留了