最近需要做的

【架构】可能是理想的Android欢迎界面实现

2018-10-01  本文已影响504人  Geeny

我们在使用Android时,很多APP打开都会有启动画面(欢迎界面),它会停留若干秒后再进入主界面。

先看一下Demo效果。

开机效果

源码:GitHub地址


欢迎界面的意义

欢迎界面固然有展示品牌形象的作用,但关于欢迎界面我们需要明白是:

  1. 理论上界面越快消失越好,让用户尽早使用到APP
  2. 欢迎界面的停留可能用于广告的展示
  3. 显示欢迎界面的意义不是为了单纯的“炫”,它是给加载APP运行时需要的数据作掩护
  4. 欢迎界面显示过程中,应该是全屏的、不能返回的、不能取消的
  5. 欢迎界面所做的任务如果超时,我们不应该一直停留在欢迎界面上,而是应当直接进入主界面,防止用户等待过久并认为是APP卡死

关于上面第3点的启动时加载数据,情况有不少,比如从服务器拿到一些用户特有的配置(如Taplytics数据)、从本地SharePreference或数据库载入数据到内存等,这些都需要在进入主界面前完成。当然,也有一些在进入主界面前不用必须完成的,比如检测版本更新,如果检测到一半,其它的启动任务完成了,随时可以关闭欢迎界面。

欢迎界面的设计点

在设计欢迎界面时,我们可以得出几点信息:

  1. 需要定义和实现APP的启动任务,任务分两类,进入主界面前必须完成的和不必须完成的
  2. APP启动时就执行启动任务并展示欢迎界面,所有进入主界面前必须完成的启动任务全部执行完毕后,关闭欢迎界面,进入主界面并加载主界面的UI和数据
  3. 启动任务执行时间超时后,直接关闭欢迎界面

流程图如下,倒也没什么复杂。

但实现起来还是有不少要注意的细节,请看下文。


欢迎界面的显示流程图

代码的实现

1. 任务接口的定义

/**
 * 启动时的任务接口定义
 */
public interface StartTaskInterface {
    void execute(AppStarter.OnTaskListener listener);  // 启动任务的方法,方法中当任务结束时将回调listener

    // 隐藏欢迎界面前,是否需要确保这个任务已经完成
    // 如果为true,则此任务是必要执行的,在没完成任务前欢迎界面将一直显示(直到超时)
    boolean isNeedDoneBeforeHideWelcomeDialog();
}

2. 我们定义三个任务

/**
 * 启动APP时得到配置信息任务(调用API或读取本地数据)
 */
public class GetConfigStartTaskImpl implements StartTaskInterface {
    @Override
    public void execute(AppStarter.OnTaskListener listener) {

        // 这里可根据实际需求替换成网络请求或异步任务
        new Handler().postDelayed(() -> {
            if (listener != null) {
                listener.onFinished("GetConfigFinished");
            }
        }, 3000);  // 假设花了3秒完成请求API得到APP的配置变量
    }

    @Override
    public boolean isNeedDoneBeforeHideWelcomeDialog() {
        return true;
    }
}
/**
 * 显示广告的任务
 */
public class ShowAdsStartTaskImpl implements StartTaskInterface {
    @Override
    public void execute(AppStarter.OnTaskListener listener) {

        // 这里可根据实际需求替换成网络请求或异步任务
        new android.os.Handler().postDelayed(() -> {
            if (listener != null) {
                listener.onFinished("ShowAdsFinished");  // task完成后回调并传入标识,执行结束操作
            }
        }, 2000);  // 假设展示广告时间为2秒
    }

    @Override
    public boolean isNeedDoneBeforeHideWelcomeDialog() {
        return true;  // 这个任务没完成前,不要关闭欢迎界面
    }
}
/**
 * 超时任务
 */
public class TimeOutTask implements StartTaskInterface {

    private static final int TIMEOUT = 5000;  // 我们设定欢迎界面显示的超时时间为5秒

    private AppStarter appStarter;

    public TimeOutTask(AppStarter appStarter) {
        this.appStarter = appStarter;
    }

    @Override
    public void execute(AppStarter.OnTaskListener listener) {
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                appStarter.setOnTaskListener(null);
                WelcomeDialogController.getInstance().dismiss();  // 超时后关闭欢迎界面
            }
        }, TIMEOUT);
    }

    @Override
    public boolean isNeedDoneBeforeHideWelcomeDialog() {
        return false;  // 关掉欢迎界面时,不需要理睬这个任务是否完成,其它必要的任务完成后就可以关闭欢迎界面了
    }
}

可以看到,这些任务的定义具有很好的灵活性,当之后我们希望加入更多的任务时,直接实现StartTaskInterface接口就可以了。

另外,上面的任务中,获取配置的任务耗时3秒,显示广告耗时2秒,超时任务5秒。前两个是进入主界面前必须完成的任务,如果没意外,这个欢迎界面将显示3秒就会关闭。

3. 设计欢迎界面的Dialog

值得注意的是,这里的欢迎界面是通过Dialog实现而不是Actvity。原因是欢迎界面使用Actvity太过于“杀鸡用牛刀”,另外,我们显示欢迎界面的目的是加载数据或广告,理论上欢迎界面和主界面MainActivtiy的状态是相关的。将Dialog和MainActivity的状态附着在一起处理是更恰当的。

public class WelcomeDialog extends Dialog {
    WelcomeDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, themeResId);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.dlg_welcome);

        // 动态地将欢迎界面的图片根据地屏幕参数撑满整个屏幕
        ImageView iv = findViewById(R.id.iv_welcome_picture);
        iv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                Matrix m = new Matrix();
                float scale;
                Drawable drawable = ContextCompat.getDrawable(iv.getContext(), R.drawable.welcome);
                final int dwidth = drawable.getIntrinsicWidth();
                final int dheight = drawable.getIntrinsicHeight();

                final int vwidth = iv.getMeasuredWidth();
                final int vheight = iv.getMeasuredHeight();

                if (dwidth * vheight > vwidth * dheight) {
                    scale = (float) vheight / (float) dheight;
                } else {
                    scale = (float) vwidth / (float) dwidth;
                }
                m.setScale(scale, scale);
                iv.setImageMatrix(m);

                iv.getViewTreeObserver().removeOnPreDrawListener(this);
                return false;
            }
        });
        WindowManager.LayoutParams lp = getWindow().getAttributes();
        lp.width = DeviceRelatedUtil.getInstance().getDisplayWidth();
        lp.height = DeviceRelatedUtil.getInstance().getDisplayHeight();
        getWindow().setAttributes(lp);
    }
}

4. 实现对欢迎界面Dialog进行管理控制

/**
 * 欢迎界面的管理类
 */
public class WelcomeDialogController {

    private static final int DELAY = 500;
    private static WelcomeDialogController sInstance;
    // 从MainActivity传过来的回调,在欢迎界面关闭时执行。一般来说就是欢迎界面消失进入主界面后加载view和data的操作
    private DialogInterface.OnDismissListener mOnDismissListener;
    private boolean mFinished;
    private Dialog mSplashDialog;
    private final Runnable mDismissRunnable = this::destroy;
    private AppStarter.OnTaskListener mWelcomeDialogShowListener;
    private boolean mIsDispatched;

    public static WelcomeDialogController getInstance() {
        if (sInstance == null) {
            sInstance = new WelcomeDialogController();

            // 关闭欢迎界面的回调,将用于AppStarter里所有任务完成后执行
            sInstance.mWelcomeDialogShowListener = key -> {
                if (!sInstance.mFinished) {
                    sInstance.dismiss();
                }
            };

        }
        return sInstance;
    }

    public boolean isNeedShow() {
        return !mFinished;
    }

    public void show(Activity activity, DialogInterface.OnDismissListener listener) {
        mOnDismissListener = listener;
        if (mSplashDialog == null) {

            // R.style.full_screen_dialog指定dialog不能返回、全屏、没有title、没有背景等一堆限定,让用户在欢迎界面出现时什么都不能操作
            mSplashDialog = new WelcomeDialog(activity, R.style.full_screen_dialog);

            mSplashDialog.setCancelable(false);
        }

        if (!activity.isFinishing()) {
            mSplashDialog.show();
        }
    }

    // 关闭欢迎界面和做一些操作
    public void dismiss() {
        sInstance.mFinished = true;
        dispatchListener();
        delayDismissDialog(DELAY);
    }

    // 执行MainActivity传过来的回调,即在进入主界面后要做的操作
    private void dispatchListener() {
        if (mOnDismissListener != null && !mIsDispatched) {
            mOnDismissListener.onDismiss(mSplashDialog);
            mIsDispatched = true;
        }
    }

    // 过一会关闭掉dialog(算是给dispatchListener做主界面的加载一点时间,提高用户体验)
    private void delayDismissDialog(int time) {
        new Handler(Looper.getMainLooper()).postDelayed(mDismissRunnable, time);  // 调用mDismissRunnable,即destory方法
    }

    public void destroy() {
        try {
            mSplashDialog.dismiss();
        } catch (Exception ignored) {
        }
    }


    public AppStarter.OnTaskListener getListener() {
        return mWelcomeDialogShowListener;
    }
}

5. 实现对启动任务进行控制

public class AppStarter {

    private static AppStarter sInstance = new AppStarter();
    private final List<StartTaskInterface> mStaterTasks = new ArrayList<>();  // 存储所有的启动任务
    private OnTaskListener mWelcomeDialogShowListener;  // 关闭欢迎界面的回调

    private int mCountOfNeedDoneTask;  // 必要完成的启动任务个数
    // 这个回调用于每一个必要完成的启动任务执行完成后调用,工作大致是记录完成的个数,当全部完成时,调用
    final OnTaskListener neeWaitTasksListener = new OnTaskListener() {
        private int mCountOfFinishedTask;  // 完成并调用了回调的任务个数

        @Override
        public synchronized void onFinished(String key) {
            mCountOfFinishedTask++;
            if (mCountOfFinishedTask == mCountOfNeedDoneTask && mWelcomeDialogShowListener != null) {
                mWelcomeDialogShowListener.onFinished(null);  // 当必要完成的启动任务全部执行完毕后,便可以关闭欢迎界面了
            }
        }
    };

    public static AppStarter getInstance() {
        if (sInstance == null) {
            sInstance = new AppStarter();
        }
        return sInstance;
    }

    public OnTaskListener getOnTaskListener() {
        return mWelcomeDialogShowListener;
    }

    public void setOnTaskListener(OnTaskListener onTaskListener) {
        this.mWelcomeDialogShowListener = onTaskListener;
    }

    // 增加一个启动任务
    public void add(StartTaskInterface task) {
        mStaterTasks.add(task);
    }

    public void start(OnTaskListener listener) {

        mWelcomeDialogShowListener = listener;

        // 计算下欢迎界面关闭前必需完成的任务数
        for (StartTaskInterface t : mStaterTasks) {
            if (t.isNeedDoneBeforeHideWelcomeDialog()) {
                mCountOfNeedDoneTask += 1;
            }
        }

        for (StartTaskInterface t : mStaterTasks) {
            if (t.isNeedDoneBeforeHideWelcomeDialog()) {
                t.execute(neeWaitTasksListener);  // 需要在欢迎界面关闭前完成的任务执行完成后,将会回调neeWaitTasksListener
            } else {
                t.execute(null);  // 在欢迎界面关闭不一定要完成的任务直接执行
            }
        }
    }

    public interface OnTaskListener {
        void onFinished(String key);
    }
}

6. 启动应用时执行启动任务

关于启动任务的执行,理论上越早执行越好,我们把启动任务的执行放在Application的子类里,关于Application类的官方介绍如下。

The Application class in Android is the base class within an Android app that contains all other components such as activities and services. The Application class, or any subclass of the Application class, is instantiated before any other class when the process for your application/package is created. This class is primarily used for initialization of global state before the first Activity is displayed. Note that custom Application objects should be used carefully and are often not needed at all.

Application与组件和数据的关系
/**
 * APP一启动,将调用Application类
 */
public class WelcomeApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        DeviceRelatedUtil.init(this);

        addStartTask();
    }

    // 添加启动任务到AppStarter里
    private void addStartTask() {
        AppStarter.getInstance().add(new ShowAdsStartTaskImpl());
        AppStarter.getInstance().add(new GetConfigStartTaskImpl());
        AppStarter.getInstance().add(new TimeOutTask(AppStarter.getInstance()));

        AppStarter.getInstance().start(WelcomeDialogController.getInstance().getListener());  // 执行启动任务
    }
}

之后,在MainActivity中的onCreate方法,只要满足首次进入或重新加载的条件,就会显示欢迎界面。当所有在进入主界面前必须完成的任务执行完毕或显示超时后关闭欢迎界面。

我们将整个APP的组件和数据概括在我们的自己实现的Application类里。

<application
    android:name=".WelcomeApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:largeHeap="true"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
        android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

这样就大致实现了界面的显示。

值得注意的

1. 打开APP时可能出现短暂的白屏或黑屏

因为APP是先打开MainActivity然后再加载欢迎界面的,在欢迎界面加载前(很短暂的时间),显示的是MainActivity的空白状态。如果没有设置MainActivity的背景,可能会造成启动APP时出现短暂的白屏或黑屏。

解决的方法有二,一是把MainActivity的背景设置为透明,进入主界面后再设置为白或空,另一个方法则是先把背景设置为和欢迎界面一样,进入主界面后再设置为白或空。
由于第一种方案会导致打开APP看起来有一小会没动静的效果(即那【短暂的空白画面被透明化】),我们采用第二种,可以实现一打开APP就看到欢迎界面的图片(当然,如果屏幕匹配不好,欢迎界面弹出来时可能会造成画面的闪动)。

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"
    android:screenOrientation="portrait"
    android:theme="@style/MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

我们设定了MainActivity的主题@style/MainActivity,里面便设置了MainActivity的背景。

<style name="MainActivity" parent="AppTheme">
    <item name="android:windowBackground">@drawable/welcome</item> 
    <item name="android:windowTranslucentStatus">true</item>
</style>
上一篇下一篇

猜你喜欢

热点阅读