最近用到了自定义锁屏这个功能,写完之后整理了一下代码,把代码贴出来,方便大家学习使用。
先上效果图:
混乱的锁屏控制
Android自4.0版本, 也就是API level 14开始, 加入了锁屏控制的功能, 相关的类是RemoteControlClient, 这个类在API level 21中被标记为deprecated, 被新的类MediaSession所替代. 我们的音乐App中最开始使用的是原生锁屏控制API, 说实话这个API不好用, 遇到了一些小坑, 最要命的是不同品牌的手机, 锁屏界面长的还不一样, 就连我自己都没见过原生4.0的锁屏控制界面是什么样的. 国内的手机厂商都自以为自己的审美很强, 设计了千奇百怪的锁屏控制界面, MIUI更奇怪, MIUI 6是在原生4.4.4的基础上改的, 竟然有一段时间都没有锁屏控制界面, 后来更新才有. 而原生Android在5.0时, 将锁屏和通知栏控制合并, 整个逻辑非常混乱. 我们还是决定像QQ音乐/酷狗音乐那样, 自己做一个锁屏控制页面
在这里简单的说一下原理,
实现锁屏的思路及方法:
1.首先想到的因该是做一个锁屏,
也就是使用Android的API, 做一个锁屏应用, 和输入法等应用一样, 但这个方法成本很高. 国内的那些锁屏应用, 首先要做的就是引导用户设置锁屏应用, 步骤相当繁杂, 只是为了一个播放控制就用一个锁屏应用, 没有哪个用户会这么有耐心。这种方法可以说是打炮打蚊子,费时费力。
2.悬浮窗:
得益于我国程序员的脑洞, 我们有了第二种思路: 悬浮窗.
悬浮窗的一个比较严谨的名字叫系统警告窗口, 国内外的一些流氓厂商, 经常用悬浮窗弹一些广告, 这个悬浮窗是浮在正常的app的上面的, 所以如果它不消失, 很可能你连正常使用手机都有问题.
这是一个比较打扰用户的东西, 而且也有一定的安全风险. MIUI的权限管理默认是将悬浮窗关闭的, 而有道词典的复制查词功能, 就是用悬浮窗做的, 如果你没给有道词典打开这个权限, 复制查词这个功能就废了.
3.普通的Activity伪造锁屏
文章开头的GIF图片展示的效果, 就是用一个普通Activity做的.
国内的app们, 最终都选择了这条道路, 不知道他们是谁抄的谁, 第一个想到使用普通Activity伪造一个锁屏的开发者, 我只能说非常有创造力.
首先来说一下大概的步骤:
1。监听锁屏事件
准确来说我们监听的是屏幕熄灭事件, 关屏事件的Intent是Intent.ACTION_SCREEN_OFF, 不需要任何权限就可以监听, 但是必须使用代码注册, 也就是说我们必须有一个Service在后台监听才行, 对音乐类app来说, 这不是问题, 音乐app本身就是使用Service来控制MediaPlayer的. 只需要在Service中注册监听Intent.ACTION_SCREEN_OFF就行. 监听到这个事件, 我们就启动一个Activity, 这就是我们的锁屏Activity.
这是主Activity,在主activity里启动一个服务,服务里监听屏幕的熄灭点亮事件。
public class Test extends Activity {
private Button btn_test;
private Bitmap bitmap;
private RegisterService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.suo);
btn_test = (Button) findViewById(R.id.button);
service = new RegisterService();
bitmap = BitmapFactory.decodeResource(this.getResources(),R.mipmap.tableback);
btn_test.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Test.this,RegisterService.class);
startService(intent);
}
});
}
}
在service里注册广播接收器:
service代码:
package com.dfwy.cxy.slidelock;
import android.app.Service;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
public class RegisterService extends Service {
private ScreenReceiver receiver;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
//1.创建广播接受者对象
receiver = new ScreenReceiver();
//2.创建intentfilter对象
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
//3.注册广播接受者
registerReceiver(receiver,filter);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
}
广播接收器的代码:
在onReceive里处理结果,如果屏幕关闭的话调到我们的伪锁屏界面的Activity
package com.dfwy.cxy.slidelock;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* Created by cxy on 2016/12/22.
*/
public class ScreenReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_SCREEN_OFF.equals(action)){
Log.i(“tag”,”=====receiver==屏幕关闭==”);
Intent lockscreen = new Intent(context,LockScreenActivity.class);
lockscreen.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(lockscreen);
}else if (Intent.ACTION_SCREEN_ON.equals(action)){
Log.i(“tag”,”=====receiver==屏幕打开==”);
}
}
}
锁屏界面图片:
解锁屏幕与显示在锁屏之上
在显示我们的假锁屏的时候, 我们应当帮用户解锁, 这样我们才能冒充锁屏, 而不会出现用户”解锁”两次的情况, 但我们只能要求系统解锁没有密码的锁屏, 有密码的情况下, 我们是不能解锁屏幕的, 这时我们应该覆盖在锁屏界面上, 幸好, 在API level 5中就引入了两个Flag,FLAG_DISMISS_KEYGUARD和FLAG_SHOW_WHEN_LOCKED
在锁屏Activity的onCreate方法中给Activity加上两个Flag
this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
一定要两个一起用, 否则效果不大好, 当时测试了好久, 后来看了一下QQ音乐的实现, 才发现两个一起用效果才好, 否则会有一些奇怪的问题.
隐藏踪迹与独立的Task
作为一个锁屏界面, 应当是独立的, 也就是说, 我们这个Activity应当独立于我们的App存在, 至少看起来是这样. 从Android的角度来看, 我们app的主界面里的所有Activity, 应当在一个Task里, 而锁屏Activity, 应当在一个独立的Task里, 因此我们需要给锁屏Activity一个独立的Task, 而且无论何时, 都只有一个锁屏Activity实例存在.
另外, Android有查看近期任务的功能, 我们不希望锁屏界面这个独立的Task显示在里面, 所以锁屏Activity不能显示在近期任务中.
说了这么多, 要做很简单, 只需要在Manifest里面声明Activity时加入几个属性即可
<activity android:excludeFromRecents="true"
android:exported="false"
android:launchMode="singleInstance"
android:name=".view.lockscreen.LockScreenActivity"
android:screenOrientation="portrait"
android:taskAffinity="com.package.name.lockscreen"
android:theme="@style/LockScreenTheme">
activity>
上面的属性中android:excludeFromRecents=”true”让锁屏Activity不显示在近期任务中,android:launchMode=”singleInstance”和android:taskAffinity=”com.package.name.lockscreen”保证锁屏Activity有一个单独的Task, 且这个Task里永远只有它一个实例.
处理黑色闪屏
我们的锁屏Activity在滑动”解锁”之后, 理论上是直接进入下面的界面, 但有时如果下面不是launcher, 而是一个app, 有可能会闪一下黑屏, 这个其实是底下activity的入场动画导致的, 某些Android版本会对顶部activity透明时处理有些奇怪, 我们不能保证其他的应用不闪黑屏, 但是对自己的的应用还是可以的, 只需要在我们的主体activity的style中加上
<item name="android:windowAnimationStyle">@nullitem>
就不会有这种情况发生了, 但是这样的话入场动画也没了, 总之如何取舍看大家了
屏蔽物理按键
//监听系统的物理按键
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
Log.i("tag", "===BACK====");
} else if (keyCode == KeyEvent.KEYCODE_HOME) {
Log.i("tag", "===HOME====");
} else if (keyCode == KeyEvent.KEYCODE_MENU) {
Log.i("tag", "===KEYCODE_MENU====");
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
Log.i("tag", "===KEYCODE_MENU====");
finish();
}
return true;
}
最后我们在伪锁屏的Activity里对解锁进行监听即可,如果解锁完成之后把这个Activity finish掉就可以。
自定义View的代码就不再这里展示了,最后放上代码的地址:
自定义锁屏:
http://download.csdn.net/detail/xiaoyu940601/9725749