Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> ListView使用總結

ListView使用總結

編輯:關於Android編程

雖然隨著RecyclerView的不斷普及,相應的資源也越來越多,許多的項目都在使用RecyclerView,但作為他的前輩ListView,加深對ListView的使用有助於我們更好的適應到RecyclerView的使用中。

首先看一下我們實現的效果一些簡單效果

這裡寫圖片描述

這只是前面的一些簡單效果,後面會有一些進階的效果,希望能耐心的看下去。

ListView的優化

ListView的優化主要包括兩個方面,分別是對自身的優化以及其適配器(Adapter)的優化。

ListView自身的優化

主要包括一條。對於ListViewlayout_heightlayout_width設置為match_parent,如果設置為match_parent,一般ListView的寬高會測量三次以上。具體的源碼沒有深入研究。但為什麼會要測量多次,如果對於自定義View稍微有點基礎的會知道,對於View的測量大小有三個類型:
- UNSPECIFIED:未指定的,父類不對子類施加任何限制。
- EXACTLY:確定的,父類確定其子類控件的大小。
- AT_MOST:最大值,需要子類去測量自身大小確定。

如果我們設置寬高為wrap_content,即AT_MOST,表示其寬高有控件本身去測量確定,而如果是match_parent,則EXACTLY,表示確定的大小。

Adapter優化。

對於Adapter的優化,主要包括以下幾個步驟:

復用convertView,減少子布局的生成。

定義ViewHolder,減少findViewById()的次數。

下面看一下代碼


/**
 * 最基礎的adapter
 * Created by Alex_MaHao on 2016/5/17.
 */

public class SimpleBaseAdapter extends BaseAdapter {

    private List datas;


    public SimpleBaseAdapter(List datas) {
        this.datas = datas;
    }

    @Override
    public int getCount() {
        return datas==null?0:datas.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        ViewHolder vHolder;

        if(convertView==null){
            //初始化item布局
            convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_sample,parent,false);

            //創建記事本類
            vHolder = new ViewHolder();

            //查找控件,保存控件的引用
            vHolder.tv = ((TextView) convertView.findViewById(R.id.listview_sample_tv));

            //將當前viewHolder與converView綁定
            convertView.setTag(vHolder);
        }else{
            //如果不為空,獲取
            vHolder = (ViewHolder) convertView.getTag();
        }
        vHolder.tv.setText(datas.get(position));

        return convertView;
    }


    /**
     * 筆記本類,保存對象的引用
     */
    class ViewHolder{
        TextView tv;
    }
}

根據代碼分析思路:
- ListView中的item滑出屏幕時,滑出的item會在getView()方法中返回,及converView參數。所以在這裡判斷,是否為null,如果不為null,表示我們可以對該布局重新設置數據並返回到列表中顯示。
- ViewHolder,保存item中子控件的引用。因為我們復用了converView,那麼對於同一個converView布局,其子控件的引用應該是不變的。所以我們可以獲取到其上的所有子控件的引用並通過ViewHolder進行保存。
- setTag(),getTag():該方法實現了ViewHolderconverView的綁定,就類似於通過setTag()方法,將ViewHolder打包成一個包裹,放在了converView上,再通過getTag()方法,將這個包裹取出。

在很多優化中,會將ViewHolder定義為static。但在這裡我並沒加上。因為經過測試,加或不加,ViewHolder的創建次數不變。網上查了很多資料,也沒有找到一個讓我信服的理由。唯一有點理的就是基於java的特性。靜態內部類的對象不依賴於其所在的外部類對象。

Adapter的封裝–BaseAppAdapter

上一節說了Adapter的優化,但如果我們每次寫都要寫這麼多的優化代碼,這不符合程序員懶惰的天性。那我們只能把他封裝,提取出一個公共的基類,在基類中,我們把布局加載,優化等都默認實現,只需讓子類構造布局文件,以及綁定數據。

那麼,從我們上一節的代碼看,有以下模塊都可以提取為基類:

數據集合datas:對於數據集合,我們通常都是一個List集合,在這裡定義泛型來表述其所包含的內容。 數據優化:布局的復用以及ViewHolderconverView的綁定。 ViewHolder:定義一個ViewHolder,通過map保存控件與id;

子類所需實現的:

數據的初始化 確定item的布局文件 將數據與視圖綁定。

那麼直接看一下我們繼承好的代碼:

public abstract  class BaseAppAdapter extends BaseAdapter {

    /**
     * 泛型,保存數據
     */
    protected List datas;

    /**
     * 構造方法,子類必須實現其構造方法,並初始化數據
     * @param datas
     */
    public BaseAppAdapter(List datas) {
        this.datas = datas;
    }

    @Override
    public int getCount() {
        return datas==null?0:datas.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        BaseViewHolder vHolder;

        if(convertView==null){
            convertView = LayoutInflater.from(parent.getContext()).inflate(getItemLayoutId(),parent,false);

            vHolder = new BaseViewHolder(convertView);

            convertView.setTag(vHolder);
        }else{
            vHolder = (BaseViewHolder) convertView.getTag();
        }

        /**
         * 數據綁定的回調
         */
        bindData(vHolder,datas.get(position));

        return convertView;
    }

    /**
     * 子類實現,獲取item布局文件的id
     * @return
     */
    protected  abstract int getItemLayoutId();

    /**
     * 子類實現,綁定數據
     * @param vHolder  對應position的ViewHolder
     * @param data 對應的數據綁定
     */
    protected abstract void bindData(BaseViewHolder vHolder,T data);


    /**
     * ViewHolder類
     */
    class BaseViewHolder{

        /**
         * 保存view,以id-view的形式
         */
        Map mapView;
        View rootView;

        public BaseViewHolder(View rootView){
            this.rootView = rootView;
            mapView = new HashMap();
        }


        /**
         * 通過id查找控件
         * @param id
         * @return
         */
        public View getView(Integer id){
            View view = mapView.get(id);
            if(view==null){
                view = rootView.findViewById(id);
                mapView.put(id,view);
            }

            return view;
        }

    }

}

在以上代碼中有幾個關鍵點:

有參的構造方法:子類實現時,必須調用父類的構造方法,用以保存數據。 getItemLayoutId():item的布局文件id。抽象方法,子類必須實現。對於每一個BaseAppAdapter的子類,都需要通過該方法返回他們所特有的item的id。 bindData(BaseViewHolder vHolder,T data):數據綁定方法,子類必須實現。子類在此方法中將數據設置到試圖上 BaseViewHolder中的getView(Integer id):該方法設計比較巧妙,因為,對於基類我們不知道有哪些id需要查詢,如果直接通過findViewById()方法,則並沒有減少查詢次數。在這裡,通過保存一個map對象,通過id先從map中取,如果不存在,說明是第一次獲取,則使用findViewById查找控件,並將控件以鍵值對的形式保存到map中,則下次查找,會從map中取,減少了findViewById的次數。可能有人會認為,添加了一個map對象,內存占用不是增大了嗎,但是,第一,我們map中保存的都是引用。第二,findViewById()是按照深度優先遍歷查詢的,如果不懂,遍歷肯定能明白吧。

ListView 分割線的高度和顏色設置。

簡單的兩個屬性

android:divider:設置顏色,背景 android:dividerHeight:設置高度

如果設置無分割線,可以設置android:divider="@null"

該兩個屬性必須同時使用,如果只設置divider,則沒有效果,同時默認的分割線也會消失。

當然,我們也可以在item布局文件中添加分割線(在底部添加一個線),麻煩點而已。

隱藏ListView 的滾動條

android:scrollbars="none"

取消ListView的點擊反饋效果

對於ListView,在android5.0一下是一個變色,在android5.0以上是一個波紋動畫。我們可以通過一些設置取消掉他的反饋效果。使用如下代碼

android:listSelector="#0000":點擊顏色設置為透明

設置空試圖

ListView作為列表顯示的控件,那麼肯定會存在數據為空的情況,如果我們就直接顯示一面白無疑是不友好的,ListView可以通過設置一個視圖,當列表為null,顯示該視圖。

關鍵方法為setEmpty(View)。具體使用方法如下:

xml文件中,在ListView下設置一個圖片,如下

 

    

在java代碼中設置如下

        //設置空視圖,調用此方法會默認隱藏Empty_view
        mLv.setEmptyView(findViewById(R.id.listview_empty_view));

注意:我們無需設置ImageViewvisible屬性,因為通過setEmpty()方法,已將其顯示與隱藏於ListView所綁定,由ListView來管理。

ListView的滾動

對於顯示數據,我們可能會有一些特殊的要求,比如初始顯示到第幾列,或者要調到第幾條顯示。對於此種要求,ListView已經實現了相應的方法讓我調用。

public void setSelection(int position):position為需要顯示在第一條的數據。該方法為瞬間滾動,無滑動效果,類似圖片上瞬間滑動按鈕實現的效果。 public void smoothScrollToPosition(int position):與上面方法作用相同,不過其有過度效果,類似平滑滾動實現的效果。

ListView 下拉頂部空白回彈效果

ListView中,有如下方法,他控制了回彈設置的值:

 protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)

maxOverScrollY參數便是設置我們可以下拉的空白區域的高度。具體實現如下

public class MyListView extends ListView {

    /**
     * 下拉回彈效果,下拉的最大距離
     */
    private int mMaxOverDistance;

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //初始化最大距離
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        float density = metrics.density;
        mMaxOverDistance = (int) (100*density);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {

        //注意第九個參數,設置為了我們自定義的值
        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mMaxOverDistance, isTouchEvent);
    }
}

該方法是保護類型的,所以我們需要通過繼承ListView來修改此方法的實現。

在看到此方法時,第一反應是ScrollView有沒有該方法,查了以後,發現ScrollView也有該方法。那麼,同理,ScrollView也能通過重寫overScrollBy實現下拉回彈的效果。

ListView 進階–實現跟隨滑動消失和顯現的ToolBar

首先看一下效果圖:

這裡寫圖片描述

分析其邏輯:

當我們向上滑動列表時,toolbar慢慢消失到頂部。 當我們向下滑動列表時,toolbar慢慢的從頂部顯現。

實現邏輯:
- setOnTouchListener,設置觸摸監聽。
- 對當前手指移動事件進行判斷。
- 如果是向下移動,且ToolBar是隱藏的,則給toolbar設置一個向下顯現的動畫。
- 如果是向上移動,且toolbar是顯現的,則給toolbar設置一個向上隱藏的動畫。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPr+00rvPwsq1z9a0+sLro7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> /** * Created by Alex_MaHao on 2016/5/17. */ public class ToorBarListViewActivity extends AppCompatActivity implements View.OnTouchListener { private Toolbar mToolBar; private ListView mLv; private SimpleBaseAdapter mAdapter; private List datas = new ArrayList<>(); //系統默認的滑動最小偏移量 private int mTouchSlop; //手指初次觸摸時的Y坐標 private float mFirstY; //當前手指觸摸的Y坐標 private float mCurrentY; //手指移動的方向,0代表向下滑動,1代表向上滑動 private int direction; //toobar的顯示狀態 private boolean isShow = true; //toobar顯示和隱藏的動畫 private Animator mAnimator; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_listview_toobar); // 把toolbar替代ActionBar mToolBar = ((Toolbar) findViewById(R.id.toolbar)); setSupportActionBar(mToolBar); mLv = ((ListView) findViewById(R.id.listview_toolbar_lv)); initDatas(); mAdapter = new SimpleBaseAdapter(datas); mLv.setAdapter(mAdapter); /** * 添加一個頭部View,不然使用RelativeLayout,會被遮擋 */ addHeadView(); //最小移動距離,判斷是否滑動 mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); //設置觸摸監聽 mLv.setOnTouchListener(this); } /** * 添加頭布局,避免被遮擋 */ private void addHeadView() { //創建一個與ToolBar等高的空白view,添加到ListView的頭布局中 View head = new View(this); TypedArray actionbarSizeTypedArray = getApplicationContext().obtainStyledAttributes(new int[] { android.R.attr.actionBarSize }); head.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, (int) actionbarSizeTypedArray.getDimension(0, 0))); mLv.addHeaderView(head); } private void initDatas() { for(int i = 0;i<20;i++){ datas.add("添加數據~~"+i); } } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()){ case MotionEvent.ACTION_DOWN: //記錄初次觸摸的Y坐標 mFirstY = event.getY(); break; case MotionEvent.ACTION_MOVE: mCurrentY = event.getY(); if(mCurrentY-mFirstY>mTouchSlop){ //向下滑動 direction = 0; }else if(mFirstY-mCurrentY>mTouchSlop){ //向上滑動 direction = 1; } if(direction==1){ //向上滑動,且toolbar在顯示狀態,則隱藏 if(isShow){ toolbarAnim(false); isShow = !isShow; } }else if(direction==0){ //向下滑動,且toolbar在隱藏狀態,則顯示 if(!isShow){ toolbarAnim(true); isShow = !isShow; } } break; } return false; } public void toolbarAnim(boolean isShow){ if(mAnimator!=null&&mAnimator.isRunning()){ mAnimator.cancel(); } if(isShow){ //顯現toobar mAnimator = ObjectAnimator.ofFloat(mToolBar,"translationY",mToolBar.getTranslationY(),0); }else{ //隱藏toobar mAnimator = ObjectAnimator.ofFloat(mToolBar,"translationY",mToolBar.getTranslationY(),-mToolBar.getHeight()); } //啟動動畫 mAnimator.start(); } }

代碼注釋非常詳細,需要注意的有以下三點:

因為我們的父布局是RelativeLayout,如果不給ListView設置一個headView,則在最頂部時,ListView頂部數據會被隱藏。當然,可能會想到為什麼不用垂直布局,把ListView放在toolbar的下面呢,這樣會導致另一個問題,即當toobar消失時,會導致頂部有一個空白效果。如下圖所示:
這裡寫圖片描述

監聽兩個條件,滑動方向和toolbar當前顯示的狀態,如果不監聽當前toobar狀態,會導致動畫重復調用。

會存在一種情況,及toolbar處在某一個動畫的時候,突然該表方向,我們需要先停止當前動畫,並以當前偏移量上為基礎,創建新的動畫。這也是創建動畫時使用mToolBar.getTranslationY()作為參數的原因。

ListView 進階–聊天列表的實現

在顯示數據中,可能會有這種需求,顯示的列表的布局不同,例如聊天界面,根據不同的聊天人,顯示的位置不同,及加載不同的itemView那麼我們如何實現呢。先看一下實現效果:

這裡寫圖片描述

請忽略他的丑陋,目的是為了實現效果,下面直接上源碼,看一下實現過程,最後在總結實現的關鍵點:

聊天的實體類,在聊天實體類中,我們必須通過類型區分該聊天內容是自己還是朋友。

/**
 * 聊天的實體類
 * Created by Alex_MaHao on 2016/5/18.
 */
public class ChatBean {

    /**
     * 聊天的兩種類型,自己和朋友
     */
    public static final int CHAT_MYSELF = 0;
    public static final int CHAT_FIRENDS = 1;

    /**
     * 聊天的頭像
     */
    private Drawable userIcon;

    /**
     * 發送的消息
     */
    private String message;

    /**
     * 確定是否為當前聊天者
     */
    private int type;


    public ChatBean() {
    }

    public ChatBean(String message, int type, Drawable userIcon) {
        this.message = message;
        this.type = type;
        this.userIcon = userIcon;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public Drawable getUserIcon() {
        return userIcon;
    }

    public void setUserIcon(Drawable userIcon) {
        this.userIcon = userIcon;
    }
}

ListView所在的布局文件activity_listview_chat.xml



    

因為聊天條目沒有點擊反饋效果,所以通過android:listSelector設置無反饋效果。使用android:divider="#0000"android:dividerHeight="10dp"設置每一個聊天條目的間隔。

我們需要加載不同的布局,所以我們有兩個item布局文件,用來顯示自己的消息和朋友的消息:

item_listview_chat_left,顯示在左邊,朋友消息顯示布局



    

    



item_listview_chat_right,顯示在右邊,自己消息的顯示布局




    

    




關鍵點來了,構造ChatAdapter,用以顯示數據。

/**
 * 聊天的Adapter,實現加載不同的布局
 * Created by Alex_MaHao on 2016/5/18.
 */
public class ChatAdapter extends BaseAdapter {

    List chatBeens ;

    public ChatAdapter(List chatBeens) {
        this.chatBeens = chatBeens;
    }

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

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public int getItemViewType(int position) {
        //返回值需要從0開始,對應數據所要加載的對應布局標識
        return chatBeens.get(position).getType();
    }

    @Override
    public int getViewTypeCount() {
        //不同布局的種類數量
        return 2;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        //獲取當前數據的類型
        int type = getItemViewType(position);

        ViewHolder vHolder = null;
        if(convertView==null){
            switch (type){
                case ChatBean.CHAT_MYSELF:
                    //加載自己消息顯示的布局
                    convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_chat_right,parent,false);
                    vHolder = new ViewHolder();
                    vHolder.icon = ((ImageView) convertView.findViewById(R.id.chat_right_icon));
                    vHolder.msg = ((TextView) convertView.findViewById(R.id.chat_right_msg));
                    break;
                case ChatBean.CHAT_FIRENDS:
                    //加載朋友消息顯示的布局
                    convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview_chat_left,parent,false);
                    vHolder = new ViewHolder();
                    vHolder.icon = ((ImageView) convertView.findViewById(R.id.chat_left_icon));
                    vHolder.msg = ((TextView) convertView.findViewById(R.id.chat_left_msg));
                    break;
            }
            //控件應用與控件綁定
            convertView.setTag(vHolder);
        }else{
            //獲取控件引用
            vHolder = (ViewHolder) convertView.getTag();
        }

        //設置數值
        vHolder.icon.setImageDrawable(chatBeens.get(position).getUserIcon());
        vHolder.msg.setText(chatBeens.get(position).getMessage());

        return convertView;
    }


    class ViewHolder{
        ImageView icon;
        TextView msg;
    }
}

adapter中有兩個關鍵的方法:

getViewTypeCount():返回布局的種類數,比如當前我們分為自己的消息和朋友的消息兩個布局,那麼就返回2.

-getItemViewType():返回對應條目下,item布局的類型。ListView通過該方法,實現在getView()中返回相同類型的converView復用。注意,該返回類型要從0開始,不然會數組越界。

因為在這裡,比較巧合,我們的item布局控件都一樣,只不過顯示不一樣,所以只定義了一個ViewHolder類。

關鍵的實現了,那麼看一下activity中的代碼吧

public class ChatListViewActivity extends AppCompatActivity {

    private ListView mChatLv;

    private List chatBeanList = new ArrayList<>();

    private ChatAdapter adapter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_listview_chat);

        mChatLv = ((ListView) findViewById(R.id.listview_chat_lv));

        initChatBeanList();

        adapter = new ChatAdapter(chatBeanList);

        mChatLv.setAdapter(adapter);


        mChatLv.postDelayed(new Runnable() {
            @Override
            public void run() {
                //設置我們ListView顯示在底部
                mChatLv.setSelection(adapter.getCount());
            }
        },200);
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

    /**
     * 初始化聊天內容
     */
    private void initChatBeanList() {
        for (int i = 0;i<10;i++){
            ChatBean chat = new ChatBean();
            if(i%2==0){
                chat.setType(ChatBean.CHAT_MYSELF);
                chat.setUserIcon(getApplicationContext().getResources().getDrawable(R.mipmap.qq));
                chat.setMessage("微信,微信,我是QQ");
            }else{
                chat.setType(ChatBean.CHAT_FIRENDS);
                chat.setUserIcon(getApplicationContext().getResources().getDrawable(R.mipmap.weixin));
                chat.setMessage("QQ,QQ,我是微信");
            }

            chatBeanList.add(chat);
        }
    }
}

實現該布局的兩個關鍵方法(adapter中的方法)
- getViewTypeCount():返回布局的種類數,比如當前我們分為自己的消息和朋友的消息兩個布局,那麼就返回2.
- getItemViewType():返回對應條目下,item布局的類型。ListView通過該方法,實現在getView()中返回相同類型的converView復用。注意,該返回類型要從0開始,不然會數組越界。

ListView 的上拉加載和下拉刷新

決定使用多種方式實現下拉刷新和上拉加載,會在下一篇博客中實現。絕對滿滿的干貨。

該項目源碼已經共享到我的github,在模塊systemwidgetdemo中。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved