1. 程式人生 > >Android列表用法之二:實戰ListView高階用法

Android列表用法之二:實戰ListView高階用法

在我們的專案中,並不是所有列表都是簡單的使用。類似於新聞列表、QQ聊天列表等,具有圖文並排的列表,每個item都有它不同的佈局型別,都有其不同的實現方式。這類複雜的列表表現形式,在各類知名應用當中,不可或缺的存在著。今天我們就來看看,使用ListView能不能實現此類複雜佈局。

今天的例項為眾所周知的QQ聊天介面:
這裡寫圖片描述

一、縱觀全域性
1、一個ListView。
2、圓形頭像和訊息框。
3、時間分隔行。

二、技術選型
1、列表當然選擇ListView,我們今天使用的就是這個。
2、圓形頭像,目前三方開源的比較多,有CircleImageView、AvatarImageView等可以選擇,今天我就選擇AvatarImageView。為什麼要選擇這個,它是我常用的控制元件之一,只是最近使用它的頻率比較高,就選擇它了。後續我會有專門貼子說明圓形頭像控制元件的選擇。
3、文字框+layout組合成訊息框。文字框顯示訊息內容,layout顯示訊息背景泡泡。
4、item的實現有幾種方法:
1)寫一個佈局xml檔案。裡面include三個xml佈局,一是自己傳送訊息,訊息顯示在右邊的佈局。二是對方發訊息,訊息顯示在介面左邊的佈局。三是時間佈局,顯示在中間,並起到分隔作用。然後根據不同的訊息型別,控制佈局的顯示與隱藏。
2)根據資料時間變化,以及不同訊息的型別。直接構造不同的佈局檔案。這樣就比較輕量級。不會在同一個佈局當中,存在許多無用的佈局。
3)在此我選擇第一種方式,因為ListView有所限制,使用第二種方式不是特別方便。第二種方式我會使用後續的RecyclerView來講解。

三、碼程式碼
1、在上一篇listview簡單用法當中,我們使用的viewholder,是整合在adapter當中進行優化的。另外,在這裡我引入另外一個解耦合比較高的ViewHolder的寫法。
另外新建一個專門的ViewHolder類,在裡面實現靜態的getViewHolder方法,比較簡單,只是將內容移了個地方而已,直接看程式碼:

package oliver.zhantao.oliverproject.listviewhigh;

import android.view.View;
import android.view.ViewStub;
import android.widget.TextView;

import
cn.carbs.android.avatarimageview.library.AvatarImageView; import oliver.zhantao.oliverproject.R; /** * ViewHolder的抽離,單獨形成一個類,可以讓程式碼更加明瞭 * 另外,也可以使adapter更加輕量級,降低耦合性。 * <p> * 將ViewHolder的實現方法完全封裝在其本身的類中,功能更強大 * 其方法與Adapter的getView()方法隔離,條理更加清晰,降低耦合性 * 當getView()中需要實現多種樣式時,不需要寫重複程式碼 * <p> * Created by ZhanTao on 2017/4/19. */
public class HighViewHolder { public View mineView; public View otherView; public View timeView; public AvatarImageView mineView_head; public AvatarImageView otherView_head; public TextView mineView_message; public TextView otherView_message; public TextView timeView_time; // 建構函式中就初始化View public HighViewHolder(View convertView) { mineView = convertView.findViewById(R.id.view_mine); otherView = convertView.findViewById(R.id.view_other); timeView = convertView.findViewById(R.id.view_time); mineView_head = (AvatarImageView) mineView.findViewById(R.id.aiv_head); otherView_head = (AvatarImageView) otherView.findViewById(R.id.aiv_head); mineView_message = (TextView) mineView.findViewById(R.id.tv_message); otherView_message = (TextView) otherView.findViewById(R.id.tv_message); timeView_time = (TextView) timeView.findViewById(R.id.tv_time); } // 得到一個ViewHolder public static HighViewHolder getViewHolder(View convertView) { HighViewHolder viewHolder = (HighViewHolder) convertView.getTag(); if (viewHolder == null) { viewHolder = new HighViewHolder(convertView); convertView.setTag(viewHolder); } return viewHolder; } }

2、再來看我們的adapter,在這個adapter當中,直接通過靜態方法就可以獲取ViewHolder例項,是不是簡單明瞭多了。注意看getView方法。


package oliver.zhantao.oliverproject.listviewhigh;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

import java.util.List;

import oliver.zhantao.oliverproject.R;

/**
 * 聊天列表介面卡,在這裡將資料與控制元件進行繫結
 * <p>
 * 專案中都是自定義介面卡,很少用到系統提供的簡易介面卡。 所以果斷拋棄系統的簡易介面卡,自定義想咋樣就咋樣,一個字,爽^_^
 * <p>
 * Created by ZhanTao on 2017/4/17.
 */

public class ListViewHighAdapter extends BaseAdapter {
    private Context mContext;
    // 餐廳列表
    private List<MessageBean> messageList;
    private LayoutInflater mInflater;

    public ListViewHighAdapter(Context context, List<MessageBean> messageList) {
        this.mContext = context;
        this.messageList = messageList;
        mInflater = LayoutInflater.from(context);
    }

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

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

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

    @Override
    public int getViewTypeCount() {
        // 已經知道有三種佈局了,直接寫死。當然也可以做成一個可擴充套件的,專案中靈活性更強。
        return 3;
    }

    @Override
    public int getItemViewType(int position) {
        return messageList.get(position).getLayoutType();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        HighViewHolder holder;
        // 1、優化框架搭起
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.activity_listview_high_item, null);
        }
        //這樣使用Holder,簡單明瞭,還不佔用adapter
        holder = HighViewHolder.getViewHolder(convertView);
        // 2、繫結資料到控制元件
        bindViewData(position, convertView, holder);
        // 3、控制VIEW的顯示與隱藏
        controlViewVisible(position, holder);
        // 4、繫結監聽事件
        bindViewClickListener(position, convertView, holder);

        return convertView;
    }

    /**
     * 繫結資料到控制元件,在這裡做統一處理,方便資料抽離。
     *
     * @param position 列表索引
     * @param convertView 每個position對應的itemView
     * @param holder holder物件
     */
    private void bindViewData(int position, View convertView, HighViewHolder holder) {
        switch (getItemViewType(position)) {
            case MessageBean.LAYOUT_TYPE_MINE:
                holder.mineView_head.setImageResource(R.drawable.qq_head_mine);
                holder.mineView_message.setText(messageList.get(position).getMsg());
                break;
            case MessageBean.LAYOUT_TYPE_OTHER:
                holder.otherView_head.setImageResource(R.drawable.qq_head_other);
                holder.otherView_message.setText(messageList.get(position).getMsg());
                break;
            case MessageBean.LAYOUT_TYPE_TIME:
                holder.timeView_time.setText(messageList.get(position).getMsg());
                break;
        }
    }

    /**
     * 控制每個頁面的顯示與隱藏,根據item型別顯示需要的佈局。
     *
     * @param position 列表索引
     * @param holder holder物件
     */
    private void controlViewVisible(int position, HighViewHolder holder) {
        switch (getItemViewType(position)) {
            case MessageBean.LAYOUT_TYPE_MINE:
                holder.mineView.setVisibility(View.VISIBLE);
                holder.otherView.setVisibility(View.GONE);
                holder.timeView.setVisibility(View.GONE);
                break;
            case MessageBean.LAYOUT_TYPE_OTHER:
                holder.mineView.setVisibility(View.GONE);
                holder.otherView.setVisibility(View.VISIBLE);
                holder.timeView.setVisibility(View.GONE);
                break;
            case MessageBean.LAYOUT_TYPE_TIME:
                holder.mineView.setVisibility(View.GONE);
                holder.otherView.setVisibility(View.GONE);
                holder.timeView.setVisibility(View.VISIBLE);
                break;
        }
    }

    /**
     * 繫結控制元件的各種View的事件,在這裡做統一處理,比如點選事件,長安事件。
     *
     * @param position 列表索引
     * @param convertView 每個position對應的itemView
     * @param holder holder物件
     */
    private void bindViewClickListener(int position, View convertView, HighViewHolder holder) {

    }
}

3、上面只是為了展示另外一種ViewHolder的寫法和用法,當然也貼也許多本例當中複雜列表的實現程式碼,這也是為了簡單。以下我們再來分析一下這種複雜佈局的實現。
首先,我們要在Adapter當中重寫如下兩個方法,這兩個方法用於標識我們的列表當中,有幾種不同的佈局。在這裡我們有三種佈局。
其次,這個型別,我們需要在訊息當中儲存起來,當我們在getView當中,繪製佈局的時候,就需要明確的知道,我們使用的是哪種佈局,那麼就需要通過資料當中的layoutType來判斷。

    @Override
    public int getViewTypeCount() {
        // 已經知道有三種佈局了,直接寫死。當然也可以做成一個可擴充套件的,專案中靈活性更強。
        return 3;
    }

    @Override
    public int getItemViewType(int position) {
        return messageList.get(position).getLayoutType();
    }

4、資料實體類

package oliver.zhantao.oliverproject.listviewhigh;

import java.io.Serializable;

/**
 * 訊息資料物件
 *
 * Created by ZhanTao on 2017/4/28.
 */

public class MessageBean implements Serializable {
    //常量,用於標識佈局。
    public static final int LAYOUT_TYPE_MINE = 0;
    public static final int LAYOUT_TYPE_OTHER = 1;
    public static final int LAYOUT_TYPE_TIME = 2;

    //訊息內容
    private String msg;
    //頭像URL
    private String headUrl;

    //訊息傳送時間
    private long time;

    //標識佈局型別
    private int layoutType;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getHeadUrl() {
        return headUrl;
    }

    public void setHeadUrl(String headUrl) {
        this.headUrl = headUrl;
    }

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public int getLayoutType() {
        return layoutType;
    }

    public void setLayoutType(int layoutType) {
        this.layoutType = layoutType;
    }
}

5、下面我們來檢視佈局activity_listview_high_item.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/dp_10"
    android:paddingTop="@dimen/dp_10">

    <include
        android:id="@+id/view_mine"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/activity_listview_high_item_mine" />

    <include
        android:id="@+id/view_other"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/activity_listview_high_item_other" />

    <include
        android:id="@+id/view_time"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/activity_listview_high_item_time" />

</RelativeLayout>

6、自己傳送訊息的佈局。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <cn.carbs.android.avatarimageview.library.AvatarImageView
        android:id="@+id/aiv_head"
        android:layout_width="@dimen/dp_40"
        android:layout_height="@dimen/dp_40"
        android:layout_marginRight="@dimen/dp_16"
        android:layout_alignParentRight="true" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/dp_80"
        android:layout_toLeftOf="@id/aiv_head"
        android:background="@drawable/bg_mine"
        android:paddingTop="@dimen/dp_13"
        android:paddingBottom="@dimen/dp_13"
        android:paddingLeft="@dimen/dp_13"
        android:paddingRight="@dimen/dp_13"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/color_000000"
            android:gravity="right"
            android:text=""
            android:textSize="@dimen/dp_14" />

    </LinearLayout>
</RelativeLayout>

7、對方傳送訊息的佈局。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <cn.carbs.android.avatarimageview.library.AvatarImageView
        android:id="@+id/aiv_head"
        android:layout_width="@dimen/dp_40"
        android:layout_height="@dimen/dp_40"
        android:layout_marginLeft="@dimen/dp_16" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginRight="@dimen/dp_80"
        android:layout_toRightOf="@id/aiv_head"
        android:background="@drawable/bg_other"
        android:paddingTop="@dimen/dp_13"
        android:paddingBottom="@dimen/dp_13"
        android:paddingLeft="@dimen/dp_13"
        android:paddingRight="@dimen/dp_13"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_message"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/color_000000"
            android:text=""
            android:textSize="@dimen/dp_14" />

    </LinearLayout>
</RelativeLayout>

8、時間分隔佈局。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/dp_10"
    android:paddingTop="@dimen/dp_10">

    <TextView
        android:id="@+id/tv_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text=""
        android:textColor="@color/transparent_45"
        android:textSize="@dimen/dp_12" />

</RelativeLayout>

9、以上佈局,具體的實現方式,直接看程式碼, 我就不贅述了,下面貼Activity的程式碼。

package oliver.zhantao.oliverproject.listviewhigh;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

import java.util.ArrayList;
import java.util.List;

import oliver.zhantao.oliverproject.R;
import oliver.zhantao.oliverproject.constants.Constants;
import oliver.zhantao.oliverproject.listview.RestaurantBean;

/**
 * 實戰專案中簡單的列表實現,自定義介面卡。
 * 仿寫百度地圖當中,收藏夾裡面,收藏點的列表實現。
 * 一切脫離實戰的例項,都是耍流氓。
 * <p>
 * Created by ZhanTao on 2017/4/14.
 */

public class ListViewHighActivity extends AppCompatActivity {
    private ListView listViewHigh;
    private ListViewHighAdapter listViewHighAdapter;
    private List<MessageBean> datas = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listview_high);
        datas = new ArrayList<>();
        //1、找到佈局當中的列表控制元件
        listViewHigh = (ListView) findViewById(R.id.lv_resturant);
        //2、建立介面卡物件, 上下文必須傳(非常有用)、資料必須傳
        listViewHighAdapter = new ListViewHighAdapter(ListViewHighActivity.this, datas);
        //3、將介面卡與listview進行繫結
        listViewHigh.setAdapter(listViewHighAdapter);
        //4、構造資料、重新整理列表
        getDatasFromNetwork();
    }

    /**
     * 資料封裝,模擬從網路上獲取到的資料
     *
     * @return 返回資料列表
     */
    private List<MessageBean> getDatasFromNetwork() {
        MessageBean messageBean = new MessageBean();
        messageBean.setMsg("哪個喊她穿高根鞋去追公交啊");
        //這裡可以放網路圖片,在此我就不放了,我直接放了兩張頭像圖片在工程當中
        //adapter裡我就直接使用了。
        messageBean.setHeadUrl("");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("死神到了,逃不脫");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("看來你是不看新聞的");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
        datas.add(messageBean);

        messageBean = new MessageBean();
        //這裡為什麼要加360000,怎麼來的?6分鐘*60秒*1000毫秒=360000
        //我們在這裡模擬超過5分鐘的訊息,要顯示一個時間。要注意有一個佈局是時間。
        messageBean.setTime(System.currentTimeMillis() + 360000);
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_TIME);
        //在這裡處理時間,如果這條訊息時間跟上一條時間相隔5分鐘以上,就顯示一個時間View。
        //我這裡是做得比較簡單,原版時間演算法,顯示規則更加複雜些。
        if(messageBean.getTime() - datas.get(datas.size() -1 ).getTime() > 300000){
            //處理時間
            messageBean.setMsg("下午2:41");
        }
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("博越油耗相當高啊。12L");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("百公里");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_MINE);
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("你才知道啊");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("手動的大概9L");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
        datas.add(messageBean);

        messageBean = new MessageBean();
        messageBean.setMsg("自動的最小10L");
        messageBean.setTime(System.currentTimeMillis());
        messageBean.setLayoutType(MessageBean.LAYOUT_TYPE_OTHER);
        datas.add(messageBean);

        //重新整理列表,如果是真實的網路資料,則放到請求回撥函式當中使用。
        //注意如果是在非同步執行緒中,應該怎麼使用?它必須在主執行緒中執行!!
        listViewHighAdapter.notifyDataSetChanged();
        return datas;
    }
}

10、activity_listview_high.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="oliver.zhantao.oliverproject.listview.ListViewSimpleActivity">

    <ListView
        android:id="@+id/lv_resturant"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:listSelector="@drawable/listviewsimple_bg_selector"
        android:divider="@null"
        android:background="@color/color_ECEDF1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

下面我們來看看實際的執行效果,背景泡泡是我從網上扒的,太醜了。在專案中,有UI專門製作的。

這裡寫圖片描述

以上,完了。程式碼貼完就完了。如有問題或好的建議希望大家留言,感激。

作者:Amir
部落格:http://blog.csdn.NET/amir_zt/
以上原創,轉載請註明出處,謝謝。
http://blog.csdn.net/amir_zt/article/details/72547935