android 半浮层框架

1. 前言

        相信大家都使用过支付宝或者微信等app的半浮层支付功能,本文就针对半浮层的实现进行了探讨,并尽量抽象出具有共用性的一个半浮层框架。如果你来不及看文章,就想直接撸代码,请移步https://github.com/hanxiao2018/half-float

2. 目标

2.1 效果

这里先直接了当的来描述我们要实现的效果:

(1)首先,必须是个半浮层,可以填充各种自定义视图

(2)半浮层每个视图之间可以相互切换,跳转

(3)具备一定的解耦性,每个视图只负责处理自身事件,不care其他视图事件。

3. 实现

        目标确认,ok,那就来一步一步朝目标迈进即可。本着能动手就不要yy的精神,现在就必须要开始coding...

3.1 方案

        半浮层故名思议可以理解为是一个占据手机非整个屏幕的视图,可以有很多实现方案,比如自定义特定高度的view、采用fragment、dialog等。从直观感受来看,dialog显然是具有天然实现半浮层的基因,但使用过dialog的人一定知道,dialog带来的问题还是很让人不舒服的,况且google已经推荐用另一个控件了,故本方案将不再采用dialog。至于自定义view可能会严重依赖宿主,不具备独立性,因此也放弃使用。那么还有什么可以使用呢,答案就是DialogFragment,没用过的可以去google一下,本半浮层框架就用DialogFragment作为半浮层容器。容器确定了,接下来就要确定半浮层的切换问题了。

        显然android中的viewpager可以用不同视图来进行切换,但是这个使用过程中相对繁琐,还是放弃好了,这里将采用android提供的一个自带控件 ViewFlipper,ViewFlipper底层实际上是基于FrameLayout实现的,非常方便用于不同视图的切换。没有用过的也可自行google。

        最后一个问题,不同视图之间的事件可以有自身进行处理,很好办。但是对于视图之间的交互该如何处理?举个例子,假如现在有两个浮层页面,一个是支付主页,显示了当前支付方式;另一个是支付方式页,用于选择不同的支付方式。现在我点击支付主页要选择支付方式,这个时候就要跳大支付方式页,选择之后再回到支付主页,那么支付主页的支付方式显然是需要改变的,这就涉及到了不同页面之间的事件交互。因此需要考虑到这个情况。这个实现方式有很多,比如注册回调方法,注册监听等等,然后这有不可避免的产生一定耦合,因此为避免这种情况可以采用基于事件驱动的框架,本文采用otto,没用过的可自行google。

        故,本悬浮曾框架将会基于DialogFragment + ViewFlipper + otto 进行实现。

3.2 交互

    实现的基础设施都已备好,那么剩下的就该谈下交互了。

3.2.1 事件类型

    本框架既然采用了事件机制,那么首先区分有哪几种事件

(1) 视图切换事件

        这个事件旨在触发视图切换,因为浮层容器中的不同页面会根据不同情况发生切换,比如从A页面切换到B页面,这个时间显然是业务无关的,故可以抽象出来,作为一个单独的事件。本文定义为:FlipperRequestEvent事件。

(2)具体业务事件

        这类事件就和具体的页面有关了,用于携带页面交互数据、页面视图更新等。本文定义为UpdateViewEvent。前文中我们提到采用otto来处理不同页面之间的数据交互,但是想一想,一个半浮层有一两个页面还好,如果有5个以上两两都需要交互的场景,即便是用这个框架,那页面交互来回注册接收事件也是相当的繁琐,因此必须对这一块的交互进行更为合理的优化。

3.2.2 视图控制中心

        如上面所述,为了避免页面之间来回注册接收事件的杂乱局面,本文抽象出一个消息控制中心,该中心的作用是用于事件的派发,包括视图切换事件以及具体业务事件,本位定义为:IViewController。这控制中心用于事件的派发,进而避免交互消息的杂乱无章;

3.2.3 事件的数据结构

    前文提到了两种事件:FlipperRequestEvent和UpdateViewEvent,那么其数据结构该怎么设计呢?

(1)FlipperRequestEvent

    这个事件数据结构相对简单,因为要跳转,就必须要告知控制中心跳转到那一页,因此其数据结构如下:

        public class FlipperRequestEvent {

            public final boolean showNext;//方向标识,true则展示下一页,否则展示前一页

            public final int whichChild;//子view在半浮层中的位置,从0开始,这个下文会介绍

            public FlipperRequestEvent(int whichChild,boolean showNext) {

            this.whichChild = whichChild;

            this.showNext = showNext;

    }    }

(2)UpdateViewEvent

    UpdateViewEvent可要比FlipperRequestEvent复杂多了,因为浮层中的页面五花八门,各种数据完全不一样,怎么设计能达到一种通用性呢?这里就必须用到泛型了,因此其数据结构设计如下:

        public class UpdateViewEvent {

            public int whichChild;//更新那个子页面

            public String arg1;//提供预置的简单参数便于视图更新

            public String arg2;

            public boolean flag1;//提供预置的boolean标识判断

            public boolean flag2;

            public T obj;//真正的页面数据

            public UpdateViewEvent(int whichChild) {

            this.whichChild = whichChild;

}}

    事实上,这里认为交互双方“知此知彼”的默契才这样设计的,也就是说实际上发送方永远会知道接收方需要什么数据。

3.2.4 注册中心

        ok,前面事件封装中都有whichChild这个属性,那么这个是干什么的?其实这个就是用于描述页面在半浮层中位置的。本文设计了一个注册中心,定义为ChildIndex,所有的半浮层页面都必须在此进行注册,以便容器能感知其位置,进而根据事件(数据结构中的whichChild)来定位目标页面。那这么说,发送方必须要知道接收方的index索引了?当然不需要,如果真是靠0、1、2这种索引来进行页面的定位,那真是太不人性化了,事实上注册中心保留的是有意义的符号,比如我当前半浮层有三个页面,那么在注册中心注册如下:

        public final class ChildIndex {

            public static final int DEMO_FLIPPER_PAGE0 =0;

            public static final int DEMO_FLIPPER_PAGE1 =1;

            public static final int DEMO_FLIPPER_PAGE2 =2;

}

    是的,就这么简单。

3.3 实现

3.3.1 抽象半浮层容器

        首先我们抽象出一个容器基类,用于提供容器的窗口这是以及切换配置。

        //BaseDialogFragment 是个抽象容器,基于此可以实现不同的浮层容器

        public abstract class BaseDialogFragment extends DialogFragment {

            private FrameLayout mView;//半浮层视图

            protected ViewFlipper mViewFlipper;//视图载体

            @Nullable

            @Override

            public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle                 savedInstanceState) {

            //这个view_flipper_base_layout其实就是个ViewFilpper

            mView = (FrameLayout) inflater.inflate(R.layout.view_flipper_base_layout,null);

            mViewFlipper = (ViewFlipper)mView.findViewById(R.id.view_flipper);

            //getContentLayout是由具体容器来进行布局的,所以是个抽象方法。这个容器最重要的实现也就是这句了~

            inflater.inflate(getContentLayout(),mViewFlipper,true);

            return mView;

        }

        @Override

        public void onActivityCreated(Bundle savedInstanceState) {

            super.onActivityCreated(savedInstanceState);

            initDialog(getDialog());

           }

        private void initDialog(Dialog dialog) {

            dialog.setCanceledOnTouchOutside(false);

            //对半浮层窗口属性进行设置,这里其实限定了半浮层必须位于底部,这也是常见的场景,事实上可以对这个位置进行抽象

            Window window = dialog.getWindow();

            window.setGravity(Gravity.BOTTOM);

            window.setWindowAnimations(getStyle());

            window.setLayout(WindowManager.LayoutParams.MATCH_PARENT,WindowManager.LayoutParams.MATCH_PARENT);

            window.setBackgroundDrawable(new ColorDrawable());

        }

           这样就初始化了半浮层基类容器。这个容器设置了半浮层窗口的位置、style等。然后,最重要的是抽象了一个布局接口getContentLayout,用于自定义半浮层视图布局。

3.3.2 具体半浮层容器

        //实现抽象容器

        public class ConcreteDialogFragment extends BaseDialogFragment {

            @Override

             public void onCreate(Bundle savedInstanceState) {

                   super.onCreate(savedInstanceState);

                   BusHolder.getBus().register(this);//解耦的bus事件来了,下面会介绍

              }

            //@Subscribe是otto的语法,不了解的自行google。这里receiveFlipperRequestEvent

            @Subscribe

            public void receiveFlipperRequestEvent(FlipperRequestEvent event) {

            if (event.showNext) {//是否发生切换

                //根据索引定位目标页面

                setNextDisplayedChild(event.whichChild);

                return;

            }

            if (mViewFlipper.getChildAt(0) ==mViewFlipper.getCurrentView()) {

                dismiss();

                return;

            }

            setPreviousDisplayedChild(event.whichChild);

      }

            @Subscribe

            public void updateView(UpdateViewEvent event) {

            //视图控制中心进行不同页面之间的事件派发,目标页面接收事件后根据需要进行视图更新或其他处理。

            IViewController viewController = (IViewController) getChildViewInViewFlipper(event.whichChild);

            viewController.updateView(event);

        }

            @Override

            public void onPause() {

                super.onPause();

            //此处用来清除每次pause后的弹出动画,如果需要每次都展示进入动画,则可以屏蔽该代码

                if (getDialog() !=null) {

                    getDialog().getWindow().setWindowAnimations(R.style.window_exit_style);

                }

    }

        @Override

        protected int getContentLayout() {//这就是具体的布局实现

                    return R.layout.demo_base_container;//可参见文章给出的git源码

        }

       @Override

    public void onDestroy() {

            super.onDestroy();

            BusHolder.getBus().unregister(this);

}}

3.3.3 bus 封装

        这里对otto的Bus实例进行了一层包裹,确保其为单例即可,因为只需要一个实例就可完成不同业务方的注册、注销。单例相比大家都会写,但是一个相对规范、线程安全的单例却不太容易,本文实现如下(可供参考):

        public class BusHolder {

            private BusHolder() {

            }

            public static final Bus getBus() {

            return BusInstance.sBus;

            }

            private static class BusInstance {

                private static Bus sBus =new Bus();

            }}

        ok,到这里整个框架就实现完了,接下来的测试我就不在这里贴出来了,参见git地址即可:https://github.com/hanxiao2018/half-float

简书地址:https://www.jianshu.com/p/293b051985e3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值