使用建造者模式封装通用弹窗PopupWindow

2,484 阅读5分钟

先来简单回顾下建造者模式

定义

把一个复杂对象表示和构建过程分离开来,使得同样的构建过程可以创建不同的表示

场景

  1. 相同方法,不同的执行顺序,产生不同的事件结果
  2. 多个部件可以装配到一个对象,但是产生的运行结果又不相同时
  3. 产品类复杂,或者,产品类调用顺序不同产生不同作用
  4. 当初始化一个对象很复杂,如参数多,且很多参数有默认值

UML结构图

  • 产品类:一般是一个较为复杂的对象,也就是说创建对象的过程比较复杂,一般会有比较多的代码量。在本类图中,产品类是一个具体的类,而非抽象类。实际编程中,产品类可以是由一个抽象类与它的不同实现组成,也可以是由多个抽象类与他们的实现组成。
  • 抽象建造者:引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现。这样更容易扩展。一般至少会有两个抽象方法,一个用来建造产品,一个是用来返回产品。
  • 建造者:实现抽象类的所有未实现的方法,具体来说一般是两项任务:组建产品;返回组建好的产品
  • 导演类:负责调用适当的建造者来组建产品,导演类一般不与产品类发生依赖关系,与导演类直接交互的是建造者类。一般来说,导演类被用来封装程序中易变的部分。

实际开发过程,Director角色经常会被忽略,直接使用Builder进行对象的组装,Builder通常为链式调用,结构更简单,对产品有更精细的控制

优点

  • 良好的封装性,客户端不必关心产品内部组成细节
  • 建造者独立,易扩展

缺点

产生多余的Builder对象以及Direactor对象,消耗内存

通用弹窗的封装

弹窗的场景符合建造者模式场景4,所以这边使用建造者模式封装此(省略导演类和抽象构造者类),先看如下调用代码

  • MainActivity.java
package cn.llj.pop.demo;

import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity
{
    private MyPopWindow mPopWindow;
    private View mPopView;
    private TextView mPopTv;
    
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mPopView = View.inflate(this, R.layout.pop_view, null);
        mPopTv=mPopView.findViewById(R.id.tv);
        mPopTv.setOnClickListener(new View.OnClickListener()//点击弹窗内的控件,弹窗消失
        {
            @Override
            public void onClick(View view)
            {
                if (mPopWindow !=null && mPopWindow.isShowing())
                    mPopWindow.dismiss();
            }
        });
    }
    
    public void showPop(View view)
    {
        if (mPopWindow != null)
            mPopWindow.show();
        else
        {   //这里参数如果与默认值一致,无需再设置
            mPopWindow = new MyPopWindow.Builder(this) 
                    .setLayoutView(mPopView)//设置弹窗布局
                    .setOutsideTouchable(true)//弹窗外是否可点击是否消失
                    .setBgTranslucent(true)//弹窗外是否半透明
                    .setHideStateBar(false)//是否隐藏状态栏
                    .setAnimStyle(R.style.pop_anim_style)//设置弹窗动画
                    .setHeight(400)//设置弹窗高度 单位px
                    .setWidth(ViewGroup.LayoutParams.MATCH_PARENT)//设置弹窗宽度,与屏幕同宽
                    .setOrientation(Gravity.BOTTOM)//弹窗出现的方向,Gravity.BOTTOM 底部弹出,Gravity.RIGHT 右侧弹出
                    .setPopDismissListener(new MyPopWindow.OnPopDismissListener()//设置弹窗关闭回调
                    {
                        @Override
                        public void onDismiss()
                        {
                            Toast.makeText(MainActivity.this,"弹窗关闭了",Toast.LENGTH_SHORT).show();
                        }
                    })
                    .build();
            mPopWindow.show();
           // mPopWindow.show(10,20); //设置弹窗距离屏幕的距离
        }
    }
    
    @Override
    protected void onDestroy()
    {
        if (mPopWindow != null)
            mPopWindow.destroy();//弹窗销毁,防止内存泄漏
        super.onDestroy();
    }
}

  • 自定义弹窗类MyPopWindow.java
package cn.llj.pop.demo;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.PopupWindow;

/**
 * Created by llj on 2019/4/2.
 * desc:适用于该项目的通用底部弹出的pop,有需要拓展的设置,在此类拓展
 */
public class MyPopWindow implements PopupWindow.OnDismissListener, ValueAnimator
        .AnimatorUpdateListener
{
    
    private final int ALPHA_ANIMATOR_DURATION = 200;//半透明动画消失时间
    private static boolean mOutsideTouchable = true;
    private static boolean mFocusable = true;
    private static boolean mIsHideStateBar = false;
    private static View mLayoutView;
    private static int mWidth = ViewGroup.LayoutParams.MATCH_PARENT;//px
    private static int mHeight = ViewGroup.LayoutParams.MATCH_PARENT;//px
    private static int mAnimStyle = R.style.pop_anim_style;
    private PopupWindow mPopWindow;
    private ValueAnimator mBgAlphaAnimator;
    private static Context mContext;
    private static OnPopDismissListener mPopDismissListener;
    private static boolean mIsBgTranslucent = true;
    private static int mOrientation = Gravity.BOTTOM;
    
    public MyPopWindow()
    {
        mPopWindow = new PopupWindow(mLayoutView, mWidth, mHeight);
        mPopWindow.setAnimationStyle(mAnimStyle);
        mPopWindow.setFocusable(mFocusable);
        mPopWindow.setOutsideTouchable(mOutsideTouchable);
        mPopWindow.setOnDismissListener(this);
    }
    
    @Override
    public void onAnimationUpdate(ValueAnimator animation)
    {
        if (mContext != null)
        {
            ((Activity) mContext).getWindow().getAttributes().alpha = (float) animation
                    .getAnimatedValue();
            ((Activity) mContext).getWindow().setAttributes(((Activity) mContext).getWindow()
                    .getAttributes());
        }
    }
    
    @Override
    public void onDismiss()
    {
        if (mIsBgTranslucent)
        {
            mBgAlphaAnimator = ValueAnimator.ofFloat(0.5f, 1.0f);
            mBgAlphaAnimator.start();
            mBgAlphaAnimator.setDuration(ALPHA_ANIMATOR_DURATION);
            mBgAlphaAnimator.addUpdateListener(this);
        }
        
        if (mPopDismissListener != null)
            mPopDismissListener.onDismiss();
    }
    
    public void show()
    {
        if (mPopWindow != null && mLayoutView != null && !mPopWindow.isShowing())
        {
            setAnimAndStateBar();
            mPopWindow.showAtLocation(mLayoutView, mOrientation, 0, 0);
        }
    }
    
    /**
     * 相对于屏幕的距离
     * @param xoff
     * @param yoff
     */
    public void show(int xoff, int yoff)
    {
        if (mPopWindow != null && mLayoutView != null && !mPopWindow.isShowing())
        {
            setAnimAndStateBar();
            mPopWindow.showAtLocation(mLayoutView, mOrientation, xoff, yoff);
        }
    }
    
    private void setAnimAndStateBar()
    {
        if (mIsBgTranslucent)
        {
            mBgAlphaAnimator = ValueAnimator.ofFloat(1.0f, 0.5f);
            mBgAlphaAnimator.setDuration(ALPHA_ANIMATOR_DURATION);
            mBgAlphaAnimator.start();
            mBgAlphaAnimator.addUpdateListener(this);
        }
        if (mIsHideStateBar)
        {
            ((Activity) mContext).getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }
    }

    /**
     * 弹窗关闭
     */
    public void dismiss()
    {
        if (mPopWindow != null)
            mPopWindow.dismiss();
        
    }
    
    /**
     * 弹窗销毁,防止内存泄漏
     */
    public void destroy()
    {
        if (mPopWindow.isShowing())
            mPopWindow.dismiss();
        mPopWindow = null;
        mContext = null;
        mBgAlphaAnimator = null;
    }
    
    public boolean isShowing()
    {
        return mPopWindow != null && mPopWindow.isShowing();
    }
    
    /**
     * Listener that is called when this popup window is dismissed.
     */
    public interface OnPopDismissListener
    {
        /**
         * Called when this popup window is dismissed.
         */
         void onDismiss();
    }
    
    public static class Builder
    {
        
        public Builder(Context context)
        {
            if (context == null || !(context instanceof Activity) || ((Activity) context)
                    .isFinishing())
            {
                throw new RuntimeException("activity is invalidate which can not create a " +
                        "popupwindow:" + context);
            }
            mContext = context;
        }
        
        /**
         * 弹窗出现的方向
         *
         * @param orientation Gravity.BOTTOM  Gravity.RIGHT
         * @return
         */
        public Builder setOrientation(int orientation)
        {
            mOrientation = orientation;
            return this;
        }
        
        public Builder setFocusable(boolean focusable)
        {
            mFocusable = focusable;
            return this;
        }
        
        /**
         * 是否隐藏状态栏
         *
         * @param hideStateBar
         * @return
         */
        public Builder setHideStateBar(boolean hideStateBar)
        {
            mIsHideStateBar = hideStateBar;
            return this;
        }
        
        public Builder setOutsideTouchable(boolean outsideTouchable)
        {
            mOutsideTouchable = outsideTouchable;
            return this;
        }
        
        public Builder setLayoutView(View layout)
        {
            mLayoutView = layout;
            return this;
        }
        
        public Builder setWidth(int width)
        {
            mWidth = width;
            return this;
        }
        
        public Builder setHeight(int height)
        {
            mHeight = height;
            return this;
        }
        
        /**
         * 设置出现消失动画,默认自下而上
         *
         * @param animStyle
         * @return
         */
        public Builder setAnimStyle(int animStyle)
        {
            mAnimStyle = animStyle;
            return this;
        }
        
        public Builder setBgTranslucent(boolean isBgTranslucent)
        {
            mIsBgTranslucent = isBgTranslucent;
            return this;
        }
        
        public Builder setPopDismissListener(OnPopDismissListener listener)
        {
            mPopDismissListener = listener;
            return this;
        }
        
        public MyPopWindow build()
        {
            
            return new MyPopWindow();
        }
        
    }
    
}

  • 这里预置几种动画类型,根据实际业务自行拓展

style.xml

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
        <!-- Customize your theme here. -->
    </style>
    <!--底部弹出动画-->
    <style name="pop_anim_style" parent="android:Animation">
        <item name="android:windowEnterAnimation">@anim/pop_enter_anim</item>
        <item name="android:windowExitAnimation">@anim/pop_exit_anim</item>
    </style>
    <!--右侧弹出动画-->
    <style name="pop_right_anim_style" parent="android:Animation">
        <item name="android:windowEnterAnimation">@anim/pop_right_enter_anim</item>
        <item name="android:windowExitAnimation">@anim/pop_right_exit_anim</item>
    </style>
    <!--右上角缩放弹出动画-->
    <style name="pop_fade_anim_style">
        <item name="android:windowEnterAnimation">@anim/pop_fade_in</item>
        <item name="android:windowExitAnimation">@anim/pop_fade_out</item>
    </style>
</resources>
  • 相关动画文件

pop_enter_anim.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="200"
        android:fromYDelta="100%"
        android:toYDelta="0" />
</set>

pop_exit_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="200"
        android:fromYDelta="0"
        android:toYDelta="100%" />
</set>

pop_fade_in.xml

<?xml version="1.0" encoding="utf-8"?>
<scale   xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:fromXScale="0.001"
    android:toXScale="1.0"
    android:fromYScale="0.001"
    android:toYScale="1.0"
    android:pivotX="100%"
    android:pivotY="10%"
    android:duration="200" />

pop_fade_out.xml

<?xml version="1.0" encoding="utf-8"?>
<scale   xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:fromXScale="1.0"
    android:toXScale="0.001"
    android:fromYScale="1.0"
    android:toYScale="0.001"
    android:pivotX="100%"
    android:pivotY="10%"
    android:duration="200" />

pop_right_enter_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:duration="200"
        android:fromXDelta="100%"
        android:toXDelta="0" />
<!--    <alpha
        android:duration="200"
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />-->
</set>

pop_right_exit_anim.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="200"
        android:fromXDelta="0"
        android:toXDelta="100%" />
</set>

Android源码中的建造者模式

  • AlertDialog

如下为AlertDialog源码结构:

具体细节可自行查看源码,本文的例子相当于AlertDialog的简易版

  • 图片加载框架Glide、ImageLoader等