优雅的使用ActivityLifecycleCallbacks管理Activity和区分App前后台

一、ActivityLifecycleCallbacks接口介绍

API 14之后,在Application类中,提供了一个应用生命周期回调的注册方法,用来对应用的生命周期进行集中管理,这个接口叫registerActivityLifecycleCallbacks,可以通过它注册自己的ActivityLifeCycleCallback,每一个Activity的生命周期都会回调到这里的对应方法。之前我们想做类似限制制定Activity个数的时候都要自己去添加和计数,有了ActivityLifeCycleCallback接口,所有Activity的生命周期都会在这里回调,我们可以根据条件随心处理。

Activity生命周期图:

这里写图片描述

ActivityLifecycleCallbacks接口代码:

这里写图片描述

两者几乎是一一对应的,不管是做Activity的限制还是Activity的状态统计都是非常方便的,里面还有一个void onActivitySaveInstanceState(Activity activity, Bundle outState) 方法,非常方便我们来保存Activity状态数据,是不是很周到美滋滋!

二、限制指定Activity的数量

我们需要建立一个集合存储指定打开的Activity,使用java中的Stack集合最好,Stack堆栈 is a last-in-first-out (LIFO) stack of objects,先进后出很天然的复合Activity堆栈的容器集合:

//这里我们为了限制商品详情页Activity的最多打开个数
    public static Stack<ActivityDetail> store = = new Stack<>();

在只需要在Application中处理就可以了,请看代码:

/**
 * Created by dawish on 2017/2/16.
 */
public class App extends Application {
    private static App mApp;
    public static Stack<ActivityDetail> store;
    //商品详情页最多个数,这里为了测试只写了2,大家根据自己的情况设值
    private static final int MAX_ACTIVITY_DETAIL_NUM = 2;

    @Override
    public void onCreate() {
        super.onCreate();
        mApp = this;
        store = new Stack<>();
        //注册监听器
        registerActivityLifecycleCallbacks(new SwitchBackgroundCallbacks());
    }

    public static App getAppContext() {
        return mApp;
    }

    private class SwitchBackgroundCallbacks implements ActivityLifecycleCallbacks {

        @Override
        public void onActivityCreated(Activity activity, Bundle bundle) {
            if(activity instanceof ActivityDetail) {
                if(store.size() >= MAX_ACTIVITY_DETAIL_NUM){
                    store.peek().finish(); //移除栈底的详情页并finish,保证商品详情页个数最大不超过指定
                }
                store.add((ActivityDetail) activity);
            }
        }

        @Override
        public void onActivityStarted(Activity activity) {

        }

        @Override
        public void onActivityResumed(Activity activity) {

        }

        @Override
        public void onActivityPaused(Activity activity) {

        }

        @Override
        public void onActivityStopped(Activity activity) {

        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

        }

        @Override
        public void onActivityDestroyed(Activity activity) {
            store.remove(activity);
        }
    }

    /**
     * 获取当前的Activity
     *
     * @return
     */
    public Activity getCurActivity() {
        return store.lastElement(); //返回栈顶Activity
    }
}

写的App不要忘记在manifest清单文件中注册一下。

三、控制同一个商品只会有一个ActivitDetail被打开

这个稍微麻烦点,我们需在在打开的商品ActivityDetail之前判断该商品的详情页是否已经在Activity栈中,这样的情况在电商APP中应该很常见吧,商品之间会互相推荐,我们点击推荐商品去打开详情页,因为会互相推荐,所以存在一个商品会被重复打开详情页。
ActivityDetail的代码,关键的是要保持当前商品的ID,方便区分不同的商品详情页:

    private String ID;

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

        setContentView(R.layout.activity_detail);
        AnnotateUtils.inject(ActivityDetail.this);

        ID = getIntent().getStringExtra("ID");
        currentGoodId.setText("当前详情页展示商品ID: "+ID);

    }

    public String getID() {
        return ID;
    }

如果当前点击打开的商品详情页已经被打开了,我们直接把之前打开的ActivityDetail调到前台不就行了么?

    /**
     *
     * @param id
     * @return
     */
    public static boolean toGoodsDetail(String id){

        if(store == null || store.empty()){
            return false;
        }
        for(ActivityDetail activityDetail : store){
            if(id.equalsIgnoreCase(activityDetail.getID())){ //当前商品的详情页已经打开就直接调到前台
                ActivityManager am = (ActivityManager) getAppContext().getSystemService(Activity.ACTIVITY_SERVICE);
                am.moveTaskToFront(activityDetail.getTaskId(), ActivityManager.MOVE_TASK_NO_USER_ACTION);
                return true;
            }
        }
        return false;
    }

注意: ActivityManager 的moveTaskToFront是你需要在AndroidManifest.xml中添加"Android.permission.STOP_APP_SWITCHES"用户权限,前提是必须是系统应用才可以。

扎心不?蛋疼不?
想着实现除非你是手机厂商预装的App,或者是root掉手机,把你的app放在system/app目录下面,但是这是不可能的。

最终的解决办法就是,要打开的商品详情页如果已经打开,我们就把之前的关闭掉,然后重新打开,这样就在 Activity的栈顶重新创建了一个。有点类似把之前打开的Activity调到了栈顶,但是之前的ActivityDetail的状态你可以自己保存下来传到到新的ActivityDetail中来恢复现场,做到隐藏式无缝。
在每次打开商品详情页之前都调用一下App中的
toGoodsDetail(String id)方法来检查:

    /**
     *
     * @param id
     * @return
     */
    public static boolean toGoodsDetail(String id){

        if(store == null || store.empty()){
            return false;
        }
        for(ActivityDetail activityDetail : store){
            if(id.equalsIgnoreCase(activityDetail.getID())){ //当前商品的详情页已经打开
                activityDetail.finish();
//                这是你需要在AndroidManifest.xml中添加"Android.permission.STOP_APP_SWITCHES"用户权限,前提是必须是系统应用才可以。
//                ActivityManager am = (ActivityManager) getAppContext().getSystemService(Activity.ACTIVITY_SERVICE);
//                am.moveTaskToFront(activityDetail.getTaskId(), 0);
                return true;
            }
        }
        return false;
    }

ActivityDetail中的实现:

/**
 * Created by dawish on 2017/8/10.
 */

public class ActivityDetail extends AppCompatActivity {

    @ViewInject(R.id.recGoods1)
    private Button recGoods1;

    @ViewInject(R.id.recGoods2)
    private Button recGoods2;

    @ViewInject(R.id.recGoods3)
    private Button recGoods3;

    @ViewInject(R.id.currentGoodsId)
    private TextView currentGoodsId;

    private String ID;

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

        setContentView(R.layout.activity_detail);
        AnnotateUtils.inject(ActivityDetail.this);

        ID = getIntent().getStringExtra("ID");
        currentGoodsId.setText("当前详情页展示商品ID: "+ID);

    }

    public String getID() {
        return ID;
    }

    @OnClick({R.id.recGoods1, R.id.recGoods2, R.id.recGoods3})
    public void recGoodClick(View v){

        int id = v.getId();

        switch (id){
            case R.id.recGoods1:
                String goodId1 = "101";
                toGoodDetail(goodId1);
                break;
            case R.id.recGoods2:
                String goodId2 = "102";
                toGoodDetail(goodId2);
                break;
            case R.id.recGoods3:
                String goodId3 = "103";
                toGoodDetail(goodId3);
                break;
        }

    }

    /**
     * 根据推荐商品的点击打开对应的详情页
     * @param id
     */
    public void toGoodDetail(String id){
        App.toGoodsDetail(id); //调用App中的方法去检测点击的商品详情页是否被打开,被打开就将其关闭
        Intent intent = new Intent(ActivityDetail.this, ActivityDetail.class);
        intent.putExtra("ID", id);
        startActivity(intent);
    }

}

效果图:


这里写图片描述

这样的话不管我们怎么点击下面的推荐商品,同一个商品不会存在多个详情页,我们详情页的总个数也可以控制。完美!

四、判断App前后台状态

App 前后台的切换一般情况下都是按Home来进行,当然也有别的方式,但是此时Activity的生命周期是一样的:

HOME键前后台切换Activity的执行顺序:onPause->onStop->onRestart->onStart->onResume

BACK键前后台切换Activity键的顺序: onPause->onStop->onDestroy->onCreate->onStart->onResume

其实按BACK按键就是退出app了,不算是前台后切换。

现在我们知道App的由前台切换到后台所有打开的Activity会走:

onPause->onStop

后台切换到前台所有打开的Activity会走:

->onRestart->onStart->onResume

前后台切换App所有打开的Activity的生命周期都是一样的,这样我就可以在ActivityLifecycleCallbacks回调接口中记录生命周期:

App类最终完整代码:


/**
 * Created by dawish on 2017/2/16.
 */
public class App extends Application {
    //记录Activity的总个数
    public int count = 0;
    private static App mApp;
    public static Stack<ActivityDetail> store;
    //商品详情页最多个数,这里为了测试只写了2,大家根据自己的情况设值
    private static final int MAX_ACTIVITY_DETAIL_NUM = 2;

    @Override
    public void onCreate() {
        super.onCreate();
        mApp = this;
        store = new Stack<>();
        registerActivityLifecycleCallbacks(new SwitchBackgroundCallbacks());
    }

    public static App getAppContext() {
        return mApp;
    }

    /**
     *
     * @param id
     * @return
     */
    public static boolean toGoodsDetail(String id){

        if(store == null || store.empty()){
            return false;
        }
        for(ActivityDetail activityDetail : store){
            if(id.equalsIgnoreCase(activityDetail.getID())){ //当前商品的详情页已经打开
                activityDetail.finish();
//                这是你需要在AndroidManifest.xml中添加"Android.permission.STOP_APP_SWITCHES"用户权限,前提是必须是系统应用才可以。
//                ActivityManager am = (ActivityManager) getAppContext().getSystemService(Activity.ACTIVITY_SERVICE);
//                am.moveTaskToFront(activityDetail.getTaskId(), 0);
                return true;
            }
        }
        return false;
    }

    private class SwitchBackgroundCallbacks implements ActivityLifecycleCallbacks {

        @Override
        public void onActivityCreated(Activity activity, Bundle bundle) {
            if(activity instanceof ActivityDetail) {
                if(store.size() >= MAX_ACTIVITY_DETAIL_NUM){
                    store.peek().finish(); //移除栈底的详情页并finish,保证商品详情页个数最大为10
                }
                store.add((ActivityDetail) activity);
            }
        }

        @Override
        public void onActivityStarted(Activity activity) {
            if (count == 0) { //后台切换到前台
                Log.v("danxx", ">>>>>>>>>>>>>>>>>>>App切到前台");
            }
            count++;
        }

        @Override
        public void onActivityResumed(Activity activity) {

        }

        @Override
        public void onActivityPaused(Activity activity) {

        }

        @Override
        public void onActivityStopped(Activity activity) {
            count--;
            if (count == 0) { //前台切换到后台
                Log.v("danxx", ">>>>>>>>>>>>>>>>>>>App切到后台");
            }
        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {

        }

        @Override
        public void onActivityDestroyed(Activity activity) {
            store.remove(activity);
        }
    }

    /**
     * 获取当前的Activity
     *
     * @return
     */
    public Activity getCurActivity() {
        return store.lastElement();
    }
}

Github代码请点击

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,050评论 25 707
  • 启动与销毁Activity 不同于使用 main() 方法启动应用的其他编程范例,Android 系统会通过调用对...
    安卓Boy阅读 1,729评论 3 5
  • 曾经我以为是自己作,导致每段关系都无疾而终。心想,若再不改变自己,即便是遇到优秀的人都会被自己的作吓跑。但是当我遇...
    长腿菇凉阅读 2,410评论 0 0
  • 一次偶然的机会在图书馆淘到一本《论中美教育》,当时选中这边书,无疑是到了要孩子的时候,想提前了解下孩子教育方面...
    星期八_静阅读 934评论 0 1
  • 应该连续做了一个月的饭了,从最简单的水煮西蓝花,到现在的西红柿牛腩,进步还是蛮大的。 一开始他们都说你不会坚持的,...
    流水账高手罗色阅读 191评论 0 0