先上图
ProgressDialog对于Android开发者来说已经是老朋友了,本意是进度对话框,常用于更新包下载进度提示等。本文重点不在于此,主要目标是实现类似上图中的加载中提示框,探索ProgressDialog在实战项目中的使用和封装,这在公司项目中是很重要的一个工具。接下来从简单的使用开始,详细内容可查看ProgressDialog源码。
1、简单使用
ProgressDialog pd1 = new ProgressDialog(this);
pd1.setMessage("加载中...");
...
pd1.show();
ProgressDialog pd2 = ProgressDialog.show(this, "温馨提示", "加载中...");
上面是ProgressDialog的两种创建方式,这个根据源码可以灵活使用:
public ProgressDialog(Context context) {
super(context);
...
}
public ProgressDialog(Context context, int theme) {
super(context, theme);
...
}
...
public static ProgressDialog show(Context context, CharSequence title,
CharSequence message, boolean indeterminate,
boolean cancelable, OnCancelListener cancelListener) {
ProgressDialog dialog = new ProgressDialog(context);
dialog.setTitle(title);
dialog.setMessage(message);
dialog.setIndeterminate(indeterminate);
dialog.setCancelable(cancelable);
dialog.setOnCancelListener(cancelListener);
dialog.show();
return dialog;
}
以登录为例,点击登录时progressDialog.show()
,同时发起登录网络请求,请求结束时progressDialog.dismiss()
,这是普通情况;考虑特殊情况,如果有两个网络请求同时发起了,就会调用两次progressDialog.show()
,这时就可能出现两个progressDialog
重叠的情况(见第一张图),这种情景在实际项目中是不合理的,想象一下多个progressDialog
重叠的情况。。
通过分析以上情况,可以发现实际项目中可能会有多个网络请求同时进行的情景,但是必须保证progressDialog
只显示一个,这时就必须考虑封装一个全局的progressDialog
了。经过一番尝试,新的方案出炉了。
2、全局ProgressDialog的封装
public class NormalProgressDialog extends ProgressDialog implements DialogInterface.OnCancelListener {
private WeakReference<Context> mContext = new WeakReference<>(null);
private volatile static NormalProgressDialog sDialog;
private NormalProgressDialog(Context context) {
this(context, -1);
}
private NormalProgressDialog(Context context, int theme) {
super(context, theme);
mContext = new WeakReference<>(context);
setOnCancelListener(this);
}
@Override
public void onCancel(DialogInterface dialog) {
// 点手机返回键等触发Dialog消失,应该取消正在进行的网络请求等
Context context = mContext.get();
if (context != null) {
Toast.makeText(context, "cancel", Toast.LENGTH_SHORT).show();
MyHttpClient.cancelRequests(context);
}
}
public static synchronized void showLoading(Context context) {
showLoading(context, "loading...");
}
public static synchronized void showLoading(Context context, CharSequence message) {
showLoading(context, message, true);
}
public static synchronized void showLoading(Context context, CharSequence message, boolean cancelable) {
if (sDialog != null && sDialog.isShowing()) {
sDialog.dismiss();
}
if (context == null || !(context instanceof Activity)) {
return;
}
sDialog = new NormalProgressDialog(context);
sDialog.setMessage(message);
sDialog.setCancelable(cancelable);
if (sDialog != null && !sDialog.isShowing() && !((Activity) context).isFinishing()) {
sDialog.show();
}
}
public static synchronized void stopLoading() {
if (sDialog != null && sDialog.isShowing()) {
sDialog.dismiss();
}
sDialog = null;
}
}
分析上面的代码,首先定义了全局静态的sDialog
,保证了progressDialog
的唯一;接下来最重要的就是保证progressDialog.show()
的唯一,关键代码是:
if (sDialog != null && sDialog.isShowing()) {
sDialog.dismiss();
}
sDialog = new NormalProgressDialog(context);
如果当前已经是sDialog.isShowing()
,就先dismiss()
再new NormalProgressDialog()
,确保同时只显示一个,这样就解决了多个progressDialog
同时显示重叠的隐患。
上边这个方案主要是针对系统自带的ProgressDialog的封装,对于普通的项目已经满足需求了。但是多数情况下需求都会变成第三张图那样的风格,针对这种需求一般会考虑自定义ProgressDialog。
3、自定义ProgressDialog
public class CustomProgressDialog extends Dialog implements DialogInterface.OnCancelListener {
private WeakReference<Context> mContext = new WeakReference<>(null);
private volatile static CustomProgressDialog sDialog;
private CustomProgressDialog(Context context, CharSequence message) {
super(context, R.style.CustomProgressDialog);
mContext = new WeakReference<>(context);
@SuppressLint("InflateParams")
View view = LayoutInflater.from(context).inflate(R.layout.dialog_custom_progress, null);
TextView tvMessage = (TextView) view.findViewById(R.id.tv_message);
if (!TextUtils.isEmpty(message)) {
tvMessage.setText(message);
}
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(view, lp);
setOnCancelListener(this);
}
@Override
public void onCancel(DialogInterface dialog) {
// 点手机返回键等触发Dialog消失,应该取消正在进行的网络请求等
Context context = mContext.get();
if (context != null) {
Toast.makeText(context, "cancel", Toast.LENGTH_SHORT).show();
MyHttpClient.cancelRequests(context);
}
}
public static synchronized void showLoading(Context context) {
showLoading(context, "loading...");
}
public static synchronized void showLoading(Context context, CharSequence message) {
showLoading(context, message, true);
}
public static synchronized void showLoading(Context context, CharSequence message, boolean cancelable) {
if (sDialog != null && sDialog.isShowing()) {
sDialog.dismiss();
}
if (context == null || !(context instanceof Activity)) {
return;
}
sDialog = new CustomProgressDialog(context, message);
sDialog.setCancelable(cancelable);
if (sDialog != null && !sDialog.isShowing() && !((Activity) context).isFinishing()) {
sDialog.show();
}
}
public static synchronized void stopLoading() {
if (sDialog != null && sDialog.isShowing()) {
sDialog.dismiss();
}
sDialog = null;
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/custom_progress_dialog_bg"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="20dp"
android:paddingLeft="32dp"
android:paddingRight="32dp"
android:paddingTop="20dp">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminateTint="@android:color/white"
android:indeterminateTintMode="src_atop"/>
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="@android:color/white"
tools:text="加载中..."/>
</LinearLayout>
</LinearLayout>
<style name="CustomProgressDialog" parent="@android:style/Theme.Dialog">
<!-- Dialog的windowFrame框为无 -->
<item name="android:windowFrame">@null</item>
<!-- 是否浮现在activity之上 -->
<item name="android:windowIsFloating">true</item>
<!-- 是否半透明 -->
<item name="android:windowIsTranslucent">true</item>
<!-- 是否显示title -->
<item name="android:windowNoTitle">true</item>
<!-- 设置dialog的背景 -->
<item name="android:background">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/transparent</item>
<!-- 用来控制灰度的值,当为1时,界面除了我们的dialog内容是高亮显示的,dialog以外的区域是黑色的,完全看不到其他内容,系统的默认值是0.5 -->
<item name="android:backgroundDimAmount">0.2</item>
<!-- 是否模糊 -->
<item name="android:backgroundDimEnabled">true</item>
</style>
首先这里没有继承ProgressDialog而是选择了Dialog,一点是因为Dialog更灵活,这点从继承关系可以看出:
<img src="http://upload-images.jianshu.io/upload_images/1868508-937f1def072b2849.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"/>
另一点是从Android O开始ProgressDialog被抛弃了:
<img src="http://upload-images.jianshu.io/upload_images/1868508-d46fb8f7f9936993.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"/>
所以继承Dialog显然更合适。
这里自定义ProgressDialog最关键的是自定义布局和样式:
private CustomProgressDialog(Context context, CharSequence message) {
super(context, R.style.CustomProgressDialog);
mContext = new WeakReference<>(context);
@SuppressLint("InflateParams")
View view = LayoutInflater.from(context).inflate(R.layout.dialog_custom_progress, null);
TextView tvMessage = (TextView) view.findViewById(R.id.tv_message);
if (!TextUtils.isEmpty(message)) {
tvMessage.setText(message);
}
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(view, lp);
setOnCancelListener(this);
}
这几步操作就实现了自定义风格样式,也满足了产品的需求。其他代码跟前面封装系统自带的ProgressDialog一样,都容易理解。到此,自定义ProgressDialog完成了。
4、more important
以上是针对公司实际项目中常用的加载提示对话框的探索和封装过程。下面延伸一点,探讨一下点手机返回键等触发Dialog消失,同时取消正在进行的网络请求的情况,这个需求其实是很重要的,但是好多项目都没有处理,所以遗留了很大的隐患。比如正在显示对话框时当前Activity执行了finish,请求成功时处理UI就容易造成空指针。针对这个问题,必须在Dialog非正常消失时及时取消当前正在进行的网络请求。要取消网络请求,就得先找项目中使用的网络框架对应的取消请求的方法,这里以android-async-http为例:
android-async-http
取消网络请求的方法在AsyncHttpClient中:
public class AsyncHttpClient {
public void cancelRequests(final Context context, final boolean mayInterruptIfRunning) {
private void cancelRequests(final List<RequestHandle> requestList, final boolean mayInterruptIfRunning) {
public void cancelAllRequests(boolean mayInterruptIfRunning) {
public void cancelRequestsByTAG(Object TAG, boolean mayInterruptIfRunning) {
这几个方法具体的使用和区别就不讲了,可以看源码研究一下。这里以第一个方法为例看一下封装过程。
使用网络框架时一般都需要定义一个全局的Client类来负责网络请求的调度,以android-async-http
,通常会用下面的方式:
public class MyHttpClient {
private static final AsyncHttpClient mClient = new AsyncHttpClient();
static {
try {
mClient.setTimeout(15000); // 默认10秒超时
if(BuildConfig.DEBUG){
mClient.setSSLSocketFactory(MySSLSocketFactory.getFixedSocketFactory()); //不验证书
}else{
mClient.setSSLSocketFactory(SSLSocketFactory.getSocketFactory()); //验证书
}
mClient.addHeader("app-platform", "Android");
mClient.addHeader("Content-Type", "application/json");
} catch (Exception e) {
e.printStackTrace();
}
}
public static AsyncHttpClient getHttptClient(){
return mClient;
}
/**
* 取消与当前context关联的所有请求
* @param context 当前context
*/
public static void cancelRequests(Context context){
mClient.cancelRequests(context, true);
}
/**
* 取消所有请求
*/
public static void cancelAllRequests(){
mClient.cancelAllRequests(true);
}
}
以上面的封装方式看,只需要在Dialog消失的时候调用cancelRequests(Context context)
就可以达到取消网络请求的目的。所以接下来重点看Dialog的消失事件,这个前面的代码已经给出了:
public class CustomProgressDialog extends Dialog implements DialogInterface.OnCancelListener {
private WeakReference<Context> mContext = new WeakReference<>(null);
...
private CustomProgressDialog(Context context, CharSequence message) {
super(context, R.style.CustomProgressDialog);
mContext = new WeakReference<>(context);
...
setOnCancelListener(this);
}
@Override
public void onCancel(DialogInterface dialog) {
// 点手机返回键等触发Dialog消失,应该取消正在进行的网络请求等
Context context = mContext.get();
if (context != null) {
Toast.makeText(context, "cancel", Toast.LENGTH_SHORT).show();
MyHttpClient.cancelRequests(context);
}
}
到此,点手机返回键等触发Dialog消失,同时取消正在进行的网络请求初步实现了,但是这里只是针对cancelRequests(Context context)
的探索,每个项目使用的网络框架不一样,取消的方法也不一样,需要针对不同的需求对症下药才能真正解决这个问题。
ProgressDialog封装就到这里了,这里只是我个人的一些探索和想法,并没有解决实际的需求,具体需要针对产品的需求和项目的情况设计对应的方案,没有最好的方案,只有最合适的方案。相关代码都在android-blog-samples。
~ the end ~