Android项目实战 ProgressDialog自定义和封装过程

先上图


系统自带的ProgressDialog.gif

封装系统自带的ProgressDialog.gif

自定义ProgressDialog.gif

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 ~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容