Android ListView

Android ListView

專門用于處理那種內容元素很多,手機屏幕無法展示出所有內容的情況。ListView可以使用列表的形式來展示內容,超出屏幕部分的內容只需要通過手指滑動就可以移動到屏幕內了。

ListView屬性

設置分割線

  • android:divider,設置分割線風格,可以是顏色,也可以是圖片。當不需要分割線時,賦值@null即可。
  • android:dividerHeight,設置分割線高度。

滾動條設置

android:scrollbars,通過該屬性可以設置滾動條狀態。不需要滾動條時,賦值none。

取消Item點擊效果

android:listSelector,賦值為#00000000(color ARGB),或者@android:color/transparent。

設置ListView Item顯示位置

默認顯示第一個,調用
ListView.setSelection(pos)從指定item開始顯示。

但是此方法是瞬間完成滾動操作,可以使用以下三種方法實現平滑滾動:

  • smoothScrollBy(distance, duration),指定滾動距離,distance的正,負決定滾動的方向(正值向上,負值向下)。滾動速度有duration決定,即滾動時間。
  • smoothScrollByOffset(offset),方法參數是指現在顯示的第一個item視圖的偏移量,負號代表向上移動,正號代表向下移動.
  • smoothScrollToPosition(pos),平滑移動到指定的position item.

ListView基礎用法

動態修改ListView

在某些情況下,ListView的數據更新了,那么就需要動態的修改ListView item的顯示??梢哉{用Adapter.notifyDataSetChanged()方法。代碼如下:

mList.add("new");
mAdapter.notifyDataSetChanged();

mList是BaseAdapter的數據。更新它之后調用notifyDataSetChanged()更新視圖。注意調用notifyDataSetChanged()之后是更新了整個ListView的所有item視圖,也就是重新繪制了ListView,所以效率有點差,可以試試單獨更新某個item。

通過add添加新數據.gif

遍歷item

當需要遍歷ListView的item時,可以使用getFirstVisiblePosition(),getChildAt(),getChildCount()等方法。不過需要注意:

  • getFirstVisiblePosition(),返回的是當前屏幕上顯示的第一個item(包括不完整的)在所有item中的位置index。
  • getChildCount(),返回當前屏幕顯示的item數量。
  • getChildAt(),返回指定位置的item視圖。指定位置的范圍不能超過當前屏幕顯示的數量,因為返回的視圖是在當前顯示的item中的。

通過以上三種方法,更進一步了解了視圖緩存機制。ListView并沒有給所有數據項創建item視圖,只給需要顯示的創建。

item.getTop()

當通過item視圖調用getTop()時,返回的坐標是以item左上角點在以ListView左上角為原點構建的坐標系的Y坐標。

空ListView

當列表沒有數據時,可以設置一個提示用戶的View。通過ListView.setEmptyView()來設置。

protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_empty_list_view);
        initView();
    }

    private void initView() {
        mListView = (ListView) findViewById(R.id.id_list_view_empty);
        mEmptyImg = (ImageView) findViewById(R.id.img_empty_view);
        mListView.setEmptyView(mEmptyImg);
        mList = new ArrayList<>();
        mAdapter = new ViewHolderAdapter(mList, this);

        mListView.setAdapter(mAdapter);
    }

    public void btnAddItem(View view) {
        mList.add("new");
        mAdapter.notifyDataSetChanged();
        mListView.smoothScrollToPosition(mList.size() - 1);
    }

布局代碼

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <ListView
        android:id="@+id/id_list_view_empty"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>

    <ImageView
        android:id="@+id/img_empty_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:src="@mipmap/ic_launcher"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="btnAddItem"
        android:text="@string/add_item"/>
</LinearLayout>

效果:

空ListView顯示圖像,通過add添加新數據.gif

滑動監聽

OnTouchListener

mListView.setOnTouchListener(new View.OnTouchListener() {
            int position = 0;
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //觸摸時操作
                        break;
                    case MotionEvent.ACTION_MOVE:
                        //移動時操作
                        break;
                    case MotionEvent.ACTION_UP:
                        //手指離開時操作
                        //用getTop()方法獲取到的坐標是相對于父控件坐標系的坐標.
                        position = mListView.getChildAt(0).getTop();
                        mView.setText("顯示的第一個Item在ListView中的坐標:" + position);
                        break;
                }
                return false;
            }
        });
TouchListener,底部TextView顯示第一個item Y軸坐標.gif

OnScrollListener

mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            int pos = 0;
            int lastVisibleItemPos = 0;
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                switch(scrollState) {
                    case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:
                        //滑動停止時
                        pos = mListView.getChildAt(0).getTop();
                        mTextView.setText("顯示的第一個Item在ListView中的坐標:" + pos);
                        break;
                    case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                        //正在滾動時
                        mTextView.setText("正在滑動...");
                        break;
                    case AbsListView.OnScrollListener.SCROLL_STATE_FLING:
                        //手指拋動時,手指用力滑動后,手指離開屏幕ListView由于慣性繼續滑動
                        mTextView.setText("漂移中...");
                        break;
                    default:
                        break;
                }
            }
            //滾動時一直調用
            //firstVisibleItem當前顯示的第一個item在所有item中的index
            //visibleItemCount當前顯示的item數量
            //totalItemCount所有item 數量
            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                //判斷滑動方向
                if(firstVisibleItem > lastVisibleItemPos) {
                    //判斷是否滾動到最后一項
                    if(firstVisibleItem + visibleItemCount == totalItemCount
                            && totalItemCount > 0) {
                        Log.d(TAG, "滾動到了最后一個item");
                    }
                    Log.d(TAG, "上滑");
                }else if(firstVisibleItem < lastVisibleItemPos) {
                    Log.d(TAG, "下滑");
                }
                lastVisibleItemPos = firstVisibleItem;
            }
        });
OnSrollListener,底部TextViw根據不同滑動狀態顯示不同的內容,停止滑動是顯示第一個item Y軸坐標.gif

ListView點擊事件

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//                lastScrollY = mListView.getScrollY();
//                Log.d(TAG, lastScrollY + "");
                lastScrollY = getScrollY();
                Log.d(TAG, lastScrollY + "");
                mTextView.setText(String.valueOf(lastScrollY));
                Intent intent = new Intent(RestoreListView.this, JumpActivity.class);
                startActivity(intent);
                mListView.setSelection(0);
            }
        });

ListView基礎優化(ViewHolder)

優化的原理是基于ListView的RecycleBin機制

可以參考Android ListView工作原理完全解析,帶你從源碼的角度徹底理解

ViewHolder的優化就是利用了ListView 的視圖緩沖機制,一般情況下代碼都差不多,關鍵在于BaseAdapter。所以可以參照以下模版來寫。

BaseAdapter代碼

public class ViewHolderAdapter extends BaseAdapter {
    private List<String> mData;
    private LayoutInflater mInflater;

    public ViewHolderAdapter(List<String> data, Context context) {
        mData = data;
        mInflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public Object getItem(int position) {
        return mData.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) {
            holder = new ViewHolder();
            //通過LayoutInflater實例化布局
            convertView = mInflater.inflate(R.layout.viewholder_item, null);
            holder.img = (ImageView) convertView.findViewById(R.id.img_view_holder);
            holder.title = (TextView) convertView.findViewById(R.id.tv_view_holder);
            convertView.setTag(holder);
        } else {
            //通過Tag找到緩存布局
            holder = (ViewHolder) convertView.getTag();
        }
        //設置布局中空間要顯示的視圖
        holder.img.setBackgroundResource(R.mipmap.ic_launcher);
        holder.title.setText(mData.get(position));
        return convertView;
    }

    public class ViewHolder {
        public ImageView img;
        public TextView title;
    }
}

代碼分析,在上面的模版中看到覆寫了幾個方法:

  • getCount(),返回item數量
  • getItem(int position),返回指定位置的item
  • getItemId(int position),返回指定位置item的行號
  • getView(),返回顯示的item view

整個優化的關鍵在于ViewHolder模式充分利用了ListView的視圖緩存機制,避免每一次調用getView()時都去實例化item布局,并且調用findViewById()實例化控件。

ListView進階用法

ListView位置恢復

有兩種方法,第一種是在監聽ListView時,通過ListView.getScrollY()方法獲取最終滾動的Y坐標,然后調用smoothScrollBy()方法恢復;第二種是通過記錄當前ListView顯示的第一個Item的index,調用smoothToPosition()來恢復。第一中方法完全不可行,因為得到的永遠都是0(通過ListView調用getScrollY()方法,得到的坐標是ListView左上角在以ListView父視圖左上角為原點構建的坐標系中的Y軸坐標,所以一直是0)。第二種方法不精確。

下面采用以下方法:

public int getScrollY() {
        View child = mListView.getChildAt(0);
        if(child == null) {
            return 0;
        }
        int top = -child.getTop();
        int firstVisibleItemPos = mListView.getFirstVisiblePosition();
        return top + firstVisibleItemPos * child.getHeight();
    }

當然這是一種理想狀態,默認為所有item的高度都相等。

恢復ListView

mListView.post(new Runnable() {
            @Override
            public void run() {
                mListView.smoothScrollBy(scrolledY, 0);
            }
        });
ListView位置恢復.gif

動態改變ListView布局

有兩種方案:一種是兩種布局都寫,通過控制布局的顯示隱藏來達到切換布局的效果;另一種是在getView()時通過判斷來選擇加載不同的布局。以下主要以第二種方案來實現。

關鍵思想,要獲取不同的布局肯定要覆寫getView()方法。而ListView提供了兩種方法,封裝好了布局的判斷:

  • getItemViewType(),獲取指定位置item的布局類型。
  • getViewTypeCount(),獲取不同布局的總數。

關鍵代碼

@Override
    public int getItemViewType(int position) {
        ChatItemListViewBean bean = mData.get(position);

        return bean.getType();
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if(convertView == null) {
            holder = new ViewHolder();
            if(getItemViewType(position) == 0) {
                convertView = mInflater.inflate(R.layout.chat_item_in, null);
                holder.mIcon = (ImageView) convertView.findViewById(R.id.id_img_chat_item_in);
                holder.mTv = (TextView) convertView.findViewById(R.id.id_tv_chat_item_in);
            }else {
                convertView = mInflater.inflate(R.layout.chat_item_out, null);
                holder.mIcon = (ImageView) convertView.findViewById(R.id.id_img_chat_item_out);
                holder.mTv = (TextView) convertView.findViewById(R.id.id_tv_chat_item_out);
            }
            convertView.setTag(holder);
        }else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.mIcon.setImageBitmap(mData.get(position).getIcon());
        holder.mTv.setText(mData.get(position).getText());
        return convertView;
    }

    public class ViewHolder{
        public ImageView mIcon;
        public TextView mTv;
    }

效果:

聊天.png

雜技

獲取ActionBar高度

getResources().getDimensionPixelOffset(
                    android.support.v7.appcompat.R.dimen.abc_action_bar_default_height_material
            )

android.support.v7.appcompat.R.dimen.abc_action_bar_default_height_material是V7包內的actionBar height 的資源id。

獲取最小滑動距離

mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();

問題

  • 如何實現單獨更新某個指定的item數據
  • View.post()
  • 如何實現彈性ListView
  • 如何自動隱藏和顯示Toolbar
  • View.getTranslationY()獲取到的是什么坐標
  • ListView的適配器,BaseAdapter,ArrayAdapter...

參考

判斷ListView的第一個item是否完全顯示

記錄和恢復ListView的滑動位置

對于getScrollX() 的理解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容