观察者设计模式

1. 说明


观察者设计模式中有观察者和被观察者,注册订阅的概念,使用场景:常见多条目筛选、ListVIew的notifySetDataChanged()、RxJava等等。

观察者:我观察你订阅的一些东西,比如我订阅了鸿阳的一些公众号,这个时候会有人来给你推送,然后你去看这些内容,去做一些相应的处理

在常见多条目筛选中使用步骤:
1.1 自己定义一个抽象的 MenuOberver,然后定义一个抽象方法closeMenu();

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/3/10 8:10
 * Version 1.0
 * Params:
 * Description:   使用观察者设计模式
*/
public abstract class MenuOberver {

    // 参考ListView源码
    public abstract void closeMenu() ;
}

1.2 在基类BaseMenuAdapter中放一些观察者,写注册、反注册、提供一个方法,让子类调用,而这个方法就相当于是ListView中的notifyDataSetChanged()方法,起通知作用;

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/3/8 9:10
 * Version 1.0
 * Params:
 * Description:  Adapter设计模式使用,将方法放到基类中,让子类继承然后实现方法即可
*/
public abstract class BaseMenuAdapter {


    // 在基类BaseMenuAdapter放一些观察者,obervers就好比是微信公众号,
    // 待会如果有人来注册也就是订阅的用户,就存储到obervers的list集合中
    private MenuOberver mOberver ;

    public void registerDataSetObserver(MenuOberver observer) {
        mOberver = observer ;
    }

    public void unregisterDataSetObserver(MenuOberver observer) {
        mOberver = null ;
    }

    // 这个方法就相当于是setAdapter中的 notifySetDataChanged()方法,起通知作用
    public void closeMenu(){
        if (mOberver != null){
            mOberver.closeMenu();
        }
    }





    // 获取总共有多少条 就是上边的筛选菜单
    public abstract int getCount() ;
    // 获取当前的TabView
    public abstract View getTabView(int position , ViewGroup parent) ;
    // 获取当前的菜单内容
    public abstract View getMenuView(int position , ViewGroup parent) ;


    /**
     * 菜单打开
     * @param tabView
     */
    public void menuOpen(View tabView){
    }

    /**
     * 菜单关闭
     * @param tabView
     */
    public void menuClose(View tabView){
    }
}

1.3 在setAdapter方法中:先判断观察者非空时就去反注册,然后创建一个具体的观察者实例对象,然后再注册,并且在setAdapter()方法所在的类中,创建一个刚才的具体的观察者的类继承第一步中抽象的MenuObserver,并实现其中的方法

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/3/8 8:32
 * Version 1.0
 * Params:
 * Description:  多条目筛选的自定义View  这里直接使用在代码中创建Tab、内容部分和阴影,而没有采用写xml文件
*/
public class ListDataScrrenView extends LinearLayout implements View.OnClickListener {


    private Context mContext ;
    // 创建LinearLayout作为头部,用来存放Tab
    private LinearLayout mMenuTabView;
    // 创建 FrameLayout 用来存放 = 阴影(View)+ 菜单内容布局(FrameLayout)
    private FrameLayout mMenuMiddleView;
    // 创建阴影 可以直接创建一个View就行 不用设置LayoutParams 默认就是MATCH_PARENT MATCH_PARENT
    private View mShadowView;
    // 阴影的颜色
    private int mShadowColor = 0x88888888 ;
    // 创建菜单 用来存放 菜单内容
    private FrameLayout mMenuContainerView;
    // 下拉的内容部分高度
    private int mMenuContainerHeight;
    // 筛选菜单的 Adapter
    private BaseMenuAdapter mAdapter ;
    // 当前打开的位置
    private int mCurrentPosition = -1 ;
    // 动画执行时长
    private long DURATION_TIME = 350;
    // 动画是否正在执行
    private boolean mAnimatorExecute ;

    public ListDataScrrenView(Context context) {
        this(context,null);
    }

    public ListDataScrrenView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs ,0);
    }

    public ListDataScrrenView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context ;
        initLayout() ;
    }


    /**
     * 1. 将布局实例化号(组合控件)
     */
    private void initLayout() {
        // 在这里实例化布局文件有2种方式:第一种:创建xml文件 然后加载,再findviewbyid  第二种:直接使用代码创建,这里直接采用第二种

        // 整体是垂直的线性布局
        setOrientation(VERTICAL);

        // 1.1 创建LinearLayout作为头部,用来存放Tab
        mMenuTabView = new LinearLayout(mContext);
        // 设置布局参数
        mMenuTabView.setLayoutParams(new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT , ViewGroup.LayoutParams.WRAP_CONTENT));
        addView(mMenuTabView);


        // 1.2  创建 FrameLayout 用来存放 = 阴影(View)+ 菜单内容布局(FrameLayout)
        mMenuMiddleView = new FrameLayout(mContext);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT , 0) ;
        params.weight = 1 ;
        mMenuMiddleView.setLayoutParams(params);
        addView(mMenuMiddleView);

        // 1.3  创建阴影 可以直接创建一个View就行 不用设置LayoutParams 默认就是MATCH_PARENT MATCH_PARENT
        mShadowView = new View(mContext);
        // 阴影背景颜色
        mShadowView.setBackgroundColor(mShadowColor);
        // 透明度
        mShadowView.setAlpha(0f);
        // 点击事件
        mShadowView.setOnClickListener(this);
        // 刚开始让阴影的 View 隐藏
        mShadowView.setVisibility(View.GONE);
        // 把阴影添加到上边创建的 FrameLayout,即就是mMenuMiddleView
        mMenuMiddleView.addView(mShadowView);

        // 1.4  创建菜单 用来存放 菜单内容
        mMenuContainerView = new FrameLayout(mContext);
        // 设置菜单背景颜色
        mMenuContainerView.setBackgroundColor(Color.WHITE);
        // 把菜单添加到上边创建的 FrameLayout,即就是mMenuMiddleView
        mMenuMiddleView.addView(mMenuContainerView);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获取整体高度
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (mMenuContainerHeight ==0 && height > 0) {
            // 下拉的内容部分高度不是整体高度 应该整个 View的高度的 75%
            mMenuContainerHeight = (int) (height * 75f / 100);
            // 给菜单内容设置布局参数
            ViewGroup.LayoutParams params = mMenuContainerView.getLayoutParams();
            params.height = mMenuContainerHeight;
            mMenuContainerView.setLayoutParams(params);
            // 刚进来的时候阴影不显示 ,下拉部分的内容也是不显示的(把下拉部分的高度移上去就行)
            mMenuContainerView.setTranslationY(-mMenuContainerHeight);  // 这里是位移动画,Y方向上的
        }
    }





    // 使用观察者来写的 示例代码
    // 具体的观察者
    private class AdapterMenuOberver extends MenuOberver{

        //  这个方法是复写MenuObserver的方法  
        @Override
        public void closeMenu() {
            // 如果有注册就会收到通知      这个方法是我们自己操作的,需要关闭菜单的方法
            ListDataScrrenView.this.closeMenu();
        }
    }


    private AdapterMenuOberver mMenuObserver ;







    /**
     * 给 Tab 和 每一个 Tab 对应的内容区域setAdapter  设置内容
     * @param adapter
     */
    public void setAdapter(BaseMenuAdapter adapter){



        // 观察者 就相当于是微信的公众号用户
        if (mAdapter != null && mMenuObserver != null){
            // 取消订阅,因为有可能设置两次Adapter
            mAdapter.unregisterDataSetObserver(mMenuObserver);
        }

        this.mAdapter = adapter ;
        // 注册观察者 具体的观察者实例对象
        mMenuObserver = new AdapterMenuOberver() ;
        // 订阅
        mAdapter.registerDataSetObserver(mMenuObserver);




        // 获取有多少上边条菜单
        int count = mAdapter.getCount() ;
        for (int i = 0; i < count; i++) {
            // 根据角标 和 存放所有Tab 的 LinearLayout 来获取对应菜单的 Tab
            View tabView = mAdapter.getTabView(i, mMenuTabView);
            // 把获取到的每一个tabView 标签添加到 存放所有Tab 的 LinearLayout
            mMenuTabView.addView(tabView);

            // 给每一个Tab 设置布局参数
            LinearLayout.LayoutParams params = (LayoutParams) tabView.getLayoutParams();
            // 使用权重
            params.weight = 1 ;
            tabView.setLayoutParams(params);

            // 设置 Tab 的点击事件
            setTabClick(tabView , i) ;

            // 获取菜单内容
            View menuView = mAdapter.getMenuView(i, mMenuContainerView);
            // 刚开始让菜单内容隐藏
            menuView.setVisibility(View.GONE);
            // 把获取到的每一个菜单内容 添加到 存放菜单内容的 mMenuContainerView 中
            mMenuContainerView.addView(menuView);

        }
    }


    /**
     * 给每个Tab 设置点击事件
     * @param tabView
     * @param position
     */
    private void setTabClick(final View tabView, final int position) {
        tabView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {

                // 表示当前没有打开菜单,点击后就打开
                if (mCurrentPosition == -1){
                    openMenu(position , tabView) ;
                }else{
                    // 表示菜单已经打开,点击后就关闭
                    if (mCurrentPosition == position){
                        closeMenu() ;
                    }else{
                        // 切换一下显示
                        // 根据当前位置获取当前的菜单,让其隐藏,然后关闭,然后记录位置
                        View currentMenu = mMenuContainerView.getChildAt(mCurrentPosition) ;
                        currentMenu.setVisibility(View.GONE);
                        mAdapter.menuClose(mMenuTabView.getChildAt(mCurrentPosition));
                        mCurrentPosition = position ;


                        // 再去根据 新纪录的位置,获取当前的Tab,然后让其显示,然后再重新打开
                        currentMenu = mMenuContainerView.getChildAt(mCurrentPosition) ;
                        currentMenu.setVisibility(VISIBLE);
                        mAdapter.menuOpen(mMenuContainerView.getChildAt(mCurrentPosition));
                    }
                }
            }
        });
    }


    /**
     * 关闭菜单
     *     1.  开启位移动画
     *     2.
     */
    private void closeMenu() {
        if (mAnimatorExecute){
            return;
        }
        // 关闭动画  此处涉及到  translationY位移动画 竖直的Y方向  透明度动画
        // 此处直接使用属性动画

        // translationY位移动画 竖直的Y方向
        ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mMenuContainerView, "translationY", 0, -mMenuContainerHeight);
        translationAnimator.setDuration(DURATION_TIME) ;
        translationAnimator.start();
        mShadowView.setVisibility(VISIBLE);

        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mShadowView, "alpha", 1f, 0f);
        alphaAnimator.setDuration(DURATION_TIME) ;
        // 等关闭动画执行完才能去隐藏当前菜单
        alphaAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // 根据位置获取当前具体菜单的内容,然后隐藏,并且把当前位置置为-1
                View menuView = mMenuContainerView.getChildAt(mCurrentPosition);
                menuView.setVisibility(View.GONE);
                mCurrentPosition = -1 ;

                mShadowView.setVisibility(View.GONE);
                mAnimatorExecute = false ;
            }


            @Override
            public void onAnimationStart(Animator animation) {
                // 根据当前位置获取对应Tab
                mAdapter.menuClose(mMenuTabView.getChildAt(mCurrentPosition));
                mAnimatorExecute = true ;

            }
        });
        alphaAnimator.start();
    }


    /**
     * 打开菜单
     * @param position
     * @param tabView
     */
    private void openMenu(final int position, final View tabView) {

        if (mAnimatorExecute){
            return;
        }
        mShadowView.setVisibility(View.VISIBLE);
        // 根据当前位置显示当前菜单,菜单加到了菜单容器
        View menuView = mMenuContainerView.getChildAt(position);
        menuView.setVisibility(VISIBLE);

        // 打开菜单 开启动画  位移动画 透明度动画
        ObjectAnimator translationAnimator = ObjectAnimator.ofFloat(mMenuContainerView, "translationY", -mMenuContainerHeight, 0);
        translationAnimator.setDuration(DURATION_TIME) ;
        translationAnimator.start();


        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(mShadowView, "alpha", 0f, 1f);
        alphaAnimator.setDuration(DURATION_TIME) ;

        // 透明动画执行完毕 记录当前位置
        alphaAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mAnimatorExecute = false ;
                mCurrentPosition = position ;
            }

            @Override
            public void onAnimationStart(Animator animation) {
                // 把当前 Tab 传递到 外面
                mAdapter.menuOpen(tabView);
                mAnimatorExecute = true ;
            }
        });

        alphaAnimator.start();
    }

    @Override
    public void onClick(View v) {
        closeMenu();
    }
}

这段代码只需要去注意setAdapter中的注册和反注册,还有自己new 的具体的观察者实例对象AdapterMenuObserver即可;

1.4 最后直接调用基类的 BaeMenuAdapter 中 自己给 Observer定义的closeMenu()方法即可;

/**
 * Email: 2185134304@qq.com
 * Created by JackChen 2018/3/8 14:06
 * Version 1.0
 * Params:
 * Description:  子类的Adapter  -  继承基类BaseMenuAdapter  来给顶部的Tab、内容区域填充数据
*/
public class ListScreenMenuAdapter extends BaseMenuAdapter {


    private Context mContext ;

    public ListScreenMenuAdapter(Context context){
        this.mContext = context ;
    }

    private String[] mItems = {"类型","品牌","价格","更多"} ;



    @Override
    public int getCount() {
        return mItems.length;
    }


    /**
     * 获取顶部菜单标签
     * @param position
     * @param parent
     * @return
     */
    @Override
    public View getTabView(int position, ViewGroup parent) {
        // 因为布局中只有一个TextView控件,所以这里可以直接强转成 TextView
        TextView tabView = (TextView) LayoutInflater.from(mContext).inflate(R.layout.ui_list_data_screen_tab, parent, false);
        tabView.setText(mItems[position]);
        tabView.setTextColor(Color.BLACK);
        return tabView;
    }


    /**
     * 获取菜单对应的内容
     * @param position
     * @param parent
     * @return
     */
    @Override
    public View getMenuView(int position, ViewGroup parent) {
        TextView menuView = (TextView) LayoutInflater.from(mContext).inflate(R.layout.ui_list_data_screen_menu , parent , false);
        menuView.setText(mItems[position]);

        
        // 观察者设计模式的写法 ,下边的closeMenu()是 基类的BaseMenuAdapter中的方法
        menuView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                closeMenu();   // 此处相当于调用 notitySetDataChanged()方法,通知关闭
                Toast.makeText(mContext , "关闭菜单" , Toast.LENGTH_SHORT).show();
            }
        });
        return menuView;
    }


    /**
     * 关闭菜单时文字颜色设置为黑色
     * @param tabView
     */
    @Override
    public void menuClose(View tabView) {
        TextView tabTv = (TextView) tabView;
        tabTv.setTextColor(Color.BLACK);
    }


    /**
     * 打开菜单时文字颜色设置为红色
     * @param tabView
     */
    @Override
    public void menuOpen(View tabView) {
        TextView tabTv = (TextView) tabView;
        tabTv.setTextColor(Color.RED);
    }
}

这段代码中只需要去关注 menuView.setOnClickListener()方法中调用的 closeMenu()。

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

推荐阅读更多精彩内容

  • 观察者设计模式是一个好的设计模式,这个模式我们在开发中比较常见,尤其是它的变形模式订阅/发布者模式我们更是很熟悉,...
    缘自世界阅读 288评论 0 3
  • 主目录见:Android高级进阶知识(这是总目录索引)这篇文章我们会来讨论另外一个设计模式观察者设计模式,这个设计...
    ZJ_Rocky阅读 1,061评论 0 5
  • 一对一关系一对多关系优点:松耦合使用情景:气象观测站发布天气状况关注数据:WeatherData追踪目前天气(温度...
    柿籽阅读 257评论 0 1
  • 首先来看看观察者模式的概念: 定义对象间的一种一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都能...
    圈圈猫阅读 285评论 0 0
  • 5月11日 星期四 晴 今天第一节课下课,我和一大波同班同学去风铃木的下面捡果实。 原因是:...
    曾博睿阅读 493评论 0 4