編輯:關於Android編程
入職接近半個多月,有幾天空閒,所以想著能不能自己實現一個庫來練練手,因為之前一直想要實現下拉刷新的功能,因此就有了這樣一個自制的下拉刷新庫——RefreshWidgetLib.
下拉刷新,作為一個幾乎每個應用都會出現的一種控件,不言而喻,它對於提高用戶體驗有著很重要的作用,而且也已經成為了人們習慣的一種操作。說起下拉刷新這種設計,最早的引入者是在2008年上線的Tweetie,Tweetie引入了如今隨處可見的“下拉刷新”設計,不僅有多達數百款App Store應用使用這種設計,就連蘋果的Mail應用也采納了這一元素。現在我們在各大平台的應用上都可以看見這一元素,而且樣式豐富多彩,體驗更是很不錯。當然後期也出現一些質疑下拉刷新這種設計的不人性的看法,但是對於我來說,我認為這種設計不僅成為了用戶習慣式的一種需求,而且它也有創造更多優質體驗的可能。
下拉刷新,從以前到現在,網上已經有很多開源的下拉刷新庫,而且,google官方後期也推出一個下拉刷新的控件,所以可以說,我們可以使用的資源是很豐富的。那麼還有什麼意義去實現它呢,我覺得即使我們有很多輪子可以用,如果自己有機會,也需要學會去造輪子。
回到實現思路的話題,假如現在有一個ListView,我們想要讓它具有下拉刷新的功能,那麼大的方向有兩個:
1)繼承ListView,利用ListView本身自帶的addHeaderView()方法,通過監聽滾動事件,實現下拉刷新的效果。
2)自定義一個ViewGroup,依次添加HeaderView,ListView和FooterView,通過重寫觸摸事件onInterceptTouchEvent()方法攔截觸摸事件,重寫onTouchEvent()方法處理相關邏輯。
外部ViewGroup的實例不同,相對應的,下拉刷新控件的效果也不同,這裡的ViewGroup可以是LinearLayout,也可以是FrameLayout.
從上面我們可以看出實現思路是可以有很多選擇的,本篇博客我的實現思路是,使用LinearLayout布局包裹HeaderView,ListView,FooterView,其他的實現思路我會在後面持續實現更新。
接下來是具體分析:
首先來看RefreshWidget的結構,外部布局為LinearLayout,排列方式為Vertical, 內部三個視圖依次為HeaderView,ContentView,FooterView,這裡的ContentView可以為ListView,也可以是GridView,也可以是SrcollView,取決於我們子類對於ContentView的實現方式。
這裡以ContentView實例化為ListView作為例子,最初結構圖如下:
可以看到,我們在LinearLayout中依次添加了HeaderView,ListView和FooterView,但是我們希望默認情況下,也就是沒有上拉或者下拉的時候,HeaderView和FooterView隱藏起來。這時候,我們可以通過設置topMargin或者bottomMargin來使得HeaderView和FooterView被隱藏起來,如下:
然後,接下來就是重寫觸摸事件處理邏輯的部分,我們重寫onInterceptTouchEvent()方法。因為LinearLayout內部的View仍然需要處理觸摸事件,例如ListView仍然需要處理點擊Item,處理ListView自身的滾動事件,所以我們不能完全攔截觸摸事件,也不能只重寫onTouchEvent()方法,要合理適當的攔截觸摸事件。
所以正確的做法是,在onInterceptTouchEvent()方法中判斷,當傳遞進來的是ACTION_MOVE事件,我們需要判斷此時是否處於ListView的最頂部或者處於ListView的最底部,如果是最頂部或者最底部,則對觸摸事件進行攔截,否則不做攔截,仍然把觸摸事件交給子View(這裡也就是ListView)去處理。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: mMoveY = ev.getRawY(); /* 如果不屬於觸摸滑動范圍,則跳出 */ if (Math.abs(mDownY - mMoveY) < mTouchSlop) return false; /* 如果處於頂部,且繼續下拉,進入下拉刷新狀態,同時攔截觸摸事件 */ if (mCurrentStatus == STATUS_NORMAL && isReachHeader() && mMoveY - mDownY > 0 && mRefreshEnabled) { mCurrentStatus = STATUS_REFRESH; mHeaderView.onRefresh(0); return true; } else if (mCurrentStatus == STATUS_NORMAL && isReachFooter() && mMoveY - mDownY < 0 && mLoadMoreEnabled) { /* 如果處於底部,且繼續上拉,進入上拉加載更多狀態,同時攔截觸摸事件 */ mCurrentStatus = STATUS_LOAD_MORE; mFooterView.onLoadMore(0); /* 加上這一句,可以使得ContentView在上拉的過程中保持滑到最底部的狀態 */ makeContentViewToFooter(); return true; } break; } /* 其他情況不攔截,默認返回false */ return super.onInterceptTouchEvent(ev); }
接下來,假如我們已經判斷出手指正在執行下拉刷新的操作,那麼我們將觸摸事件攔截,交由onTouchEvent()處理,通過設置topMargin和bottomMargin,使得HeaderView重新進入手機的可見區域
case MotionEvent.ACTION_MOVE: mMoveY = event.getRawY(); /* 下拉刷新狀態 且正在向下滑動 */ if ((mCurrentStatus == STATUS_REFRESH || mCurrentStatus == STATUS_RELEASE_TO_REFRESH) && mMoveY - mDownY >= 0 ) { if (mMoveY - mDownY > mHeaderHeight * mHeaderPullProportion) { mCurrentStatus = STATUS_RELEASE_TO_REFRESH; mHeaderView.onReleaseToRefresh(); setHeaderTopMargin(0); setHeaderBottomMargin((int) (mMoveY - mDownY - mHeaderHeight * mHeaderPullProportion)); } else { mCurrentStatus = STATUS_REFRESH; mHeaderView.onRefresh((mMoveY- mDownY) / ((float)mHeaderHeight * mHeaderPullProportion)); setHeaderTopMargin(-mHeaderHeight + (int) ((mMoveY - mDownY) / mHeaderPullProportion)); setHeaderBottomMargin(0); } }
當松手之後,我們還需要一個回彈的動作,這裡我們用屬性動畫來完成
/** * 下拉刷新任務 */ private void headerRefreshTask() { final int value = mHeaderLayoutParams.bottomMargin; mMainThreadHandler.post(new Runnable() { @Override public void run() { ValueAnimator valueAnimator = ValueAnimator.ofFloat(value, 0); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setDuration(HEADER_REFRESH_TIME); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); setHeaderBottomMargin((int) value); } }); valueAnimator.start(); } }); }
github地址:https://github.com/82367825/RefreshWidget
上面介紹完基本原理之後,讓我們來看看成果,先來看看效果:
工程庫庫的UML圖,主要的邏輯設計都在基類BaseRefreshWidget中實現了,不同的下拉刷新控件都是繼承自這個基類。
舉例RefreshListViewWidget的使用,就如同普通的ListView一樣使用
在布局文件中添加
然後在Activity中添加數據
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_listview); initData(); mRefreshListViewWidget = (RefreshListViewWidget) findViewById(R.id.refresh_list); mRefreshListViewWidget.setAdapter(new ListViewAdapter()); mRefreshListViewWidget.setRefreshListener(new RefreshListener() { @Override public void onRefresh() { refreshData(); } @Override public void onLoadMore() { loadMoreData(); } }); mRefreshListViewWidget.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { Toast.makeText(ListViewDemoActivity.this, "click " + position + " item", Toast.LENGTH_SHORT).show(); } }); }
當數據加載或者刷新完成,我們可以調用
mRefreshListViewWidget.completeRefresh();
我們也可以禁用上拉或者下拉
/** * 設置下拉刷新功能是否可用 * @param enabled */ void setRefreshEnabled(boolean enabled); /** * 設置上拉加載功能是否可用 * @param enabled */ void setLoadMoreEnabled(boolean enabled);
當然,這些用法都是和系統的刷新控件以及其他第三方下拉刷新控件很類似的,可能看起來就沒什麼新意,但是還是有一些補充的地方,就是HeaderView和FooterView的拓展實現。
默認情況下,如果不添加自定義的HeaderView或者FooterView,RefreshWidgetLib會默認幫你生成一個只顯示文字效果的HeaderView或者FooterView.
假如我們想要實現自己的HeaderView,當下拉的時候,加入一些自己的效果,讓控件更加好看,也是很容易的。我們只需要繼承BaseHeader,然後實現以下三個已經嵌入代碼框架裡的方法就可以了。
onRefresh()是當RefreshListViewWidget正在下拉操作的時候被調用;
onReleaseToRefresh()是當RefreshListViewWidget已經下拉超過指定范圍被調用;
onRefreshIng()則是當RefreshListViewWidget松手之後被調用。
percent是一個關鍵的參數,它表示下拉的比例大小,通常情況下為:下拉距離 / HeaderView高度
/** * 正在下拉刷新 * @param percent 下拉完成比例 */ void onRefresh(float percent); /** * 松手刷新 */ void onReleaseToRefresh(); /** * 正在刷新 */ void onRefreshIng();
舉個例子,比如我想實現上面gif圖裡的波浪下拉HeaderView,那麼就像上面說的,繼承BaseHeader,實現三個方法。
package com.zero.refreshwidgetlib.header; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.os.Handler; import android.util.AttributeSet; import android.view.Gravity; import android.view.animation.LinearInterpolator; import android.widget.TextView; import com.zero.refreshwidgetlib.utils.DrawUtils; /** * Anim HeaderView * @author linzewu * @date 16-6-29 */ public class HeaderAnimView extends BaseHeader{ private static final String TAG = "HeaderAnimView"; private static final int STATUS_NORMAL = 0; private static final int STATUS_START_TO_REFRESH = 1; private static final int STATUS_RELEASE_TO_REFRESH = 2; private static final int STATUS_REFRESH_ING = 3; private int mCurrentStatus = STATUS_NORMAL; private static final float MAX_HEIGHT = 100; private static final int DEFAULT_COLOR = 0x4400AAFF; private int mColor = DEFAULT_COLOR; private Paint mPaint; private Path mPath; protected float mWidth; protected float mHeight; protected boolean mHasInit = false; private Handler mMainThreadHandler = new Handler(); /** * 正在刷新TextView */ private TextView mRefreshingView; /** * 下拉曲線的變量點 */ private PointF mLeftPoint,mRightPoint,mControlPoint; /** * 刷新波浪的變量點 */ private PointF mPointF1,mPointF2,mPointF3,mPointF4,mPointF5; public HeaderAnimView(Context context) { super(context); init(); } public HeaderAnimView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HeaderAnimView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(widthMeasureSpec, DrawUtils.dip2px(getContext(), MAX_HEIGHT)); } @Override protected void onSizeChanged(int w, int h, int oldW, int oldH) { super.onSizeChanged(w, h, oldW, oldH); if (!mHasInit) { mWidth = w; mHeight = h; } } private void init() { mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(mColor); mRefreshingView = new TextView(getContext()); mRefreshingView.setGravity(Gravity.CENTER); mPath = new Path(); mLeftPoint = new PointF(); mRightPoint = new PointF(); mControlPoint = new PointF(); mPointF1 = new PointF(); mPointF2 = new PointF(); mPointF3 = new PointF(); mPointF4 = new PointF(); mPointF5 = new PointF(); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); mPath.reset(); if (mCurrentStatus == STATUS_START_TO_REFRESH || mCurrentStatus == STATUS_RELEASE_TO_REFRESH) { mLeftPoint.x = 0; mLeftPoint.y = mHeight - mHeight * mPercent; mRightPoint.x = mWidth; mRightPoint.y = mHeight - mHeight * mPercent; mControlPoint.x = mWidth / 2; mControlPoint.y = mHeight + mHeight * mPercent * 0.9f; mPath.moveTo(mLeftPoint.x, mLeftPoint.y); mPath.quadTo(mControlPoint.x, mControlPoint.y, mRightPoint.x, mRightPoint.y); mPath.moveTo(mLeftPoint.x, mLeftPoint.y); canvas.drawPath(mPath, mPaint); } else if (mCurrentStatus == STATUS_REFRESH_ING) { /* 繪制波浪 */ mPath.moveTo(mPointF1.x, mPointF1.y); mPath.quadTo((mPointF1.x + mPointF2.x) / 2, mPointF2.y + 20, mPointF2.x, mPointF2.y); mPath.quadTo((mPointF2.x + mPointF3.x) / 2, mPointF3.y - 20, mPointF3.x, mPointF3.y); mPath.quadTo((mPointF3.x + mPointF4.x) / 2, mPointF4.y + 20, mPointF4.x, mPointF4.y); mPath.quadTo((mPointF4.x + mPointF5.x) / 2, mPointF5.y - 20, mPointF5.x, mPointF5.y); mPath.lineTo(mPointF5.x, 0); mPath.lineTo(mPointF1.x, 0); mPath.lineTo(mPointF1.x, mPointF1.y); canvas.drawPath(mPath, mPaint); } } @Override public void onRefresh(float percent) { super.onRefresh(percent); this.mCurrentStatus = STATUS_START_TO_REFRESH; this.mPercent = percent; invalidate(); } @Override public void onReleaseToRefresh() { super.onReleaseToRefresh(); this.mCurrentStatus = STATUS_RELEASE_TO_REFRESH; this.mPercent = 1; invalidate(); } @Override public void onRefreshIng() { super.onRefreshIng(); this.mCurrentStatus = STATUS_REFRESH_ING; drawWave(); } /** * draw the wave */ private void drawWave() { mPointF1.x = -mWidth; mPointF1.y = 0.3f * mHeight; mMainThreadHandler.post(new Runnable() { @Override public void run() { ValueAnimator valueAnimator = ValueAnimator.ofFloat(-mWidth, 0); valueAnimator.setInterpolator(new LinearInterpolator()); valueAnimator.setDuration(1500); valueAnimator.setRepeatMode(ValueAnimator.RESTART); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mPointF1.x = (float) animation.getAnimatedValue(); mPointF1.y = 0.3f * mHeight; mPointF2.x = mPointF1.x + mWidth * 0.5f; mPointF2.y = mPointF1.y; mPointF3.x = mPointF1.x + mWidth; mPointF3.y = mPointF1.y; mPointF4.x = mPointF1.x + mWidth * 1.5f; mPointF4.y = mPointF1.y; mPointF5.x = mPointF1.x + mWidth * 2; mPointF5.y = mPointF1.y; invalidate(); } }); valueAnimator.start(); } }); } }
通過繼承BaseHeader,我們實現了自己的HeaderView,然後我們再調用添加,同樣的,FooterView也一樣。
mRefreshListViewWidget.addHeaderView(new HeaderAnimView(ListViewDemoActivity.this)); mRefreshListViewWidget.addFooterView(new FooterAnimView(ListViewDemoActivity.this));
通過這個庫,我們可以輕松自定義自己想要的效果的headerView和footerView,也可以實現讓自己希望的控件具備下拉刷新的功能。上面提到RefreshListViewWidget使得ListView具備了下拉刷新的功能,當然也可以讓GridView具備下拉刷新的效果,還可以讓ScrollView具備下拉刷新的效果。
哈哈,當然前提是,目標View本身像ListView、GridView等具有滾動的能力。
怎麼做呢,繼承基類BaseRefreshWidget,然後實現幾個關鍵的方法,以ScrollView為例子:
/** * 滑動到頭部 * @return */ protected boolean isReachHeader(){ return mContentView.getScrollY() == 0; } /** * 滑動到底部 * @return */ protected boolean isReachFooter(){ View contentView = ((ScrollView)mContentView).getChildAt(0); return contentView.getMeasuredHeight() <= mContentView.getScrollY() + mContentView.getHeight(); } @Override protected View getContentView() { return new ScrollView(getContext()); } /** * 使得ContentView保持滾到底部 */ @Override protected void makeContentViewToFooter() { View contentView = ((ScrollView)mContentView).getChildAt(0); int realHeight = contentView.getMeasuredHeight(); mContentView.scrollTo(0, realHeight - mContentView.getHeight()); } /** * 使得ContentView不再保持滾到底部 */ @Override protected void makeContentViewRestore() { }
具體更多的代碼細節可以看我的源碼,也是上面提到的我的工程源碼:
https://github.com/82367825/RefreshWidget
這個庫算是我第一個完全靠自己思考和編碼的一個工程庫,可能會有很多不如人意或者錯漏的地方,以後我也會不斷地更新和維護這個工程,希望做得更好。
如有疑問,歡迎指正~
以前看別人的程序的drawable文件夾裡有xml資源,說實話第一次見到這樣的xml圖像資源時,我真心不知道是干什麼的。抽空學習了一下圖像資源,才了解了這類圖像資源的妙用
系統版本:Android 4.2.2_r1 本文主要是在Android中添加思源字體的過程記錄。思源字體是Google和Adobe在2014.07.18發布的中文字體。
ListView 控件可使用四種不同視圖顯示項目。通過此控件,可將項目組成帶有或不帶有列標頭的列,並顯示伴隨的圖標和文本。 可使用 ListView 控件將稱作 List
這是我在 MDCC 上分享的內容(略微改動),也是源碼解析第一期發布時介紹的源碼解析後續會慢慢做的事。從總體設計和原理上對幾個圖片緩存進行對比,沒用到他們的朋友也可以了解