Material|Material Design 之 Behavior的使用和自定义Behavior

写在前面
Material 系列文章:
Material Design 之 Toolbar 开发实践总结
Material Design之 AppbarLayout 开发实践总结
前面两篇文章讲了Toolbar 和 AppbarLayout 相关的东西,还没看过的同学可以去看看。前面我们说过,CoordinatorLayout很强大,它可以协调子View的交互动作,那么CoordinatorLayout它是怎么协调子View的呢?其实核心就是Behavior。那么今天讲的就是这个很重要的东西-Behavior,在上面篇文章中,我们其实已经看到过Behavior这个东西了,在AppbarLayout 与NestedScrollView 联动的时候,我们为NestedScrollView设置了一个Behavior, 通过app:layout_behavior="@string/appbar_scrolling_view_behavior",它的值是一个类的全路径,这个Behavior 是Google已经为我们提供的,AppbarLayout的内部类,专门用于处理可滚动View(如:ScrollView、RecyclerView) 与AppbarLayout 联动的。那么这篇文章我们通过介绍Google提供的一些Behavior 的使用场景、使用方式和自定义Behavior 来熟悉和掌握 Behavior。
本文目录:

  • Behavior 介绍
  • BottomSheetBehavior/BottomSheetDialog 的使用
  • SwipeDissmissBehavior 的使用
  • 自定义 Behavior
正文
1,Behavior 介绍 看一下官方的介绍:Interaction behavior plugin for child views of CoordinatorLayout. 作用于CoordinatorLayout的子View的交互行为插件。一个Behavior 实现了用户的一个或者多个交互行为,它们可能包括拖拽、滑动、快滑或者其他一些手势。
Behavior 是一个顶层抽象类,其他的一些具体行为的Behavior 都是继承自这个类。它提供了几个重要的方法:
  • layoutDependsOn
  • onDependentViewChanged
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll
  • onLayoutChild
解释一下上面几个方法和它们的调用时机:
/** * 表示是否给应用了Behavior 的View 指定一个依赖的布局,通常,当依赖的View 布局发生变化时 * 不管被被依赖View 的顺序怎样,被依赖的View也会重新布局 * @param parent * @param child 绑定behavior 的View * @param dependency依赖的view * @return 如果child 是依赖的指定的View 返回true,否则返回false */ @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return super.layoutDependsOn(parent, child, dependency); }/** * 当被依赖的View 状态(如:位置、大小)发生变化时,这个方法被调用 * @param parent * @param child * @param dependency * @return */ @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { return super.onDependentViewChanged(parent, child, dependency); }/** *当coordinatorLayout 的子View试图开始嵌套滑动的时候被调用。当返回值为true的时候表明 *coordinatorLayout 充当nested scroll parent 处理这次滑动,需要注意的是只有当返回值为true *的时候,Behavior 才能收到后面的一些nested scroll 事件回调(如:onNestedPreScroll、onNestedScroll等) *这个方法有个重要的参数nestedScrollAxes,表明处理的滑动的方向。 * * @param coordinatorLayout 和Behavior 绑定的View的父CoordinatorLayout * @param child和Behavior 绑定的View * @param directTargetChild * @param target * @param nestedScrollAxes 嵌套滑动 应用的滑动方向,看 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, *{@link ViewCompat#SCROLL_AXIS_VERTICAL} * @return */ @Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); }/** * 嵌套滚动发生之前被调用 * 在nested scroll child 消费掉自己的滚动距离之前,嵌套滚动每次被nested scroll child * 更新都会调用onNestedPreScroll。注意有个重要的参数consumed,可以修改这个数组表示你消费 * 了多少距离。假设用户滑动了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90, * 这样coordinatorLayout就能知道只处理剩下的10px的滚动。 * @param coordinatorLayout * @param child * @param target * @param dx用户水平方向的滚动距离 * @param dy用户竖直方向的滚动距离 * @param consumed */ @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); }/** * 进行嵌套滚动时被调用 * @param coordinatorLayout * @param child * @param target * @param dxConsumed target 已经消费的x方向的距离 * @param dyConsumed target 已经消费的y方向的距离 * @param dxUnconsumed x 方向剩下的滚动距离 * @param dyUnconsumed y 方向剩下的滚动距离 */ @Override public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); }/** *嵌套滚动结束时被调用,这是一个清除滚动状态等的好时机。 * @param coordinatorLayout * @param child * @param target */ @Override public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) { super.onStopNestedScroll(coordinatorLayout, child, target); }/** * onStartNestedScroll返回true才会触发这个方法,接受滚动处理后回调,可以在这个 * 方法里做一些准备工作,如一些状态的重置等。 * @param coordinatorLayout * @param child * @param directTargetChild * @param target * @param nestedScrollAxes */ @Override public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); }/** * 用户松开手指并且会发生惯性动作之前调用,参数提供了速度信息,可以根据这些速度信息 * 决定最终状态,比如滚动Header,是让Header处于展开状态还是折叠状态。返回true 表 * 示消费了fling. * * @param coordinatorLayout * @param child * @param target * @param velocityX x 方向的速度 * @param velocityY y 方向的速度 * @return */ @Override public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) { return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); }//可以重写这个方法对子View 进行重新布局 @Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { return super.onLayoutChild(parent, child, layoutDirection); }

以上就是Behavior的一些重要方法,当我们要自定义一个Behavior的时候,就会去重写上面的一些方法。自定义Behavior 会放在文章最后讲。对Behavior 有了一些了解后,接下来我们看一下Google给我提供了一些特殊场景的Behavior。
2,BottomSheetBehavior/BottomSheetDialog 的使用 BottomSheetBehavior 实现的效果在我们的项目中用的比较多,它就是从底部弹出一个布局,在很多的应用中,分享功能都有这样一个交互。在以前我们通常都是用PopupWindow来搞定,前面也写了一篇文章了,关于PupupWindow的使用和封装,通用PopupWindow,几行代码搞定PopupWindow弹窗,有了BottomSheetBehavior 实现起来就简单一点了。请看效果图:
bottomSheetBehavior.gif 看看怎么用BottomSheetBehavior:
1,在xml布局文件中为需要从底部弹出的布局绑定BottomSheetBehavior,代码如下:

注意上面这行代码: app:behavior_peekHeight="0dp",peekHeight 属性是设置bottomSheet 折叠时的高度,我们设置为0表示折叠的时候完全隐藏,默认情况时显示布局的高度,布局会显示在界面,所以,如果要一开始布局不显示在界面上的话,需要将peekHeight 设置为0。也可以在代码中设置, 通过sheetBehavior.setPeekHeight(0)。
2,在代码中获取到与布局相关联的BottomSheetBehavior,设置展开与折叠的状态就可以了,BottomSheetBehavior有5种状态:
1, STATE_EXPANDED 展开状态,显示完整布局。
2,STATE_COLLAPSED 折叠状态,显示peekHeigth 的高度,如果peekHeight为0,则全部隐藏,与STATE_HIDDEN效果一样。
3,STATE_DRAGGING 拖拽时的状态
4,STATE_HIDDEN 隐藏时的状态
5,STATE_SETTLING 释放时的状态
看代码:
View shareView = findViewById(R.id.share_view); //获取BottomSheetBehavior final BottomSheetBehavior sheetBehavior = BottomSheetBehavior.from(shareView); //设置折叠时的高度 //sheetBehavior.setPeekHeight(BottomSheetBehavior.PEEK_HEIGHT_AUTO); //监听BottomSheetBehavior 状态的变化 sheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) {}@Override public void onSlide(@NonNull View bottomSheet, float slideOffset) {} }); //下滑的时候是否可以隐藏 sheetBehavior.setHideable(true); findViewById(R.id.btn_show_bottom_sheet).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(sheetBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED){ sheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); }else { sheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN); }} });

代码很简单,重要的就是通过方法 sheetBehavior.setState()来改变状态,是显示还是隐藏。其他的几个方法都添加了注释,不用多讲。
2.1, BottomSheetDialog 上面说了BottomSheetBehavior, 接下来看一下BottomSheetDialog, 一看名字就知道,它就是一个Dialog,使用方法和Dialog 一样,它是对BootomSheetBehavior 进行了包装,从底部弹出一个Dialog。BottomSheetDialog 使用起来比BottomSheetBahvior更方便,效果更佳。看一下它的源码也非常简单,就是Dialog 显示的布局绑定了BottomSheeBehavior,源码如下:
private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) { final CoordinatorLayout coordinator = (CoordinatorLayout) View.inflate(getContext(), R.layout.design_bottom_sheet_dialog, null); if (layoutResId != 0 && view == null) { view = getLayoutInflater().inflate(layoutResId, coordinator, false); } FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet); mBehavior = BottomSheetBehavior.from(bottomSheet); mBehavior.setBottomSheetCallback(mBottomSheetCallback); mBehavior.setHideable(mCancelable); if (params == null) { bottomSheet.addView(view); } else { bottomSheet.addView(view, params); } // We treat the CoordinatorLayout as outside the dialog though it is technically inside coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) { cancel(); } } }); return coordinator; }

就这样一个方法,获取到Behavior,设置了一个监听状态的回调,设置了下滑可以隐藏。然后将Dialog 显示的布局添加到了绑定了BottomSheetBehavior 的ViewGroup 里。这个方法在setContent()方法被调用:
@Override public void setContentView(View view) { super.setContentView(wrapInBottomSheet(0, view, null)); }@Override public void setContentView(View view, ViewGroup.LayoutParams params) { super.setContentView(wrapInBottomSheet(0, view, params)); }

接下来看一下使用方法,非常简单,以网易云音乐的歌单和分享UI为例:
网易云音乐歌单UI效果 如下:
Material|Material Design 之 Behavior的使用和自定义Behavior
文章图片
网易云音乐歌单.png 来张gif图效果更清楚:
网易云音乐效果图.gif 本文通过BottomSheetDialog 实现的效果图如下:
bottomSheetDialog.gif 歌单代码如下:
private void showBottomSheetDialog(){ BottomSheetDialog dialog = new BottomSheetDialog(this); View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_dialog,null); handleList(view); dialog.setContentView(view); dialog.setCancelable(true); dialog.setCanceledOnTouchOutside(true); dialog.show(); }private void handleList(View contentView){ RecyclerView recyclerView = (RecyclerView) contentView.findViewById(R.id.recyclerView); LinearLayoutManager manager = new LinearLayoutManager(this); manager.setOrientation(LinearLayoutManager.VERTICAL); recyclerView.setLayoutManager(manager); MusicAdapter adapter = new MusicAdapter(); recyclerView.setAdapter(adapter); adapter.setData(mockData()); adapter.notifyDataSetChanged(); }

分享代码如下:
/** * share Dialog */ private void showShareDialog(){ if(mBottomSheetDialog == null){ mBottomSheetDialog = new BottomSheetDialog(this); View view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_share_dialog,null); mBottomSheetDialog.setContentView(view); mBottomSheetDialog.setCancelable(true); mBottomSheetDialog.setCanceledOnTouchOutside(true); // 解决下滑隐藏dialog 后,再次调用show 方法显示时,不能弹出Dialog View view1 = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet); final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view1); bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { Log.i("BottomSheet","onStateChanged"); mBottomSheetDialog.dismiss(); bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } }@Override public void onSlide(@NonNull View bottomSheet, float slideOffset) {} }); }else{ mBottomSheetDialog.show(); }}

代码很简单,和其他普通Dialog的用法一样。值的主意的一点是这里有个bug ,那就是当你下滑隐藏了Dialog 之后,下次直接调用show方法来显示Dialog时(没有重新new 的情况下),Dialog不能显示,原因是因为BottomSheetDialog 源码中,关闭的Dialog 是依赖BottomSheetBehavior 的,当下滑隐藏的时候,BottomSheet的状态也为STATE_HIDDEN,并且同时dismiss Dialog,下次show 的时候,是没有办法显示一个状态为STATE_HIDDEN 的布局的。 网上搜了一下,有很多人都碰到过,解决方法来自这篇文章Material之Behavior实现支付宝密码弹窗 仿淘宝/天猫商品属性选择, 解决思路:获取到BottomSheetDialog 的布局,然后拿到绑定的BottomSheetBehavior,重新设置监听,在调用dismiss 方法时,我们重新设置一些Behavior 的状态。代码如下:
// 解决下滑隐藏dialog 后,再次调用show 方法显示时,不能弹出Dialog View view1 = mBottomSheetDialog.getDelegate().findViewById(android.support.design.R.id.design_bottom_sheet); final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(view1); bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { Log.i("BottomSheet","onStateChanged"); mBottomSheetDialog.dismiss(); bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } }@Override public void onSlide(@NonNull View bottomSheet, float slideOffset) {} });

以上就是BottomSheetBehavior 和BottomSheetDialog 的用法。
3,SwipeDissmissBehavior 的使用 上面讲了BottomSheetBehavior 和BottomSheetDialog 的用法,接下来看另一种场景的Behavior-SwipeDissmissBehavior,叫滑动消失或者滑动关闭,这个Behavior 在我们项目中用得可能就不是很多了。有个场景就是Snackbar的使用了,Android 5.0 以上 ,增加了Snackbar提示消息,Snackbar 的Behavior 的就是 SwipeDissmissBehavior 的应用,当滑动Snackbar 的时候,Snackbar 消失,效果如下:
snackbar的behavir.gif 使用也非常简单,在代码中只接new 一个SwipeDismissBehavior,设置一些属性后,添加到CoordinatorLayout.LayoutParams,代码如下:
mSwipeLayout = findViewById(R.id.swipe_layout); SwipeDismissBehavior swipe = new SwipeDismissBehavior(); /** * //设置滑动的方向,有3个值 * * 1,SWIPE_DIRECTION_ANY 表示向左像右滑动都可以, * 2,SWIPE_DIRECTION_START_TO_END,只能从左向右滑 * 3,SWIPE_DIRECTION_END_TO_START,只能从右向左滑 */ swipe.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); swipe.setStartAlphaSwipeDistance(0f); swipe.setSensitivity(0.2f); swipe.setListener(new SwipeDismissBehavior.OnDismissListener() { @Override public void onDismiss(View view) { Log.e(TAG,"------>onDissmiss"); }@Override public void onDragStateChanged(int state) { Log.e(TAG,"------>onDragStateChanged"); } }); CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) mSwipeLayout.getLayoutParams(); if(layoutParams!=null){ layoutParams.setBehavior(swipe); }

有两个重要的方法,wipe.setSwipeDirection设置滑动方向,有三个取值,上面已经注释,不过多解释,还有就是swipe.setListener可以监听dissmiss 和状态改变,在这些回调里面可以做一些自己的逻辑。最后效果图:
swipeDissmissBehavir.gif 4,自定义Behavior 上面讲了Google 为我们提供的一些场景使用的Behavior,当然还有一些Google 提供的一些组件使用的Behavior,AppbarLayout内部的Behavior,如专门协调 AppbarLayout 与可滚动View(NestedScrollView,RecyclerView )的, FloatActionButton内部的Behavior ,协调和Snackbar 的关系,保证Snackbar 弹出的时候不被FAB 遮挡。还有就是上面说的Snackbar内部的Behavior 等等。但是有时候,要实现多个View之间的的交互时,我们可以自定义Behavior ,下面就说说怎么自定义一个Behavior。
自定义Behavior 最关键的就是文章第一部分介绍的Behavior 提供的那一些方法,忘了的请到回去看一下第一部分的方法注释。自定义Behavior 分为两种:
  • 第一种是通过监听一个View的状态,如位置、大小的变化,来改变其他View的行为,这种只需要重写2个方法就可以了,分别是layoutDependsOnonDependentViewChanged, layoutDependsOn方法判断是指定依赖的View时,返回true,然后在onDependentViewChanged 里,被依赖的View做需要的行为动作。
  • 第二种就是重写onStartNestedScrollonNestedPreScrollonNestedScroll等一系列方法,前面第一步分已经讲过。
上面两种方法相比,第一种很简单,第二种复杂一些,但是第二种实现的效果也要复杂。下面就以开眼首页的滑动Header效果为例,来实现一个自定义的Behavior。开眼首页滑动header效果如下:

开眼首页效果.gif 效果如上:就是列表滑动的时候是覆盖Header(不是Header缩小,Header没动),然后就是Header有一个alpha 的变化。
1,首先是整个布局,Header 固定在顶部,列表在Header 的下方,CoordinatorLayout 是一个FrameLayout,不能提供这样的布局,我们需要重写onLayoutChild 来让列表位于Header下面:
@Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { Log.i(TAG,"onLayoutChild....."); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams(); if(params!=null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT){ child.layout(0,0,parent.getWidth(),parent.getHeight()); child.setTranslationY(getHeaderHeight()); return true; }return super.onLayoutChild(parent, child, layoutDirection); }

我们需要知道Header的高度,将Header的高度写在dimens文件中,getHeaderHeight()方法如下:
/** * 获取Header 高度 * @return */ public int getHeaderHeight(){ return MaterialDesignSimpleApplication.getAppContext().getResources().getDimensionPixelOffset(R.dimen.header_height); }

2,当开始滑动的时候,利用setTranslationY 来移动列表,知道完全盖住header ,这是时候,列表就不移动了,只是列表的滑动了。当下滑到顶端的时候,又将列表向下滑动,直到header 完全显示,思路就是这样。开眼的首页向上滑洞的时候,Header 有一个alpha的变化,本例子没有实现,其实也很简单,只要重写onDependentViewChanged方法,在里面根据滑动距离算出alpha 变化的值就可以了。自定义Behavior 完整代码如下:
/** * *自定义Behavior :实现RecyclerView(或者其他可滑动View,如:NestedScrollView) 滑动覆盖header 的效果 * Created by zhouwei on 16/12/19. */public class CoverHeaderScrollBehavior extends CoordinatorLayout.Behavior { public static final String TAG = "CoverHeaderScroll"; public CoverHeaderScrollBehavior(Context context, AttributeSet attributeSet){ super(context,attributeSet); }@Override public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) { Log.i(TAG,"onLayoutChild....."); CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams(); if(params!=null && params.height == CoordinatorLayout.LayoutParams.MATCH_PARENT){ child.layout(0,0,parent.getWidth(),parent.getHeight()); child.setTranslationY(getHeaderHeight()); return true; }return super.onLayoutChild(parent, child, layoutDirection); }@Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }@Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); // 在这个方法里面只处理向上滑动 if(dy < 0){ return; }float transY =child.getTranslationY() - dy; Log.i(TAG,"transY:"+transY+"++++child.getTranslationY():"+child.getTranslationY()+"---->dy:"+dy); if(transY > 0){ child.setTranslationY(transY); consumed[1]= dy; } }@Override public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); // 在这个方法里只处理向下滑动 if(dyUnconsumed >0){ return; }float transY = child.getTranslationY() - dyUnconsumed; Log.i(TAG,"------>transY:"+transY+"****** child.getTranslationY():"+child.getTranslationY()+"--->dyUnconsumed"+dxUnconsumed); if(transY > 0 && transY < getHeaderHeight()){ child.setTranslationY(transY); } }/** * 获取Header 高度 * @return */ public int getHeaderHeight(){ return MaterialDesignSimpleApplication.getAppContext().getResources().getDimensionPixelOffset(R.dimen.header_height); }}

xml 的代码如下:

最后实现的效果如下:
仿开眼首页效果.gif 最后
以上就是关于Behavior 的全部内容,自定义Behavior 这一块,特别是处理滑动嵌套对于刚接触的同学来说还是挺难的,不过当掌握了之后,我们能做出很多炫酷的效果。所以,再困难也值得花时间去学习。本文到此结束,如有问题,欢迎交流。所有关于Material Design 的使用示例都在这里:MaterialDesignSamples
【Material|Material Design 之 Behavior的使用和自定义Behavior】参考资料:
1,自定义Behavior的艺术探索-仿UC浏览器主页
2,使用 CoordinatorLayout 实现复杂联动效果
3,Material之Behavior实现支付宝密码弹窗 仿淘宝/天猫商品属性选择

    推荐阅读