Android使用ListView實現(xiàn)滾輪的動畫效果實例
之前收到一個需求,需要把一個數(shù)據(jù)展示列表頁面做成像滾輪那樣的動畫效果:中間最大然后向上下兩端逐漸縮小。我想了想iOS那邊自帶滾輪組件,安卓得自己去實現(xiàn),目前網(wǎng)上仿ios的滾輪組件的也有一些,但是感覺不適合我,我的要求沒那么復(fù)雜,于是決定自己動手去實現(xiàn)一下。
動手前先分析一下應(yīng)該怎么做,歸根到底只是要實現(xiàn)縮放效果,由中間向兩邊變小,當(dāng)一個item越接近中間就放大,越遠(yuǎn)離中間就縮小。那么可以通過先獲取ListView的中點,然后獲取當(dāng)前可視的所有item跟ListView的中點的垂直距離計算出一個比例,然后將item的大小根據(jù)這個比例進(jìn)行縮放,各個item跟ListView的中點的垂直距離不同,計算出來的比例也就不同,然后每次滾動的時候都計算比例然后進(jìn)行縮放,這樣應(yīng)該就能實現(xiàn)我們想要的效果了。
因為一開始我的列表展示就是用ListView做的,有其他效果在里面,也不方便換其他組件,所以依然還是用ListView實現(xiàn)就好了。由于我們是每次一滾動都要進(jìn)行縮放,ListView有提供一個OnScrollListener,它的onScroll方法每次一開始滾動就會調(diào)用,所以我們選擇重寫它。當(dāng)它被調(diào)用的時候,我們就開始獲取ListView中點,然后開始計算每個item的距離進(jìn)行縮放.
/** * 中點的Y坐標(biāo) */ private float centerY = 0f; @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { //計算中點 centerY = getHeight()/2; //判斷中點的有效性 if(centerY <= 0){ return; } //開始對當(dāng)前顯示的View進(jìn)行縮放 for(int i = 0; i < visibleItemCount; i++){ //獲取item View temp_view = getChildAt(i); //計算item的中點Y坐標(biāo) float itemY = temp_view.getBottom()-(temp_view.getHeight()/2); //計算離中點的距離 float distance = centerY; if(itemY > centerY){ distance = itemY - centerY; }else{ distance = centerY - itemY; } //根據(jù)距離進(jìn)行縮放 temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); //根據(jù)距離改變透明度 temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); } }
后面不想加demo了,所以直接上整個ListView的代碼吧
/** * 模仿滾輪動畫縮放的ListView * Created by xu on 2017/3/3. */ public class XuListView extends ListView implements AbsListView.OnScrollListener { private static final String TAG = "XuListView"; /** * 中點的Y坐標(biāo) */ private float centerY = 0f; public XuListView(Context context, AttributeSet attrs) { super(context, attrs); //設(shè)置一個滾動監(jiān)聽 setOnScrollListener(this); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { //計算中點 centerY = getHeight()/2; //判斷中點的有效性 if(centerY <= 0){ return; } //開始對當(dāng)前顯示的View進(jìn)行縮放 for(int i = 0; i < visibleItemCount; i++){ //獲取item View temp_view = getChildAt(i); //計算item的中點Y坐標(biāo) float itemY = temp_view.getBottom()-(temp_view.getHeight()/2); //計算離中點的距離 float distance = centerY; if(itemY > centerY){ distance = itemY - centerY; }else{ distance = centerY - itemY; } //根據(jù)距離進(jìn)行縮放 temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); //根據(jù)距離改變透明度 temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); } } }
這樣基本就實現(xiàn)了我們想要的效果了
但是現(xiàn)在有一個問題,就是第一個item和最后一個item無法滾動到中間從而放大突出顯示。對此我暫時想了兩個方法去解決:1、在頭尾各種添加一些空白的item,使我們需要顯示的數(shù)據(jù)都可以滾動到最中間。(我現(xiàn)在就是這么做的);2、使整個列表實現(xiàn)循環(huán)滾動。
添加空白的item的話,我是通過修改adapter去實現(xiàn)的。數(shù)據(jù)源是一個數(shù)組,我在數(shù)組前面和后面各加一些特殊的數(shù)據(jù),adapter實現(xiàn)getview的時候,如果發(fā)現(xiàn)當(dāng)前item的數(shù)據(jù)是特殊數(shù)據(jù),那么item就變透明,從而實現(xiàn)了我們原本要顯示的數(shù)據(jù)都可以被滾動最中間;
先從數(shù)據(jù)源下手,從頭尾填充特殊的數(shù)據(jù)
public class MainActivity extends AppCompatActivity { XuListView mLisetview; MyAdapter adapter; ArrayList<String> nos = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLisetview = (XuListView) findViewById(R.id.list_test); ArrayList<String> temp = new ArrayList<String>(); for(int i = 0;i<10;i++){ temp.add(i+""); } adapter = new MyAdapter(this,temp); mLisetview.setAdapter(adapter); resetitem(mLisetview); } /** * 在頭尾填充透明的item數(shù)據(jù) */ private void resetitem(ListView listview) { if(listview == null){ return; } //獲取屏幕高度 WindowManager wm =getWindowManager(); int displayheight = wm.getDefaultDisplay().getHeight(); //計算一個item的高度 int itemhight = 0; if(adapter!=null){ View v=(View)adapter.getView(0, null, listview); v.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); itemhight=v.getMeasuredHeight(); } //根據(jù)Item的高度和屏幕的高度計算需要多少個item可以填滿一半的屏幕 int newcount = ((displayheight/2)/itemhight); //填充前面的空白item for (int i = 1; i <= newcount; i++) { nos.add("full"); } //添加我們需要顯示的數(shù)據(jù) for(int i = 0;i<10;i++){ nos.add(i+""); } //填充后面的空白item for (int i = 1; i <= newcount; i++) { nos.add("full"); } //刷新數(shù)據(jù) adapter.refreshData(nos); } }
然后adapter里面對頭尾的特殊數(shù)據(jù)進(jìn)行識別,將item變?yōu)橥该鞯摹?br />
public class MyAdapter extends BaseAdapter { ArrayList<String> nos = new ArrayList<String>(); private Context context; public MyAdapter(Context context, ArrayList<String> nos){ this.context = context; this.nos = nos; } public void refreshData(ArrayList<String> nos) { this.nos = nos; notifyDataSetChanged(); } @Override public int getCount() { return nos.size(); } @Override public Object getItem(int position) { return nos.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { // 如果是第一次顯示該頁面(要記得保存到viewholder中供下次直接從緩存中調(diào)用) holder = new ViewHolder(); convertView = LayoutInflater.from(context).inflate(R.layout.item_test, null); holder.tv_no = (TextView) convertView.findViewById(R.id.tv_no); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.tv_no.setText(nos.get(position)); if(nos.get(position).equals("full")){ convertView.setVisibility(View.INVISIBLE); }else{ convertView.setVisibility(View.VISIBLE); } return convertView; } private class ViewHolder { TextView tv_no; } }
這樣我們就實現(xiàn)可以第一種解決方法
第二種方法,就是讓整個ListView實現(xiàn)循環(huán)滾動,實現(xiàn)的方式有很多,大家可以自行百度,我這里就通過修改adapter的getCount方法去實現(xiàn),就是在getCount方法return一個很大的值,getView獲取數(shù)據(jù)的時候要模原本的數(shù)組大小,不然數(shù)組就越界了。然后ListView滾動到最中間,這樣就實現(xiàn)偽循環(huán)滾動了
public class MainActivity extends AppCompatActivity { XuListView mLisetview; MyAdapter adapter; ArrayList<String> nos = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mLisetview = (XuListView) findViewById(R.id.list_test); ArrayList<String> temp = new ArrayList<String>(); for(int i = 0;i<10;i++){ temp.add(i+""); } adapter = new MyAdapter(this,temp); mLisetview.setAdapter(adapter); //滾動到中間 mLisetview.setSelection(adapter.getCount()/2); } }
/** * Created by xu on 2017/6/27. */ public class MyAdapter extends BaseAdapter { ArrayList<String> nos = new ArrayList<String>(); private Context context; public MyAdapter(Context context, ArrayList<String> nos){ this.context = context; this.nos = nos; } @Override public int getCount() { return Integer.MAX_VALUE; } @Override public Object getItem(int position) { return nos.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { // 如果是第一次顯示該頁面(要記得保存到viewholder中供下次直接從緩存中調(diào)用) holder = new ViewHolder(); convertView = LayoutInflater.from(context).inflate(R.layout.item_test, null); holder.tv_no = (TextView) convertView.findViewById(R.id.tv_no); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.tv_no.setText(nos.get(position%nos.size())); return convertView; } private class ViewHolder { TextView tv_no; } }
這樣我們就實現(xiàn)了使列表進(jìn)行循環(huán)滾動,從而達(dá)到每個item都可以滾動到中間突出顯示的效果了
既然我們把動畫效果都做出來了,那么也可以直接做成一個滾輪選擇器,只需要加多兩步:1、把最靠近中間的item滾動到中間;2、把中間的item的序號通過一個接口返回出去。 我就直接貼代碼吧,反正也不難。
把最靠近中間的item滾動到中間的實現(xiàn)我是這么做的:首先決定好整個ListView可視的的item數(shù)量是多少,必須是奇數(shù),這樣才能只有一個item處于正中間,然后根據(jù)ListView的高度計算出每個item符合要求的高度,然后更改現(xiàn)有的item的高度,同時對內(nèi)容進(jìn)行縮放(不縮放內(nèi)容單純高度變小的話布局就亂了)。最后我們利用smoothScrollToPosition方法回正可視item中的第一個item,就實現(xiàn)了將最中間的item回滾到最中間的效果了。因為可視的item我們是根據(jù)ListView的高度去計算item的高度的,所有的可視item剛好鋪滿ListView,所以只要頂部那個回正了,其他的item也會回正。所以我們可以重寫一下OnScrollListener的onScrollStateChanged方法,每次滾動結(jié)束就執(zhí)行一次回滾
/** * 可視的item數(shù) */ private int mVisibleItemCount = -1; /** * 沒調(diào)整之前每個item的高度 */ private float olditemheight = 0; /** * 調(diào)整過后的每個item的高度 */ private float newitemheight = -1; /** * 調(diào)整每個可視的item的高度 以及對內(nèi)容進(jìn)行縮放 */ public void reSetItemHeight() { for (int i = 0; i < getChildCount(); i++) { //獲取item View temp_view = getChildAt(i); //設(shè)置item的高度 ViewGroup.LayoutParams lp = temp_view.getLayoutParams(); lp.height = (int) newitemheight; temp_view.setLayoutParams(lp); //縮放內(nèi)容 我的item的內(nèi)容用一個LinearLayout包了起來 所以直接縮放LinearLayout LinearLayout item_ll_value = (LinearLayout) temp_view.findViewById(R.id.item_ll_value); item_ll_value.setScaleY((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight)); item_ll_value.setScaleX((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight)); } } /** * 計算在給定的可視item數(shù)目下 每個item應(yīng)該設(shè)置的高度 * */ private void getNewItemHeight() { //先把舊的item存起來 olditemheight = getChildAt(0).getHeight(); //計算新的高度 newitemheight = getHeight() / mVisibleItemCount; if ((getHeight() / mVisibleItemCount) % newitemheight > 0) { //除不盡的情況下把余數(shù)分給各個item,暫時發(fā)現(xiàn)分一次余數(shù)就夠了,如果效果不理想就做個遞歸多分幾次 float remainder = (getHeight() / mVisibleItemCount) % newitemheight; newitemheight = remainder / mVisibleItemCount; } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //滾動結(jié)束之后開始回滾item if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1) { //使離中間最近的item回滾到中點位置 smoothScrollToPosition(getFirstVisiblePosition()); } }
實現(xiàn)了把最靠近中間的item滾動到中間,那么選擇的item就是滾動結(jié)束后處于最中間的item。對此我們需要再次重寫一下OnScrollListener的onScrollStateChanged方法。每次滾動結(jié)束后,取可視item中的第一個item的序號加上我們之前設(shè)置的可視item數(shù)的一半(舍去小數(shù)部分)就是最中間的item的序號了,也是當(dāng)前選擇項的序號。
/** * 當(dāng)前選中項發(fā)生變化的監(jiān)聽者 */ private onSelectionChangeLisenter selectionChangeLisenter; /** * 設(shè)置選中項的監(jiān)聽者 */ public void setSelectionChangeLisenter(onSelectionChangeLisenter selectionChangeLisenter) { this.selectionChangeLisenter = selectionChangeLisenter; } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //滾動結(jié)束之后開始正?;貪Litem并記錄最中間的item為選中項 (必須設(shè)置可視項,ListView才會改為選擇器模式) if( scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1){ //使離中間最近的item回滾到中點位置 smoothScrollToPosition(getFirstVisiblePosition()); //計算當(dāng)前選中項的序號 int nowPosition = getFirstVisiblePosition() + mVisibleItemCount/2; //把當(dāng)前選中項的序號存起來并通過listener回調(diào)出去 if(selectionChangeLisenter != null && nowPosition != curPosition){ curPosition = nowPosition; selectionChangeLisenter.onSelectionChange(curPosition); } } }
此處我是使用了一個接口去,用以實時把最新的數(shù)據(jù)返回出去
/** * Created by xu on 2017/3/3. */ public interface onSelectionChangeLisenter { void onSelectionChange(int position); }
使用這個滾輪選擇器的方法也非常簡單,除了跟正常的ListView初始化和綁定adapter之外,只需要額外調(diào)用兩個方法就行了
//設(shè)置ListView的可視item數(shù)(必須是奇數(shù)) mLisetview.setVisibleItemCount(3);
//設(shè)置監(jiān)聽者監(jiān)聽選中項的變化 mLisetview.setSelectionChangeLisenter(new onSelectionChangeLisenter() { @Override public void onSelectionChange(final int position) { mHandler.post(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this,"選擇項發(fā)生變化 當(dāng)前選中序號:"+(temp.get(position)),Toast.LENGTH_SHORT).show(); } }); } });
這樣我們就實現(xiàn)滾輪數(shù)字選擇器的效果了
現(xiàn)在貼下整個滾輪選擇器的完整代碼
/** * 模仿滾輪動畫縮放的ListView * Created by xu on 2017/3/3. */ public class XuListView extends ListView implements AbsListView.OnScrollListener { private static final String TAG = "XuListView"; /** * 中點的Y坐標(biāo) */ private float centerY = 0f; /** * 可視的item數(shù) */ private int mVisibleItemCount = -1; /** * 沒調(diào)整之前每個item的高度 */ private float olditemheight = 0; /** * 調(diào)整過后的每個item的高度 */ private float newitemheight = -1; /** * 當(dāng)前選中項發(fā)生變化的監(jiān)聽者 */ private onSelectionChangeLisenter selectionChangeLisenter; /** * 當(dāng)前選中項的序號 */ private int curPosition = -1; public XuListView(Context context, AttributeSet attrs) { super(context, attrs); //設(shè)置一個滾動監(jiān)聽 setOnScrollListener(this); } /** * 設(shè)置選中項的監(jiān)聽者 */ public void setSelectionChangeLisenter(onSelectionChangeLisenter selectionChangeLisenter) { this.selectionChangeLisenter = selectionChangeLisenter; } /** * 設(shè)置ListView的顯示item數(shù) * @param count :必須是奇數(shù) 如果為-1 則表示只是使用動畫效果的普通ListView */ public boolean setVisibleItemCount(int count){ if(count % 2 == 0){ return false; }else{ mVisibleItemCount = count; return true; } } /** * 在這里第一次調(diào)整item高度 */ @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if(mVisibleItemCount != -1){ getNewItemHeight(); reSetItemHeight(); } } /** * 調(diào)整每個可視的item的高度 以及對內(nèi)容進(jìn)行縮放 */ public void reSetItemHeight(){ for(int i = 0; i < getChildCount(); i++){ //獲取item View temp_view = getChildAt(i); //設(shè)置item的高度 ViewGroup.LayoutParams lp = temp_view.getLayoutParams(); lp.height = (int)newitemheight; temp_view.setLayoutParams(lp); //縮放內(nèi)容 我的item的內(nèi)容用一個LinearLayout包了起來 所以直接縮放LinearLayout LinearLayout item_ll_value = (LinearLayout)temp_view.findViewById(R.id.item_ll_value); item_ll_value.setScaleY((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight)); item_ll_value.setScaleX((newitemheight / olditemheight) < 0 ? 0 : (newitemheight / olditemheight)); } } /** * 計算在給定的可視item數(shù)目下 每個item應(yīng)該設(shè)置的高度 */ private void getNewItemHeight(){ //先把舊的item存起來 olditemheight = getChildAt(0).getHeight(); //計算新的高度 newitemheight = getHeight()/mVisibleItemCount; if((getHeight()/mVisibleItemCount) % newitemheight > 0){ //除不盡的情況下把余數(shù)分給各個item,暫時發(fā)現(xiàn)分一次余數(shù)就夠了,如果效果不理想就做個遞歸多分幾次 float remainder = (getHeight()/mVisibleItemCount) % newitemheight; newitemheight = remainder/mVisibleItemCount; } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { //滾動結(jié)束之后開始正常回滾item并記錄最中間的item為選中項 (必須設(shè)置可視項,ListView才會改為選擇器模式) if( scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE && mVisibleItemCount != -1){ //使離中間最近的item回滾到中點位置 smoothScrollToPosition(getFirstVisiblePosition()); //計算當(dāng)前選中項的序號 int nowPosition = getFirstVisiblePosition() + mVisibleItemCount/2; //把當(dāng)前選中項的序號存起來并通過listener回調(diào)出去 if(selectionChangeLisenter != null && nowPosition != curPosition){ curPosition = nowPosition; selectionChangeLisenter.onSelectionChange(curPosition); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { //計算中點 centerY = getHeight()/2; //判斷中點的有效性 if(centerY <= 0){ return; } //開始對當(dāng)前顯示的View進(jìn)行縮放 for(int i = 0; i < visibleItemCount; i++){ //獲取item View temp_view = getChildAt(i); //計算item的中點Y坐標(biāo) float itemY = temp_view.getBottom()-(temp_view.getHeight()/2); //計算離中點的距離 float distance = centerY; if(itemY > centerY){ distance = itemY - centerY; }else{ distance = centerY - itemY; } //根據(jù)距離進(jìn)行縮放 temp_view.setScaleY(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); temp_view.setScaleX(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); //根據(jù)距離改變透明度 temp_view.setAlpha(1.1f - (distance / centerY) < 0 ? 0 : 1.1f - (distance / centerY)); } } }
注釋很詳細(xì) 相信小白看了也沒什么難度。
滾輪效果的實現(xiàn)方式有很多,解決頭尾兩個item無法滾動到中間的方法也很多,我說的方法僅供參考,沒有最好的方法,只有最符合自己的需求的方法。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Notification與NotificationManager詳細(xì)介紹
在Android系統(tǒng)中,發(fā)一個狀態(tài)欄通知還是很方便的。下面我們就來看一下,怎么發(fā)送狀態(tài)欄通知,狀態(tài)欄通知又有哪些參數(shù)可以設(shè)置2012-11-11Android手機開發(fā) 使用線性布局和相對布局實現(xiàn)Button垂直水平居中
本文主要結(jié)合自己的理解分別對使用LinearLayout和RelativeLayout兩種方式實現(xiàn)居中做了總結(jié),希望對大家有所幫助。2016-05-05Android事件分發(fā)機制(上) ViewGroup的事件分發(fā)
這篇文章主要為大家詳細(xì)介紹了Android ViewGroup的事件分發(fā)機制上篇,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01