0%

Menu,PopupWindow和Dialog

PopupWindow 的根本构造是

1
2
//内容控件不能有 parent
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
//此处 View 主要是提供窗口服务的 IBinder 对象,因此随便一个控件即可以。
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

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();

关于 OverflowButton

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
public void 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), //负责旋转ObjectAnimator.ofFloat(view, "translationX", -300, 0).setDuration(2000), //负责左侧ObjectAnimator.ofFloat(view, "alpha", 0, 1).setDuration(2000*3/2)

复合效果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)

广告对话框

广告对话框