項目地址:github.com/razerdp/Fri…java
下篇連接:http://www.jianshu.com/p/94e1e267b3b3android
題目命名是否是很簡單粗暴←_←ios
咳咳,進入正題,關於本項目什麼的,在GitHub都寫得清清楚楚了,咱們就不廢話,直接進入主題。git
微信朋友圈在我認識的版本中,有兩個(廢話orz),一個是IOS,一個是Android,(再次廢話)。github
其中IOS由於得天獨厚的UI實現優點,能夠輕鬆地作出各類看起來順眼並且又頗有逼格的動畫,這可苦了Android了,相較之下,Android爲了實現幾個動畫就必須得多寫N行代碼,就好比朋友圈的下拉刷新。微信
朋友圈的下拉刷新在兩個系統裏有一個很明顯的區別,在於刷新的那個icon,在android中,刷新的Icon永遠都處於headerview中,並且是在headerview的底部,沒法突破headerview的限制,而在ios版本中,icon不受listview控制,這二者彷佛是分離的。所以在ios中,刷新的icon是能夠隨着listview的下拉而被一塊兒拉下來。框架
上文提及來也許有點不清楚,你們能夠找找兩個系統的手機一塊兒刷一次,留意一下刷新Icon的動做,就知道怎麼回事了。ide
那麼做爲一枚高逼格(苦逼)的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的一部分,那麼我暫時想到如下兩個方案:
爲了方便(偷懶),我採用了第一個方案。因而咱們的佈局文件就出來了: 我知道直接複製xml代碼是又長又臭的,因此在下截了個圖:
能夠看到,咱們的佈局十分簡潔,從上到下是listview->imageview->actionbar,爲何我要這麼放呢,這就關乎到佈局文件的繪製順序問題了,
繪製(Drawing)是從佈局的根結點開始的,佈局層次的繪製順序爲聲明的順序,例如,父view的繪製先於它的子view,而子view的繪製順序也是按照聲明的順序。
簡單的說,在視覺上,就是先畫上面的,再畫下面的。
因此咱們的佈局就這麼寫:
寫到這裏,咱們大概就知道實現的方案:
上面的方案看起來很複雜,事實上也確實有點複雜,但幸運的是,下拉框架已經實現了最麻煩的接口,得益於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。
華麗的分割線
這幾天收到了一些評論,大體以下:
如今回答以下:
更新代碼以下:
/**刷新完成*/
@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();
}
複製代碼
兩個方法都保留了