電子市場項目總結(三)

電子市場項目總結(三)

ListView的抽取和封裝


1.BaseFragment中我們留下了三個抽象方法,留給子類去實現,先回顧一下是哪三個方法

  • protected abstract View initSuccessLayout(); // 加載成功後是什麼樣的佈局
  • protected abstract State loadData(); // 加載數據的邏輯
  • protected abstract void initListener(); // 監聽的初始化

還記得我們使用了簡單工廠的方式去生產對應序號的Fragment,那麼接下來我們就爲工廠中添加產品模型。

第一個繼承自BaseFragment的Fragment:

public class HomeFragment extends BaseFragment{
    // 監聽器的添加都可以安排在這裏,但是該方法應當由調用者安排放在自己的調用位置
    protected void initListener() {

    }

    // 加載數據,調用者根據自己的數據加載程度返回對應的加載狀態
    protected State loadData() {
        return null;
    }

    // 加載成功的佈局,調用者可以返回自己的佈局
    protected View initSuccessLayout() {
        return null;
    }
}

2.分析佈局,完成initSuccessLayout()

項目演示:

我們先從佈局的實現開始,通過觀察完整的項目我們可以看到大多Fragment都使用了listview(因爲是學習還是先從listview開始入手)來進行展示,涉及到listview數據展示那麼肯定也會使用到對應BaseAdapter,同時可以看到每一個listview都具備上拉加載更多的功能。有這麼多的相似點我們首先想到的就是將listview進行封裝。

3.抽取MyBaseAdapter

首先可以想到的抽取就是數據獲取的抽取,代碼如下:

public abstract class MyBaseAdapter<T> extends BaseAdapter {
    public MyBaseAdapter(List<T> mDataList) {
        this.mDataList = mDataList;
    }

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

    @Override
    public T getItem(int position) {
        return mDataList.get(position);
    }

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

爲Adapter添加泛型T,這樣Adapter中就可以使用泛型來規範整個類中的數據。

其次getView()方法同樣也可以進行封裝,但是getView中都是不同的item佈局又該如何進行封裝呢?先看一看我們以前是如何使用getView的,代碼如下:

public View getView(int position, View convertView, ViewGroup parent) {
    // 1.判斷是否存在複用的view
    if(convertView == null){
        // 2.加載對應的item的佈局文件
        convertView = UIUtils.initLayout(R.layout.xxx);
        // 3.將複用的view放到viewholder中去,讓viewholder進行FindViewById
        // 同時將viewholder存儲在convertView的tag中
        convertView.setTag(new ViewHolder(convertView));
    }
    // 4.從tag中獲取viewholder
    ViewHolder viewHolder = (ViewHolder) convertView.getTag();
    // 5.再對viewholder中已經findViewById完成的view進行數據填充
    viewHolder.textView.setText("XXX");
    // 6.返回這個複用view
    return convertView;
}
/* 
這裏很多人會問到爲什麼要使用static的內部類,實際上static的內部類在使用方式上相當於一個外部類,可以直接new Adapter.ViewHolder()這樣來創建,相反創建的方式爲new Adapter().new ViewHolder()。
當然這裏是可以直接new ViewHolder()的,那麼爲什麼要使用靜態的呢?1.Java大牛們都是這麼寫的有什麼問題你問他們啊!
2.這樣寫可以看出類的加載順序,非static情況時必須要有一個Adapter對象纔會加載這個ViewHolder,word天,這不很耗內存嗎,在android這種實時要考慮內存分配問題的編程環境裏,一個非static內部類,一直持有上一個item對象的指引這將導致內存泄漏啊!
所以,推薦使用static的內部類來讓viewholder不再持有Adapter的引用這樣會起到一定的優化。
*/
public static class ViewHolder{
    TextView textView;
    public ViewHolder(View view){
        // 在構造方法中去findViewById
        textView = (TextView) view.findViewById(R.id.xxx);
    }
}

關於爲什麼使用static的viewholder可以看http://blog.csdn.net/caoyang521/article/details/49847881

可以看到在getView中我們一定會做的事情就是以上6步,那麼這些步驟如果每次都讓調用者去寫的話,這麼多listview也未免太重複了。那麼該怎麼辦呢?

4.封裝viewholder

實際上在最新的開發中已經不推薦使用listview了,取而代之的是以viewholder爲中心的Recycleview,我們把主要的目光放在viewholder上處理對應的佈局,而不是如何獲取getview。

首先可以看到viewholder中我們已經可以findViewById了
那麼同理我們也可以在這裏進行 setTag和設置數據,代碼如下:

public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        // 生成一個新的BaseViewHolder並拿到它已經實現好的view
        convertView = getBaseViewHolder().getRootView();
    }
    // 獲取對應的viewHolder
    BaseViewHolder holder = (BaseViewHolder) convertView.getTag();
    // 使用viewholder去填充數據
    holder.setData(getItem(position));//如果時普通的佈局時加載數據
    // 返回複用的view
    return convertView;
}

// 返回一個BaseViewHolder 以供顯示item使用
protected abstract BaseViewHolder<T> getBaseViewHolder();

// 抽象出來的ViewHolder T爲item對應的數據類型
public abstract class BaseViewHolder<T> {

    protected View mRootView;
    protected T data;
    public BaseViewHolder() {
        // 加載佈局,同時將自身holder打上tag
        mRootView = initLayout();
        mRootView.setTag(this);
    }

    // 加載佈局由對應的調用者加載,並進行findViewById
    protected abstract View initLayout();

    // 刷新界面的過程由調用者實現
    protected abstract void refreshView(T item);

    // 返回加載的佈局
    public View getRootView() {
        return mRootView;
    }
    // 設置數據(雖然這個方法只是比refreshView多了一句設置data,但這一句很重要。
    因爲refreshView是交給子類去實現的,那麼子類應當只關心如何填充數據,同時在父類中進行這一步,在MyBaseAdapter中就可以使用對應的data)
    public void setData(T data){
        this.data=data;
        refreshView(data);
    }
    // 獲取當前的數據
    public T getData(){
        return data;
    }
}

可以看到viewholder進行了兩個主要事情,讓子類去加載佈局和讓子類去填充數據,同時還有將自己標記在加載出來的佈局上。

那麼這樣的viewHolder應用面就變得更加廣泛了,它不僅可以使用在listview中,所有的需要findViewById和setData的場合都可以使用它。

5.使用ViewHolder在MyBaseAdapter中添加上拉加載更多

上拉加載更多的實現方法有很多種,在之前我們是通過將一個layout添加在listview的腳步佈局中,然後監聽listview 的scroll事件,當其展示lastViewPosition爲最後一個對象且scroll處於IDLE空閒狀態時就讓layout的padding值變爲0,當不需要顯示時將padding值設置爲負值。通過設置監聽的方式,將加載事件寫在layout展現之後。通過flag的形式控制layout的顯示和關閉。

那麼這裏我們已經封裝了viewholder能否換一種做法呢?代碼如下

public class LoadingMoreViewHolder extends BaseViewHolder<Integer> {
    public static int state_has_more = 1;
    public static int state_error = -1;
    public static int state_no_more = 0;

    private ProgressBar mLoadingPb;
    private TextView mMLoadingTv;

    //是否是可見的,在有的listview裏可能並不需要下拉加載
    private boolean isShow;

    //初始化時聲明是否顯示加載
    public LoadingMoreViewHolder(boolean isShow) {
        super();
        this.isShow = isShow;
        // 聲明當前的初始狀態,並重新設置
        setData(isShow ? state_has_more : state_no_more);
    }

    // 重寫父類的方法,並進行findViewById
    protected View initLayout() {
        mRootView = UIUtils.initLayout(R.layout.list_item_loading_more);
        mLoadingPb = (ProgressBar) mRootView.findViewById(R.id.pb_loading);
        mMLoadingTv = (TextView) mRootView.findViewById(R.id.tv_loading_desc);

        return mRootView;
    }

    // 根據數據進行刷新,這裏是loadingMore的邏輯判斷
    public void refreshView(Integer item) {
        if (!isShow)
            item = state_no_more;
        //設置 進度條的可見性
        mLoadingPb.setVisibility(item == state_has_more ? View.VISIBLE : View.GONE);

        //設置描述文字的內容
        if (item == state_has_more)
            mMLoadingTv.setText("正在加載中");
        else if (item == state_error)
            mMLoadingTv.setText("加載失敗");
        //設置描述文字的可見性,加載數據爲空時不可見
        mMLoadingTv.setVisibility(item == state_no_more ? View.GONE : View.VISIBLE);
    }
}

如上代碼是封裝了一個LoadingMore的ViewHolder,可以通過設置setData的方式來控制其狀態。那麼我們怎麼將其添加到Adapter中呢?實際上絕大多數步驟和爲listview添加多種類型是一樣的方法,代碼如下:

// 定義了幾種不同的狀態,這寫狀態是用來判斷當前展示的是loadingMore還是普通的item
public static final int TYPE_NORMAL = 1;
public static final int TYPE_LOAD_MORE = 0;
private boolean isLoading = false;

// 對應的count需要加1
public int getCount() {
    return mDataList.size() + 1;
}

// 對應getItem應當改變
public T getItem(int position) {
    return position < getCount() - 1 ? mDataList.get(position) : null;
}

// 當前所顯示的種類
public int getViewTypeCount() {
    return 2;
}

//返回對應position對應的種類
public int getItemViewType(int position) {
    if (position == getCount() - 1) {
        // 這樣判斷,有利於更多類型的拓展,因爲只有唯一不變的那個類型被限制了
        return TYPE_LOAD_MORE;
    } else {
        return getMoreViewType(position);
    }
}

//提供給子類進行重寫,拓展子類中類型
public int getMoreViewType(int position) {
    return TYPE_NORMAL;
}

//封裝viewholder,將settag和findviewbyid以及view的刷新封裝出來
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        //獲取一個BaseViewHolder 並獲取其中的方法
        if (getItemViewType(position) == TYPE_LOAD_MORE)
            // canload方法提供給子類來控制是否存在上拉加載
            convertView = new LoadingMoreViewHolder(canLoad()).getRootView();
        else {
            // 這樣寫判斷有利於拓展 baseviewholder的返回類型
            convertView = getBaseViewHolder(position).getRootView();
        }
    }

    BaseViewHolder holder = (BaseViewHolder) convertView.getTag();

    // 當上拉加載的視圖可見時,應當刷新數據,同時改變加載狀態的view
    if (getItemViewType(position) == TYPE_LOAD_MORE) {
        // 刷新過程由子類來實現
        loadingMore(holder);
    } else
        holder.setData(getItem(position));//如果時普通的佈局時加載數據

    return convertView;
}

//設置是否可以加載,默認返回true可加載
public boolean canLoad() {
    return true;
}

protected void loadingMore(final BaseViewHolder holder) {
    // 當item可視時會不停的加載,所以應當對加載進行判斷
    if (isLoading || holder.getData() != Integer.valueOf(LoadingMoreViewHolder.state_has_more)) {
        return;
    }
    // 同樣與BaseFragment中封裝的load方法相同,父類將開闢子線程和對佈局狀態控制進行了封裝,子類只需要關心如何去加載更多
    ThreadManager.getInstance().execute(new Runnable() {
        @Override
        public void run() {
            isLoading = true;
            // 這裏仍然將加載更多的方法交給子類去實現
            final ArrayList<T> data = (ArrayList<T>) loadingMore();

            UIUtils.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    isLoading = false;
                    // 設置的是下一次的加載情況
                    // 判斷是否加載到數據
                    if (data != null) {
                        mDataList.addAll(data);
                        // 該處小與20僅僅判斷的一種形式,可以是其他任何邏輯
                        if (data.size() < 20) {
                            ToastUtil.show(MyApplication.getContext(), "沒有更多更新了~");
                            holder.setData(LoadingMoreViewHolder.state_no_more);
                        } else
                            holder.setData(LoadingMoreViewHolder.state_has_more);
                    } else
                        holder.setData(LoadingMoreViewHolder.state_error);

                    notifyDataSetChanged();
                }
            });
        }
    });
}

//上拉加載的過程,返回加載的結果
protected abstract List<T> loadingMore();

5.總結,封裝完成後子類應該做的事情

  1. protected abstract BaseViewHolder getBaseViewHolder(int position); // 子類提供一個用於處理item數據的holder
  2. protected abstract List loadingMore(); // 子類完成加載更多的邏輯,返回對應的數據給Adapter來進行判斷處理
  3. public int getMoreViewType(int position); // 子類可以重寫這個方法,因爲這裏父類只有兩種類型,但如果子類中有更多種類就可通過重寫這個方法來實現更多種類型的判斷。
  4. public boolean canLoad(); // 通過重寫該方法,子類可以很控制是否需要加載更多這一功能
  5. protected abstract View initLayout(); // viewholder中要實現佈局的加載和findViewById
  6. protected abstract void refreshView(T item); // viewholder中實現數據填充的邏輯

ViewHolder的封裝看似複雜,實則簡單,其是主要邏輯是將initView的部分在構造器中直接執行,refreshView的部分交給調用者在有需要時調用,很好的將initView和setData封裝在了一起,使得在開發時,每一個模塊只需要關心自己的事情不會與大量的不相干的代碼混雜在一起。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章