PopupWindow 的根本构造是
1 2
| public PopupWindow(View contentView, int width, int height, boolean focusable)
|
本质上是利用 WindowManager 将 View 添加到屏幕上去,这也是它为何有包括 内容布局,尺寸,是否聚焦,Transition动画,BackgroundDrawable,Elevation,AnimationStyle等在内众多参数的原因,这些参数为 WindowManager.LayoutParams 所必须。
值得一提的是动画类型 AnimationStyle 是 style定义的,如果要改变动画类型,亦需要提供 style 资源。
1 2 3 4
| <style name="popupwindow_anim_style"> <item name="android:windowEnterAnimation">@android:anim/fade_in</item> <item name="android:windowExitAnimation">@android:anim/fade_out</item> </style>
|
PopupWindow 在屏幕上显示的方法如下
1 2
| void showAtLocation(View parent, int gravity, int x, int y)
|
应该注意的是内容控件被套了两层 FrameLayout 的包装,第一层设置 BackgrondDrawable,第二场才被设置 WindowManager.LayoutParams 参数并添加到屏幕上去。
也可以使用下列方法来显示,不过期参数已经指向左上角
1
| public void showAsDropDown(View anchor)
|
动画效果
设置 AnimationStyle 可以达到动画效果
1 2 3 4
| <style name="PopupW"> <item name="android:windowEnterAnimation">@anim/slide_in_left</item> <item name="android:windowExitAnimation">@anim/slide_out_right</item> </style>
|
在 23 以上,还可以采用Transition 达到动画效果
1 2
| p.setEnterTransition(new TransitionSet().addTransition(new Slide(Gravity.BOTTOM))); p.setExitTransition(new TransitionSet().addTransition(new Slide(Gravity.RIGHT)));
|
菜单
菜单包括如活动中的Option和Context菜单,Toolbar上的菜单,Navi上的菜单以及 PopupMenu 等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <item android:title="更多"> <menu> <item android:title="设置"/> <item android:title="关于"/> </menu> <item android:id="@+id/action_search" android:icon="@drawable/ic_action_search" android:title="@string/action_search" app:showAsAction="collapseActionView|always|withText" app:actionViewClass="android.support.v7.widget.SearchView" /> </item>
|
MenuInflater 类负责将XML文件实例化为 Menu 对象,其对于 Menu ,Group,SubMenu 等标签有不同的解析方法。
菜单中的 ActionView,可以改变菜单外观,如搜索框中所含的 SearchView 控件
1 2 3 4
| MenuItem searchItem = menu.findItem(R.id.action_search); SearchView searchView = (SearchView) searchItem.getActionView(); MenuItemCompat.setOnActionExpandListener(searchItem, ...); MenuItemCompat.expandActionView(searchItem);
|
你也可以主动为菜单设置 ActionView。
PopupMenu 内部有一个 Menu 对象,将xml资源加载到该对象上,显示在锚定控件下方,可以指定对齐方式。
1 2 3 4
| PopupMenu popupMenu = new PopupMenu(this, textView); popupMenu.getMenuInflater().inflate(R.menu.main, popupMenu.getMenu()); textView.setOnTouchListener(popupMenu.getDragToOpenListener()); popupMenu.show();
|
1.是否永久显示 overflow菜单?
使用反射修改 ViewConfiguration 的 sHasPermanentMenuKey 参数既可以永久显示。
2.获取并修改 Overflow 按钮
a.可以使用主题修改
1 2 3 4 5 6
| <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="android:actionOverflowButtonStyle">@style/OverflowStyle</item> </style> <style name="OverflowStyle"> <item name="android:src">@android:drawable/arrow_up_float</item> </style>
|
b.可以使用代码修改
1 2 3 4 5
| ArrayList<View> outViews = new ArrayList<View>(); String overflowDescription = activity.getString("more options"); decorView.findViewsWithText(outViews, overflowDescription, View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION); TintImageView overflow=(TintImageView) outViews.get(0); overflow.setImageResource(imageID);
|
Dialog
实际上 Activity 只要使用 Dialog 主题就可以看做Dialog,但 Dialog 实际上是没有生命周期的。
主要的话框是 AlertDialog,以及较新的 DialogFragment 和 BottomDialog ,也包括一些特定功能的对话框如 ProgressDialog,颜色,日期等。
AlertDialog 只不过预设了大量布局且可以根据构造方法来选择控件而已,这里提一下单选和多选对话框的使用。创建时应使用适配器,这样可以在对话框上添加条目
1
| Builder setAdapter(final ListAdapter adapter, final OnClickListener listener)
|
ProgressDialog 的实现原理就是内置了一个 ProgressDialog 。
BottomSheetDialog
使用方法如下
1 2 3
| BottomSheetDialog dialog=new BottomSheetDialog(this); dialog.setContentView(dialogView); dialog.show();
|
这里设置布局时加入布局时做了一番手脚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) { CoordinatorLayout coordinator = inflate(context, R.layout.design_bottom_sheet_dialog, null); FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet); mBehavior = BottomSheetBehavior.from(bottomSheet); mBehavior.setBottomSheetCallback(mBottomSheetCallback); bottomSheet.addView(view, params); coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) { cancel(); } } }); return coordinator; }
|
这里设置布局后,将载入一个 CoordinatorLayout,其中有一个 FrameLayout,配置 BottomSheetBehavior,并将内容添加进去。
加载的主题是 Window.FEATURE_NO_TITLE
,即没有标题的对话框。
1
| private BottomSheetBehavior<FrameLayout> mBehavior;
|
DialogFragment
使用 DialogFragment 是因为 Dialog 无生命周期,在横竖屏切换时会导致丢失对话框,而 DialogFragment 本质是一个带 Dialog 对象的碎片,能够恢复和重建 。
实际上它不需要重写 onCreateView 方法来建立视图,只需要重写下列方法建立 Dialog 对象即可,真正显示的是 Dialog 对象而不是视图。
1
| Dialog onCreateDialog(Bundle savedInstanceState)
|
而后我们使用下列方法显示对话框
1 2
| void show(FragmentManager manager, String tag) int show(FragmentTransaction transaction, String tag)
|
这实际是添加到任务栈,用第二个方法你可以的碎片在栈中的 id。这样碎片就会追随所依附活动的状态,一旦碎片处于 onStart 状态 Dialog 就会显示,而 碎片处于 onStop 状态时 Dialog 就会隐藏(不是dismiss)。
如果突然关闭所在页面,活动销毁会导致碎片销毁视图,经历 DestroyView 状态,则 Dialog 消失(即dismiss),接着会触发碎片的销毁,一般而言是将碎片出栈,以释放内存。
此外下列方法也将完成碎片的释放
问题1:显示的是 Dialog 还是 View?
下列方法被改写了,这在创建视图时也影响了 Dialog 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { if (!mShowsDialog) { return super.getLayoutInflater(savedInstanceState); } mDialog = onCreateDialog(savedInstanceState); if (mDialog != null) { setupDialog(mDialog, mStyle); return (LayoutInflater) mDialog.getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); } return (LayoutInflater) mHost.getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); }
|
首先 mShowsDialog 标记了是否显示对话框,如果其为 false,那么正常加载视图进行显示。
而后创建 Dialog 对象,在 onActivityCreated 方法中将决定显示哪个?如果同时设置了 View,则将 View 作为 Dialog 的内容视图,此外还要给 Dialog 打上 dismiss 监听器,保证碎片的释放。
因此如果想采用所创建的对话框,第一要保证 mShowsDialog 标记为真,第二不要画蛇添足去实现 onCreateView 方法。
更改 Dialog 主题
以下是V7包里 AlertDialog 的实际布局,实际是由 标题块(带分割线),message块,自定义块和按钮块 组成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <LinearLayout android:id="@+id/parentPanel"> <LinearLayout android:id="@+id/topPanel"> <LinearLayout android:id="@+id/title_template"> <ImageView android:id="@+id/icon" android:src="@drawable/ic_dialog_info" /> <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle" style="?android:attr/textAppearanceLarge" android:singleLine="true" /> </LinearLayout> <ImageView android:id="@+id/titleDivider" android:src="@android:drawable/divider_horizontal_dark" /> </LinearLayout> <LinearLayout android:id="@+id/contentPanel> <ScrollView android:id="@+id/scrollView"> <TextView android:id="@+id/message" style="?android:attr/textAppearanceMedium" /> </ScrollView> </LinearLayout> <FrameLayout android:id="@+id/customPanel"> <FrameLayout android:id="@+android:id/custom" /> </FrameLayout> <LinearLayout android:id="@+id/buttonPanel" > <LinearLayout style="?android:attr/buttonBarStyle"> <LinearLayout android:id="@+id/leftSpacer" android:layout_weight="0.25" android:layout_width="0dip" android:layout_height="wrap_content" android:orientation="horizontal" android:visibility="gone" /> <Button android:id="@+id/button1" android:layout_width="0dip" android:layout_weight="1" style="?android:attr/buttonBarButtonStyle" /> <Button android:id="@+id/button3" android:layout_width="0dip" android:layout_gravity="center_horizontal" android:layout_weight="1" style="?android:attr/buttonBarButtonStyle" /> <Button android:id="@+id/button2" android:layout_width="0dip" android:layout_weight="1" style="?android:attr/buttonBarButtonStyle" /> <LinearLayout android:id="@+id/rightSpacer" android:layout_width="0dip" android:layout_weight="0.25" android:visibility="gone" /> </LinearLayout> </LinearLayout> </LinearLayout>
|
想要修改主题可以用如下构造方法
1
| new AlertDialog.Builder(this, R.style.Test);
|
这里可以传入主题,也可以传入样式,其中主题 id 均大于 0x01000000,如果是样式,则将样式应用到主题上。
1
| context.getTheme().resolveAttribute(R.attr.alertDialogTheme, outValue, true);
|
Dialog 的扩展:布局定制和动画效果
Dialog 的扩展主要包括两个方面:布局定制和动画效果。
想实现 Dialog 的入场动画效果,最简单的方法如下
1 2 3 4 5 6 7
| dialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface d) { View view = dialog.getWindow().getDecorView(); view.animate().rotation(360).setDuration(2000).start(); } });
|
这种方法即在 DecorView 做动画,但局限性很大。
更一般的方式是定制一个布局,或像 BottomSheetDialog 那样,对传入的布局采用 wrap 操作;最终留出根布局的接口,以便对此进行动画。
布局定制的核心是改造 onCreate 方法
1 2 3 4 5 6
| @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.app_bar_main); }
|
动画效果的核心是设置具体的动画效果,对于显示动画,可以放在 OnShowListener 接口中,因为该接口执行的时候,布局已经添加到 Dialog 中去了,但对于消失动画而已,则无法利用 onDiamissListener 接口,因此此接口执行时,布局已经被移除。
此时,可以通过改造 dismiss 方法来实现消失动画。
1 2 3 4 5 6 7 8 9 10 11 12
| @Override public void dismiss() { animatorSet = new AnimatorSet(); setupAnim(); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationEnd(Animator animation) { AnimDialog.super.dismiss(); } }); animatorSet.resume(); }
|
这个方法一定要具体的子类( 此处是AnimDialog)来执行,不能交给父类AlertDialog执行,因此创建一个构造方法
1 2 3
| public static AnimDialog create(Context context) { return new AnimDialog(context); }
|
动画的具体效果
rotationY 是绕着y轴旋转的,为了动画之后显示正常,结束值应该设置为0。起始值如果为正,就是侧对着的感觉,有“掀开”的感觉。
1
| ObjectAnimator.ofFloat(view, "rotationY", -90, 0).setDuration(mDuration)
|
rotation 是绕着z轴旋转的,所有旋转操作的中心点都在中心处。
translationX等是平移的效果,改变方向可以取得从四边滑入的效果,也可以取得震动效果
1
| ObjectAnimator.ofFloat(view, "translationX", 0, .10f, -25, .26f, 25,.42f, -25, .58f, 25,.74f,-25,.90f,1,0)
|
scaleX等可以取得展开和收缩的效果。
复合效果1:左侧旋转滚入,一般而言此类效果的方向是垂直的,从左侧滚入,那么旋转就是绕Y轴的
1
| ObjectAnimator.ofFloat(view, "rotationY", 90, 0).setDuration(2000),
|
复合效果2:弹出展开与收缩展开
1 2 3 4
| ObjectAnimator.ofFloat(view, "rotationY", 90,88,88,45,0).setDuration(2000), ObjectAnimator.ofFloat(view, "alpha", 0,0.4f,0.8f, 1).setDuration(2000*3/2), ObjectAnimator.ofFloat(view, "scaleX", 0,0.5f, 0.9f, 0.9f, 1).setDuration(2000), ObjectAnimator.ofFloat(view,"scaleY",0,0.5f, 0.9f, 0.9f, 1).setDuration(2000)
|
弹出展开的特殊例子是新闻风格,核心是快速旋转多个来回。
1 2 3 4
| ObjectAnimator.ofFloat(view, "rotation", 1080,720,360,0).setDuration(2000), ObjectAnimator.ofFloat(view, "alpha", 0, 1).setDuration(2000*3/2), ObjectAnimator.ofFloat(view, "scaleX", 0.1f, 0.5f, 1).setDuration(2000), ObjectAnimator.ofFloat(view,"scaleY",0.1f,0.5f,1).setDuration(2000)
|