背景
先说说背景吧,这是本人从WinCE系统转到Android之后,接到的第一个任务就是修改Android原生的解锁界面,之前看了两个星期的书和网络博客,Java的也有、Android应用开发的也有、Linux开发的也有、Android框架介绍的也有。然后写了几个APK试了了一下,觉得自己有能力了,便充满自信地找到组长接任务。组长没有说什么,拿出一个竞争对手公司的样机,玩了几下拿个我,说道:这是竞争对手公司的方案,他们的解锁效果不错,很方便,你看看能不能也做出来。
说实话接到这个任务当时真的有点失望,心里面一直想,当初做WinCE做的几乎全是驱动,整天和寄存器、指针、协议打交道,现在转到Android,做这些应用的东西真的不太习惯。不过转头一想,不管什么东西,要做就要做好,慢慢来嘛,于是拿走样机回到工位上,安心开始研究。
样机解锁界面效果类似如下
其实这也是我后面做出的效果,样机因为没有ROOT不好调试截图,功能是将解锁的图标添加了最近运行的运用的图标,这样的话更方便用户去使用
1.分析
1.1.
Android
锁屏功能分析
Android锁屏相关的代码在以下几个路径:
锁屏的具体实现:
|
\frameworks\base\policy\src\com\android\internal\policy\impl |
其中的主要代码如下:
锁屏控件的View类
\frameworks\base\core\java\com\android\internal\widget\multiwaveview |
锁屏控件使用到的资源
\frameworks\base\core\res\res\values-sw600dp-land\arrays.xml \frameworks\base\core\res\res\drawableXXX |
Android上常用的锁屏方法有以下几种:默认锁屏方式(LockScreen)、SIM卡解锁方式(SimUnlockScreen)、图案解锁方式(PatternUnlockScreen)、密码解锁方式(PasswordUnlockScreen)、账号解锁方式(AccountUnlockScreen),这些解锁方式都有对应的源码实现,我们这里讨论的是最常用的默认解锁方式,在Android4.0之后,解锁控件变为“波纹解锁”,即如概述介绍的那样,通过控制中心的圆圈来实现解锁,这种解锁方式,实际上可以进一步增强。
1.1.1. Android
启动后从窗口管理器运行到解锁界面的动作:
1.开机启动后执行到PhoneWindowManager.systemReady()。
2.调用KeyguardViewMediator.onSystemReady()进行待机锁屏及解锁逻辑。
3.KeyguardViewMediator是整个待机解锁屏业务的调度器,负责调度锁屏界面的相关动作及查询解锁屏状态。
1.1.2. KeyguardViewMediator的作用
1.查询锁屏状态,及当前处于锁屏状态还是已解锁状态,PhoneWindowManager持有KeyguardViewMediator的引用,当用户触摸屏幕或者按下某个键是,PhoneWindowManager会通过KeyguardViewMediator查询锁屏状态(锁定/解锁),进行不同的响应处理。如果处于锁定状态,系统输入事件会受到限制。
2.响应电源事件(黑/亮屏)。判断锁屏界面应该处于什么状态(显示或者重置)。手机黑屏后,锁屏界面马上就会显示出来,以便下一次亮屏后,马上就能显示锁屏界面,而不会出现闪烁或延时。
3.其他应用程序或者服务也可以请求禁止锁屏(通过调用KeyguardViewMediator的setKeyguardEnabled(boolean)方法)。例如接听来电界面。
KeyguardViewMediator类在WindowManagerPolicy(在手机系统中是PhoneWindowManager实例)初始化时被创建,并运行在它的线程上,锁屏的UI界面也是在这个线程上创建及显示的。KeyguardViewMediator类提供的状态查询api可以被诸如android.view.WindowManager、com.android.server.InputManager等其它线程调用,所以,KeyguardViewMediator类上的这些api方法都是线程同步的(synchronized)。
1.1.3. KeyguardViewMediator可以进行的调度操作
1) 点亮屏幕pokeWakelock();
2) 报告锁屏权限验证是否成功keyguardDone(boolean);
3) 响应SIM卡状态变化并对锁屏界面做相应的调整onSimStateChanged()。
4) 调度待机锁屏UI界面的管理,包括:
1.显示handleShow ()、
2.隐藏handleHide ()、
3.重置handleReset ()、
4.点亮屏幕handleWakeWhenReady()等。
KeyguardViewMediator实现这部分调度是通过持有一个KeyguardViewManager来实现的。总之KeyguardUpdateMonitor是所有会影响整个待机解/锁屏业务的事件的监控器。(除了作为监控器,它还发挥着类似上下文的作用,也许我们应该把这个类命名为(KeyguardContext)。它监控诸如时间改变、电池状态改变、时区改变、SIM卡状态变化、电话状态变化、电话信号变化等事件。它是一个观察者模式的被观察对象。观察者通过调用KeyguardUpdateMonitor的以下方法进行注册,观察自己感兴趣的变化。
registerInfoCallback(InfoCallback)registerSimStateCallback(SimStateCallback) |
KeyguardUpdateMonitor的观察者包括KeyguardViewMediator、LockScreen、PatternUnlockScreen、AccountUnlockScreen、PasswordUnlockScreen、SimUnlockScreen等。观察者通过调用KeyguardUpdateMonitor的removeCallback(Object)取消观察。
KeyguardViewManager负责管理待机屏UI界面的创建、显示、隐藏、重置以及通过一个回调KeyguardViewCallback通知调度器KeyguardViewMediator进行相关的调度。
LockPatternKeyguardView(KeyguardViewBase)是所有锁屏和解锁UI界面的宿主。它有2个模式Mode. LockScreen和Mode. UnlockScreen。它负责根据当前上下文环境切换当前应该显示的待机屏。
它提供一个回调给当前显示的待机屏并处理其回调,如果回调动作是自己处理不了的,则继续报告给KeyguardViewMediator进行处理。
锁屏界面就是LockScreen;解锁界面包括SIM卡解锁SimUnlockScreen、图案解锁PatternUnlockScreen、密码解锁PasswordUnlockScreen、帐号解锁AccountUnlockScreen
解锁成功后,锁屏流程转到KeyguardViewMediator的keyguardDone(boolean, boolean) 进行后续的流程(如转到Launcher桌面)。
1.2. 解锁界面布局
解锁界面布局在LockScreen类的构造函数中进行,LockScreen构造函数内容如下:
- LockScreen(Context context, Configuration configuration, LockPatternUtils lockPatternUtils,
- KeyguardUpdateMonitor updateMonitor,
- KeyguardScreenCallback callback) {
- super(context);
- mLockPatternUtils = lockPatternUtils;
- mUpdateMonitor = updateMonitor;
- mCallback = callback;
- mEnableMenuKeyInLockScreen = shouldEnableMenuKey();
- mCreationOrientation = configuration.orientation;
- mKeyboardHidden = configuration.hardKeyboardHidden;
- if (LockPatternKeyguardView.DEBUG_CONFIGURATION) {
- Log.v(TAG, "***** CREATING LOCK SCREEN", new RuntimeException());
- Log.v(TAG, "Cur orient=" + mCreationOrientation
- + " res orient=" + context.getResources().getConfiguration().orientation);
- }
- final LayoutInflater inflater = LayoutInflater.from(context);
- if (DBG) Log.v(TAG, "Creation orientation = " + mCreationOrientation);
- if (mCreationOrientation != Configuration.ORIENTATION_LANDSCAPE) {
- inflater.inflate(R.layout.keyguard_screen_tab_unlock, this, true);
- } else {
- inflater.inflate(R.layout.keyguard_screen_tab_unlock_land, this, true);
- }
- mStatusViewManager = new KeyguardStatusViewManager(this, mUpdateMonitor, mLockPatternUtils,
- mCallback, false);
- setFocusable(true);
- setFocusableInTouchMode(true);
- setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
- mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
- mSilentMode = isSilentMode();
- mUnlockWidget = findViewById(R.id.unlock_widget);
- if (mUnlockWidget instanceof SlidingTab) {
- SlidingTab slidingTabView = (SlidingTab) mUnlockWidget;
- slidingTabView.setHoldAfterTrigger(true, false);
- slidingTabView.setLeftHintText(R.string.lockscreen_unlock_label);
- slidingTabView.setLeftTabResources(
- R.drawable.ic_jog_dial_unlock,
- R.drawable.jog_tab_target_green,
- R.drawable.jog_tab_bar_left_unlock,
- R.drawable.jog_tab_left_unlock);
- SlidingTabMethods slidingTabMethods = new SlidingTabMethods(slidingTabView);
- slidingTabView.setOnTriggerListener(slidingTabMethods);
- mUnlockWidgetMethods = slidingTabMethods;
- } else if (mUnlockWidget instanceof WaveView) {
- WaveView waveView = (WaveView) mUnlockWidget;
- WaveViewMethods waveViewMethods = new WaveViewMethods(waveView);
- waveView.setOnTriggerListener(waveViewMethods);
- mUnlockWidgetMethods = waveViewMethods;
- } else if (mUnlockWidget instanceof MultiWaveView) {
- MultiWaveView multiWaveView = (MultiWaveView) mUnlockWidget;
- MultiWaveViewMethods multiWaveViewMethods = new MultiWaveViewMethods(multiWaveView);
- multiWaveView.setOnTriggerListener(multiWaveViewMethods);
- mUnlockWidgetMethods = multiWaveViewMethods;
- } else {
- throw new IllegalStateException("Unrecognized unlock widget: " + mUnlockWidget);
- }
- // Update widget with initial ring state
- mUnlockWidgetMethods.updateResources(context);
- if (DBG) Log.v(TAG, "*** LockScreen accel is "
- + (mUnlockWidget.isHardwareAccelerated() ? "on":"off"));
- }
通过添加打印发现在480分辨率下采用的Layout文件为\layout-sw480dp\keyguard_screen_tab_unlock_land.xml,此文件的内容如下:
从文件中可以看出,解锁界面的数字时钟、充电状态、波纹解锁等控件均在其中布局,对解锁控件的大小修改也是通过修改该文件进行的。
1.3. MultiWaveView控件分析
对于Android4.0默认的LockScreen,采用的是MultiWaveView控件,LockScreen中创建该控件的代码如下:
- mUnlockWidget = findViewById(R.id.unlock_widget);
- if (mUnlockWidget instanceof SlidingTab) {
- SlidingTab slidingTabView = (SlidingTab) mUnlockWidget;
- slidingTabView.setHoldAfterTrigger(true, false);
- slidingTabView.setLeftHintText(R.string.lockscreen_unlock_label);
- slidingTabView.setLeftTabResources(
- R.drawable.ic_jog_dial_unlock,
- R.drawable.jog_tab_target_green,
- R.drawable.jog_tab_bar_left_unlock,
- R.drawable.jog_tab_left_unlock);
- SlidingTabMethods slidingTabMethods = new SlidingTabMethods(slidingTabView);
- slidingTabView.setOnTriggerListener(slidingTabMethods);
- mUnlockWidgetMethods = slidingTabMethods;
- } else if (mUnlockWidget instanceof WaveView) {
- WaveView waveView = (WaveView) mUnlockWidget;
- WaveViewMethods waveViewMethods = new WaveViewMethods(waveView);
- waveView.setOnTriggerListener(waveViewMethods);
- mUnlockWidgetMethods = waveViewMethods;
- } else if (mUnlockWidget instanceof MultiWaveView) {
- MultiWaveView multiWaveView = (MultiWaveView) mUnlockWidget;
- MultiWaveViewMethods multiWaveViewMethods = new MultiWaveViewMethods(multiWaveView);
- multiWaveView.setOnTriggerListener(multiWaveViewMethods);
- mUnlockWidgetMethods = multiWaveViewMethods;
- } else {
- throw new IllegalStateException("Unrecognized unlock widget: " + mUnlockWidget);
- }
这是LockScreen的构造函数中的代码,函数根据R.id.unlock_widget定义的类型选择不同的控件类,其中第一种是Android2.3的滑动解锁类,第二种的简单的波纹解锁类,第三种才是我们使用的MultiWaveView类,函数并创建了一个MultiWaveViewMethods类,这个类实际上是为了更好地使用而进行的封装,它的代码如下:
- class MultiWaveViewMethods implements MultiWaveView.OnTriggerListener,
- UnlockWidgetCommonMethods {
- private final MultiWaveView mMultiWaveView;
- private boolean mCameraDisabled;
- MultiWaveViewMethods(MultiWaveView multiWaveView) {
- mMultiWaveView = multiWaveView;
- final boolean cameraDisabled = mLockPatternUtils.getDevicePolicyManager()
- .getCameraDisabled(null);
- if (cameraDisabled) {
- Log.v(TAG, "Camera disabled by Device Policy");
- mCameraDisabled = true;
- } else {
- // Camera is enabled if resource is initially defined for MultiWaveView
- // in the lockscreen layout file
- mCameraDisabled = mMultiWaveView.getTargetResourceId()
- != R.array.lockscreen_targets_with_camera;
- }
- }
- public void updateResources() {
- int resId;
- if (mCameraDisabled) {
- // Fall back to showing ring/silence if camera is disabled by DPM...
- resId = mSilentMode ? R.array.lockscreen_targets_when_silent
- : R.array.lockscreen_targets_when_soundon;
- } else {
- resId = R.array.lockscreen_targets_with_camera;
- }
- mMultiWaveView.setTargetResources(resId);
- }
- public void onGrabbed(View v, int handle) {
- }
- public void onReleased(View v, int handle) {
- }
- public void onTrigger(View v, int target) {
- if (target == 0 || target == 1) { // 0 = unlock/portrait, 1 = unlock/landscape
- mCallback.goToUnlockScreen();
- } else if (target == 2 || target == 3) { // 2 = alt/portrait, 3 = alt/landscape
- if (!mCameraDisabled) {
- // Start the Camera
- Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(intent);
- mCallback.goToUnlockScreen();
- } else {
- toggleRingMode();
- mUnlockWidgetMethods.updateResources();
- mCallback.pokeWakelock();
- }
- }
- }
- public void onGrabbedStateChange(View v, int handle) {
- // Don't poke the wake lock when returning to a state where the handle is
- // not grabbed since that can happen when the system (instead of the user)
- // cancels the grab.
- if (handle != MultiWaveView.OnTriggerListener.NO_HANDLE) {
- mCallback.pokeWakelock();
- }
- }
这个类有两个方法特别重要,一个是更新MultiWaveView的资源,即public void updateResources(),另一个是对解锁后的响应,即public void onTrigger(View v, int target),这方法传入的第二个参数为解锁选择的图标编号,图标是从右向左逆时针编号的,即最右边的图标编号为0,在此函数中即可进行解锁的处理,选择是进入主界面还是启动其他的Activity。Android4.0默认target = 0对应的是进入主界面,
target = 2或者target = 3启动Camera,其解锁界面的图标是每次都是使用固定的资源,在完成解锁后按下Power键或者系统再次进入锁定状态,LockScreen都会再构造一次,会重新布局并加载资源,因此可以使每次解锁界面都不一样。
2. 实现
2.1. 获取最近运行程序
在Android中可以通过ActivityManager获取到最近运行的Activity,详细的用法如下:
输入参数是需要查询的最大最近运行任务个数,查询的方式(默认采用ActivityManager.RECENT_IGNOR_UNAVAILABLE),返回ActivityManager.RecentTaskInfo对象,其定义如下:
其中的baseIntent为启动最近运行任务的Intent,通过它我们可以很方便地获取Activity的图标并启动最近运行的Activity。
相关的代码如下:
final ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE); List recentLs = am.getRecentTasks(7, ActivityManager.RECENT_IGNORE_UNAVAILABLE); |
2.1.1. 获取Activity图标的方法
通过PackManager我们可以很方便地获取到Activity的图标,对应的代码如下:
final PackageManager pm = context.getPackageManager(); pm.getActivityIcon(recentLs.get(i).baseIntent) |
2.2. 修改/system/build.prop文件
这个文件中包含了大量Android中的配置信息,其中也有和显示分辨率相关的,修改的方法如下:
1.通过ADB将文件获取到主机任意文件夹。
adb pull /system/build.prop |
2.用编辑软件打开build.prop,找到如下行
2.将ro.sf.lcd_density=120修改为=160或者直接删除掉这一行(这样就使用默认160)。
3.执行
adb push build.prop /system/ |
2.3. 修改MultiWaveView控件
修改以下位置的源文件:
Z:\exdroid\android4.0.1\frameworks\base\core\java\com\android\internal\widget\multiwaveview\MultiWaveView.java |
重载setTargetResources方法,重载后的方法如下:
- public void setTargetResources(ArrayList<Drawable> drawables)
- {
- Resources res = getContext().getResources();
- int count = drawables.size();
- ArrayList<TargetDrawable> targetDrawables = new ArrayList<TargetDrawable>(count);
- for (int i = 0; i < count; i++) {
- Drawable drawable = drawables.get(i);
- targetDrawables.add(new TargetDrawable(res, drawable));
- Log.v(TAG,"Add a Drawable");
- }
- mTargetDrawables = targetDrawables;
- updateTargetPositions();
- }
重载后的方法支持直接传入图片链表。这个方法是提供给LockScreen调用的
2.4.修改LockScreen.java文件
重载其中的MultiWaveViewMethods类的updateResources方法,重载后的方法如下:
- public void updateResources(Context context)
- {
- ArrayList<Drawable> drawableAl = new ArrayList();
- final ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
- final PackageManager pm = context.getPackageManager();
- drawableAl.add(getView().getResources().getDrawable(R.drawable.ic_lockscreen_unlock));
- List<ActivityManager.RecentTaskInfo> recentLs = am.getRecentTasks(7, ActivityManager.RECENT_IGNORE_UNAVAILABLE);
- for(int i=0;i<recentLs.size();i++)
- {
- try
- {
- drawableAl.add(pm.getActivityIcon(recentLs.get(i).baseIntent));
- mIntentList.add(recentLs.get(i).baseIntent);
- }
- catch(Exception e)
- {
- Log.v(TAG,"Catch Exception");
- }
- }
- mMultiWaveView.setTargetResources(drawableAl);
- }
重载后的updateResources函数实际上先获取最近运行的任务列表,再获取任务图标,最后用获取的图标设置MultiWaveView控件。
修改解锁响应onTigger:
- public void onTrigger(View v, int target) {
- Log.v(TAG,"onTrigger = " + target);
- if(target ==0)
- {
- mCallback.goToUnlockScreen();
- }
- else
- {
- if(mIntentList.get(target-1)!=null)
- {
- Intent intent = mIntentList.get(target-1);
- mContext.startActivity(intent);
- mCallback.goToUnlockScreen();
- }
- }
- }
这样解锁事件的响应被修改成0号图标解锁,其余启动对应的Activity。
2.5. 调整解锁圆圈大小
启动应用程序的功能实现了,但是解锁圆圈的大小还是太大,特别是在更改dpi之后,圆圈几乎占据了半个显示屏,显得并不雅观。
实际上,解锁圆圈的资源就是/framework/cors/res/res/drawable-xxx中的unlock_ring.png,对于不同分辨率的设备,会采用不同的大小。我们可以通过修改位图的大小来进行更改,但在实际测试中修改了所有的unlock_ring.png,也未见解锁图标变小。
决定采用代码配合更改图案大小的功能,如图:
mOuterRing为外圆圈的资源,mOuterRadius为解锁圆圈的活动半径,修改代码后直接从图片获取,这样将采用drawable-sw480dp-mdpi下的图片,通过PhotoShop等软件将圆圈图片缩小,最终显示出的圆圈就变小了。
3. 关于Android屏幕的知识(摘自网上)
3.1. density
density表示每英寸有多少个显示点(逻辑值),它的单位是dpi:dot per inch,通常屏幕大时,density就大,屏幕小时,density就小,通常:
屏幕实际分辨率为240px*400px时,density=120
屏幕实际分辨率为320px*533px,density=160
屏幕实际分辨率为480px*800px,density=240
3.2. 分辨率
是整个屏是多少点,比如800x480,它是软件的显示单位,实际上会因为不同的显示屏的像素大小不同,造成density不同。
3.3. 资源目录名称
res/xxx-hdpi 当density为240时,使用此目录下的资源
res/xxx-mdpi 当density为160时,使用此目录下的资源
res/xxx-ldpi 当density为120时,使用此目录下的资源
res/xxx 不常后缀,为默认设置,同xxx-mdpi
3.4. 资源单位(xml文件中定义大小的单位)
a)dp=dip=dx (Density independent pixel)
基于屏幕密度的抽象单位,设备无关的点,用于说明与密度无关的尺寸和位置。这些单位是相对于一个160dpi的屏幕,所有一个dp是160dpi屏幕上的一个点。
b)px (Pixel)
px指软件的单位点,设备相关的点
3.5. 获取屏幕信息的相关代码片段
- public static String getDisplayMetrics(Context cx) {
- String str = "";
- DisplayMetrics dm = new DisplayMetrics();
- dm = cx.getApplicationContext().getResources().getDisplayMetrics();
- int screenWidth = dm.widthPixels;
- int screenHeight = dm.heightPixels;
- float density = dm.density;
- float xdpi = dm.xdpi;
- float ydpi = dm.ydpi;
- str += "The absolute width:" + String.valueOf(screenWidth) + "pixels\n";
- str += "The absolute heightin:" + String.valueOf(screenHeight)
- + "pixels\n";
- str += "The logical density of the display.:" + String.valueOf(density)
- + "\n";
- str += "X dimension :" + String.valueOf(xdpi) + "pixels per inch\n";
- str += "Y dimension :" + String.valueOf(ydpi) + "pixels per inch\n";
- return str;
- }
3.6. 获取状态栏和标题栏的高度
- TextView tv1;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.layout_test2);
- tv1 = (TextView) findViewById(R.id.TextView01);
- tv1.post(new Runnable(){
- public void run()
- {
- processLayout();
- }
- });
- }
- private void processLayout(){
- Rect rect= new Rect();
- Window window= getWindow();
- tv1.getWindowVisibleDisplayFrame(rect);
- //状态栏高度
- int statusBarHeight= rect.top;
- int contentViewTop= window.findViewById(Window.ID_ANDROID_CONTENT).getTop();
- //标题栏高度
- int titleBarHeight= contentViewTop - statusBarHeight;
- //测试结果:ok之后 100多 ms 才运行了
- Log.v("test", "=-init-= statusBarHeight="+statusBarHeight+
- " contentViewTop="+contentViewTop+
- " titleBarHeight="+titleBarHeight);
- }
4.总结
也算是费了一些功夫,总算是把First Task完成得差不多了,毕业一年以来都是在做WinCE驱动,虽然在大学里面学过一点Linux的知识,但是初次转到Android还是有点不适应,好在有网络这个好东西,Android学习才不是那么难,但是感觉
也不能太依靠网络。从长期来看,在消费电子这一块,智能系统的进化将会越来越快,这对我等的考验会越来越大。但是当今世界就是一个不断学习的世界,今后要多把自己的学习总结出来,这样进步才会更快。