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

這段時間在高仿微信,圖方便就把整體的效果也展示了,讀者關注刷新頁面即可。布局主要就是一個SwipeRefreshLayout內嵌一個RecyclerView,滑動到頂端向下拖動時,出來的進度圈是朋友圈的那個彩虹圈,位置在左邊,而且隨著向下拖動會不斷繞中心轉啊轉,此外,進度圈在到達某個位置後就不會再往下了。跟默認效果不同的還有recyclerview,默認是主布局是不會跟著拖動的,而微信的有一個拖動反彈效果,背景是黑色。開始刷新後,主布局反彈到頭部,進度圈在那裡轉啊轉,刷新完畢後進度圈就消失了,整個過程就是這樣。那麼就一步一步來.
1. 調整進度圈位置
首先要將進度圈調整到左邊,根據View的繪制原理,進度圈的位置應該是由父布局也就是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就是轉載進度圈的View,因此應該把最後一句注釋掉,改為:
// mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
// (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
// 修改進度圈的X坐標使之位於左邊
mCircleView.layout(childLeft, mCurrentTargetOffsetTop,
childLeft+circleWidth, mCurrentTargetOffsetTop + circleHeight);
這樣你就會很高興地發現進度圈已經調到左邊了。
2. 實現拖動反彈效果
接下來先修改recyclerview的拖動反彈效果,SwipeRefreshLayout默認的效果是不拖動的,如果要修改其實也很簡單,無非就是記錄下手指運動的距離並讓recyclerview設置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);
// 記錄手指移動的距離,mInitialMotionY是初始的位置,DRAG_RATE是拖拽因子,默認為0.5。
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
// 賦值給mTarget的top使之產生拖動效果
mTarget.setTranslationY(overscrollTop);
if (mIsBeingDragged) {
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEvent.ACTION_UP: {
// 手指松開時啟動動畫回到頭部
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;
}
不相關的我都略過了,修改的地方我也注釋了,很清晰。這樣就解決了拖動反彈的問題,得益於SwipeRefreshLayout的框架,不用考慮沖突問題,修改起來還是很簡單的。
3. 修改圖標和拖動時的動畫
接下來就是比較麻煩的圖標和動畫了。修改圖標其實不難,因為CircleView是繼承ImageView的,完全可以通過反射取到CircleView的實例變量,然後setBitmap將你的圖標傳進去。但是這樣的話就沒有動畫了,顯然也是沒啥意義的。讀者可以大致看看MaterialProgressDrawable的源碼,要實現默認的動畫還是比較復雜的,我這裡要改為微信的效果,就一個圈圈轉啊轉,還是比較簡單的,下面就結合上篇文章所解析的流程看看如何修改。
首先新建一個CustomProgressDrawable類,並繼承自MaterialProgressDrawable(需要將源碼復制出來),還需要在SwipeRefreshLayout添加set方法,方便把自定義的類傳進去。
public void setProgressView(MaterialProgressDrawable mProgress){
this.mProgress = mProgress;
mCircleView.setImageDrawable(mProgress);
}
要在CustomProgressDrawable中繪制自定義的圖標,就需要暴露一個setBitmap的方法以便繪制。上篇文章提到,手指移動時會調用moveSpinner方法,並把移動的距離傳進去,該方法內首先會經過一堆數學的處理得出一個rotation,再把它傳入mProgress的setProgressRotation,也就是說setProgressRotation方法是通過傳入的角度來轉圈圈的。朋友圈的效果就是一直讓中心轉,所以很容易改寫:
private float rotation;
private Bitmap mBitmap;
public void setBitmap(Bitmap mBitmap) {
this.mBitmap = mBitmap;
}
@Override
public void setProgressRotation(float rotation) {
// 取負號是為了和微信保持一致,下拉時逆時針轉加載時順時針轉,旋轉因子是為了調整轉的速度。
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);
}
就是不斷旋轉canvas再繪制bitmap。這樣你就會很高興地發現下拉的時候圈圈也轉起來了。
4. 設置進度圈下拉界限和實現加載時的動畫
此時正在刷新的時候圈圈是不會轉的,而且圈圈默認是跟著手指拖動的,沒有界限,而朋友圈的效果是圈圈在下拉到一個位置後就不再繼續下拉了,先來解決下拉位置的問題。
在moveSpinner方法中,調用完setProgressRotation方法來轉圈後,就會調用setTargetOffsetTopAndBottom來改變mProgress的位置,代碼就不貼了。既然我們要限定下拉的位置,那就應該在這裡加以限制,當下移到刷新的位置時就不再下移了,代碼如下:
private void moveSpinner(float overscrollTop) {
…
// setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
// 最終刷新的位置
int endTarget;
if (!mUsingCustomStart) {
// 沒有修改使用默認的值
endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop));
} else {
// 否則使用定義的值
endTarget = (int) mSpinnerFinalOffset;
}
if(targetY>=endTarget){
// 下移的位置超過最終位置後就不再下移,第一個參數為偏移量
setTargetOffsetTopAndBottom(0, true /* requires update */);
}else{
// 否則繼續繼續下移
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
}
}
這裡先計算出一個endTarget,就是最終的位置,其他注釋的比較詳細不說了,這樣就限制住了下移的位置。
接下來要讓刷新的時候圈圈繼續轉,那就需要知道刷新時是執行哪裡的動畫。上篇文章也提到了,轉圈的動畫是在mProgress的start方法裡的,來看看源碼:
@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);
// 將轉圈圈的動畫傳入
mParent.startAnimation(mAnimation);
} else {
mRing.setColorIndex(0);
mRing.resetOriginals();
mAnimation.setDuration(ANIMATION_DURATION);
// 將轉圈圈的動畫傳入
mParent.startAnimation(mAnimation);
}
}
主要其實就最後一句,將轉圈圈的動畫傳入,mAnimation就是默認的轉動動畫,感興趣可以自己去看看,我們只需要自定義轉圈圈的動畫並傳入該方法就可以了。有了剛才的setProgressRotation方法,只需要定義一個動畫並不斷改變rotation的值並執行這個方法就好了,代碼如下:
private void setupAnimation() {
// 初始化旋轉動畫
mAnimation = new Animation(){
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
setProgressRotation(-interpolatedTime);
}
};
mAnimation.setDuration(5000);
// 無限重復
mAnimation.setRepeatCount(Animation.INFINITE);
mAnimation.setRepeatMode(Animation.RESTART);
// 均勻轉速
mAnimation.setInterpolator(new LinearInterpolator());
}
@Override
public void start() {
mParent.startAnimation(mAnimation);
}
這樣就OK了!
5. 修改加載完畢的動畫
現在已經基本完成了,最後還有一個結束的動畫,默認是scale動畫,而微信的是向上運動至消失,最後的動畫是通過執行SwipeRefreshLayout的startScaleDownAnimation方法完成的,在方法內部定義了一個scale動畫,我們只需要注釋掉並自己定義一個動畫就好了:
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);
}
也就是一個偏移動畫~
在activity中進行一些設置,傳入朋友圈的圖標後就能得到開頭的效果了:
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 {
// 在子線程睡眠三秒後發送消息停止刷新。
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
}).start();
}
});
以上就基本通過修改SwipeRefreshLayout的源碼仿照了朋友圈的下拉刷新效果了。從源碼可以看出SwipeRefreshLayout確實是寫得比較封閉的,不修改源碼是基本沒法自定義樣式的,不過這樣跟著源碼過了一遍思路就比較清晰了。以後如果有機會再試著封裝一下吧~
最後再附上CustomProgressDrawable的完整代碼吧。SwipeRefreshLayout的太長就不發了,該改的地方應該都提到了。
public class CustomProgressDrawable extends MaterialProgressDrawable{
// 旋轉因子,調整旋轉速度
private static final int ROTATION_FACTOR = 5*360;
// 加載時的動畫
private Animation mAnimation;
private View mParent;
private Bitmap mBitmap;
// 旋轉角度
private float rotation;
private Paint paint;
public CustomProgressDrawable(Context context, View parent) {
super(context, parent);
mParent = parent;
paint = new Paint();
setupAnimation();
}
private void setupAnimation() {
// 初始化旋轉動畫
mAnimation = new Animation(){
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
setProgressRotation(-interpolatedTime);
}
};
mAnimation.setDuration(5000);
// 無限重復
mAnimation.setRepeatCount(Animation.INFINITE);
mAnimation.setRepeatMode(Animation.RESTART);
// 均勻轉速
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) {
// 取負號是為了和微信保持一致,下拉時逆時針轉加載時順時針轉,旋轉因子是為了調整轉的速度。
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);
}
}
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持本站。
Android 讓自定義TextView的drawableLeft與文本一起居中
前言 TextView的drawableLeft、drawableRight和drawableTop是一個常用、好用的屬性,可以在文本的上下左右放置一個圖片,
Android仿UC浏覽器左右上下滾動功能
本文要解決在側滑菜單右邊加個文本框,並能實現文本的上下滑動和菜單的左右滾動。這裡推薦可以好好看看android的觸摸事件的分發機制,這裡我就不詳細講了,我只講講這個應用。
VLC for Android 基於 Opencv 對 RTSP視頻 實時人臉檢測
最近項目上需要在Android客戶端 通過獲取 RTSP 的視頻進行實時人臉檢測, 要做就就是以下幾點:1、通過VLC 獲取 獲取RTSP2、對VLC中播放的視頻進行實時
android界面開發:ViewPager的詳解,並用於仿微博滑動實例和圖片滾動實例
1.ViewPager簡單使用ViewPager是android擴展包android.support.v4 裡的一個繼承與ViewGroup組件,通過布局管理器可以實現左