Android 自訂ScrollView ListView 體驗各種縱向滑動的需求
1、概述
群裡的一個哥們有個需求是這樣的:問題;主要功能就是:1、迴圈的一個滑動;2、每次滑動結束,保持每個Item的完整。然後我當時給他寫了個Demo,所有代碼都在Activity裡面,後期看來其太噁心了,修改也不方便;貌似那哥們還因為那代碼修改到12點,大大的贊一下這哥們的毅力,也深表歉意,今天特意把代碼抽取成自訂的ScrollView,然後提供一些回調出來;
本部落格首先介紹使用自訂的ScrollView解決上面的問題,然後自訂ListView實現每次拖動完成後保持每個Item的完整;
關於橫向滑動可以參考:
Android 自訂 HorizontalScrollView 打造再多圖片(控制項)也不怕 OOM 的橫向滑動效果
Android 自訂RecyclerView 實現真正的Gallery效果
橫向和縱向很類似哈,一個改寫HorizontalScrollView,一個是ScrollView;一個改寫ListView,一個是RecyclerView;不多具體需求還是有出入的,大家如果有這方面需求可以借鑒。關於橫向有同學問如何保證滑動結束實現單個Item完整,可以參考今天縱向的代碼。
2、自訂ScrollView實現迴圈Item拖動
現展示下,然後講解下原理,最後分析下代碼~三部曲~
1、:
2、實現的原理
a、根據使用者提供的Adapter(主要就兩個方法,getCount【決定多少個Item】,getView【每個Item的樣子】),根據getCount我會在初始化時,在ScrollView內部LinearLayout中添加對應數量的Item,當然也會根據數量平分螢幕高度;
b、監聽onTouch方法,當使用者觸發ACTION_MOVE時,螢幕跟隨使用者手指上面移動,當達到頂部時,會動態為頂部添加一個Item,同時移除底部最後一個Item;使用者滑動到達底部也一樣,底部動態添加一個Item,頂部第一個移除;ScrollView內會始終維持使用者指定數量+1個Item,為什麼加1呢,因為拖動過程中最多會顯示+1個Item(第一個一半,最後一個一半)
c、監聽ACTION_UP,判斷如果當然顯示小於半個,縮回去;大於半個則慢慢顯示出完整的Item;
3、程式碼分析
首先我們定義一個Adapter,使用者給使用者佈建每個Item的樣子以及個數;類似ListView的Adapter
/** * 適配器 * @author zhy * */public static abstract class Adapter{public abstract View getView(SingleItemScrollView parent, int pos);public abstract int getCount();}
構造方法中得到螢幕的高度
public SingleItemScrollView(Context context, AttributeSet attrs){super(context, attrs);// 計算螢幕的高度WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics outMetrics = new DisplayMetrics();wm.getDefaultDisplay().getMetrics(outMetrics);mScreenHeight = outMetrics.heightPixels;mScreenHeight -= getStatusHeight(context);}
onMeasure中對Item進行添加,以及設定高度
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){//防止多次調用if (!flag){mContainer = (ViewGroup) getChildAt(0);//根據Adapter的方法,為容器添加Itemif (mAdapter != null){mItemCount = mAdapter.getCount();mItemHeight = mScreenHeight / mItemCount;mContainer.removeAllViews();for (int i = 0; i < mAdapter.getCount(); i++){addChildView(i);}}addChildView(0);}super.onMeasure(widthMeasureSpec, heightMeasureSpec);}/** * 在容器末尾添加一個Item * @param i */private void addChildView(int i){View item = mAdapter.getView(this, i);//設定參數android.view.ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, mItemHeight);item.setLayoutParams(lp);//設定Tagitem.setTag(i);//添加事件item.setOnClickListener(this);mContainer.addView(item);}
接下來就是複寫onTouchEvent
@Overridepublic boolean onTouchEvent(MotionEvent ev){flag = true;int action = ev.getAction();int scrollY = getScrollY();switch (action){case MotionEvent.ACTION_MOVE:Log.e(TAG, scrollY = + scrollY);// 表示此時ScrollView的頂部已經達到螢幕頂部if (scrollY == 0){addChildToFirst();}// ScrollView的頂部已經到達螢幕底部if (Math.abs(scrollY - mItemHeight) <= mItemCount){addChildToLast();}break;case MotionEvent.ACTION_UP:checkForReset();return true;default:break;}return super.onTouchEvent(ev);}/** * 在底部添加一個View,並移除第一個View */private void addChildToLast(){Log.e(TAG, addChildToLast);int pos = (Integer) mContainer.getChildAt(1).getTag();addChildView(pos);mContainer.removeViewAt(0);this.scrollTo(0, 0);}/** * 在頂部添加一個View,並移除最後一個View */protected void addChildToFirst(){Log.e(TAG, addChildToFirst);int pos = (Integer) mContainer.getChildAt(mItemCount - 1).getTag();addChildView(pos, 0);mContainer.removeViewAt(mContainer.getChildCount() - 1);this.scrollTo(0, mItemHeight);}/** * 檢查當前getScrollY,顯示完成Item,或者收縮此Item */private void checkForReset(){int val = getScrollY() % mItemHeight;if (val >= mItemHeight / 2){smoothScrollTo(0, mItemHeight);} else{smoothScrollTo(0, 0);}}可以看到ACTION_MOVE中我們只判斷是否到達頂部或者底部;
if (Math.abs(scrollY - mItemHeight) <= mItemCount)這樣的判斷,主要是因為,擔心使用者佈建的個數拿螢幕的高度來除並不是整除的,整數除整數難免有個四捨五入什麼的,所以我設定了一個小的範圍;
當到達頂部的時候,頂部其實已經沒有Item了,此時我們為頂部添加一個Item,同時移除底部的最後一個;並且我們需要將scrollY移動到未添加前的位置;給使用者一個無縫添加的體驗;
到達底部同理了~
當ACTION_UP的時候,我們對當前的scrollY進行檢查,如果對Item高度取餘後小於Item的高度的一半,小於Item的高度的一半意味中隱藏部分是小於一般的,而顯示部分是大於一半的,於是我們smoothScrollTo(0, 0);把完整的Item顯示出來;反之則反之;
取餘以後得到的值其實去超出螢幕的部分,也就是隱藏的部分,它要是大於一半,那麼顯示部分就小於一半;反之則反之;
MainActivity中使用者的使用
package com.example.zhy_wangandroid_hownoon;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import android.app.Activity;import android.graphics.Color;import android.os.Bundle;import android.view.LayoutInflater;import android.view.View;import android.view.Window;import android.widget.ImageView;import android.widget.Toast;import com.example.zhy_wangandroid_hownoon.SingleItemScrollView.Adapter;import com.example.zhy_wangandroid_hownoon.SingleItemScrollView.OnItemClickListener;public class MainActivity extends Activity{private SingleItemScrollView mScrollView;private Adapter mAdapter;private LayoutInflater mInflater;private List mDatas = new ArrayList(Arrays.asList(//new Bean(R.drawable.icon_about, Color.rgb(123, 34, 45)),//new Bean(R.drawable.icon_map, Color.rgb(12, 34, 145)),//new Bean(R.drawable.icon_message, Color.rgb(177, 234, 145)),//new Bean(R.drawable.icon_oil, Color.rgb(88, 134, 145))//));@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);requestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);mInflater = LayoutInflater.from(this);mScrollView = (SingleItemScrollView) findViewById(R.id.id_scrollview);mAdapter = new Adapter(){@Overridepublic View getView(SingleItemScrollView parent, int pos){View view = mInflater.inflate(R.layout.item, null);ImageView iv = (ImageView) view.findViewById(R.id.id_title);iv.setImageResource(mDatas.get(pos).getResId());view.setBackgroundColor(mDatas.get(pos).getColor());return view;}@Overridepublic int getCount(){return 4;}};mScrollView.setAdapter(mAdapter);mScrollView.setOnItemClickListener(new OnItemClickListener(){@Overridepublic void onItemClick(int pos, View view){Toast.makeText(getApplicationContext(), pos + ,Toast.LENGTH_SHORT).show();}});}}
使用者拿到控制項,設定Adapter,在getView裡面設定資料以及決定Item的樣子;提供一個setOnItemClickListener的回調,供使用者佈建點擊每個Item的處理;和ListView的使用風格基本是一致的;
4、布局檔案
主布局檔案:
Item的布局
3、自訂ListView使用滑動結束完整顯示Item1、:
2、實現原理
先分析一下:
想要結束顯示完整的,那麼有個前提是,你的Item初始化一螢幕的時候應該剛好是固定數量完整的Item;
這樣,就需要一個參數指定一螢幕顯示多少個Item;同時會根據Item數量為每個Item設定高度;
存在一個問題,Item布局是在Adapter中設定的,也就是說,我們必須在getView中設定這個高度,但是這個高度實在ListView中計算的,所以對外公布出這個高度;
最後無非就是監聽滾動結束後,判斷當前顯示是否是完整的,如果是則,如果不是則;
3、實現代碼
a、自訂屬性
很簡單就一個屬性;
b、布局檔案:
c、MyListView
package com.example.zhy_wangandroid_hownoon;import android.content.Context;import android.content.res.TypedArray;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.util.Log;import android.view.WindowManager;import android.widget.AbsListView;import android.widget.AbsListView.OnScrollListener;import android.widget.ListView;public class MyListView extends ListView implements OnScrollListener{private static final String TAG = MyListView.class.getSimpleName();/** * 每個螢幕顯示多少個Item */private int mItemCountInOneScreen ;/** * 每個Item的高度 */private int mItemHeight;/** * 記錄第一個顯示的Item */private int mFirstVisibleItem;public MyListView(Context context, AttributeSet attrs){this(context, attrs, 0);}/** * 構造方法中拿到自訂屬性ItemCount,計算每個Item高度 * @param context * @param attrs * @param defStyle */public MyListView(Context context, AttributeSet attrs, int defStyle){super(context, attrs, defStyle);WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics outMetrics = new DisplayMetrics();wm.getDefaultDisplay().getMetrics(outMetrics);//擷取MyListView_itemCountTypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.MyListView, defStyle, 0);int n = a.getIndexCount();for (int i = 0; i < n; i++){int attr = a.getIndex(i);switch (attr){case R.styleable.MyListView_itemCount:mItemCountInOneScreen = a.getInt(attr, 6);break;}}a.recycle();//計算每個Item高度mItemHeight = (outMetrics.heightPixels - getStatusHeight(context))/ mItemCountInOneScreen;//設定一個滾動監聽setOnScrollListener(this);Log.e(TAG, mItemCountInOneScreen + );}@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState){//滾動結束if (scrollState == OnScrollListener.SCROLL_STATE_IDLE){// Log.e(TAG, getChildAt(0).getTop() + getTop());Log.e(TAG, mFirstVisibleItem + SCROLL_STATE_IDLE);//擷取第一個Item的topint top = getChildAt(0).getTop();if (top == 0)return;//絕對值不為0時,如果絕對值大於mItemHeight的一半,則收縮,即顯示下一個Itemif (Math.abs(top) > mItemHeight / 2){this.setSelection(mFirstVisibleItem + 1);// this.scrollTo(x, y)// smoothScrollToPosition(mFirstVisibleItem - 1);// scrollBy(0, mItemHeight- Math.abs(top));// smoothScrollBy(mItemHeight- Math.abs(top), 200);} else//絕對值不為0時,如果絕對值小于于mItemHeight的一半,則展開,顯示當前完整的Item{this.setSelection(mFirstVisibleItem);// this.scrollTo(x, y)// smoothScrollBy( -Math.abs(top), 200);// smoothScrollByOffset(offset);// scrollBy(0, -Math.abs(top));// smoothScrollToPosition(mFirstVisibleItem);}// smoothScrollToPosition(mFirstVisibleItem);}}/** * 滾動過程中不斷記錄當前顯示的第一個Item */@Overridepublic void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount){mFirstVisibleItem = firstVisibleItem;// Log.e(TAG, mFirstVisibleItem + );}/** * 對外公布每個Item的高度 * @return */public int getItemHeight(){return mItemHeight;}/** * 獲得狀態列的高度 * * @param context * @return */public int getStatusHeight(Context context){int statusHeight = -1;try{Class clazz = Class.forName(com.android.internal.R$dimen);Object object = clazz.newInstance();int height = Integer.parseInt(clazz.getField(status_bar_height).get(object).toString());statusHeight = context.getResources().getDimensionPixelSize(height);} catch (Exception e){e.printStackTrace();}return statusHeight;}}
代碼比較少,也比較簡單,大家可以關注下,我在onScrollStateChanged注釋掉的那些方法,原則上說每一個應該都能實現;但是我最終選擇了setSelection(mFirstVisibleItem);有興趣的可以拿別的方法試試;
d、MainActivity中
package com.example.zhy_wangandroid_hownoon;import java.util.ArrayList;import java.util.List;import java.util.Random;import com.zhy.utils.CommonAdapter;import com.zhy.utils.ViewHolder;import android.app.Activity;import android.graphics.Color;import android.os.Bundle;import android.view.View;import android.view.ViewGroup;import android.view.Window;public class MainActivity extends Activity{private MyListView mListView;private CommonAdapter mAdapter;private List mDatas;@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);requestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);mListView = (MyListView) findViewById(R.id.id_lv);initDatas();initViews();}private void initViews(){mListView = (MyListView) findViewById(R.id.id_lv);mAdapter = new CommonAdapter(this, mDatas, R.layout.item){@Overridepublic void convert(ViewHolder helper, String item){View convertView = helper.getConvertView();convertView.setBackgroundColor(Color.rgb(new Random().nextInt(255), new Random().nextInt(255),new Random().nextInt(255)));ViewGroup.LayoutParams lp = convertView.getLayoutParams();lp.height = mListView.getItemHeight();convertView.setLayoutParams(lp);helper.setText(R.id.id_title, item);}};mListView.setAdapter(mAdapter);}private void initDatas(){mDatas = new ArrayList();for (int i = 'A'; i <= 'Z'; i++){mDatas.add(String.valueOf((char) +i));}}}
使用就沒撒說的了,我用了我們之前封裝的CommonAdapter,如果不瞭解可以參考:Android 快速開發系列 打造萬能的ListView GridView 適配器;如果看不慣自行改成傳統的Adapter,相信大家應該沒問題~
還有些需求是這樣的,既要滿足每次停止顯示完整的Item,還希望即使使用者給了個加速度,在使用者抬起手指時,不會自動的滾動;
4、ListView顯示完整Item,屏蔽自動滾動
這個比較容易了,只需要複寫onTouchEvent方法,然後在ACTION_UP後,進行檢查是顯示/影藏當前Item,然後return true;
@Overridepublic boolean onTouchEvent(MotionEvent ev){int action = ev.getAction();if (action == MotionEvent.ACTION_UP){checkForReset();return true;}return super.onTouchEvent(ev);}@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState){// 滾動結束if (scrollState == OnScrollListener.SCROLL_STATE_IDLE){checkForReset();}}關鍵代碼如上,其他所有代碼和上例一致;
:
好了,就介紹到這了;
源碼點擊下載