1. 程式人生 > >簡單的Dialog框架(讓你像使用Activity一樣使用Dialog)

簡單的Dialog框架(讓你像使用Activity一樣使用Dialog)

最近開發中遇到一個問題,就是在一個Activity啟動後,後續的大量流程都由Dialog來承載。Android提供的Dialog其實大部分情況下只是一個Alert的作用,並不是一個能承載複雜業務的Controller,所以在Dialog之間互相跳轉需要傳遞值或回撥時,就會遇到問題。前人寫的程式碼中包含了大量dialog互相傳遞的程式碼,比如ADialog跳轉到BDialog之後,BDialog的某個操作需要呼叫A中的某個方法或傳遞值給A,於是出現了把A的引用傳給B,把A中的Handler傳給B這樣的“沒有辦法的辦法”的程式碼,看了甚是痛心。由於沒有統一規範,當業務膨脹之後各種dialog互相傳遞導致各種耦合帶來大量的問題,於是苦思冥想該如何解決這個問題。最近靈機一動,安卓開發者最熟悉的莫過於使用Activity了,如果能像使用Activity一樣使用Dialog,那學習成本是不是就最低呢,於是乎,就冒出了寫這麼個框架的想法。
宣告一下,這個框架目前只是個demo,也算是拋磚引玉,當然我們也可以借用EventBus這樣的成熟框架,不過既然想到了,就當練手吧,希望各路大神如果看中這個思路希望一起來優化這個框架。
另外這個框架封裝了基礎的Dialog以及對Dialog展示消失製作自己的動畫,前提是你有nineoldandroids的jar,好了,廢話不多說,上程式碼。

package com.amuro.dialog_framework.base;

import com.amuro.dialog_framework.animation.BaseAnimatorSet;
import com.amuro.dialog_framework.utils.DialogIntent;
import com.amuro.dialog_framework.utils.DisplayUtils;
import com.nineoldandroids.animation.Animator;

import android.annotation.SuppressLint;
import
android.app.Dialog; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import
android.view.Window; import android.view.WindowManager.LayoutParams; import android.widget.LinearLayout; public abstract class BaseDialog extends Dialog { protected Context context; private DialogIntent intent; private int screenWidth; private int screenHeight; private int contentWidth; private int contentHeight; private int dialogMaxHeight; private float widthScale; private float heightScale; private LinearLayout linearLayoutRoot; private LinearLayout linearLayoutContent; private BaseAnimatorSet showAnimation; private BaseAnimatorSet dismissAnimation; private boolean isShowAnimPlaying = false; private boolean isDimissAnimPlaying = false; private boolean isOutsideTouchDismiss = true; private DialogManager dialogManager; private int resultCode = -1; private Bundle resultData; public BaseDialog(Context context) { super(context); this.context = context; dialogManager = DialogManager.getInstance(); initData(); setDialogTheme(); initParams(); } public DialogIntent getIntent() { return intent; } public void setIntent(DialogIntent intent) { this.intent = intent; } protected abstract void initParams(); private void initData() { screenWidth = DisplayUtils.getScreenWidth(context); screenHeight = DisplayUtils.getScreenHeight(context); dialogMaxHeight = screenHeight - DisplayUtils.getStatusBar(context); widthScale = 0.88f; heightScale = 0.5f; } private void setDialogTheme() { requestWindowFeature(Window.FEATURE_NO_TITLE);// android:windowNoTitle getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));// android:windowBackground getWindow().addFlags(LayoutParams.FLAG_DIM_BEHIND);// android:backgroundDimEnabled預設是true的 } @Override protected void onCreate(Bundle savedInstanceState) { linearLayoutRoot = new LinearLayout(context); linearLayoutRoot.setGravity(Gravity.CENTER); linearLayoutContent = new LinearLayout(context); linearLayoutContent.setOrientation(LinearLayout.VERTICAL); linearLayoutContent.setBackgroundColor(Color.WHITE); linearLayoutContent.addView(onCreateContentView()); linearLayoutRoot.addView(linearLayoutContent); setContentView(linearLayoutRoot, new ViewGroup.LayoutParams(screenWidth, dialogMaxHeight)); } protected abstract View onCreateContentView(); @Override public void onAttachedToWindow() { super.onAttachedToWindow(); initContentView(); initShowAnimation(); } private void initContentView() { if (widthScale == 0) { contentWidth = ViewGroup.LayoutParams.WRAP_CONTENT; } else if(widthScale == 1) { contentWidth = ViewGroup.LayoutParams.MATCH_PARENT; } else { contentWidth = (int) (screenWidth * widthScale); } if (heightScale == 0) { contentHeight = ViewGroup.LayoutParams.WRAP_CONTENT; } else if (heightScale == 1) { contentHeight = ViewGroup.LayoutParams.MATCH_PARENT; } else { contentHeight = (int) (dialogMaxHeight * heightScale); } linearLayoutContent.setLayoutParams(new LinearLayout.LayoutParams(contentWidth, contentHeight)); linearLayoutRoot.setOnTouchListener(new OnTouchListener() { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if(isOutsideTouchDismiss) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { if (!inRangeOfView(linearLayoutContent, event)) { dismiss(); } } } return false; } }); } private void initShowAnimation() { if (showAnimation != null) { showAnimation.listener(new BaseAnimatorSet.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { isShowAnimPlaying = true; } @Override public void onAnimationRepeat(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { isShowAnimPlaying = false; } @Override public void onAnimationCancel(Animator animator) { isShowAnimPlaying = false; } }).playOn(linearLayoutContent); } else { BaseAnimatorSet.reset(linearLayoutContent); } } private boolean inRangeOfView(View view, MotionEvent ev) { int[] location = new int[2]; view.getLocationOnScreen(location); int x = location[0]; int y = location[1]; if (ev.getX() < x || ev.getX() > (x + view.getWidth()) || ev.getY() < y || ev.getY() > (y + view.getHeight())) { return false; } return true; } @Override public void dismiss() { if (dismissAnimation != null) { dismissAnimation.listener(new BaseAnimatorSet.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { isDimissAnimPlaying = true; } @Override public void onAnimationRepeat(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { isDimissAnimPlaying = false; superDismiss(); } @Override public void onAnimationCancel(Animator animator) { isDimissAnimPlaying = false; superDismiss(); } }).playOn(linearLayoutContent); } else { superDismiss(); } } private void superDismiss() { super.dismiss(); } @Override public void onBackPressed() { if (isShowAnimPlaying || isDimissAnimPlaying) { return; } super.onBackPressed(); } public BaseDialog setOutsideTouchDismiss(boolean isOutsideTouchDismiss) { this.isOutsideTouchDismiss = isOutsideTouchDismiss; return this; } /** set window dim or not(設定背景是否昏暗) */ public BaseDialog setDimEnabled(boolean isDimEnabled) { if (isDimEnabled) { getWindow().addFlags(LayoutParams.FLAG_DIM_BEHIND); } else { getWindow().clearFlags(LayoutParams.FLAG_DIM_BEHIND); } return this; } /** set dialog width scale:0-1(設定對話方塊寬度,佔螢幕寬的比例0-1) */ public BaseDialog setWidthScale(float widthScale) { this.widthScale = widthScale; return this; } /** set dialog height scale:0-1(設定對話方塊高度,佔螢幕寬的比例0-1) */ public BaseDialog setHeightScale(float heightScale) { this.heightScale = heightScale; return this; } /** set show anim(設定顯示的動畫) */ public BaseDialog setShowAnim(BaseAnimatorSet showAnim) { this.showAnimation = showAnim; return this; } /** set dismiss anim(設定隱藏的動畫) */ public BaseDialog setDismissAnim(BaseAnimatorSet dismissAnim) { this.dismissAnimation = dismissAnim; return this; } protected void setResult(int resultCode) { this.resultCode = resultCode; this.resultData = null; } protected void setResult(int resultCode, Bundle data) { this.resultCode = resultCode; this.resultData = data; } public int getResultCode() { return resultCode; } public Bundle getResultData() { return resultData; } protected void startDialogForResult(DialogIntent intent, int requestCode) { dialogManager.startDialogForResult(intent, requestCode); } protected void onDialogResult(int requestCode, int resultCode, Bundle data) { } }

BaseDialog這種也算是標準配置了,解釋一下思路:
1. 安卓的Window和View的關係我就不多說了,不理解的去看讀書筆記二。我們按照標準的Dialog配置,把Window的背景置為透明,加上陰影,去掉醜陋的系統Title。然後丟一個Root LinearLayout在後面,它的大小就是全屏,然後背景也是透明。
2. Root上面再加一個content LinearLayout作為承載我們所有自定義View的容器,而這個content的大小通過widthScale和heightScale來調整,預設就是系統AlertDialog差不多的大小。
3. 因為背景已經完全被root覆蓋,所以需要我們自己處理onTouchOutsideDismiss方法,思路很簡單,root被點選的時候判斷這個點的座標是否在content範圍內,是就遮蔽事件,不是就dismiss。
4. 重點就是startDialogForResult方法和onDialogResult方法,是不是很眼熟,沒錯,就是讓你感覺和Activity一樣。邏輯也是差不多的,startDialogForResult方法呼叫了DialogManager的startDialogForResult方法,onDialogResult方法則為空方法,待子類自己去玩。
5. 動畫的基類為BaseAnimatorSet,封裝了必要的動畫方法,提供介面供呼叫者自行設定想要的動畫。
下面先來看DialogManager類:

package com.amuro.dialog_framework.base;

import java.lang.reflect.Constructor;

import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;

import com.amuro.dialog_framework.utils.DialogIntent;

public class DialogManager
{
    private DialogManager(){}

    private final static class DialogManagerHolder
    {
        private static DialogManager instance = new DialogManager();
    }

    public static DialogManager getInstance()
    {
        return DialogManagerHolder.instance;
    }

    private Context context;
    private BaseDialog fromDialog;
    private BaseDialog toDialog;
    private int requestCode;

    public DialogManager setContext(Context context)
    {
        if(this.context == null)
        {
            this.context = context;
        }

        return this;
    }

    public void startDialog(DialogIntent intent)
    {
        startDialogForResult(intent, -1);
    }

    public void startDialogForResult(DialogIntent intent, int requestCode)
    {
        try
        {
            fromDialog = intent.getFromDialog();

            Constructor<? extends BaseDialog> c = 
                    intent.getDialogClass().getConstructor(Context.class);

            this.requestCode = requestCode;

            toDialog = c.newInstance(context);
            toDialog.setIntent(intent);
            toDialog.setOnDismissListener(new OnDismissListener()
            {

                @Override
                public void onDismiss(DialogInterface dialog)
                {
                    notifyResult();
                }
            });

            toDialog.show();

        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    private void notifyResult()
    {
        if(fromDialog == null)
        {
            return;
        }

        if(requestCode == -1)
        {
            return;
        }

        fromDialog.onDialogResult(requestCode, toDialog.getResultCode(), toDialog.getResultData());
    }
}

一看就懂的程式碼,記錄發出請求的Dialog,再反射拿到新的Dialog,在新的Dialog dismiss的時候,回撥請求Dialog的onDialogResult方法,其形式和Activity幾乎如出一轍。當然demo就是demo,這裡只是最簡單的實現,後續可以通過參考Activity stack和map來維護整個dialog佇列,實現更復雜的需求。為了傳遞資料方便,這裡乾脆封裝了Intent,弄了個DialogIntent,複雜的資料傳遞,就可以直接複用Intent的程式碼啦~

package com.amuro.dialog_framework.utils;

import com.amuro.dialog_framework.base.BaseDialog;

import android.content.Intent;

public class DialogIntent extends Intent
{
    private BaseDialog fromDialog;
    private Class<? extends BaseDialog> toDialogClass;

    public DialogIntent(BaseDialog fromDialog,
            Class<? extends BaseDialog> dialogClass)
    {
        super();
        this.fromDialog = fromDialog;
        this.toDialogClass = dialogClass;
    }

    public BaseDialog getFromDialog()
    {
        return fromDialog;
    }

    public void setFromDialog(BaseDialog fromDialog)
    {
        this.fromDialog = fromDialog;
    }

    public Class<? extends BaseDialog> getDialogClass()
    {
        return toDialogClass;
    }

    public void setDialogClass(Class<? extends BaseDialog> dialogClass)
    {
        this.toDialogClass = dialogClass;
    }

}

好了,最後再看下動畫的基類BaseAnimatorSet:、

package com.amuro.dialog_framework.animation;

import android.view.View;
import android.view.animation.Interpolator;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.view.ViewHelper;

public abstract class BaseAnimatorSet
{
    public interface AnimatorListener
    {
        void onAnimationStart(Animator animator);

        void onAnimationRepeat(Animator animator);

        void onAnimationEnd(Animator animator);

        void onAnimationCancel(Animator animator);
    }

    protected long duration = 500;
    protected AnimatorSet animatorSet = new AnimatorSet();
    private Interpolator interpolator;
    private long delay;
    private AnimatorListener listener;

    public abstract void setAnimation(View view);

    protected void start(final View view)
    {
        reset(view);
        setAnimation(view);

        animatorSet.setDuration(duration);
        if (interpolator != null)
        {
            animatorSet.setInterpolator(interpolator);
        }

        if (delay > 0)
        {
            animatorSet.setStartDelay(delay);
        }

        if (listener != null)
        {
            animatorSet.addListener(new Animator.AnimatorListener()
            {
                @Override
                public void onAnimationStart(Animator animator)
                {
                    listener.onAnimationStart(animator);
                }

                @Override
                public void onAnimationRepeat(Animator animator)
                {
                    listener.onAnimationRepeat(animator);
                }

                @Override
                public void onAnimationEnd(Animator animator)
                {
                    listener.onAnimationEnd(animator);
                }

                @Override
                public void onAnimationCancel(Animator animator)
                {
                    listener.onAnimationCancel(animator);
                }
            });
        }

        animatorSet.start();
    }

    /** 設定動畫時長 */
    public BaseAnimatorSet duration(long duration)
    {
        this.duration = duration;
        return this;
    }

    /** 設定動畫時長 */
    public BaseAnimatorSet delay(long delay)
    {
        this.delay = delay;
        return this;
    }

    /** 設定動畫插補器 */
    public BaseAnimatorSet interpolator(Interpolator interpolator)
    {
        this.interpolator = interpolator;
        return this;
    }

    /** 動畫監聽 */
    public BaseAnimatorSet listener(AnimatorListener listener)
    {
        this.listener = listener;
        return this;
    }

    /** 在View上執行動畫 */
    public void playOn(View view)
    {
        start(view);
    }

    public static void reset(View view) 
    {
        ViewHelper.setAlpha(view, 1);
        ViewHelper.setScaleX(view, 1);
        ViewHelper.setScaleY(view, 1);
        ViewHelper.setTranslationX(view, 0);
        ViewHelper.setTranslationY(view, 0);
        ViewHelper.setRotation(view, 0);
        ViewHelper.setRotationY(view, 0);
        ViewHelper.setRotationX(view, 0);
    }
}

關於屬性動畫可參考相關的blog,這裡不再贅述,想要實現自己酷炫的動畫效果,就繼承之後折騰吧。下面貼三個實現類來展示一下使用效果,Main,Login,Register

package com.amuro.example.dialogs;

import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import com.amuro.dialog_framework.animation.AnimatorBottomExit;
import com.amuro.dialog_framework.animation.AnimatorTopEnter;
import com.amuro.dialog_framework.animation.BaseAnimatorSet;
import com.amuro.dialog_framework.base.BaseAnimationDialog;
import com.amuro.dialog_framework.utils.DialogIntent;
import com.amuro.dialogframework.R;
import com.amuro.example.bean.User;
import com.amuro.example.utils.custom_view.TitleBar;
import com.amuro.example.utils.custom_view.ToastUtils;

public class MainDialog extends BaseAnimationDialog
{
    private TitleBar titleBar;
    private Button button1;
    private Button button2;

    public MainDialog(Context context)
    {
        super(context);
    }

    @Override
    protected BaseAnimatorSet getShowAnim()
    {
        return new AnimatorTopEnter();
    }

    @Override
    protected BaseAnimatorSet getDismissAnim()
    {
        return new AnimatorBottomExit();
    }

    @Override
    protected View onCreateContentView()
    {
        View view = View.inflate(context, R.layout.dialog_main_layout, null);
        return view;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        initTitle();
        initView();
    }

    private void initTitle()
    {
        titleBar = (TitleBar)findViewById(R.id.tb);
        titleBar.setTitleText("主頁");
    }

    private void initView()
    {
        button1 = (Button)findViewById(R.id.bt1);
        button1.setOnClickListener(new View.OnClickListener()
        {

            @Override
            public void onClick(View v)
            {
                DialogIntent intent = new DialogIntent(MainDialog.this, LoginDialog.class);
                intent.putExtra("test", "From MainDialog");
                startDialogForResult(intent, 1);
                dismiss();
            }
        });

        button2 = (Button)findViewById(R.id.bt2);
        button2.setOnClickListener(new View.OnClickListener()
        {

            @Override
            public void onClick(View v)
            {
                DialogIntent intent = new DialogIntent(MainDialog.this, RegisterDialog.class);
                intent.putExtra("test", "From MainDialog");
                startDialogForResult(intent, 2);
            }
        });
    }

    @Override
    protected void onDialogResult(int requestCode, int resultCode, Bundle data)
    {
        if(requestCode == 1)
        {
            if(resultCode == LoginDialog.LOGIN_SUCCESS)
            {
                User user = null;
                if(data != null)
                {
                    user = data.getParcelable("User");
                }

                String userName = "null";
                if(user != null)
                {
                    userName = user.getName();
                }

                ToastUtils.showToast(context, "登入成功, 登入使用者為:" + userName);
            }
            else if(resultCode == LoginDialog.LOGIN_FAILED)
            {
                ToastUtils.showToast(context, "登入失敗");
            }
            else if(resultCode == LoginDialog.LOGIN_CANCEL)
            {
                ToastUtils.showToast(context, "登入取消");
            }
        }
        else if(requestCode == 2)
        {
            if(resultCode == RegisterDialog.REGISTER_SUCCESS)
            {
                User user = null;
                if(data != null)
                {
                    user = data.getParcelable("User");
                }

                String userName = "null";
                if(user != null)
                {
                    userName = user.getName();
                }

                ToastUtils.showToast(context, "註冊成功, 註冊使用者為:" + userName);
            }
            else if(resultCode == RegisterDialog.REGISTER_FAILED)
            {
                ToastUtils.showToast(context, "註冊失敗");
            }
            else if(resultCode == RegisterDialog.REGISTER_CANCEL)
            {
                ToastUtils.showToast(context, "註冊取消");
            }
        }
    }


}
package com.amuro.example.dialogs;

import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import com.amuro.dialog_framework.animation.AnimatorBottomExit;
import com.amuro.dialog_framework.animation.AnimatorTopEnter;
import com.amuro.dialog_framework.animation.BaseAnimatorSet;
import com.amuro.dialog_framework.base.BaseAnimationDialog;
import com.amuro.dialogframework.R;
import com.amuro.example.bean.User;
import com.amuro.example.utils.custom_view.TitleBar;
import com.amuro.example.utils.custom_view.ToastUtils;

public class LoginDialog extends BaseAnimationDialog
{
    public static final int LOGIN_SUCCESS = 0;
    public static final int LOGIN_FAILED = 1;
    public static final int LOGIN_CANCEL = 2;

    private TitleBar titleBar;

    private Button button1;
    private Button button2;

    public LoginDialog(Context context)
    {
        super(context);
    }

    @Override
    protected BaseAnimatorSet getShowAnim()
    {
        return new AnimatorTopEnter();
    }

    @Override
    protected BaseAnimatorSet getDismissAnim()
    {
        return new AnimatorBottomExit();
    }

    @Override
    protected void initParams()
    {
        super.initParams();
        setOutsideTouchDismiss(false);
    }

    @Override
    protected View onCreateContentView()
    {
        return View.inflate(context, R.layout.dialog_login_layout, null);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        String test = getIntent().getStringExtra("test");
        ToastUtils.showToast(context, "來自上一個Dialog的msg: " + test);

        initTitle();
        initView();
    }

    private void initTitle()
    {
        titleBar = (TitleBar)findViewById(R.id.tb);
        titleBar.setTitleText("登入");
    }

    private void initView()
    {
        button1 = (Button)findViewById(R.id.bt_success);
        button1.setOnClickListener(new View.OnClickListener()
        {

            @Override
            public void onClick(View v)
            {
                Bundle bundle = new Bundle();
                User user = new User("1", "張三");
                bundle.putParcelable("User", user);
                setResult(LOGIN_SUCCESS, bundle);
                dismiss();
            }
        });

        button2 = (Button)findViewById(R.id.bt_fail);
        button2.setOnClickListener(new View.OnClickListener()
        {

            @Override
            public void onClick(View v)
            {
                setResult(LOGIN_FAILED);
                dismiss();
            }
        });

    }

    @Override
    public void onBackPressed()
    {
        super.onBackPressed();
        setResult(LOGIN_CANCEL);
    }

}
package com.amuro.example.dialogs;

import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import com.amuro.dialog_framework.base.BaseDialog;
import com.amuro.dialogframework.R;
import com.amuro.example.bean.User;
import com.amuro.example.utils.custom_view.TitleBar;
import com.amuro.example.utils.custom_view.ToastUtils;

public class RegisterDialog extends BaseDialog
{
    public static final int REGISTER_SUCCESS = 0;
    public static final int REGISTER_FAILED = 1;
    public static final int REGISTER_CANCEL = 2;

    private TitleBar titleBar;

    private Button button1;
    private Button button2;

    public RegisterDialog(Context context)
    {
        super(context);
    }

    @Override
    protected void initParams()
    {
        setOutsideTouchDismiss(false);
    }

    @Override
    protected View onCreateContentView()
    {
        return View.inflate(context, R.layout.dialog_register_layout, null);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        String test = getIntent().getStringExtra("test");

        ToastUtils.showToast(context, "來自上一個Dialog的msg: " + test);

        initTitle();
        initView();
    }

    private void initTitle()
    {
        titleBar = (TitleBar)findViewById(R.id.tb);
        titleBar.setTitleText("註冊");
    }

    private void initView()
    {
        button1 = (Button)findViewById(R.id.bt_success);
        button1.setOnClickListener(new View.OnClickListener()
        {

            @Override
            public void onClick(View v)
            {
                Bundle bundle = new Bundle();
                User user = new User("1", "李四");
                bundle.putParcelable("User", user);
                setResult(REGISTER_SUCCESS, bundle);
                dismiss();
            }
        });

        button2 = (Button)findViewById(R.id.bt_fail);
        button2.setOnClickListener(new View.OnClickListener()
        {

            @Override
            public void onClick(View v)
            {
                setResult(REGISTER_FAILED);
                dismiss();
            }
        });

    }

    @Override
    public void onBackPressed()
    {
        super.onBackPressed();
        setResult(REGISTER_CANCEL);
    }

}

好了,Dialog之間通訊再也不必用蛋疼的監聽器和互相傳引用傳handler了,像Activity一樣,你值得擁有。