Adapter源码简析及自定义实战

今天来分享下做导航栏的另外一种方法,导航栏可以放在顶部,也可以放在底部,之前分享过一片底部导航栏的实现方式一行代码实现底部导航栏TabLayout,用的是Android自带的控件FragmentTabLayout。今天我们用的一种更为灵活的方式,采用国际惯例Adapter来自定义一个导航栏,可以自己定义每个Tab的布局,可以方便的改变导航栏里面标签的个数。
本文会分享到的内容:

1.ListView Adapter源码分析

2.自定义Adapter

3.自定义控件

4.观察者设计模式

看下Demo:


初始页.png

点击添加按钮可以添加标签:


添加.png

点击删除按钮可以删除标签:

删除.png

接下来我们正式开车~~~

1.使用

我们先来看下怎么使用,首先看下布局文件,很简单就是,在底部放置自定义控件TabLinearLayout。

    <Button
        android:text="点击添加"
        android:id="@+id/add"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:text="点击删除"
        android:id="@+id/delete"
        android:layout_below="@id/add"
        android:layout_centerInParent="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <com.example.juexingzhe.adapterbottomtab.TabLinearLayout
        android:background="@color/colorPrimary"
        android:layout_width="match_parent"
        android:layout_centerVertical="true"
        android:id="@+id/bottom_tab"
        android:layout_alignParentBottom="true"
        android:layout_height="70dp"/>

然后在Activity中就是构造Adapter传进去数据,然后将Adapter设置给tabLinearLayout


defaultTabAdapter = new DefaultTabAdapter(datas);
tabLinearLayout.setTabAdapter(defaultTabAdapter);

那么增加或者删除Tab

defaultTabAdapter.notifyChanged();

以上,是不是so easy?接下来我们看看背后的逻辑。

2.ListView Adapter背后逻辑

ListView地球人都知道,平时我们在用的时候基本有下面两句话:

listView.setAdapter(adapter);
adapter.notifyDataSetChanged();

这两句话背后发生了什么使得ListView可以根据数据做出显示?下面看下源码,为了看了方便做了些调整。对adapter通过registerDataSetObserver注册了观察者mDataSetObserver。

public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        // AbsListView#setAdapter will update choice mode states.
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);
}

这个观察者mDataSetObserver在ListView中找不到,我们到他父类AbsListView中找,定义了一个内部类AdapterDataSetObserver。

class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
        @Override
        public void onChanged() {
            super.onChanged();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            if (mFastScroll != null) {
                mFastScroll.onSectionsChanged();
            }
        }
    }

setAdapter中的逻辑就先到这,ListView中的源码还是比较多的,这个不是我们今天的重点。我们今天只要知道setAdapter中做了一件事就是给adapter注册了一个观察者,这个就是在数据变化的时候adapter可以通知ListView。

我们简单说下观察者模式,这个模式可谓是无处不在,主要对象可以简单分为观察者和被观察者。打个比喻,办公室分成三类人,一类是产品经理,一类是前台秘书,一类是程序猿。上班时间程序猿不想撸代码,就委托前台秘书如果产品经理过来了就给大家发个消息,准备好开撕啊。这里程序猿就是观察者,产品经理就是被观察者,当被观察者有动静时,前台秘书就通知观察者。

再说个我们常用的点击事件,Button就是被观察者,OnClickListener就是观察者,二者通过setOnClickListener达成关系,view在状态变化的时候自动通知(当然这里是Android系统做的工作)观察者OnClickListener。所以这里Button就对应上面栗子中的产品经理,OnClickListener就对应程序猿,setOnClickListener就对应的前台秘书的作用。

观察者模式.png

对应到ListView就是在ListView内部声明了一个观察者AdapterDataSetObserver,在上面setAdapter中AdapterDataSetObserver注册到adapter中。adapter就起到中介者的作用,在数据(被观察者)变化再通知ListView。

接着看另外一句话adapter.notifyDataSetChanged()的源码,很简单一句话,就是调用mDataSetObservablenotifyChanged

private final DataSetObservable mDataSetObservable = new DataSetObservable();

public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }
public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
}

接着跟到mDataSetObservablenotifyChanged中,就是调用观察者的onChanged方法。

public class DataSetObservable extends Observable<DataSetObserver> {
    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

对于ListView,观察者就是AdapterDataSetObserver,这个是在AbdListView中,super就是AdapterView

public void onChanged() {
            super.onChanged();
}

我们进去看到了很熟悉的一句话requestLayout,就是进行ListView的重绘。

class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            ……
            requestLayout();
        }
}

到这里就恍然大悟了,就是在ListView中注册观察者,要实现抽象类DataSetObserver

public abstract class DataSetObserver {
    public void onChanged() {
        // Do nothing
    }

    public void onInvalidated() {
        // Do nothing
    }
}

在adapter中含有一个DataSetObservable,类似于中介作用,在数据变化时通知观察者,也就是DataSetObserver,然后onChanged就会被回调了。

public class DataSetObservable extends Observable<DataSetObserver> {
    public void notifyChanged() {
        synchronized(mObservers) {
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }
}

3.自定义Adapter

前面源码分析有木有很枯燥?我们接着来点实战提提精神。这里adapter我们就不包含,直接继承DataSetObservable。然后扩展三个方法,都比较简单,见名知意就不多说了。

public abstract class TabAdapter<T> extends DataSetObservable {
    public abstract int getCount();

    public abstract View getView(ViewGroup parent, View convertView, int position);

    public abstract T getItem(int position);
}

为了方便用户使用,我们实现一个默认的TabAdapter.为了内存优化,这里也是使用了ViewHolder进行重用。布局也很简单就是上面一个ImageView,底下是一个TextView。

public class DefaultTabAdapter extends TabAdapter {
    private List<TabBean> mData = new ArrayList<>();
    private ViewHolder viewHolder;

    DefaultTabAdapter(ArrayList<TabBean> data) {
        mData = data;
    }

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

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

        if (convertView == null){
            viewHolder = new ViewHolder();
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            convertView =  inflater.inflate(R.layout.tab_item, parent, false);
            viewHolder.imageView = (ImageView) convertView.findViewById(R.id.tab_img);
            viewHolder.textView = (TextView) convertView.findViewById(R.id.tab_txt);
            convertView.setTag(viewHolder);
        }else {
            viewHolder = (ViewHolder) convertView.getTag();
        }

        viewHolder.imageView.setBackgroundResource(mData.get(position).tabImgSourceUnSelect);
        viewHolder.textView.setText(mData.get(position).tabTxt);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 1);
        convertView.setLayoutParams(layoutParams);

        return convertView;
    }

    @Override
    public TabBean getItem(int position) {
        return mData.get(position);
    }

    private static class ViewHolder{
        ImageView imageView;
        TextView textView;
    }
}

4.自定义TabLinearLayout

我们先看下adapter的观察者模式的逻辑实现。
1.首先声明一个观察者,实现DataSetObserver的两个方法。

private DataSetObserver mTabDataSetObserver = new DataSetObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            tabOnChanged();
        }

        @Override
        public void onInvalidated() {
            super.onInvalidated();
            removeAllViews();
        }
};

2.接着,在setTabAdapter中将观察者mTabDataSetObserver注册到mTabAdapter中。

public void setTabAdapter(TabAdapter tabAdapter) {
        this.mTabAdapter = tabAdapter;
        removeAllViews();
        mTabAdapter.registerObserver(mTabDataSetObserver);
        mTabAdapter.notifyChanged();
}

3.最后,在用户调用defaultTabAdapter.notifyChanged();后会调用我们观察者的onChanged方法,我们在里面实现更新。

以上就是adapter的观察者逻辑,和Android源码略微不一样的地方就是我们这里adapter就是一个DataSetObservable,源码中是adapter中包含了一个DataSetObservable,就这样。

从上面可以看出,增加和删除的逻辑主要是在tabOnChanged()方法中。首先从adapter中getView获得布局,然后动态添加到LinearLayour中。

private void tabOnChanged() {
        removeAllViews();
        mContainer.clear();
        int count = mTabAdapter.getCount();

        for (int i = 0; i < count; i++) {
            LinearLayout layout = (LinearLayout) mTabAdapter.getView(this, null, i);
            final int finalI = i;
            layout.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    notifyClickEvent(finalI);
                }
            });
            addView(layout);
            mContainer.add(layout);
        }
        mContainer.get(0).getChildAt(0).setBackgroundResource(mTabAdapter.getItem(0).tabImgSourceSelected);
}

同时也将布局文件添加到数组中,这个是为了点击事件的处理,在点击其中一个Tab时需要更新剩下的Tab。

private void notifyClickEvent(int finalI) {
        for (int i = 0; i < mContainer.size(); i++) {
            if (i == finalI) {
                mContainer.get(i).getChildAt(0).setBackgroundResource(mTabAdapter.getItem(i).tabImgSourceSelected);
                continue;
            }
            mContainer.get(i).getChildAt(0).setBackgroundResource(mTabAdapter.getItem(i).tabImgSourceUnSelect);
        }
}

以上就是自定义TabLinearLayout的内容了。

5.总结

我们通过分析Android源码的Adapter实现原理,结合观察者设计模式的讲解,应该是比较清晰的。学习并实践,动手实现了一个Adapter,用来充当被观察者,自定义一个TabLinearLayout导航栏作为观察者,从而实现动态添加Tab和删除Tab。

到这里我们的实现导航栏的第二种方式已经分享完毕,大家可以下车喽,希望我有说清楚让大家有点收获。

感谢@右倾倾的支持与理解!

你们的赞是我最大的动力,谢谢!

欢迎关注公众号:JueCode

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,280评论 25 707
  • 这是我,见过第二次神奇的数字。一个小时过的很慢,一分钟过的很快,日常生活里,我们错过太多的岁月。 想要提高做某...
    沐星之星星阅读 277评论 0 0
  • 我与朱自清初识,是在中学课本,就是借助初中时的《春》、高中时的《荷塘月色》和《绿》三篇经典课文,以及后来读到的《匆...
    阿YAO阅读 4,125评论 10 14
  • socket服务器可通过python来搭建 源程序分享在百度云,下载链接如下:链接: http://pan.bai...
    dibadalu阅读 6,024评论 12 12