《代码里的世界》 —UI篇
用文字札记描绘自己 android学习之路
转载请保留出处 by Qiao
【导航】
- 自定义弹出式对话框的简单用法 列举各种常见的对话框实现方案
1.概述
android原生控件向来以丑著称(新推出的Material Design当另说),因此几乎所有的应用都会特殊定制自己的UI样式。而其中弹出式提示框的定制尤为常见,本篇我们将从模仿QQ退出提示框来看一下常见的几种自定义提示框的实现方式。
这里使用的几种弹出框实现方法概括为以下几种:
- 自定义Dialog
- 自定义PopupWindow
- 自定义Layout View
- Activity的Dialog样式
- FragmentDialog
先看下最终的效果图:
2.实践
前面提到几种实现方式均可以达到同样的演示效果,但其中又是各有不同。这里先逐一列举各种具体实现,最后加以综述总结和归纳吧。
在此之前呢,先看一下这里实现的对话框共用布局layout/confirm_dialog.xml 。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="@drawable/confirm_dialog_bg"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="vertical" >
<TextView
android:id="@+id/title_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:paddingBottom="10dp"
android:paddingTop="15dp"
android:text="Message Title"
android:textColor="@android:color/black"
android:textSize="20sp"
android:visibility="visible" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/transparent"
android:orientation="vertical" >
<TextView
android:id="@+id/text_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="10dp"
android:textColor="@android:color/black"
android:text="this is message content"
android:textSize="16dip"/>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="15dip"
android:background="#c5c5c5" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="50dip"
android:background="@android:color/transparent"
android:gravity="center_horizontal"
android:orientation="horizontal" >
<!-- 取消按钮 -->
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="Cancel"
android:textStyle="bold"
android:textColor="#0072c6"
android:background="@drawable/confirm_dialog_cancel_selector"
android:textSize="15sp" />
<!-- 确认按钮 -->
<View
android:layout_width="1px"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:background="#c5c5c5"/>
<Button
android:id="@+id/btn_ok"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="OK"
android:textStyle="bold"
android:textColor="#0072c6"
android:background="@drawable/confirm_dialog_ok_selector"
android:textSize="15sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
仅仅通过布局预览就可以看到效果了:
下边我们分别通过上述几种方式来使用这个布局展示消息提示框。
2.1 Dialog
这个是最基本也最常见的非阻塞式对话框。具体形式可分为七种,详细参见网上各种文章,随便引用一篇7种形式的Android Dialog使用举例。
(注:官方在fragmentDialog推出后就不在推荐直接使用Dialog来创建对话框,这是后话)
我们这里自定义的提示框ConfirmDialog继承自Dialog,使用confirm_dialog.xml 初始化布局,绑定相应事件。
public class ConfirmDialog extends Dialog {
private Context context;
private TextView titleTv,contentTv;
private View okBtn,cancelBtn;
private OnDialogClickListener dialogClickListener;
public ConfirmDialog(Context context) {
super(context);
this.context = context;
initalize();
}
//初始化View
private void initalize() {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.confirm_dialog, null);
setContentView(view);
initWindow();
titleTv = (TextView) findViewById(R.id.title_name);
contentTv = (TextView) findViewById(R.id.text_view);
okBtn = findViewById(R.id.btn_ok);
cancelBtn = findViewById(R.id.btn_cancel);
okBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
if(dialogClickListener != null){
dialogClickListener.onOKClick();
}
}
});
cancelBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
if(dialogClickListener != null){
dialogClickListener.onCancelClick();
}
}
});
}
/**
*添加黑色半透明背景
*/
private void initWindow() {
Window dialogWindow = getWindow();
dialogWindow.setBackgroundDrawable(new ColorDrawable(0));//设置window背景
dialogWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);//设置输入法显示模式
WindowManager.LayoutParams lp = dialogWindow.getAttributes();
DisplayMetrics d = context.getResources().getDisplayMetrics();//获取屏幕尺寸
lp.width = (int) (d.widthPixels * 0.8); //宽度为屏幕80%
lp.gravity = Gravity.CENTER; //中央居中
dialogWindow.setAttributes(lp);
}
public void setOnDialogClickListener(OnDialogClickListener clickListener){
dialogClickListener = clickListener;
}
/**
*添加按钮点击事件
*/
public interface OnDialogClickListener{
void onOKClick();
void onCancelClick();
}
}
2.2 PopupWindow
PopupWindow是阻塞式对话框,只有在退出操作时候程序才会继续运行。另外PopupWindow可以根据自由确定自身位置。按照位置有无偏移分,可以分为偏移和无偏移两种;按照参照物的不同,可以分为相对于某个控件(Anchor锚)和相对于父控件。具体如下
showAsDropDown(View anchor):相对某个控件的位置(正左下方),无偏移
showAsDropDown(View anchor, int xoff, int yoff):相对某个控件的位置(正下方),有偏移
showAtLocation(View parent, int gravity, int x, int y):相对于父控件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以设置偏移或无偏移
这里只是达到同样的显示效果,仅示范下showAtBottom的使用:
public class ConfirmPopWindow extends PopupWindow{
private Context context;
private TextView titleTv,contentTv;
private View okBtn,cancelBtn;
private OnDialogClickListener dialogClickListener;
public ConfirmPopWindow(Context context) {
super(context);
this.context = context;
initalize();
}
private void initalize() {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.confirm_dialog, null);
setContentView(view);
initWindow();
titleTv = (TextView) view.findViewById(R.id.title_name);
contentTv = (TextView) view.findViewById(R.id.text_view);
okBtn = view.findViewById(R.id.btn_ok);
cancelBtn = view.findViewById(R.id.btn_cancel);
okBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
if(dialogClickListener != null){
dialogClickListener.onOKClick();
}
}
});
cancelBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
if(dialogClickListener != null){
dialogClickListener.onCancelClick();
}
}
});
}
private void initWindow() {
this.setBackgroundDrawable(new ColorDrawable(0));
DisplayMetrics d = context.getResources().getDisplayMetrics();
this.setWidth((int) (d.widthPixels * 0.8));
this.setHeight(LayoutParams.WRAP_CONTENT);
this.setFocusable(true);
this.setOutsideTouchable(true);
this.update();
}
public void showAtBottom(View view){
showAsDropDown(view, Math.abs((view.getWidth() - getWidth())/2), 20);
}
public void setOnDialogClickListener(OnDialogClickListener clickListener){
dialogClickListener = clickListener;
}
public interface OnDialogClickListener{
void onOKClick();
void onCancelClick();
}
}
2.3 自定义Layout
前边两种是系统封装好的View ,同样的,我们也可以自定义layout布局来实现的弹出式对话框效果。既然是自定义,有必要细致讲述一下,
ConfirmLayout继承自FrameLayout,通过获取窗口管理器WindowManager 将我们的自定义view添加到窗口最前端并显示出来,达到预期效果。
1.初始化View
先初始化半透明黑色背景和对应的confirm_layout,然后给窗体添加按键返回事件
protected void initialize() {
initBackground();//初始化黑色背景
initContentView();//初始化confirm_layout 对应的View
windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
setOnKeyListener(new OnKeyListener() { //添加按键返回事件
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (KeyEvent.KEYCODE_BACK == keyCode && KeyEvent.ACTION_DOWN == event.getAction()) {
hide();//隐藏当前view
return true;
}
return false;
}
setFocusable(true); //可获得焦点
setFocusableInTouchMode(true); //可触碰获得焦点
}
2.显示自定义VIew : show()
调用显示的时候保证在主线程中,如果当前View没有被添加至窗口中,则添加;然后使用动画渐变效果显示背景,最后动画完成时显示当前对话框View.
public void show() {
((Activity) getContext()).runOnUiThread(new Runnable() {
@Override
public void run() {
if (getParent() == null) { //没有添加则添加至窗体
//获取窗体的布局属性,设置左上角对齐,填充父容器
WindowManager.LayoutParams wlp = new WindowManager.LayoutParams();
wlp.type = WindowManager.LayoutParams.TYPE_APPLICATION;
wlp.format = PixelFormat.TRANSPARENT;
wlp.gravity = Gravity.LEFT | Gravity.TOP;
wlp.width = LayoutParams.MATCH_PARENT;
wlp.height = LayoutParams.MATCH_PARENT;
windowManager.addView(ConfirmLayout.this, wlp);
}
showBackGround();//显示背景动画和自定义View
}
});
}
/**
*显示背景动画
*/
protected void showBackGround() {
if (isShowing)
return;
isShowing = true;
background.clearAnimation();
background.setVisibility(View.VISIBLE);
AlphaAnimation an = new AlphaAnimation(0, 1);
an.setDuration(durationMillis);
background.startAnimation(an);
}
3.隐藏自定义VIew : hide()
隐藏对话框的方法跟show()恰恰相反,首先调用隐藏动画,动画结束从窗体中移除View
public void hide() {
((Activity) getContext()).runOnUiThread(new Runnable() {
@Override
public void run() {
hideBackGround();//隐藏背景
if (getParent() != null)
windowManager.removeView(ConfirmLayout.this);//移除view
}
});
}
/**
*隐藏背景背景动画
*/
protected void hideBackGround() {
if (!isShowing)
return;
isShowing = false;
background.clearAnimation();
AlphaAnimation an = new AlphaAnimation(1, 0);
an.setDuration(durationMillis);
an.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
background.setVisibility(View.GONE);
}
});
background.startAnimation(an);
}
其他部分同上,不再一一贴出,详细可查看示例源码。
2.4 Activity的Dialog样式
通过使用主题Theme来实现Activity作为一个dialog来显示的效果。我们首先在 AndroidManifest.xml 中配置该activity,使得
android:theme=”@android:style/Theme.Dialog”
同样我们可以自定义继承于Theme.Dialog的style样式增加自定义属性,比如:
<resources>
<style name="DialogStyle" parent="@android:style/Theme.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:backgroundDimEnabled">true</item>
</style>
</resources>
然后使用
android:theme=”@style/DialogStyle”
达到上述效果。具体实现跟dialog类似:
public class ConfirmActivity extends Activity{
private TextView titleTv,contentTv;
private View okBtn,cancelBtn;
private OnDialogClickListener dialogClickListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.confirm_dialog);
initViews();
initListeners();
}
private void initViews() {
initWindow();
titleTv = (TextView) findViewById(R.id.title_name);
contentTv = (TextView) findViewById(R.id.text_view);
okBtn = findViewById(R.id.btn_ok);
cancelBtn = findViewById(R.id.btn_cancel);
okBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
if(dialogClickListener != null){
dialogClickListener.onOKClick();
}
}
});
}
private void initWindow() {
getWindow().setBackgroundDrawable(new ColorDrawable(0));
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN |
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
}
private void initListeners() {
cancelBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
if(dialogClickListener != null){
dialogClickListener.onCancelClick();
}
}
});
}
public void setOnDialogClickListener(OnDialogClickListener clickListener){
dialogClickListener = clickListener;
}
public interface OnDialogClickListener{
void onOKClick();
void onCancelClick();
}
}
2.5 DialogFragment
DialogFragment在android 3.0时被引入并被加以推广。
我们在使用DialogFragment时,至少需要实现onCreateView或者onCreateDIalog方法。这里在onCreateDIalog中直接返回前面写好的ConfirmDialog来实现这个Fragment。
public class ConfirmFragment extends DialogFragment{
@Override
@NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new ConfirmDialog(getActivity());
}
public void setOnDialogClickListener(OnDialogClickListener clickListener){
((ConfirmDialog)getDialog()).setOnDialogClickListener(clickListener);
}
}
当然并不推荐偷懒直接返回前面定义好的ConfirmDialog。我们实际上使用fragment的onCreateView也更合理和简介,他可以产生同样的Dialog效果,同时可以作为内嵌fragment引用。从使用总结来看,FragmentDialog 相较于Dialog有两点好处:
- 在手机配置变化,导致Activity需要重新创建时,例如旋屏,DialogFragment对话框将会由FragmentManager自动重建,然而Dialog实现的对话框则不会重新生成;
- DialogFragment还拥有fragment的优点,即可以在一个Activity内部实现回退(因为FragmentManager会管理一个回退栈 ,另外,他可以直接作为一个普通Fragment嵌套在其他布局里边;
3.小结
从实现效果来看我们确实有很多选择,当然我们用的最多的必然要数Dialog(FragmentDialog)和PopupWindow了。但在一般情况下,选择Dialog和PopupWindow是由我们的具体使用场景来定。比如有些提示消息,比较适合Dialog,而弹出一些具体选项,需要等待选择结果等情况,更倾向于使用PopupWindow了。
- FragmentDialog的几种使用场景
- PopupWindow的几种使用场景
4.补充
其实还有种长按弹出菜单,这种除了可以通过上述方法弹出菜单选项外,还可以通过系统提供的 View.setOnCreateContextMenu()方法来实现。比如:
itemView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
menu.add("删除").setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
//执行删除操作
return true;
}
});
}
});
长按弹出,基本效果为:
有兴趣的不妨试一下。
后边的话我们先从源码角度来看一下这里讲的几种实现方案的具体原理,最后通过一些简易封装来做一个类似IOS上的ActionSheet控件的效果。
演示效果大概为:
详情请继续关注接下来的博文。
最后附上本篇所讲内容的源码:
示例源码demo下载地址(刚上传,暂时不可用)