一,前期基础知识储备

最近在为几个应用接入积分系统,其中使用到弹窗较为频繁,比如“登录弹窗”,“转盘弹窗”,“商城弹窗”等等,样式各有不同,所以就把此前使用的自定义弹窗在修改了一下,然后统一使用该弹窗,为了追求较好的应用内展示表现和稳定的性能。

实例:开发中使用到的一种表现比较可控的自定义弹窗-LMLPHP 解锁资源的弹窗
实例:开发中使用到的一种表现比较可控的自定义弹窗-LMLPHP 每日登录弹窗

展示弹窗的地方较多,为了追求统一性,会对弹窗做一些改善。

二,上代码,具体展示

1)写入布局

注意需要①控制所有弹窗居中显示;②为所有弹窗写入一个颜色更深的透明弹窗,以突出弹窗内容

以“解锁资源”弹窗为例,布局结构如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/trans70_black">

    <LinearLayout
        android:id="@+id/template_dialog_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:orientation="vertical">

        <androidx.cardview.widget.CardView
            android:id="@+id/dialog_done_root"
            android:layout_width="330dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="8dp"
            app:cardBackgroundColor="@color/pureWhite"
            app:cardCornerRadius="12dp"
            app:cardElevation="0dp"
            app:cardMaxElevation="0dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="center"
                android:background="@color/pureWhite"
                android:gravity="center"
                android:orientation="vertical"
                android:padding="10dp">

                <androidx.cardview.widget.CardView
                    android:layout_width="300dp"
                    android:layout_height="300dp"
                    app:cardBackgroundColor="#d9d9d9"
                    app:cardCornerRadius="4dp"
                    app:cardElevation="0dp"
                    app:cardMaxElevation="0dp">

                    <androidx.appcompat.widget.AppCompatImageView
                        android:id="@+id/dialog_show_img"
                        android:layout_width="295dp"
                        android:layout_height="295dp"
                        android:layout_gravity="center"
                        android:layout_margin="3dp"
                        android:scaleType="fitCenter" />
                </androidx.cardview.widget.CardView>

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="9dp"
                    android:orientation="horizontal">

                    <TextView
                        android:id="@+id/dialog_ad_title"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:gravity="center"
                        android:text="@string/reward_ad_title"
                        android:textColor="@color/maincolor"
                        android:textSize="20sp"
                        android:textStyle="bold" />

                    <com.yiwent.viewlib.ShiftyTextview
                        android:id="@+id/temp_store_coin"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:gravity="center"
                        android:text="1000"
                        android:textColor="@color/maincolor"
                        android:textSize="20sp"
                        android:textStyle="bold" />
                </LinearLayout>

                <TextView
                    android:id="@+id/dialog_ad_content"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="15dp"
                    android:layout_marginTop="12dp"
                    android:layout_marginEnd="15dp"
                    android:gravity="center"
                    android:text="@string/reward_ad_content"
                    android:textColor="@color/reward_content_txt"
                    android:textSize="14sp" />

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginStart="10dp"
                    android:layout_marginTop="12dp"
                    android:layout_marginEnd="10dp"
                    android:layout_marginBottom="22dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal">

                    <androidx.appcompat.widget.AppCompatImageView
                        android:id="@+id/dialog_ad_coins_img"
                        android:layout_width="135dp"
                        android:layout_height="40dp"
                        android:scaleType="fitCenter"
                        app:srcCompat="@drawable/template_coins_btn" />

                    <androidx.appcompat.widget.AppCompatImageView
                        android:id="@+id/dialog_ad_watch_img"
                        android:layout_width="135dp"
                        android:layout_height="40dp"
                        android:layout_marginStart="20dp"
                        android:scaleType="fitCenter"
                        app:srcCompat="@drawable/template_watch_video_btn" />

                </LinearLayout>

            </LinearLayout>

        </androidx.cardview.widget.CardView>

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/dialog_show_back"
            android:layout_width="44dp"
            android:layout_height="44dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="40dp"
            android:scaleType="fitCenter"
            app:srcCompat="@drawable/ic_done_dialog_back" />
    </LinearLayout>
</RelativeLayout>

相对布局作为整个弹窗的父容器,同时设置一个颜色更深的透明黑色背景;然后写入一个线性布局,此布局含纳所有的布局元素,然后居中显示在相对布局的中心位置。这样就满足了前面的需求。布局结构如下图所示:

实例:开发中使用到的一种表现比较可控的自定义弹窗-LMLPHP 布局层级

2)代码里写入控制,包括弹窗展示和点击事件控制

注意需要控制①按钮的点击事件;②弹窗进出界面时的状态栏导航栏的控制

private Dialog templateDialog;
private void showRewardAdDialog(String imgPath) {
        View view = View.inflate(getActivity(), R.layout.dialog_reward_ad_show, null);
        AppCompatImageView showImg = view.findViewById(R.id.dialog_show_img);
        AppCompatImageView dialogBack = view.findViewById(R.id.dialog_show_back);
        RequestOptions options = new RequestOptions().error(R.drawable.ic_no_network_error)
                .centerCrop().skipMemoryCache(true).diskCacheStrategy(DiskCacheStrategy.NONE).placeholder(R.color.pureBlack);
        Glide.with(getActivity()).load(imgPath).apply(options).into(showImg);
        TextView adTitle = view.findViewById(R.id.dialog_ad_title);
        TextView adContent = view.findViewById(R.id.dialog_ad_content);
        ShiftyTextview temp_store_coin = view.findViewById(R.id.temp_store_coin);
        temp_store_coin.setDuration(10);
        temp_store_coin.setNumberString(String.valueOf(treasure.getCoinNum()));

        adTitle.setTypeface(Typeface.createFromAsset(getActivity().getAssets(), "fonts/Roboto-Regular.ttf"));
        adContent.setTypeface(Typeface.createFromAsset(getActivity().getAssets(), "fonts/Roboto-Regular.ttf"));
        temp_store_coin.setTypeface(Typeface.createFromAsset(getActivity().getAssets(), "fonts/Roboto-Regular.ttf"));
        ImageView adWatchLl = view.findViewById(R.id.dialog_ad_watch_img);
        ImageView adCoins = view.findViewById(R.id.dialog_ad_coins_img);
        templateDialog = new Dialog(getActivity());
        templateDialog.setContentView(view);
        templateDialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        int divierId = getResources().getIdentifier("android:id/titleDivider", null, null);
        View divider = templateDialog.findViewById(divierId);
        if (divider != null) {
            divider.setBackgroundColor(Color.TRANSPARENT);
        }
        adWatchLl.setOnClickListener(v -> {
            ObjectAnimator scaleAnimatorX = ObjectAnimator.ofFloat(adWatchLl, "scaleX", 1f, 0.9f);
            ObjectAnimator scaleAnimatorSX = ObjectAnimator.ofFloat(adWatchLl, "scaleX", 0.9f, 1f);
            ObjectAnimator scaleAnimatorY = ObjectAnimator.ofFloat(adWatchLl, "scaleY", 1f, 0.9f);
            ObjectAnimator scaleAnimatorSY = ObjectAnimator.ofFloat(adWatchLl, "scaleY", 0.9f, 01f);
            scaleAnimatorSY.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    if (ToastUtil.isFastClick()) {
                        isPainted_video = false;
                        isSpin = false;
                        isDoubleCoins = false;
                        isNewImg_video = true;
                        showRewardAd(); //点击观看视频广告按钮
                    }
                }
            });
            AnimatorSet animSet = new AnimatorSet();
            animSet.play(scaleAnimatorX).with(scaleAnimatorY).before(scaleAnimatorSX).before(scaleAnimatorSY);
            animSet.setDuration(200L);
            animSet.start();
        });
        adCoins.setOnClickListener(v -> {
            ObjectAnimator scaleAnimatorX = ObjectAnimator.ofFloat(adCoins, "scaleX", 1f, 0.9f);
            ObjectAnimator scaleAnimatorSX = ObjectAnimator.ofFloat(adCoins, "scaleX", 0.9f, 1f);
            ObjectAnimator scaleAnimatorY = ObjectAnimator.ofFloat(adCoins, "scaleY", 1f, 0.9f);
            ObjectAnimator scaleAnimatorSY = ObjectAnimator.ofFloat(adCoins, "scaleY", 0.9f, 01f);
            scaleAnimatorSY.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    if (ToastUtil.isFastClick()) {
                        if (treasure.getCoinNum() >= COIN50) {
                            /*跳转编辑页 不用做额外的处理 因为付费模板进去肯定会编辑一次 只要编辑过 本地就保存了文件 下次即可直接进入*/
                            Intent data = new Intent(getActivity(), PaintActivity.class);
                            data.putExtra(Constant.HOME_FRAGMENT_PAINT, svgResGo);
                            data.putExtra(Constant.HOME_FRAGMENT_PAINT_NAME, svgNameGo);
                            data.putExtra(Constant.HOME_FRAGMENT_PAINT_PERCENTAGE, percentageGo);
                            data.putExtra(PaintApplication.HOME_TO_PAINT, false); //展示视频广告 则不展示插页广告
                            startActivityForResult(data, PAINT);
                            /*消耗积分 增加道具*/
                            int addCoins;
                            int currentCoins = treasure.getCoinNum();
                            int totalCoins;
                            addCoins = -COIN50;
                            totalCoins = addCoins + currentCoins;
                            treasure.setCoinNum(totalCoins);
                            CoinsEvent event = new CoinsEvent();
                            event.setCoins(totalCoins);
                            event.setcurCoins(currentCoins);
                            EventBus.getDefault().post(event);
                            /*弹窗内积分数值变化*/
                            temp_store_coin.setDuration(1500);
                            temp_store_coin.setNumberString(String.valueOf(currentCoins), String.valueOf(totalCoins));
                            templateDialog.dismiss();
                        } else {
                            ToastUtil.showToast(getActivity(), R.string.toast_do_not_have_coins);
                        }
                    }
                }
            });
            AnimatorSet animSet = new AnimatorSet();
            animSet.play(scaleAnimatorX).with(scaleAnimatorY).before(scaleAnimatorSX).before(scaleAnimatorSY);
            animSet.setDuration(200L);
            animSet.start();
        });
        dialogBack.setOnClickListener(v -> {
            ObjectAnimator scaleAnimatorX = ObjectAnimator.ofFloat(dialogBack, "scaleX", 1f, 0.9f);
            ObjectAnimator scaleAnimatorSX = ObjectAnimator.ofFloat(dialogBack, "scaleX", 0.9f, 1f);
            ObjectAnimator scaleAnimatorY = ObjectAnimator.ofFloat(dialogBack, "scaleY", 1f, 0.9f);
            ObjectAnimator scaleAnimatorSY = ObjectAnimator.ofFloat(dialogBack, "scaleY", 0.9f, 01f);
            scaleAnimatorSY.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    if (ToastUtil.isFastClick()) {
                        templateDialog.dismiss();
                    }
                }
            });
            AnimatorSet animSet = new AnimatorSet();
            animSet.play(scaleAnimatorX).with(scaleAnimatorY).before(scaleAnimatorSX).before(scaleAnimatorSY);
            animSet.setDuration(200L);
            animSet.start();
        });
        final Window window = templateDialog.getWindow();
        try {
            Util.focusNotAle(window);
            templateDialog.show();
            Util.hideNavigationBar(window);
            Util.clearFocusNotAle(window);
        } catch (Exception e) {
            e.printStackTrace();
        }
        android.view.WindowManager.LayoutParams p = templateDialog.getWindow().getAttributes();
        p.width = WindowManager.LayoutParams.MATCH_PARENT;
        p.height = WindowManager.LayoutParams.MATCH_PARENT;
        p.gravity = Gravity.CENTER;
        templateDialog.setCancelable(false);
        templateDialog.setCanceledOnTouchOutside(false);
        templateDialog.getWindow().setAttributes(p);
    }

①首先,抽出控制弹窗展示的代码部分

        View view = View.inflate(context, R.layout.dialog_reward_ad_show, null);

        templateDialog = new Dialog(context);
        templateDialog.setContentView(view);
        templateDialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        int divierId = context.getResources().getIdentifier("android:id/titleDivider", null, null);
        View divider = dialog.findViewById(divierId);
        if (divider != null) {
            divider.setBackgroundColor(Color.TRANSPARENT);
        }


        final Window window = templateDialog.getWindow();
        try {
            focusNotAle(window);
            dialog.show();
            hideNavigationBar(window);
            clearFocusNotAle(window);
        } catch (Exception e) {
            e.printStackTrace();
        }
        android.view.WindowManager.LayoutParams p = window.getAttributes();
        p.width = WindowManager.LayoutParams.MATCH_PARENT;
        p.height = WindowManager.LayoutParams.MATCH_PARENT;
        p.gravity = Gravity.CENTER;
        if (ScreenUtils.getScreenHeight() / ScreenUtils.getScreenWidth() > 1.9) {
            p.y = -Math.round(SizeUtils.dp2px(20));
        } else {
            p.y = -Math.round(SizeUtils.dp2px(45));
        }
        templateDialog.setCancelable(false);
        templateDialog.setCanceledOnTouchOutside(false);
        templateDialog.getWindow().setAttributes(p);

其中使用到三个方法,focusNotAle(window) hideNavigationBar(window) clearFocusNotAle(window),可以控制在显示弹窗时,系统导航栏和状态栏不发生变化,对于弹窗的体验很重要。三个方法具体如下:

    public static void focusNotAle(Window window) {
        window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
    }

    public static void clearFocusNotAle(Window window) {
        window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
    }

    /**
     * 隐藏虚拟栏 ,显示的时候再隐藏掉
     *
     * @param window
     */
    public static void hideNavigationBar(final Window window) {
        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
        window.getDecorView().setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
            @Override
            public void onSystemUiVisibilityChange(int visibility) {
                int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
                        //布局位于状态栏下方
                        View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
                        //全屏
                        View.SYSTEM_UI_FLAG_FULLSCREEN |
                        //隐藏导航栏
                        View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
                if (Build.VERSION.SDK_INT >= 19) {
                    uiOptions |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
                } else {
                    uiOptions |= View.SYSTEM_UI_FLAG_LOW_PROFILE;
                }
                window.getDecorView().setSystemUiVisibility(uiOptions);
            }
        });
    }

另外一个比较关键的地方,就是代码设置弹窗的大小时,要设置为全屏,即,这样配合上面的导航栏限制,体验良好。

        p.width = WindowManager.LayoutParams.MATCH_PARENT;
        p.height = WindowManager.LayoutParams.MATCH_PARENT;
        p.gravity = Gravity.CENTER;

控制按钮的点击事件,一般来说,UI会做图控制,这里提供一种代码写就的动画 — “放大缩小”

        dialogBack.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MobclickAgent.onEvent(context, "main_click_level_mode");
                ObjectAnimator scaleAnimatorX = ObjectAnimator.ofFloat(dialogBack, "scaleX", 1f, 0.9f);
                ObjectAnimator scaleAnimatorSX = ObjectAnimator.ofFloat(dialogBack, "scaleX", 0.9f, 1f);
                ObjectAnimator scaleAnimatorY = ObjectAnimator.ofFloat(dialogBack, "scaleY", 1f, 0.9f);
                ObjectAnimator scaleAnimatorSY = ObjectAnimator.ofFloat(dialogBack, "scaleY", 0.9f, 01f);
                scaleAnimatorSY.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        spinDialog.dismiss();
                    }
                });
                AnimatorSet animSet = new AnimatorSet();
                animSet.play(scaleAnimatorX).with(scaleAnimatorY).before(scaleAnimatorSX).before(scaleAnimatorSY);
                animSet.setDuration(200L);
                animSet.start();
            }
        });

在动画执行完毕,在执行按钮对应的事件。

另外提一个注意的地方,因为博主的应用中接入了视频广告,而积分系统和视频广告挂钩,所以弹窗的表现和视频广告的表现也挂钩,因此,建议将弹窗实例写为成员变量,这样在视频广告的回调中可以控制弹窗的表现。这样会方便很多。

08-29 16:35