編輯:關於Android編程
這個輪子是對RecyclerView的封裝,主要完成了下拉刷新
、上拉加載更多
、RecyclerView頭部
。在我的Material Design學習項目中使用到了項目地址,感覺還不錯。趁著畢業答辯還有2個星期,先把這個輪子拆了看看,這個項目地址在XRecyclerView,先貼個效果圖,更多效果圖請進入項目中查看。
使用起來也比較簡單,首先向普通RecyclerView那樣:
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
下拉刷新和加載更多需要實現其接口即可:
mRecyclerView.setLoadingListener(new XRecyclerView.LoadingListener() {
@Override
public void onRefresh() {
//refresh data here
}
@Override
public void onLoadMore() {
// load more data here
}
});
這裡要注意的是需要人為的通知刷新和加載都已經完成,通過如下代碼
mRecyclerView.refreshComplete(); //下拉刷新完成
mRecyclerView.loadMoreComplete();//加載更多完成
首先梳理了一下框架,用UML圖畫了這個輪子的結構,這樣有利於幫助我理解,右擊-查看圖像 可以查看清晰大圖)
可以看出主要的類只有3個 XRecyclerView
,LoadingMoreFooter
,ArrowRefreshHeader
,而AVLoadingIncatorView
和SimpleViewSwitcher
是用來輔助刷新或者加載時候的動畫。
下面分析源碼時限於篇幅原因只展現出關鍵代碼,具體可以參考項目源碼。
XRecyclerView的實現XRecyclerView 的head和footer的view實現
private ArrayList mHeaderViews = new ArrayList<>();
private ArrayList mFootViews = new ArrayList<>();
……
private void init() {
if (pullRefreshEnabled) {
//若支持下拉刷新則加入Headerview列表,設置加載圖標
ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
mHeaderViews.add(0, refreshHeader);//從這裡看出headerView可以添加多個
mRefreshHeader = refreshHeader;
mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
}
//加載更多無需觸發
LoadingMoreFooter footView = new LoadingMoreFooter(getContext());
footView.setProgressStyle(mLoadingMoreProgressStyle);
addFootView(footView);//加入footerView
mFootViews.get(0).setVisibility(GONE);
}
……
/**
* @param view 對外提供添加header的方法
*/
public void addHeaderView(View view) {
if (pullRefreshEnabled && !(mHeaderViews.get(0) instanceof ArrowRefreshHeader)) {
ArrowRefreshHeader refreshHeader = new ArrowRefreshHeader(getContext());
mHeaderViews.add(0, refreshHeader);
mRefreshHeader = refreshHeader;
mRefreshHeader.setProgressStyle(mRefreshProgressStyle);
}
mHeaderViews.add(view);
sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size());//記錄viewType
}
但是這樣僅僅只是存儲了View,那麼實現的地方在哪裡呢?數據展現很顯然是在dapater中,但是在使用RecycleView時需要展示item數據,那麼header和footer如何加載?這裡就需要對傳入的數據adapter再做一層封裝。
@Override
public void setAdapter(Adapter adapter) {
mWrapAdapter = new WrapAdapter(adapter);//對傳入的adapter做封裝
super.setAdapter(mWrapAdapter);
adapter.registerAdapterDataObserver(mDataObserver);
mDataObserver.onChanged();
}
由於RecycleView支持LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager,而GridLayoutManager
、StaggeredGridLayoutManager
在添加header時候需要注意橫跨整個屏幕寬度即:
GridLayoutManager 是要設置SpanSize每行的占位大小
StaggerLayoutManager 就是要獲取StaggerLayoutManager的LayoutParams 的setFullSpan 方法來設置占位寬度,因此在WrapAdapter中做了針對性處理
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) manager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return (isHeader(position) || isFooter(position))
? gridManager.getSpanCount() : 1;
}
});
}
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams
&& (isHeader(holder.getLayoutPosition()) || isFooter(holder.getLayoutPosition()))) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
到此只是為展示head提供了必要條件,具體展示還是要靠WrapAdapter的 onCreateViewHolder配合getItemViewType方法,根據viewtype從對應的ArrayList中取出view來展示
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_REFRESH_HEADER) {
mCurrentPosition++;
return new SimpleViewHolder(mHeaderViews.get(0));
} else if (isContentHeader(mCurrentPosition)) {
if (viewType == sHeaderTypes.get(mCurrentPosition - 1)) {
mCurrentPosition++;
return new SimpleViewHolder(mHeaderViews.get(headerPosition++));
}
} else if (viewType == TYPE_FOOTER) {
return new SimpleViewHolder(mFootViews.get(0));
}
return adapter.onCreateViewHolder(parent, viewType);
}
……
@Override
public int getItemViewType(int position) {
if (isRefreshHeader(position)) {
return TYPE_REFRESH_HEADER;
}
if (isHeader(position)) {
position = position - 1;
return sHeaderTypes.get(position);
}
if (isFooter(position)) {
return TYPE_FOOTER;
}
int adjPosition = position - getHeadersCount();
int adapterCount;
if (adapter != null) {
adapterCount = adapter.getItemCount();
if (adjPosition < adapterCount) {
return adapter.getItemViewType(adjPosition);
}
}
return TYPE_NORMAL;
}
ok,到這裡就完成了head和footer的view顯示,
上拉滑動中的下拉刷新和釋放後刷新界面的緩慢消失的實現先看刷新條隨著手指滑動慢慢顯示
:
@Override
public boolean onTouchEvent(MotionEvent ev) {
//通過處理onTouchEvent處理下拉刷新
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
if (isOnTop() && pullRefreshEnabled) {
mRefreshHeader.onMove(deltaY / DRAG_RATE);//顯示刷新的關鍵代碼
if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
// Log.i("getVisibleHeight", "getVisibleHeight = " + mRefreshHeader.getVisibleHeight());
// Log.i("getVisibleHeight", " mRefreshHeader.getState() = " + mRefreshHeader.getState());
return false;
}
}
break;
default:
mLastY = -1; // reset
if (isOnTop() && pullRefreshEnabled) {
if (mRefreshHeader.releaseAction()) {
if (mLoadingListener != null) {
mLoadingListener.onRefresh();
}
}
}
break;
}
return super.onTouchEvent(ev);
}
onMove在ArrowRefreshHeader中實現,這裡多插一句getRawY():獲取點擊事件相對整個屏幕頂邊的y軸坐標,即點擊事件距離整個屏幕頂邊的距離
注意與getY()區別。
@Override
public void onMove(float delta) {
//由於下拉時候區域是動態變化因此需要動態設置
if (getVisibleHeight() > 0 || delta > 0) {
setVisibleHeight((int) delta + getVisibleHeight());
if (mState <= STATE_RELEASE_TO_REFRESH) { // 未處於刷新狀態,更新箭頭
if (getVisibleHeight() > mMeasuredHeight) {
setState(STATE_RELEASE_TO_REFRESH);
} else {
setState(STATE_NORMAL);
}
}
}
}
在onMove方法輸入參數中可以看出手指滑動距離的1/3作為刷新顯示的高度,由於init方法初始化時將刷新顯示高度設置為0,同樣在ArrowRefreshHeader中
addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));//初始化時候高度設置為0,通過後面setVisibleHeight設置可見高度
……
/**
* 設置可見高度
*
* @param height
*/
public void setVisibleHeight(int height) {
if (height < 0) height = 0;
LayoutParams lp = (LayoutParams) mContainer.getLayoutParams();
lp.height = height;
mContainer.setLayoutParams(lp);
}
這樣就不難理解onMove方法為何可以在下拉時慢慢出現下拉刷新.
在看釋放是刷新界面慢慢變為0
同樣在在XrecycleView中的onTouch方法中:
default分支:
default:
mLastY = -1; // reset
if (isOnTop() && pullRefreshEnabled) {
if (mRefreshHeader.releaseAction()) {//上彈關鍵代碼
if (mLoadingListener != null) {
mLoadingListener.onRefresh();
}
}
}
break;
在mRefreshHeader.releaseAction()
中處理了手指釋放後即刷新慢慢向上隱藏的動作,該接口在
ArrowRefreshHeader中實現
@Override
public boolean releaseAction() {
//釋放動作,此時需要處理緩慢回到頂部
boolean isOnRefresh = false;
int height = getVisibleHeight();
if (height == 0) // not visible.
isOnRefresh = false;
if (getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING) {
setState(STATE_REFRESHING);
isOnRefresh = true;
}
// refreshing and header isn't shown fully. do nothing.
if (mState == STATE_REFRESHING && height <= mMeasuredHeight) {
//return;
}
int destHeight = 0; // default: scroll back to dismiss header.
// is refreshing, just scroll back to show all the header.
if (mState == STATE_REFRESHING) {
destHeight = mMeasuredHeight;
}
smoothScrollTo(destHeight);
return isOnRefresh;
}
……
private void smoothScrollTo(int destHeight) {
ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight);
animator.setDuration(300).start();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setVisibleHeight((int) animation.getAnimatedValue());
}
});
animator.start();
}
其中主要是通過smoothScrollTo的屬性動畫+setVisibleHeight函數來實現刷新部分慢慢隱藏
加載更多實現
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
//重寫該方法主要是在IDLE態即手指滑動停止後處理加載更多
if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && loadingMoreEnabled) {
LayoutManager layoutManager = getLayoutManager();
int lastVisibleItemPosition;
if (layoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
//瀑布流布局發現最後可見的item位置
int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];
((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into);
lastVisibleItemPosition = findMax(into);
} else {
lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
}
if (layoutManager.getChildCount() > 0
&& lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader.STATE_REFRESHING) {
View footView = mFootViews.get(0);
isLoadingData = true;
if (footView instanceof LoadingMoreFooter) {
((LoadingMoreFooter) footView).setState(LoadingMoreFooter.STATE_LOADING);
} else {
footView.setVisibility(View.VISIBLE);
}
mLoadingListener.onLoadMore();
}
}
}
空白數據處理
@Override
public void onChanged() {
//重寫該方法是在數據發生變化時更換布局
Adapter adapter = getAdapter();
if (adapter != null && mEmptyView != null) {
int emptyCount = 0;
if (pullRefreshEnabled) {
emptyCount++;
}
if (loadingMoreEnabled) {
emptyCount++;
}
if (adapter.getItemCount() == emptyCount) {
mEmptyView.setVisibility(View.VISIBLE);
XRecyclerView.this.setVisibility(View.GONE);
} else {
mEmptyView.setVisibility(View.GONE);
XRecyclerView.this.setVisibility(View.VISIBLE);
}
}
if (mWrapAdapter != null) {
mWrapAdapter.notifyDataSetChanged();
}
}
ArrowRefreshHeader與LoadingMoreFooter
public void setState(int state) {
switch(state) {
case STATE_LOADING:
progressCon.setVisibility(View.VISIBLE);
mText.setText(getContext().getText(R.string.listview_loading));
this.setVisibility(View.VISIBLE);
break;
case STATE_COMPLETE:
mText.setText(getContext().getText(R.string.listview_loading));
this.setVisibility(View.GONE);
break;
case STATE_NOMORE:
mText.setText(getContext().getText(R.string.nomore_loading));
progressCon.setVisibility(View.GONE);
this.setVisibility(View.VISIBLE);
break;
}
}
可以看出,通過不同的狀態來處理文字和加載動畫。
在看ArrowRefreshHeader,稍微復雜點,主要是要處理隨著手指滑動刷新界面慢慢顯示和釋放釋放手指刷新界面慢慢返回,刷新完成後的狀態重置,這些都實現接口BaseRefreshHeader
處理。其中該自定義控件在初始化時候將高度設置為0,通過setVisibleHeight
來設置高度,這樣就可以處理刷新高度的動態變化,在介紹XRecyclerView中已經對這幾個接口方法做了詳細介紹了,這裡就不贅述了。這裡處理方式與LoadingMoreFooter相同,根據不同刷新狀態來處理控件的顯示狀態。
抽象來看,這兩個控件的核心就是使用 SimpleViewSwitcher做中轉將AVLoadingIndicatorView不同的加載動畫呈現的過稱。
其中 SimpleViewSwitcher比較簡單就是一個設置view的很普通的自定義viewgroup,而AVLoadingIndicatorView則是另一個加載動畫庫了github項目地址這次就不分析了。
到此基本上這個輪子就大致分析完了。
作為一個android彩筆,還是應該多讀讀源碼,包括android源碼和github上的一些多星的優秀項目的源碼,通過拆這個輪子,可以收獲到:
熟悉uml拆分框架 recycleView針對不同布局(如StaggeredGridLayoutManager)獲取findLastVisibleItemPositions和header的處理方式 下拉刷新手指滑動距離與刷新高度變化、釋放後刷新頭部自動消失(onTouch) 改變不同不通布局的方式,根據狀態設置empytyview可見還是 recycleView可見與否 recycleView增加頭部底部後使用對傳入的數據adapter來進行二次封裝 自定義viewgroup的使用 屬性動畫的簡單使用 view坐標系 熟悉了設計模式的裡氏替換、接口隔離、依賴倒置原則Android Gradle Build Error:Some file crunching failed, see logs for details解決辦法錯誤日志:E
本篇總結了Android開發常見過程中的內存洩漏問題。集合類洩漏??集合類如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存被占用。如果這個集合類是全局性的變量 (
集成地圖SDK國內常用的地圖SDK就是百度和高德了,二者的用法大同小異,可按照官網上的開發指南一步步來。下面是我在集成地圖SDK時遇到的問題說明:1、點擊基本地圖功能選項
前言:但行好事,莫問前程。只需努力每一天。一、概述1、四線格與基線小時候,我們在剛開始學習寫字母時,用的本子是四線格的,我們必須把字母按照規則寫在四線格內。比如:那麼問題
在前不久的谷歌2015 I/O大會上,發布了Android新