Android魔术(第五弹)—— 一步步实现滑动折叠列表
目錄
1、效果展示
2、效果分析
3、Item布局
3、實現Adapter
4、監聽滑動
5、回彈效果
6、總結一下
源碼:
1、效果展示
這個效果是一年多前完成的,是模仿了當時喵街app的首頁的效果,現在整理出來可能有些過時了,不過一些知識點和思路還是很有幫助的。實現后效果如下:2、效果分析
首先我們看靜止狀態,如圖:?
這時處于頂端展示的item相對于其他item是展開的狀態,有幾點表現:一是整體高度要高一些;二是無遮罩高亮狀態;三是文字內容大一些。這樣就達到了一個凸顯的效果。 然后我們觀察滑動中的狀態,如圖:?
當我們向上滑動的時候,可以看到第一個item開始折疊,而第二個item逐漸展開,同時遮罩效果減弱,文字內容逐漸變大。這樣就產生了滑動折疊的效果。 而且,為了能讓最后的item也可以凸顯出來,我們需要在列表的結尾插入一個footer以保證最后的item可以置頂顯示,如圖:?
3、Item布局
效果分析完了,下面我們來看看如何實現。 首先是Item的布局,這里只關注重要的部分,代碼如下: <FrameLayout?xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><RelativeLayoutandroid:id="@+id/item_content"android:layout_width="match_parent"android:layout_height="@dimen/scroll_fold_item_height"><ImageViewandroid:id="@+id/item_img"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="fitXY"/><ImageViewandroid:id="@+id/item_img_shade"android:layout_width="match_parent"android:layout_height="match_parent"android:src="#000000"/><LinearLayoutandroid:id="@+id/scale_item_content"android:layout_width="200dp"android:layout_height="wrap_content"android:layout_centerHorizontal="true"android:orientation="vertical">...</LinearLayout>...</RelativeLayout> </FrameLayout>最外層用FrameLayout,這樣當FrameLayout高度變小時,item_content可以超出FrameLayout的范圍,產生折疊的效果。
item_content的高度是固定不變的,真正改變的是外層的FrameLayout。
scale_item_content中是那些大小可變的文字內容
布局比較簡單,后面會講到如何使用這些layout達到效果。
另外還有一個footer的布局,因為很簡單就不貼出代碼了。
3、實現Adapter
列表是通過RecyclerView來實現的,所以我們先實現Adapter。代碼也比較簡單,我們挑重點說。 首先是Adapter的兩個基本方法的實現: @Override public?ViewHolder?onCreateViewHolder(ViewGroup?parent,?int?viewType)?{if(viewType?==?0)?{View?item?=?LayoutInflater.from(mContext).inflate(R.layout.scroll_fold_list_item,?null);return?new?ItemViewHolder(item);}else{View?bottom?=?LayoutInflater.from(mContext).inflate(R.layout.scroll_fold_list_footer,?null);return?new?BottomViewHolder(bottom);} }@Override public?void?onBindViewHolder(ViewHolder?holder,?int?position)?{holder.initData(position); } 這里使用viewType來區分普通的item和footer(通過getItemViewType方法)。BottomViewHolder和ItemViewHolder繼承同一個類,代碼如下: abstract?class?ViewHolder?extends?RecyclerView.ViewHolder{View?item;public?ViewHolder(View?itemView)?{super(itemView);item?=?itemView;}abstract?void?initData(int?position); }class?BottomViewHolder?extends?ViewHolder{public?BottomViewHolder(View?itemView)?{super(itemView);}@Overridevoid?initData(int?position)?{ViewGroup.LayoutParams?bottomParams?=?itemView.getLayoutParams();if(bottomParams?==?null){bottomParams?=?new?ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,?0);}bottomParams.height?=?recyclerView.getHeight()?-?itemHeight?+?10;itemView.setLayoutParams(bottomParams);} }class?ItemViewHolder?extends?ViewHolder{View?content;ImageView?image;TextView?name;public?ItemViewHolder(View?itemView)?{super(itemView);item?=?itemView;content?=?itemView.findViewById(R.id.item_content);image?=?(ImageView)itemView.findViewById(R.id.item_img);name?=?(TextView)?itemView.findViewById(R.id.item_name);}void?initData(int?position){image.setImageResource(IMGS[position]);name.setText(NAMES[position]);ViewGroup.LayoutParams?params?=?item.getLayoutParams();if(params?==?null){params?=?new?ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,?0);}params.height?=?itemSmallHeight;content.findViewById(R.id.item_img_shade).setAlpha(ITEM_SHADE_DARK_ALPHA);item.setLayoutParams(params);} }我們先看BottomViewHolder,動態的設置footer的高度為列表高度減去itemHeight,再加上10像素。這個itemHeight是展開后item的高度,即置頂的item的高度。這里之所以再加上10像素,是因為如果設置高度正好是余下的高度,當快速滑動到底部的時候有幾率會出現問題,所以這里讓高度略大于實際展示的高度。
然后來看ItemViewHolder,也是動態的設置高度為ItemSmallHeight,這個高度是收縮后item的高度,而且將遮罩設置為最暗。注意這里全部初始化為收縮狀態,沒有單獨設置一個置頂展開的狀態,這個我們后面會解釋為什么。
4、監聽滑動
上面我們完成了adapter類,添加給RecyclerView即可。不過想要實現效果,就需要監聽RecyclerView的滑動,并做相應的處理,代碼如下: list.addOnScrollListener(new?RecyclerView.OnScrollListener()?{@Overridepublic?void?onScrolled(RecyclerView?recyclerView,?int?dx,?int?dy)?{changeItemState();}@Overridepublic?void?onScrollStateChanged(RecyclerView?recyclerView,?int?newState)?{...} });可以看到在滑動過程(onScrolled)中調用changeItemState()這個函數,代碼如下: private?void?changeItemState(){int?firstVisibleIndex?=?linearLayoutManager.findFirstVisibleItemPosition();ViewGroup?first?=?(ViewGroup)?linearLayoutManager.findViewByPosition(firstVisibleIndex);int?firstVisibleOffset?=?-first.getTop();int?changeheight?=?(int)?(firstVisibleOffset?*?(ScrollFoldAdapter.ITEM_CONTENT_TEXT_SCALE?-?1));//?減少當前展示的第一個item的高度。if?(first?==?null)?{return;}changeItemHeight(first,?itemHeight?-?changeheight);changeItemState(first,?ScrollFoldAdapter.ITEM_CONTENT_TEXT_SCALE,?ScrollFoldAdapter.ITEM_SHADE_LIGHT_ALPHA);//?增大當前展示的第二個item的高度,改變內容大小,改變透明度if?(firstVisibleIndex?+?1?<?adapter.getItemCount()?-?1)?{ViewGroup?second?=?(ViewGroup)?linearLayoutManager.findViewByPosition(firstVisibleIndex?+?1);changeItemHeight(second,?itemSmallHeight?+?changeheight);float?scale?=?(float)?firstVisibleOffset?/?itemSmallHeight*?(ScrollFoldAdapter.ITEM_CONTENT_TEXT_SCALE?-?1)?+?1.0f;float?alpha?=?(ScrollFoldAdapter.ITEM_SHADE_DARK_ALPHA?-?ScrollFoldAdapter.ITEM_SHADE_LIGHT_ALPHA)*?(1?-?(float)?firstVisibleOffset?/?itemSmallHeight)+?ScrollFoldAdapter.ITEM_SHADE_LIGHT_ALPHA;changeItemState(second,?scale,?alpha);}/***?由于快速滑動,導致計算及狀態有誤?所以下面就是消除這種誤差,校準狀態。具體如下*?將第一個item上面(存在的)的和第二個Item下面的都變為收縮的高度,內容縮放到最小,透明度為0。65*/for?(int?i?=?0;?i?<=?linearLayoutManager.findLastVisibleItemPosition();?i++)?{if?(i?<?adapter.getItemCount()?-?1?&&?i?!=?firstVisibleIndex?&&?i?!=?firstVisibleIndex?+?1)?{ViewGroup?item?=?(ViewGroup)?linearLayoutManager.findViewByPosition(i);if(item?==?null){continue;}changeItemHeight(item,?itemSmallHeight);float?scale?=?1;float?alpha?=?ScrollFoldAdapter.ITEM_SHADE_DARK_ALPHA;changeItemState(item,?scale,?alpha);}} }整體思路如下:
獲取當前置頂展示的item,計算該item相對于列表頂端的偏移。這個偏移是關鍵參數,通過這個偏移計算出第一個item收縮的高度和第二個item展開的高度,并且計算第二個item遮罩的透明度和文字內容的大小。
這里調用了另外兩個函數changeItemHeight(view, int)和changeItemState(view, float, float)。其中changeItemHeight(view, int)用來改變item的高度實現展開或折疊;而changeItemState(view, float, float)用來改變遮罩透明度和文字內容大小。兩個函數代碼如下:
/***?改變一個item的高度。**?@param?item*?@param?height*/ private?void?changeItemHeight(View?item,?int?height)?{ViewGroup.LayoutParams?itemParams?=?item.getLayoutParams();itemParams.height?=?height;item.setLayoutParams(itemParams); }/***?改變一個item的狀態,包括透明度,大小等*?@param?item*?@param?scale*?@param?alpha*/ private?void?changeItemState(ViewGroup?item,?float?scale,?float?alpha)?{if?(item.getChildCount()?>?0)?{View?changeView?=?item.findViewById(R.id.scale_item_content);changeView.setScaleX(scale);changeView.setScaleY(scale);View?shade?=?item.findViewById(R.id.item_img_shade);shade.setAlpha(alpha);} }改變高度很簡單,沒必要解釋了。改變遮罩透明度就是改變其alpha,而文字內容大小的改變則是利用setScaleX和setScaleY兩個函數,實際上是將scale_item_content這個layout整個進行縮放,其內容就會隨著變大/小。
回到changeItemState()函數,改變了第一個和第二個item后,可以看到又將其他的item置為收縮狀態。這是因為快速滑動會造成某些item處于中間的狀態,做這一步操作就是校正快速滑動導致的一些問題。
上面我們提到過,所有的item都初始化成收縮狀態了。其實當RecyclerView添加到屏幕上時,是一定會產生滑動的。所以我們進入頁面的時候,我們什么都沒有操作,滑動監聽的函數卻被調用了。這樣通過changeItemState()函數就可以將置頂的item變為展開狀態,所以初始的展示狀態是正確的。
5、回彈效果
以上是滑動的時候的處理,然而這樣還不夠。當滑動停止的時候,有可能第一個item正處于顯示一半的狀態,這樣第二個item也沒有完全展開,顯示效果不好。 所以我們還需要實現一個回彈效果,當滑動停止的時候,讓列表自動調整到某一個item正好置頂的狀態。 這部分的處理在滑動監聽的onScrollStateChanged中,代碼如下: list.addOnScrollListener(new?RecyclerView.OnScrollListener()?{@Overridepublic?void?onScrolled(RecyclerView?recyclerView,?int?dx,?int?dy)?{changeItemState();}@Overridepublic?void?onScrollStateChanged(RecyclerView?recyclerView,?int?newState)?{if?(newState?==?RecyclerView.SCROLL_STATE_IDLE)?{int?firstVisibleIndex?=?linearLayoutManager.findFirstVisibleItemPosition();View?first?=?linearLayoutManager.findViewByPosition(firstVisibleIndex);int?firstVisibleOffset?=?-first.getTop();if?(firstVisibleOffset?==?0)?{return;}if?(firstVisibleOffset?<?itemSmallHeight?/?2)?{list.scrollBy(0,?-firstVisibleOffset);}?else?{list.scrollBy(0,?itemSmallHeight?-?firstVisibleOffset);}changeItemState();}} });上面是完整的滑動監聽的代碼。在onScrollStateChanged中,判斷狀態是否是滑動結束(SCROLL_STATE_IDLE)。如果滑動結束,判斷頂部顯示的item的偏移,根據偏移的大小選擇回彈方向。如果偏移很小(第一個item大部分內容顯示出來了),則下滾至第一個item置頂的狀態;否則上滾至第二個item置頂的狀態。
這樣保證了靜止狀態下一定有一個item完全置頂高亮顯示。
最后又調用了changeItemState函數,主要目的是校正一些誤差。
6、總結一下
整個效果中其實沒有太多難點,主要是考察了對RecyclerView滑動的理解。目前這個版本在快滑時還有一個小問題。 除了RecyclerView這個版本,實際上這個效果還有一個ScrollView的版本。其實在ListView和RecyclerView上實現這個效果都多少有些問題。所以我早期自己實現了能夠復用和回收的ScrollView,利用這個自定義的ScrollView實現了這個效果,并且為其自定義了scroller使其回彈有了動畫效果。ScrollView版本目前未發現任何問題,但是由于很多功能要自己實現,整體代碼比較復雜,就選用了RecyclerView這個版本來給大家講解。大家有興趣可以去github上的項目中,切到tag v1.0就可以看到了。源碼:
關注公眾號:BennuCTech,發送“FastWidget”獲取完整源碼總結
以上是生活随笔為你收集整理的Android魔术(第五弹)—— 一步步实现滑动折叠列表的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用FastJson解析时有关内部类的两
- 下一篇: Kotlin学习笔记——安装配置kotl