说说在 Android 中如何实现强制下线功能

作者: deniro | 来源:发表于2018-03-03 15:53 被阅读2941次

    在应用程序中的一个常见功能是 “强制下线”。比如 QQ 号在别处登录后,就会把当前的 QQ 号挤下线。实现思路是:在界面上弹出一个对话框,让用户无法进行任何其他操作,只能点击对话框中的确定按钮, 然后回到登录界面。

    要实现强制下线之前,必须先关闭所有的活动。首先,创建一个 Activities 类用于管理所有的活动:

    public class Activities {
    
        public static List<Activity> activities = new ArrayList<>();
    
        /**
         * 新增活动
         *
         * @param activity
         */
        public static void add(Activity activity) {
            activities.add(activity);
        }
    
        /**
         * 移除活动
         *
         * @param activity
         */
        public static void remove(Activity activity) {
            activities.remove(activity);
        }
    
        /**
         * 结束所有活动
         */
        public static void finishAll() {
            for (Activity activity : activities) {
                if (activity.isFinishing()) {
                    activity.finish();
                }
            }
        }
    }
    

    然后创建 BaseActivity 类作为所有活动的父类:

    public class BaseActivity extends AppCompatActivity {
       @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Activities.add(this);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            Activities.remove(this);
        }
    }
    

    接下来要创建一个登录界面,新建 LoginActivity(在 Android Studio 中,建议创建空的活动),编辑 activity_login.xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:orientation="horizontal">
    
            <TextView
                android:layout_width="90dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:text="账号:"
                android:textSize="18sp" />
    
            <EditText
                android:id="@+id/account"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_weight="1" />
        </LinearLayout>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:orientation="horizontal">
    
            <TextView
                android:layout_width="90dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:text="密码:"
                android:textSize="18sp" />
    
            <EditText
                android:id="@+id/password"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_weight="1"
                android:inputType="textPassword" />
        </LinearLayout>
    
        <Button
            android:id="@+id/login"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:text="登录" />
    </LinearLayout>
    
    

    我们使用 LinearLayout 编写了一个登陆布局,使用纵向排列,从上到下分别是账号、密码和登陆按钮。

    接着修改 LoginActivity 中的代码:

    public class LoginActivity extends BaseActivity  {
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
    
            final EditText accountEditText=(EditText)findViewById(R.id.account);
            final EditText passwordEditText=(EditText)findViewById(R.id.password);
    
            Button loginBtn=(Button)findViewById(R.id.login);
            loginBtn.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    String account=accountEditText.getText().toString();
                    String password=passwordEditText.getText().toString();
    
                    if(account.equals("admin")&&password.equals("1")){//登录成功
                        startActivity(new Intent(LoginActivity.this,MainActivity.class));
                        finish();
                    }else{//登录失败,弹出提示
                        Toast.makeText(LoginActivity.this, "账号或密码不正确", Toast.LENGTH_SHORT).show();
                    }
                }
            });
    
        }
    }
    

    LoginActivity 继承自 BaseActivity。然后获取账号与密码的输入值,进行判断。如果登录成功,就启动 MainActivity 活动;否则登录失败,弹出提示。

    这里假设把 MainActivity作为登陆成功后进入的主界面,修改对应的布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical">
    
        <Button
            android:id="@+id/force_offline"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="发送强制下线广播"
            />
    </LinearLayout>
    

    这里,我们只定义了一个按钮,用于触发强制下线功能。然后修改 MainActivity 中的代码:

    public class MainActivity extends BaseActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            findViewById(R.id.force_offline).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    sendBroadcast(new Intent("net.deniro.android.FORCE_OFFLINE"));
                }
            });
        }
    }
    

    在按钮的点击事件中发送了一条广播,用于通知程序让强制用户下线。

    强制用户下线的逻辑是写在接收这条广播的广播接收器里面,这样强制下线的功能就不会依附于任何的界面,不管是在程序的任何地方,只需要发出这样一条广播,就可以完成强制下线的操作啦O(∩_∩)O~

    我们在 BaseActivity 中动态注册了一个广播接收器,因为所有的活动都是继承自 BaseActivity 的,这样这些活动就都可以支持这个广播接收器咯。

    修改 BaseActivity 中的代码:

    public class BaseActivity extends AppCompatActivity {
    
        private ForceOffLineReceiver receiver;
        ...
        private class ForceOffLineReceiver extends BroadcastReceiver {
            @Override
            public void onReceive(final Context context, Intent intent) {
                AlertDialog.Builder builder = new AlertDialog.Builder(context);
                builder.setTitle("警告");
                builder.setMessage("您已被迫下线,请重新登录!");
                builder.setCancelable(false);
                builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Activities.finishAll();//销毁所有活动
                        context.startActivity(new Intent(context, LoginActivity.class));//启动【登录】活动
                    }
                });
                builder.show();
            }
        }
    
        @Override
        protected void onResume() {
            super.onResume();
    
            //注册广播接收器
            IntentFilter intentFilter = new IntentFilter();
            intentFilter.addAction("net.deniro.android.FORCE_OFFLINE");
            receiver = new ForceOffLineReceiver();
            registerReceiver(receiver, intentFilter);
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            if(receiver!=null){//注销
                unregisterReceiver(receiver);
            }
            receiver=null;
        }
    }
    

    这里使用了 AlertDialog,并 setCancelable() 让对话框不可取消(如果可取消,用户点击一下 back 键,对话框就没啦),然后使用 setPositiveButton() 方法定义【确定】按钮,当用户点击按钮时,销毁所有活动并/启动【登录】活动。

    我们重写了 onResume() 和 onPause() 这两个生命周期的函数,然后分别在这两个方法里注册和注销了自定义的广播接收器,之所以这样写是因为我们要始终需要保证只有处于栈顶的活动才能接收到下线广播,非栈顶活动不需要也没有必要接收这条广播。

    最后修改 AndroidManifest.xml:

    <activity
                android:name=".LoginActivity"
                android:label="@string/app_name">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    

    将 LoginActivity 设置为主活动。

    运行程序:

    登录界面

    输入正确的账号和密码后,点击登录进入主界面:

    主界面

    点击【发送强制广播】按钮:

    强制下线提示

    相关文章

      网友评论

      • developerzjy:Activities确实没必要,清空任务栈中所有的activitie使用两个flag即可,代码:
        Intent intent = new Intent(context, LoginActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        context.startActivity(intent);
      • 夏日里的故事:不需要写BaseActivity和Activities两个类,这样对其他对象侵入比较大,万一不用你的BaseActivity呢?或者调用第三方库弹出来的Activity呢?

        最简单的办法就是继承Application,然后registerActivityLifecycleCallbacks来监听Activity的生命周期。
        Demon2004:@夏日里的故事
        首先我承认你说的对。

        然后我想说的是面向对象的三大特征是封装继承多态,多态要靠继承来实现,但是不是继承的唯一目的,基类是用来封装一些共有的特性,共有的一些方法而存在的。在很多情况下组合优于继承,这个大部分都知道,与本问题无关。

        回到作者这篇文章本身,这个基类的封装没有任何问题,这个类就是要在某个具体的回调里做一个具体的事情,而且是每个子类都要做的,你再怎么用组合,也是要在onCreate,onDestory里面去做具体的事,你不想每个子类都写一遍相同的代码,继承一个公共类是最简单最合适的实现。

        至于大部分第三方库如果需要用这种侵入式的方式需要你继承一个基类来实现功能,我认为都不是很合适的实现,除非你是FragmentActivity AccompatActivity这种对Activity的封装。
        你都知道大部分情况下继承不是最优解,为什么对提供功能的第三方库就能这么宽容呢。

        就目前Android开发来说,第三方库需要你强行继承某个Activity的实现并不多见(见过几个,具体是什么忘记了,然后这个库被我否掉了,至于微信支付宝之类的不算,这些并不具体显示某个界面),而且这类库明显的特征是在某个具体的页面才需要,那么针对某个具体的界面,具体的功能可以做单独的处理。

        以上仅针对Activity的继承,如果谁提供的第三方库真的是需要你继承Activity实现而且还是你必须要用的,一般你的基类就可以继承这个第三方库需要你继承的类了。
        夏日里的故事:@Demon2004 如果你不用多态,继承那个基类有什么意义?大部分情况下继承都不是代码复用的最优解,和谁用没关系,这些问题,在Effective Java之类的书都有讲解的。

        我说的是你用别人的库,而不是别人用你的库,例如别人的AAR里面有个Activity,这种方法是显然做不到的。
        Demon2004:@夏日里的故事 我觉得这个是不是有点说反了,自己开发项目定义个基类处理自己的一些逻辑或者封装一些共通的东西没什么问题,第三方库的设计才应该考虑尽量不要用侵占式的方式提供功能。
      • liliLearn:.........
      • AWeiLoveAndroid:不错 加油
        Innoooo:@圣诞小熊 郭神的
        圣诞小熊:这不是郭神写的?
        deniro::)
      • 做人要简单:性能提升规范:使用LocalBroadcast 代替 Broadcast 以提高性能和安全性
      • ziabo_yu:你这种解决不了实时的情况,假如用户A和B同时在线,A已经有一段时间没有请求服务器了,B那边把A顶下来了,A是无法实时收到通知的
        truemi:在A手机登录时,记录设备唯一标识,当在B登录时,后台检验上传唯一标识是不是跟后台记录的相同,此时显然是不同的(手机不同),修改当前唯一标识,给A手机推送下线通知
      • 吃掉你了喔:服务端怎么实现呢
        deniro:@云端之人 :clap:
        吃掉你了喔:@云端之人 enen ,谢谢
        NIOAG37M:@吃掉你了喔 可以这样实现,客户端A登录的时候服务器下发token,每次请求服务器时携带token.客户端B登录后,获得最新token.此时客户端A与服务器通信时,服务器检查token invalid ,此时客户端A处理 token 失效情况。

      本文标题:说说在 Android 中如何实现强制下线功能

      本文链接:https://www.haomeiwen.com/subject/hgrtfftx.html