Android自定義SwipeRefreshLayout高仿微信朋友圈下拉刷新
上一篇文章里把SwipeRefreshLayout的原理簡(jiǎn)單過(guò)了一下,大致了解了其工作原理,不熟悉的可以去看一下:http://www.dbjr.com.cn/article/89310.htm
上一篇里最后提到,SwipeRefreshLayout的可定制性是比較差的,看源碼會(huì)發(fā)現(xiàn)跟樣式相關(guān)的幾個(gè)類(lèi)都是private的而且方法是寫(xiě)死的,只暴露出了幾個(gè)顏色設(shè)置的方法。這樣使得SwipeRefreshLayout的使用比較簡(jiǎn)單,主要就是設(shè)置一個(gè)監(jiān)聽(tīng)器在onRefresh方法里完成刷新邏輯。講道理SwipeRefreshLayout的樣式是挺美觀(guān)的,如果以后都用這種下拉刷新樣式的話(huà),程序員就清靜了,但這也是不太可能的。如果就想用官方的SwipeRefreshLayout,不想用第三方的控件,又想定制樣式,該怎么辦?基本上只能改源碼了。下面就從修改源碼的角度出發(fā),給出自定義樣式的思路。
首先需要將SwipeRefreshLayout以及內(nèi)部使用到的CircleImageView和MaterialProgressDrawable的源碼都拷貝出來(lái),放到一個(gè)包里,方便修改。從源碼可以知道,SwipeRefreshLayout中跟樣式相關(guān)的類(lèi)主要有兩個(gè):
一. CircleImageView,繼承imageview,源碼就不貼了,主要是繪制背景的,進(jìn)度圈就是繪制在這上面,如果要修改進(jìn)度圈的位置,就應(yīng)該修改CircleImageView的位置。
二. MaterialProgressDrawable,繼承Drawable實(shí)現(xiàn)Animatable接口,內(nèi)部還定義了一個(gè)Ring類(lèi),主要是繪制進(jìn)度圈的,如果要修改進(jìn)度圈的圖片和動(dòng)畫(huà),就應(yīng)該從這里開(kāi)刀。
下面就以社交APP的BOSS微信為例,仿照朋友圈的下拉刷新效果。
先上效果圖,可以跟手機(jī)里的微信比較一下,整體感覺(jué)還是可以的。第一次錄gif,錄了太長(zhǎng),處理的時(shí)候刪了一些中間的幀)

這段時(shí)間在高仿微信,圖方便就把整體的效果也展示了,讀者關(guān)注刷新頁(yè)面即可。布局主要就是一個(gè)SwipeRefreshLayout內(nèi)嵌一個(gè)RecyclerView,滑動(dòng)到頂端向下拖動(dòng)時(shí),出來(lái)的進(jìn)度圈是朋友圈的那個(gè)彩虹圈,位置在左邊,而且隨著向下拖動(dòng)會(huì)不斷繞中心轉(zhuǎn)啊轉(zhuǎn),此外,進(jìn)度圈在到達(dá)某個(gè)位置后就不會(huì)再往下了。跟默認(rèn)效果不同的還有recyclerview,默認(rèn)是主布局是不會(huì)跟著拖動(dòng)的,而微信的有一個(gè)拖動(dòng)反彈效果,背景是黑色。開(kāi)始刷新后,主布局反彈到頭部,進(jìn)度圈在那里轉(zhuǎn)啊轉(zhuǎn),刷新完畢后進(jìn)度圈就消失了,整個(gè)過(guò)程就是這樣。那么就一步一步來(lái).
1. 調(diào)整進(jìn)度圈位置
首先要將進(jìn)度圈調(diào)整到左邊,根據(jù)View的繪制原理,進(jìn)度圈的位置應(yīng)該是由父布局也就是SwipeRefreshLayout里的onLayout方法決定的,看看源碼:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}
其中的mTarget就是主布局也就是recyclerview,而mCircleView就是轉(zhuǎn)載進(jìn)度圈的View,因此應(yīng)該把最后一句注釋掉,改為:
// mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
// (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
// 修改進(jìn)度圈的X坐標(biāo)使之位于左邊
mCircleView.layout(childLeft, mCurrentTargetOffsetTop,
childLeft+circleWidth, mCurrentTargetOffsetTop + circleHeight);
這樣你就會(huì)很高興地發(fā)現(xiàn)進(jìn)度圈已經(jīng)調(diào)到左邊了。
2. 實(shí)現(xiàn)拖動(dòng)反彈效果
接下來(lái)先修改recyclerview的拖動(dòng)反彈效果,SwipeRefreshLayout默認(rèn)的效果是不拖動(dòng)的,如果要修改其實(shí)也很簡(jiǎn)單,無(wú)非就是記錄下手指運(yùn)動(dòng)的距離并讓recyclerview設(shè)置translation就好了,那么找到onTouchEvent方法,修改ACTION_MOVE和ACTION_UP的部分:
case MotionEvent.ACTION_MOVE: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
final float y = MotionEventCompat.getY(ev, pointerIndex);
// 記錄手指移動(dòng)的距離,mInitialMotionY是初始的位置,DRAG_RATE是拖拽因子,默認(rèn)為0.5。
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
// 賦值給mTarget的top使之產(chǎn)生拖動(dòng)效果
mTarget.setTranslationY(overscrollTop);
if (mIsBeingDragged) {
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEvent.ACTION_UP: {
// 手指松開(kāi)時(shí)啟動(dòng)動(dòng)畫(huà)回到頭部
mTarget.animate().translationY(0).setDuration(200).start();
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
return false;
}
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
mActivePointerId = INVALID_POINTER;
return false;
}
不相關(guān)的我都略過(guò)了,修改的地方我也注釋了,很清晰。這樣就解決了拖動(dòng)反彈的問(wèn)題,得益于SwipeRefreshLayout的框架,不用考慮沖突問(wèn)題,修改起來(lái)還是很簡(jiǎn)單的。
3. 修改圖標(biāo)和拖動(dòng)時(shí)的動(dòng)畫(huà)
接下來(lái)就是比較麻煩的圖標(biāo)和動(dòng)畫(huà)了。修改圖標(biāo)其實(shí)不難,因?yàn)镃ircleView是繼承ImageView的,完全可以通過(guò)反射取到CircleView的實(shí)例變量,然后setBitmap將你的圖標(biāo)傳進(jìn)去。但是這樣的話(huà)就沒(méi)有動(dòng)畫(huà)了,顯然也是沒(méi)啥意義的。讀者可以大致看看MaterialProgressDrawable的源碼,要實(shí)現(xiàn)默認(rèn)的動(dòng)畫(huà)還是比較復(fù)雜的,我這里要改為微信的效果,就一個(gè)圈圈轉(zhuǎn)啊轉(zhuǎn),還是比較簡(jiǎn)單的,下面就結(jié)合上篇文章所解析的流程看看如何修改。
首先新建一個(gè)CustomProgressDrawable類(lèi),并繼承自MaterialProgressDrawable(需要將源碼復(fù)制出來(lái)),還需要在SwipeRefreshLayout添加set方法,方便把自定義的類(lèi)傳進(jìn)去。
public void setProgressView(MaterialProgressDrawable mProgress){
this.mProgress = mProgress;
mCircleView.setImageDrawable(mProgress);
}
要在CustomProgressDrawable中繪制自定義的圖標(biāo),就需要暴露一個(gè)setBitmap的方法以便繪制。上篇文章提到,手指移動(dòng)時(shí)會(huì)調(diào)用moveSpinner方法,并把移動(dòng)的距離傳進(jìn)去,該方法內(nèi)首先會(huì)經(jīng)過(guò)一堆數(shù)學(xué)的處理得出一個(gè)rotation,再把它傳入mProgress的setProgressRotation,也就是說(shuō)setProgressRotation方法是通過(guò)傳入的角度來(lái)轉(zhuǎn)圈圈的。朋友圈的效果就是一直讓中心轉(zhuǎn),所以很容易改寫(xiě):
private float rotation;
private Bitmap mBitmap;
public void setBitmap(Bitmap mBitmap) {
this.mBitmap = mBitmap;
}
@Override
public void setProgressRotation(float rotation) {
// 取負(fù)號(hào)是為了和微信保持一致,下拉時(shí)逆時(shí)針轉(zhuǎn)加載時(shí)順時(shí)針轉(zhuǎn),旋轉(zhuǎn)因子是為了調(diào)整轉(zhuǎn)的速度。
this.rotation = -rotation*ROTATION_FACTOR;
invalidateSelf();
}
@Override
public void draw(Canvas c) {
Rect bound = getBounds();
c.rotate(rotation,bound.exactCenterX(),bound.exactCenterY());
Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
c.drawBitmap(mBitmap,src,bound,paint);
}
就是不斷旋轉(zhuǎn)canvas再繪制bitmap。這樣你就會(huì)很高興地發(fā)現(xiàn)下拉的時(shí)候圈圈也轉(zhuǎn)起來(lái)了。
4. 設(shè)置進(jìn)度圈下拉界限和實(shí)現(xiàn)加載時(shí)的動(dòng)畫(huà)
此時(shí)正在刷新的時(shí)候圈圈是不會(huì)轉(zhuǎn)的,而且圈圈默認(rèn)是跟著手指拖動(dòng)的,沒(méi)有界限,而朋友圈的效果是圈圈在下拉到一個(gè)位置后就不再繼續(xù)下拉了,先來(lái)解決下拉位置的問(wèn)題。
在moveSpinner方法中,調(diào)用完setProgressRotation方法來(lái)轉(zhuǎn)圈后,就會(huì)調(diào)用setTargetOffsetTopAndBottom來(lái)改變mProgress的位置,代碼就不貼了。既然我們要限定下拉的位置,那就應(yīng)該在這里加以限制,當(dāng)下移到刷新的位置時(shí)就不再下移了,代碼如下:
private void moveSpinner(float overscrollTop) {
…
// setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
// 最終刷新的位置
int endTarget;
if (!mUsingCustomStart) {
// 沒(méi)有修改使用默認(rèn)的值
endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop));
} else {
// 否則使用定義的值
endTarget = (int) mSpinnerFinalOffset;
}
if(targetY>=endTarget){
// 下移的位置超過(guò)最終位置后就不再下移,第一個(gè)參數(shù)為偏移量
setTargetOffsetTopAndBottom(0, true /* requires update */);
}else{
// 否則繼續(xù)繼續(xù)下移
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
}
}
這里先計(jì)算出一個(gè)endTarget,就是最終的位置,其他注釋的比較詳細(xì)不說(shuō)了,這樣就限制住了下移的位置。
接下來(lái)要讓刷新的時(shí)候圈圈繼續(xù)轉(zhuǎn),那就需要知道刷新時(shí)是執(zhí)行哪里的動(dòng)畫(huà)。上篇文章也提到了,轉(zhuǎn)圈的動(dòng)畫(huà)是在mProgress的start方法里的,來(lái)看看源碼:
@Override
public void start() {
mAnimation.reset();
mRing.storeOriginals();
// Already showing some part of the ring
if (mRing.getEndTrim() != mRing.getStartTrim()) {
mFinishing = true;
mAnimation.setDuration(ANIMATION_DURATION/2);
// 將轉(zhuǎn)圈圈的動(dòng)畫(huà)傳入
mParent.startAnimation(mAnimation);
} else {
mRing.setColorIndex(0);
mRing.resetOriginals();
mAnimation.setDuration(ANIMATION_DURATION);
// 將轉(zhuǎn)圈圈的動(dòng)畫(huà)傳入
mParent.startAnimation(mAnimation);
}
}
主要其實(shí)就最后一句,將轉(zhuǎn)圈圈的動(dòng)畫(huà)傳入,mAnimation就是默認(rèn)的轉(zhuǎn)動(dòng)動(dòng)畫(huà),感興趣可以自己去看看,我們只需要自定義轉(zhuǎn)圈圈的動(dòng)畫(huà)并傳入該方法就可以了。有了剛才的setProgressRotation方法,只需要定義一個(gè)動(dòng)畫(huà)并不斷改變r(jià)otation的值并執(zhí)行這個(gè)方法就好了,代碼如下:
private void setupAnimation() {
// 初始化旋轉(zhuǎn)動(dòng)畫(huà)
mAnimation = new Animation(){
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
setProgressRotation(-interpolatedTime);
}
};
mAnimation.setDuration(5000);
// 無(wú)限重復(fù)
mAnimation.setRepeatCount(Animation.INFINITE);
mAnimation.setRepeatMode(Animation.RESTART);
// 均勻轉(zhuǎn)速
mAnimation.setInterpolator(new LinearInterpolator());
}
@Override
public void start() {
mParent.startAnimation(mAnimation);
}
這樣就OK了!
5. 修改加載完畢的動(dòng)畫(huà)
現(xiàn)在已經(jīng)基本完成了,最后還有一個(gè)結(jié)束的動(dòng)畫(huà),默認(rèn)是scale動(dòng)畫(huà),而微信的是向上運(yùn)動(dòng)至消失,最后的動(dòng)畫(huà)是通過(guò)執(zhí)行SwipeRefreshLayout的startScaleDownAnimation方法完成的,在方法內(nèi)部定義了一個(gè)scale動(dòng)畫(huà),我們只需要注釋掉并自己定義一個(gè)動(dòng)畫(huà)就好了:
private void startScaleDownAnimation(Animation.AnimationListener listener) {
// mScaleDownAnimation = new Animation() {
// @Override
// public void applyTransformation(float interpolatedTime, Transformation t) {
// setAnimationProgress(1 - interpolatedTime);
// }
// };
// 最終的偏移量就是mCircleView距離頂部的高度
final int deltaY = -mCircleView.getBottom();
mScaleDownAnimation = new TranslateAnimation(0,0,0,deltaY);
// mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION);
mScaleDownAnimation.setDuration(500);
mCircleView.setAnimationListener(listener);
mCircleView.clearAnimation();
mCircleView.startAnimation(mScaleDownAnimation);
}
也就是一個(gè)偏移動(dòng)畫(huà)~
在activity中進(jìn)行一些設(shè)置,傳入朋友圈的圖標(biāo)后就能得到開(kāi)頭的效果了:
CustomProgressDrawable drawable = new CustomProgressDrawable(this,mRefreshLayout);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.moments_refresh_icon);
drawable.setBitmap(bitmap);
mRefreshLayout.setProgressView(drawable);
mRefreshLayout.setBackgroundColor(Color.BLACK);
mRefreshLayout.setProgressBackgroundColorSchemeColor(Color.BLACK);
mRefreshLayout.setOnRefreshListener(new CustomSwipeRefreshLayout.OnRefreshListener(){
@Override
public void onRefresh() {
final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
mRefreshLayout.setRefreshing(false);
}
};
new Thread(new Runnable() {
@Override
public void run() {
try {
// 在子線(xiàn)程睡眠三秒后發(fā)送消息停止刷新。
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
}).start();
}
});
以上就基本通過(guò)修改SwipeRefreshLayout的源碼仿照了朋友圈的下拉刷新效果了。從源碼可以看出SwipeRefreshLayout確實(shí)是寫(xiě)得比較封閉的,不修改源碼是基本沒(méi)法自定義樣式的,不過(guò)這樣跟著源碼過(guò)了一遍思路就比較清晰了。以后如果有機(jī)會(huì)再試著封裝一下吧~
最后再附上CustomProgressDrawable的完整代碼吧。SwipeRefreshLayout的太長(zhǎng)就不發(fā)了,該改的地方應(yīng)該都提到了。
public class CustomProgressDrawable extends MaterialProgressDrawable{
// 旋轉(zhuǎn)因子,調(diào)整旋轉(zhuǎn)速度
private static final int ROTATION_FACTOR = 5*360;
// 加載時(shí)的動(dòng)畫(huà)
private Animation mAnimation;
private View mParent;
private Bitmap mBitmap;
// 旋轉(zhuǎn)角度
private float rotation;
private Paint paint;
public CustomProgressDrawable(Context context, View parent) {
super(context, parent);
mParent = parent;
paint = new Paint();
setupAnimation();
}
private void setupAnimation() {
// 初始化旋轉(zhuǎn)動(dòng)畫(huà)
mAnimation = new Animation(){
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
setProgressRotation(-interpolatedTime);
}
};
mAnimation.setDuration(5000);
// 無(wú)限重復(fù)
mAnimation.setRepeatCount(Animation.INFINITE);
mAnimation.setRepeatMode(Animation.RESTART);
// 均勻轉(zhuǎn)速
mAnimation.setInterpolator(new LinearInterpolator());
}
@Override
public void start() {
mParent.startAnimation(mAnimation);
}
public void setBitmap(Bitmap mBitmap) {
this.mBitmap = mBitmap;
}
@Override
public void setProgressRotation(float rotation) {
// 取負(fù)號(hào)是為了和微信保持一致,下拉時(shí)逆時(shí)針轉(zhuǎn)加載時(shí)順時(shí)針轉(zhuǎn),旋轉(zhuǎn)因子是為了調(diào)整轉(zhuǎn)的速度。
this.rotation = -rotation*ROTATION_FACTOR;
invalidateSelf();
}
@Override
public void draw(Canvas c) {
Rect bound = getBounds();
c.rotate(rotation,bound.exactCenterX(),bound.exactCenterY());
Rect src = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
c.drawBitmap(mBitmap,src,bound,paint);
}
}
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android仿微信發(fā)朋友圈瀏覽圖片效果
- Android實(shí)現(xiàn)微信朋友圈發(fā)本地視頻功能
- Android 仿微信朋友圈點(diǎn)贊和評(píng)論彈出框功能
- Android 高仿微信朋友圈動(dòng)態(tài)支持雙擊手勢(shì)放大并滑動(dòng)查看圖片效果
- Android仿微信朋友圈圖片查看器
- Android仿微信朋友圈點(diǎn)擊加號(hào)添加圖片功能
- Android GridView仿微信朋友圈顯示圖片
- Android仿微信朋友圈點(diǎn)贊和評(píng)論功能
- Android+Html5混合開(kāi)發(fā)仿微信朋友圈
- Android實(shí)現(xiàn)微信朋友圈圖片和視頻播放
相關(guān)文章
Android開(kāi)發(fā)服務(wù)Service全面講解
Android的服務(wù)是開(kāi)發(fā)Android應(yīng)用程序的重要組成部分。不同于活動(dòng)Activity,服務(wù)是在后臺(tái)運(yùn)行,服務(wù)沒(méi)有接口,生命周期也與活動(dòng)Activity非常不同。通過(guò)使用服務(wù)我們可以實(shí)現(xiàn)一些后臺(tái)操作,比如想從遠(yuǎn)程服務(wù)器加載一個(gè)網(wǎng)頁(yè)等,下面來(lái)看看詳細(xì)內(nèi)容,需要的朋友可以參考下2023-02-02
Android?ViewStub使用方法學(xué)習(xí)
這篇文章主要為大家介紹了Android?ViewStub使用方法學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
詳解Android過(guò)濾emoji表情正則表達(dá)式
這篇文章主要介紹了Android過(guò)濾emoji表情正則表達(dá)式,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
Android自定義View實(shí)現(xiàn)角度選擇器
前幾天在Google Photos查看照片,用了一下它的圖片剪裁功能,于是我馬上就被其界面和操作吸引。后來(lái)想模仿做一個(gè)和Google Photos裁圖頁(yè)面幾乎一模一樣的角度選擇器,本文比較基礎(chǔ),在閱讀本文前只需要掌握最基礎(chǔ)的自定義View知識(shí)和Android事件知識(shí)。下面來(lái)一起學(xué)習(xí)下吧。2016-11-11
Android 數(shù)據(jù)庫(kù)文件存取至儲(chǔ)存卡的方法
這篇文章主要介紹了Android 數(shù)據(jù)庫(kù)文件存取至儲(chǔ)存卡的方法的相關(guān)資料,需要的朋友可以參考下2016-03-03
Android數(shù)據(jù)類(lèi)型之間相互轉(zhuǎn)換系統(tǒng)介紹
一些初學(xué)Android的朋友可能會(huì)遇到JAVA的數(shù)據(jù)類(lèi)型之間轉(zhuǎn)換的苦惱;本文將為有這類(lèi)需求的朋友解決此類(lèi)問(wèn)題2012-11-11
android開(kāi)發(fā)教程之獲取使用當(dāng)前api的應(yīng)用程序名稱(chēng)
開(kāi)發(fā)手機(jī)安全管家的時(shí)候,比如要打電話(huà),或者照相需要知道是哪個(gè)應(yīng)用程序在調(diào)用,就可以在A(yíng)PI接口中調(diào)用下面的代碼2014-02-02
Android用過(guò)TextView實(shí)現(xiàn)跑馬燈效果的示例
本篇文章主要介紹了Android用過(guò)TextView實(shí)現(xiàn)跑馬燈效果的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08

