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