1. 程式人生 > >Android:複雜滾動佈局的一種適配思路

Android:複雜滾動佈局的一種適配思路

App 的首頁向來是個寸土寸金的地方,如何在首頁吸引使用者是 PM 的工作,而對於我們開發者而言要做的就是適配。想必大家一定遇到過類似如下的設計稿:

這裡寫圖片描述

在設計稿上我們可以看到一些重點:

  • 頁面可以上下滾動
  • 元素比較繁多
  • 不同頁之間有分割線或者間隙

基於上面的幾點我們的思路無非分為兩條,第一條思路是:

使用ScrollView包裹LinearLayout並動態add多個佈局

這種方法的是可以解決問題的,各個板塊可以寫成子佈局,之間的風格線和間隙可以通過新增黑色的高度為 1 畫素的 View 和設定 MarginTop 來解決,重複的元素使用動態修改高度的 ListView 來適配。

確實簡單粗暴,但缺點是無法複用 View 比較浪費記憶體。

第二條思路,也就是本篇博文將要論述的思路:

將包括頂部的輪播圖、中間的活動版面甚至是分割線和間隙在內的東西都當成 Item 佈局,並使用單個ListView適配

較之前者,該方法不但解決了複用 View 的問題而且檢視的層次更少。

使用 RecyclerView 也是可以的不過思路其實和 ListView 一樣,這裡不再贅述。具體實現同樣也在給出的原始碼中,請自行查閱。

之後的章節筆者會詳細地闡述實現和程式碼,篇幅略長。如果你想直接看原始碼的話可以直接跳轉到文末。

BaseAdapter 的初步封裝

考慮到原有的 BaseAdapter 功能太弱,在正式開始工作之前有必要對之進行初步的封裝。首先我們在 BaseAdapter 中實現類似 RecyclerView 的 ViewHolder 機制,這裡先定義一個 AbsViewHolder 的基類,如下:

public static abstract class AbsViewHolder<Item> {

    // 這裡使用一個泛型來引用該ViewHolder對應的資料項
    private Item item;
    private int position;
    private final View view;

    // 這裡省略get和set方法,完整的程式碼參見文末GitHub
}

之後我們定義 BaseAdapter 的基類程式碼如下:

// 每個Adapter都要確定自己能適配的資料型別和使用的ViewHolder
public abstract
class CoreAdapter<Item, Holder extends AbsViewHolder<Item>> extends BaseAdapter { // 將資料來源封裝進Adapter中方便後續的開發 private final List<Item> data = new ArrayList<>(); /* 繼承 */ // inflater的單例 private LayoutInflater inflater; @Override public final int getCount () { // 由於資料來源在內部並且是final的,這裡可以寫死 return data.size(); } @Override public final Item getItem (int position) { // 同上,寫死 return data.get(position); } @Override public long getItemId (int position) { return position; } @Override public final View getView (int position, View convertView, ViewGroup parent) { // 獲取inflater的單例 if (inflater == null) { //當getView方法被呼叫時必定已經被set進ListView了 //此時parent.getContext()不會空指標 inflater = LayoutInflater.from(parent.getContext()); } //獲取position對應的資料和viewType int viewType = getItemViewType(position); Item data = getItem(position); //實現ViewHolder機制 Holder holder; if (convertView == null) { holder = createViewHolder(inflater, parent, viewType); convertView = holder.getView(); convertView.setTag(holder);// 繫結ViewHolder } else { holder = (Holder) convertView.getTag(); } // 將資料和position繫結到ViewHolder上 holder.setPosition(position); holder.setItem(data); bindViewData(holder, position, data, viewType); return convertView; } /* 回撥 */ //建立ViewHolder例項 //如果你使用過RecyclerView的Adapter的話這裡你會發現這裡的思想與之基本一致 protected abstract Holder createViewHolder (LayoutInflater inflater, ViewGroup parent, int viewType); //繫結資料到ViewHolder例項上 //也是RecyclerView.Adapter的思想 protected abstract void bindViewData (Holder holder, int position, Item data, int viewType); /* 資料展示 */ //從外部新增資料項到Adapter內部的資料來源,並重新整理檢視 public final void display (Item... items) { this.data.clear(); Collections.addAll(this.data, items); notifyDataSetChanged(); } public final void display (Collection<? extends Item> items) { this.data.clear(); this.data.addAll(items); notifyDataSetChanged(); } //後續還可以拓展上add,remove之類的方法,這裡略去。 //完整程式碼請參閱GitHub }

經過以上的修改我們已經將 ViewHolder 機制封裝進了 Adapter 之中,為後續的開發剩下了大量的程式碼。

具體實現

由於我們將所有的控制元件都當成Item佈局不用想也知道這裡會有數不清的 if-else 要寫,這顯然也是不夠優雅的。要解決這個問題我們就需要用到策略模式和策略工廠。

我們首先將每個 Item 的操作邏輯封裝成對應的策略,一個策略處理一種 Item 的例項化、繫結資料和事件回撥等邏輯。如下:

/*單個Item佈局的策略*/
public abstract class AbsItemOperator<Item, Holder extends AbsViewHolder<Item>> {

    //該策略支援的資料型別的單例
    private Type type;

    //判斷該策略是否能夠適配obj的例項
    public final boolean canHandleObject (Object obj) {
        if (type == null) {
            //獲取實現類的第一個泛型引數作為該策略的可適配型別
            //ClassUtil請參見GitHub原始碼
            Type types[] = ClassUtil.getGenericParametersType(getClass());
            type = types[0];
        }
        if (obj instanceof Collection) {
            //出於安全和可維護性考慮這裡禁止直接適配Collection的子類。
            throw new IllegalStateException("不允許直接適配Collection或者其子類");
        }
        return type == obj.getClass();
    }

    /*回撥*/

    //建立ViewHolder
    public abstract Holder createViewHolder (LayoutInflater inflater, ViewGroup parent);

    //繫結資料到ViewHolder
    public abstract void bindViewData (Holder holder, Item data);

}

我們可以在這個基類中看到兩個抽象方法:createViewHolder()bindViewData(),只要在這兩個方法裡面實現對 Item 的處理即可。

之後我們需要將 Adapter 封裝一個策略工廠,負責策略的匹配和排程。程式碼如下:

public final class FlexibleAdapter extends CoreAdapter<Object, AbsViewHolder<Object>> {

    //使用被final修飾的List來儲存所有支援的佈局策略
    private final List<AbsItemOperator> operators = new ArrayList<>();

    //在構造的時候必須指明所有支援的策略
    public FlexibleAdapter (AbsItemOperator<?, ?>... operators) {
        Collections.addAll(this.operators, operators);
    }

    /* 繼承 */

    @Override
    protected AbsViewHolder<Object> createViewHolder (LayoutInflater inflater, ViewGroup parent, int viewType) {
        //createViewHolder方法是在之前的CoreAdapter之中定義的
        //其中viewType引數來自getItemViewType(),其值也是目標策略在策略列表operators中的序數
        //取出策略,直接排程
        return (AbsViewHolder<Object>) operators.get(viewType).createViewHolder(inflater, parent);
    }

    @Override
    protected void bindViewData (CoreAdapter.AbsViewHolder<Object> viewHolder, int position, Object data, int viewType) {
        //同上,根據viewType取出策略並排程
        operators.get(viewType).bindViewData(viewHolder, data);
    }

    @Override
    public int getItemViewType (int position) {
        //獲取position對應的資料例項
        //並通過AbsItemOperator.canHandleObject()方法逐一匹配合適的策略
        Object obj = getItem(position);
        for (int i = 0, count = operators.size(); i < count; i++) {
            if (operators.get(i).canHandleObject(obj)) {
                return i;// 可處理
            }
        }
        // 沒有策略能夠適配該資料
        throw new IllegalStateException("指定資料型別不存在可用的operator");
    }

    @Override
    public int getViewTypeCount () {
        //策略列表的大小就是佈局型別數
        return operators.size();
    }

}

至此整體的框架就已經搭建完畢,往後如果有新增的Item佈局只需要新增一個獨立的策略實現類即可,十分靈活。

實現一個策略

這裡我們寫一個策略以加深對這種思路的理解。佈局檔案很簡單:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@color/core_white">

    <View
        android:layout_width="24dp"
        android:layout_height="16dp"
        android:layout_gravity="center_vertical"
        android:background="@color/core_holo_green_light"/>

    <TextView
        android:id="@+id/textView_listView_tip"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"/>
</FrameLayout>

有點經驗的開發者應該立馬就能想象到它例項化後的樣子,主體只是一個用來顯示文字的 TextView 而已。接下來是它對應的策略:

public class TipOperator extends AbsItemOperator<Tip, TipViewHolder> implements View.OnClickListener {

    //寫一個AbsViewHolder的基類,ViewHolder本身其實還可以深挖不少東西,這裡篇幅有限就不再贅述
    public static class TipViewHolder extends AbsViewHolder<Tip> {

        public TipViewHolder(View v) {
            super(v);
        }
    }

    /*繼承*/

    @Override
    public TipViewHolder createViewHolder(LayoutInflater inflater, ViewGroup parent) {
        //例項化佈局
        View view = inflater.inflate(R.layout.activity_listview_tip, parent, false);
        //繫結事件
        view.setOnClickListener(this);
        return new TipViewHolder(textView);
    }

    @Override
    public void bindViewData(TipViewHolder holder, Tip data) {
        //繫結資料
        TextView textView = (TextView) holder.getView().findViewById(R.id.textView_listView_tip);
        textView.setText(data.tip);
    }

    /*回撥*/

    @Override
    public void onClick(View v) {
        //因為CoreAdapter中已經實現了ViewHolder機制因而itemView.getTag返回的必定是對應的ViewHolder,在這裡就是TipViewHolder
        //在設計上ViewHolder和ItemView的例項是繫結在一起的,只是攜帶者的position和data一直在變化
        //因此只要獲得ViewHolder就能獲得當前的position和data
        TipViewHolder holder = (TipViewHolder) v.getTag();
        if(onTipClickListener != null){
            //回撥監聽者
            onTipClickListener.onTipClick(v,holder.getPosition(),holder.getItem());
        }
    }

    /*介面*/

    //監聽者介面
    public interface OnTipClickListener{

        void onTipClick(View v,int position,Tip tip);

    }

    private OnTipClickListener onTipClickListener;

    public TipOperator setOnTipClickListener(OnTipClickListener onTipClickListener) {
        this.onTipClickListener = onTipClickListener;
        return this;
    }
}

實際使用時如下:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ListView listView = new ListView(this);
    listView.setDivider(null);
    listView.setDividerHeight(0);
    setContentView(listView);
    setTitle(getClass().getSimpleName());

    //例項話FlexibleAdapter並填入所有的佈局策略
    FlexibleAdapter adapter = new FlexibleAdapter(new RotateOperator(), new TipOperator(), new PanelOperator(), new MsgOperator(), new DividerOperator(), new SpanOperator());
    listView.setAdapter(adapter);

    //逐一新增每個資料項
    List<Object> list = new ArrayList<>();
    Rotate rotate = new Rotate(R.layout.activity_listview_rotate_1, R.layout.activity_listview_rotate_2, R.layout.activity_listview_rotate_3);
    list.add(rotate);
    list.add(new Divider());

    list.add(new Span());

    int times = 5;
    while (times-- > 0) {
        list.add(new Divider());
        Tip tip = new Tip("這是一個Tip");
        list.add(tip);
        list.add(new Divider());
        Panel panel = new Panel("這個是內容,我就隨便輸入一些什麼東西");
        list.add(panel);
        list.add(new Divider());
        list.add(new Span());
    }

    list.add(new Divider());
    times = 20;
    while (times-- > 0) {
        list.add(new Msg("這是文字35435435453545354"));
    }

    //重新整理到介面上
    adapter.display(list);
}

以上程式碼執行起來後效果如下:

之後新增一個佈局只需要新增一個對應的策略即可。

最後附上原始碼地址:

相關推薦

Android複雜滾動佈局思路

App 的首頁向來是個寸土寸金的地方,如何在首頁吸引使用者是 PM 的工作,而對於我們開發者而言要做的就是適配。想必大家一定遇到過類似如下的設計稿: 在設計稿上我們可以看到一些重點: 頁面可以上下滾動 元素比較繁多 不同頁之間有分割線或者間隙

表現層響應式響應式佈局和螢幕佈局

1.響應式佈局:     響應式佈局是根據瀏覽器寬度,解析度,橫屏,豎屏等情況來自動改變元素展示的一種佈局方式,一般可以使用柵格方式來實現,一般有兩種思路,一種是桌面端瀏覽器優先,擴充套件到移動端瀏覽器適配,另一種是以移動端優先,擴充套件到桌面瀏覽器適配。由於移動端網路和計算

Android項目實戰(十二)解決OOM的偷懶又有效的辦法

建議 是什麽 cat 解決 blog www. android項目 roi acm 原文:Android項目實戰(十二):解決OOM的一種偷懶又有效的辦法在程序的manifest文件的application節點加入android:largeHeap=“true&

C++從零開始區塊鏈main函式的實現

前面已經把各種業務邏輯都寫好了,main函式怎麼呼叫就隨便了,這裡只是其中一種實現方法 int main(int argc, char **argv) { if (argc < 2) { std::cout << "argc error!

作業系統,核心定時器使用“訊號”建立使用者空間機制來測量一個多執行緒程式的執行時間。

      核心是一個作業系統的核心。它負責管理系統的程序、記憶體、裝置驅動程式、檔案和網路系統,決定著系統的效能和穩定性。 定時器是Linux提供的一種定時服務的機制,它在某個特定的時間喚醒某個程序來進行工作。核心在時鐘中斷髮生後檢測各定時器是否到期,在li

android Collections.sort排序的使用

//按照javabean的時間排序 Collections.sort(infosList, new Comparator<Dzbp_Info>() { @Override public int compare(Dzbp_Info lhs, Dzbp_Info rhs)

VS程式設計,WPF中兩個滾動條 ScrollViewer 同步滾動方法

這裡以兩個ScrollViewer控制元件之間的同步滾動為例。 當滑鼠拖動其中一個滾動條時,另一個滾動條跟著一起調整到相應的位置。   1、前臺建立兩個ScrollViewer控制元件,並分別給兩個滾動條控制元件命名。 <Stack

Table標題行凍結,資料行滾動方式

        這段時間在做Table標題行凍結,資料行滾動,雖然能實現,但也遇到一些問題,記錄下來。         首先說說實現,實現其實不難,估計很多人都能想象出來,那就是標題行與內容行分離。我是這麼做的,用兩個表格,一個

Android控制元件佈局(相對佈局)RelativeLayout RelativeLayout是相對佈局控制元件以控制元件之間相對位置或相對父容器位置進行排列。 相對佈局常用屬性 子類控制元件相對子

RelativeLayout是相對佈局控制元件:以控制元件之間相對位置或相對父容器位置進行排列。 相對佈局常用屬性: 子類控制元件相對子類控制元件:值是另外一個控制元件的id android:layout_above----------位於給定DI控制元件之上 android:layout_below -

Android Studio下的模組化思路

        隨著專案功能的增多,我們在Android Studio下的module也越來越多,或者說本身我們就想將功能按module劃分。比如,你想做個 “微信” 一樣的應用,有聊天模組、朋友圈模組,購物模組、遊戲模組等等,你想把這些模組以module劃分,通常情況下是將這些module打包成一個微信a

Android之ScrollView滾動佈局控制元件使用以及顯示新聞網頁

ScrollView滾動佈局使用原理: ①滾動產生的條件是,裡面的內容大於物理尺寸 ②ScrollView裡面只有一個子元素,這個子元素就是一個線性佈局LinearLayout,我們可以線上性佈局中新增我們需要的內容,所以ScrollView中得包裹一層,並且線性佈局中設計

關於處理if和複雜邏輯的思路及…

 折騰了好久的qbs的解決方案今天終於有眉目了,這個設計思路還是很巧妙的,對於處理多路if else等有非常好的效果,提高了程式碼的靈活性和設計的可重用性,程式碼更加簡潔,邏輯更加清晰,真的得好好的研究一番,明天開始正式把程式碼敲出來,慢慢的就會成為自己的東西,真正的

Android 6.0+ 動態許可權 清爽的封裝過程(以及多個許可權的處理)

Android 6.0 之前我們申請許可權直接在配置檔案中配置一下即可,但是6.0之後,谷歌官方將許可權分為普通許可權和危險許可權。對於危險許可權來說,我們就需要進行動態設定了。本文主要講解為什麼要進行Android 6.0 動態許可權的設定、動態許可權的使用、

基於 android 資料備份恢復的實現

引言 隨著 3G 時代的到來,移動網際網路的發展,手機的功能越來越強大,手機裡的資料對每個使用者來說都非常的重要,特別是通訊錄、日程、簡訊息、郵件等資料,一旦手機丟失、誤刪或其他意外使得資料無法正常使用,會給使用者帶來麻煩,資料備份與恢復這個應用可以幫助使用者解決這個問題。

android 讀取本地檔案的方式

try {String fileUri = "layout.xml";InputStream is =MainApplication.getInstance().getAssets().open(fileUri);int size = is.available();//

Androidwpa_supplicant決定選擇哪驅動

1, main 函式的入口: external/wpa_supplicant_8/wpa_supplicant/main.c 2, init.rc中通過引數指定要載入哪個驅動 3, wpa_supplicant中載入驅動的原理 main.c:  main() --&

android listview & toolbar形成的炫酷效果(外加一個圓形圖片的實現)

介面的效果用到的技術都很基本, 但實現的思路很新穎, 程式碼和佈局充滿各種技巧, 值得學習和借鑑. 效果圖: 原理圖: 佈局程式碼: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res

Android下基於Iptables的app網路訪問控制方案(

1.什麼是Iptable? 百度百科對於Iptables有詳細的介紹。簡單地說,Iptables是Linux核心提供的一套IP資訊包過濾系統,對外由Iptables命令提供設定過濾規則的入口。 Android是基於Linux的作業系統,支援Iptables。執行Iptabl

Android Studio中使用自定義Android.jar缺少方法的解決方案

最近在Android Studio專案中ListView的scrollListBy方法,這個方法在SDK 19及其以上版本才有的,而專案使用的自定義Android.jar中的ListView並不包含這個方法。 如果為了使用這個方法,而重新編譯Android.jar,代價太大

androidwifi通訊(

上傳了原始碼,自己下載看吧:http://download.csdn.net/detail/bigtree_mfc/9528424 關於wifi操作: 1、開啟wifi 2、關閉wifi 3、獲取wifi網絡卡狀態 activity_main.xml中新增三個按鈕 pu