目录
Android L SystemUI 流程简要分析
1.SystemUI 启动流程
1.1概述
1.2启动流程分析
1.3 SystemUI启动的主要的服务
1.4 SystemUI和StatusBarManagerService的交互
2. 下滑状态栏的响应流程
2.1 通知部分NotificationStackScrollLayout的展开流程
2.2 StatusBarHeaderView部分展开
2.3 快捷设置QSPanel部分展开
2.3.1解锁之后的下滑状态栏栏流程.
2.3.2锁屏页面的下滑状态栏栏流程.
2.4按back键通知栏收起
2.5 按home键时状态栏的收起流程.
3. 状态图标添加到状态栏的过程
3.1 如何实现状态栏图标和文字变色
4. Notification的添加和显示
4.1Notification滑动清除流程
4.2 一键清除通知
4.3 Notification 的排序
4.4Heads Up Notification
4.5 长按通知动画效果
4.6 通知点击打开
5.近期任务Recents
5.1视图的打开
5.2 单个任务的滑动清除和点击进入
5.3 近期任务的一键清除功能
5.4 打开近期任务列表时动画是怎么加的?
5.5上下滑动效果的实现流程
6.锁屏页面通知的显示
7.下滑通知栏,清除通知的led 灯
8.锁屏的启动
9.熄屏和亮屏
9.1熄屏
9.2.亮屏
10.上滑解锁
11.滑动进入相机/电话
SystemUI通常包含状态栏,下拉栏。
状态栏主要包含一些蓝牙,wifi,信号强度,Sim卡等模块的信息状态.
状态栏是用来实时显示系统状态信息,是需要时刻运行在系统中,在android系统中只有service 才能长期运行在系统中.其实状态栏本身就是一个长期运行的服务.在android系统启动之后,完成ActivityManagerService这些服务的启动之后,系统会提供SystemServer来启动SystemUIService,从而来启动状态栏服务,这样状态栏就开启长期运行在系统中了。
想要知道SystemUI 相关的源码的路径执行以下命令:
find android_top_dir -name "*.mk" |xargs -i grep -rwnH "SystemUI" {} // 查找systemui所在目录,android迭代后,很多功能位置发生变化.
状态栏主要的效果图如下:
SystemUI 包含的功能很多,有状态栏,导航栏,近期任务,PowerUI,音量调节进度条,截图,壁纸等等都在SystemUI里面.其中有些功能是按需启动,比如:近期任务和截图.但是想状态栏和导致栏就是一直运行的,他们是通过SystemUIService来启动运行的.
在负责启动系统各个服务的SystemServer.java中,它的大致流程是SystemServer.java -> main() - > run()-> startOtherServices().至于为什么是从main方法开始,因为java 语法是这样的..
在startOtherServices() 中通过调用ActivityManagerService.systemReady()方法通知AMS系统已经就绪。这个systemReady()拥有一个名为goingCallback的Runnable实例作为参数。顾名思义,当AMS完成对systemReady()的处理后将会回调这一Runnable的run()方法.
mActivityManagerService.systemReady(new Runnable() {
@Override
public void run() {
Slog.i(TAG, "Making services ready");
......
try {
startSystemUi(context);
} catch (Throwable e) {
reportWtf("starting System UI", e);
}
....
}
}
SystemServer.java启动其它服务时startOtherServices会调用 startSystemUi来启动SystemUIService服务.
具体时序图如下:
static final void startSystemUi(Context context) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SystemUIService"));
//Slog.d(TAG, "Starting service: " + intent);
context.startServiceAsUser(intent, UserHandle.OWNER);
}
context.startServiceAsUser会调用ContextImpl.java中startServiceAsUser,
startServiceAsUser直接转调startServiceCommon
private ComponentName startServiceCommon(Intent service, UserHandle user) {
try {
validateServiceIntent(service);
service.prepareToLeaveProcess();
ComponentName cn = ActivityManagerNative.getDefault().startService(
mMainThread.getApplicationThread(), service,
service.resolveTypeIfNeeded(getContentResolver()), user.getIdentifier());
if (cn != null) {
if (cn.getPackageName().equals("!")) {
throw new SecurityException( "Not allowed to start service " + service+ " without permission " + cn.getClassName());
} else if (cn.getPackageName().equals("!!")) {
throw new SecurityException( "Unable to start service " + service + ": " + cn.getClassName());
}
}
return cn;
} catch (RemoteException e) {
return null;
}
}
startServiceCommon通过ActivityManager.startService 到ActivityManagerService.startService,完成SystemUIService服务的启动。
启动SystemUIService服务之后,SystemUIService.onCreate会被调用
public void onCreate() {
super.onCreate();
((SystemUIApplication) getApplication()).startServicesIfNeeded();
}
接着SystemUIApplication.startServicesIfNeeded判断是否需要启动,先判断sys.boot_completed属性值,后面会依次调用SystemUI 的start() 方法.
public void startServicesIfNeeded() {
if (mServicesStarted) {
return;
}
if (!mBootCompleted) {
// check to see if maybe it was already completed long before we began
// see ActivityManagerService.finishBooting()
if ("1".equals(SystemProperties.get("sys.boot_completed"))) {
mBootCompleted = true;
if (DEBUG) Log.v(TAG, "BOOT_COMPLETED was already sent");
}
}
Log.v(TAG, "Starting SystemUI services.");
final int N = SERVICES.length;
for (int i=0; i<N; i++) {
Class> cl = SERVICES[i];
if (DEBUG) Log.d(TAG, "loading: " + cl);
try {
mServices[i] = (SystemUI)cl.newInstance();
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (InstantiationException ex) {
throw new RuntimeException(ex);
}
mServices[i].mContext = this;
//这里设置上下文对象,最后会在PhoneStatusBar里面makeStatusBarView
//方法用来用来加载super_status_bar.xml
mServices[i].mComponents = mComponents;
if (DEBUG) Log.d(TAG, "running: " + mServices[i]);
mServices[i].start();
if (mBootCompleted) {
mServices[i].onBootCompleted();
}
}
mServicesStarted = true;
}
sys.boot_completed属性值,在系统的boot启动完成时,ActivityManagerService中会进行设置.其中SERVICES数组中的全部服务都是继承 的SystemUI这个抽象类.
下面看一下所有的子服务,他们并不是Android 里面经常提到真正的服务,只是继承了SystemUI这个抽象类的服务
private final Class>[] SERVICES = new Class[] {
com.android.systemui.keyguard.KeyguardViewMediator.class,
com.android.systemui.recent.Recents.class,
com.android.systemui.volume.VolumeUI.class,
com.android.systemui.statusbar.SystemBars.class,
com.android.systemui.usb.StorageNotification.class,
com.android.systemui.power.PowerUI.class,
com.android.systemui.media.RingtonePlayer.class
};
子服务有KeyguardViewMediator,Recents,VolumeUI,SystemBars,StorageNotification,PowerUI,RingtonePlayer,
KeyguardViewMediator为锁屏模块, 包含锁屏机制;
Recents 为近期任务列表;
VolumeUI为全局音量控制UI;
SystemBars为系统栏;
StorageNotification 为存储信息通知栏;
PowerUI 为电源界面;
RingtonePlayer 为铃声播放;
其中的SystemBars 就是启动状态栏的一个核心服务.具体流程如下:
SystemUIApplication.startServicesIfNeeded()会将调用SystemBars.start()
SystemBars.java
@Override
public void start() {
if (DEBUG) Log.d(TAG, "start");
//ServiceMonitor是个监听器,这里就是监听这个BAR_SERVICE_COMPONENT是否改变
mServiceMonitor = new ServiceMonitor(TAG, DEBUG,
mContext, Settings.Secure.BAR_SERVICE_COMPONENT, this);
//这里看到注释,如果Service不存在 ,就调用onNoService->createStatusBarFromConfig();
mServiceMonitor.start(); // will call onNoService if no remote service is found
}
mServiceMonitor.start(); 将会通过handler 调用startService() 方法. 由于SystemBars实现了ServiceMonitor.Callbacks 接口, startService()就可以回调onNoService()方法,接着转掉createStatusBarFromConfig(),其中config_statusBarComponent 字符串就是com.android.systemui.statusbar.phone.PhoneStatusBar,所以最终会调用PhoneStatusBar.start(). PhoneStatusBar是SystemUI的核心类.
我们先来分析ServiceMonitor是如何处理再分析PhoneStatusBar.start() 的启动.
onNoService->createStatusBarFromConfig()也就是启动我们的config_statusBarComponent : PhoneStatusBar
public ServiceMonitor(String ownerTag, boolean debug,
Context context, String settingKey, Callbacks callbacks) {
mTag = ownerTag + ".ServiceMonitor";
mDebug = debug;
mContext = context;
mSettingKey = settingKey;
mCallbacks = callbacks;
}
public void start() {
// listen for setting changes
ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(Settings.Secure.getUriFor(mSettingKey),
false /*notifyForDescendents*/, mSettingObserver, UserHandle.USER_ALL);
// listen for package/component changes
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mContext.registerReceiver(mBroadcastReceiver, filter);
mHandler.sendEmptyMessage(MSG_START_SERVICE);
}
这个类就是来监听这个Settings.Secure.BAR_SERVICE_COMPONENT的改变,并看情况如何启动Service。再看mHandler的处理
private final Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch(msg.what) {
case MSG_START_SERVICE:
startService();
break;
case MSG_CONTINUE_START_SERVICE:
continueStartService();
break;
case MSG_STOP_SERVICE:
stopService();
break;
case MSG_PACKAGE_INTENT:
packageIntent((Intent)msg.obj);
break;
case MSG_CHECK_BOUND:
checkBound();
break;
case MSG_SERVICE_DISCONNECTED:
serviceDisconnected((ComponentName)msg.obj);
break;
}
}
};
private void startService() {
//获取Service的ComponentName
mServiceName = getComponentNameFromSetting();
if (mDebug) Log.d(mTag, "startService mServiceName=" + mServiceName);
if (mServiceName == null) {
mBound = false;
//如果没有Service,就回调,从之前的看,就是start PhoneStatusBar
mCallbacks.onNoService();
} else {
//销毁已经存在的status bar
long delay = mCallbacks.onServiceStartAttempt();
//喜闻乐见的bindService,说破天就是在启动PhoneStatusBar
mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_START_SERVICE, delay);
}
}
private ComponentName getComponentNameFromSetting() {
String cn = Settings.Secure.getStringForUser(mContext.getContentResolver(),
mSettingKey, UserHandle.USER_CURRENT);
return cn == null ? null : ComponentName.unflattenFromString(cn);
}
SystemBars.java
private void createStatusBarFromConfig() {
if (DEBUG) Log.d(TAG, "createStatusBarFromConfig");
final String clsName = mContext.getString(R.string.config_statusBarComponent);
if (clsName == null || clsName.length() == 0) {
throw andLog("No status bar component configured", null);
}
Class> cls = null;
try {
cls = mContext.getClassLoader().loadClass(clsName);
} catch (Throwable t) {
throw andLog("Error loading status bar component: " + clsName, t);
}
try {
mStatusBar = (BaseStatusBar) cls.newInstance();
} catch (Throwable t) {
throw andLog("Error creating status bar component: " + clsName, t);
}
mStatusBar.mContext = mContext;
mStatusBar.mComponents = mComponents;
mStatusBar.start();
if (DEBUG) Log.d(TAG, "started " + mStatusBar.getClass().getSimpleName());
}
继续分析PhoneStatusBar.start(),会发现主要是调用了startKeyguard()和addNavigationBar()和super.start().
其中startKeyguard() 是解锁页面相关的,
addNavigationBar()是添加虚拟导航菜单,
super.start()是调用父类BaseStatusBar的start() 方法.
BaseStatusBar的start()会调用createAndAddWindows(),但是该方法是抽象方法,具体的实现是在PhoneStatusBar里面.
public void createAndAddWindows() {
addStatusBarWindow();
}
private void addStatusBarWindow() {
makeStatusBarView();
mStatusBarWindowManager = new StatusBarWindowManager(mContext);
if(mStatusBarWindowManager!=null){
mStatusBarWindowManager.setPhoneStatusBar(this);
}
mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
}
createAndAddWindows继续转调addStatusBarWindow (),addStatusBarWindow转调用makeStatusBarView()来进行构造系统状态栏的View和一些系统状态栏的监听的初始化.makeStatusBarView() 函数返回的是PhoneStatusBarView, PhoneStatusBarView主要是负责状态栏上面的一些Icon(wifi,sim,电池)和时间等,也就是说系统是在PhoneStatusBar.java里面来加载SystemUI的视图的.
查看这一节的时候最好先看一下第3节"状态栏图标添加到状态栏的过程",这一节有参考网络上面的分析:http://blog.csdn.net/zwlove5280/article/details/48445795
我们以状态栏的图标显示为例来分析,说2者的交互就要从SystemUI 启动开始.
SystemServer.java -> main() - > run()-> startOtherServices().
SystemServer-startOtherServices()中部分代码:
if (!disableSystemUI) {
try {
Slog.i(TAG, "Status Bar");
statusBar = new StatusBarManagerService(context, wm);
ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);
} catch (Throwable e) {
reportWtf("starting StatusBarManagerService", e);
}
}
上面看到获取了StatusBarManagerService对象并添加到了ServiceManager中,已Context.STATUS_BAR_SERVICE为标志,以便其他应用获取.然后分析StatusBarManagerService具体做了什么.
从StatusBarManagerService构造函数来看,
public class StatusBarManagerService extends IStatusBarService.Stub {
private StatusBarIconList mIcons = new StatusBarIconList();
.............
/**
* Construct the service, add the status bar view to the window manager
*/
public StatusBarManagerService(Context context, WindowManagerService windowManager) {
mContext = context;
mWindowManager = windowManager;
final Resources res = context.getResources();
mIcons.defineSlots(res.getStringArray(com.android.internal.R.array.config_statusBarIcons));//这个
config_statusBarIcons数组里面就是状态栏上面的图标LocalServices.addService(StatusBarManagerInternal.class, mInternalService);
/// M: DM Lock Feature.
registerDMLock();//mtk 添加的功能,具体不知道
}
StatusBarManagerService extends IStatusBarService.Stub 就是一个IBinder对象,Android AIDL 对这个有详细解释,它用来在进程传递,用来实现进程间的通信,
mIcons= new StatusBarIconList(); 这个StatusBarIconList 实现 Parcelable 接口,也就是序列化数据,用于序列化存储和传递数据和Java中的Serializable的某些情况下要高效.之所以要序列化是因为进程间传递复杂类型的数据需要进行序列化.
其中config_statusBarIcons的内容如下(路径:frameworks/base/core/res/res/values/config.xml)
name="config_statusBarIcons">
id="id">ime
id="id">sync_failing
id="id">sync_active
id="id">cast
id="id">hotspot
id="id">location
id="id">bluetooth
id="id">nfc
id="id">headset
id="id">tty
id="id">speakerphone
id="id">zen
id="id">mute
id="id">volume
id="id">wifi
id="id">cdma_eri
id="id">data_connection
id="id">phone_evdo_signal
id="id">phone_signal
id="id">battery
id="id">alarm_clock
id="id">secure
id="id">clock
总结下,在SystemServer进程中,维系了一个StatusBarManagerService对象statusBar,而statusBar对象维系了一个StatusBarIconList对象
mIcons,这样就实现了SystemServer 就维系了一个mIcons ,而这个mIcons存储了status bar中图标(String) 和 顺序 。
前面已经说过,PhoneStatusBar是SystemUI 的核心入口.
PhoneStatusBar.java
@Override
public void start() {
//...
super.start(); // calls createAndAddWindows()
//...
addNavigationBar();
// Lastly, call to the icon policy to install/update all the icons.
//注释上看,最终调用这个PhoneStatusBarPolicy.java来安装或者更新Icon
mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController, mHotspotController);
}
先来看看spuer.start方法,也就是BaseStatusBar的start()方法,这里先说一下,BaseStatusBar是个实现接口CommandQueue.Callbacks的抽象类,也就是说实际没有实现CommandQueue.Callbacks 的方法,需要子类PhoneStatusBar来实现
BaseStatusBar.java
public void start() {
//...
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
//..
// Connect in to the status bar manager service
StatusBarIconList iconList = new StatusBarIconList();
//传入了this,也就是说这个类或者子类实现了接口
mCommandQueue = new CommandQueue(this, iconList);
int[] switches = new int[8];
ArrayList binders = new ArrayList ();
try {
mBarService.registerStatusBar(mCommandQueue, iconList, switches, binders);
} catch (RemoteException ex) {
// If the system process isn't there we're doomed anyway.
}
//...
// Set up the initial icon state
int N = iconList.size();
int viewIndex = 0;
for (int i=0; i
StatusBarIcon icon = iconList.getIcon(i);//初始化图标的名字
if (icon != null) {
addIcon(iconList.getSlot(i), i, viewIndex, icon);
viewIndex++;
}
}
}
mCommandQueue = new CommandQueue(this,iconList);
CommandQueue extends IStatusBar.Stub 也就是一个IBinder对象,其中也定义了接口,不过接口实现是在PhoneStatusBar.java中
mBarService.registerStatusBar(mCommandQueue, iconList, switches, binders); 这个mBarService就是StatusBarManagerService对象
CommandQueue.Callbacks
public interface Callbacks {
public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon);
public void updateIcon(String slot, int index, int viewIndex,
StatusBarIcon old, StatusBarIcon icon);
public void removeIcon(String slot, int index, int viewIndex);
public void disable(int state, boolean animate);
public void notificationLightPulse(int argb, int onMillis, int offMillis);
public void showScreenPinningRequest();
/// M: [SystemUI] Support "SIM indicator". @{
public void showSimIndicator(String businessType);
public void hideSimIndicator();
public void showRestoreButton(boolean flag);
}
@Override
public void registerStatusBar(IStatusBar bar, StatusBarIconList iconList,
int switches[], List binders) {
enforceStatusBarService();
Slog.i(TAG, "registerStatusBar bar=" + bar);
mBar = bar;
synchronized (mIcons) {
//把mIcons的index和icon拷贝到iconList中,mIcons就是之前说到的那个数组转换来的
iconList.copyFrom(mIcons);
}
synchronized (mLock) {
switches[0] = gatherDisableActionsLocked(mCurrentUserId);
switches[1] = mSystemUiVisibility;
switches[2] = mMenuVisible ? 1 : 0;
switches[3] = mImeWindowVis;
switches[4] = mImeBackDisposition;
switches[5] = mShowImeSwitcher ? 1 : 0;
binders.add(mImeToken);
}
}
iconList.copyFrom(mIcons) 这里把StatusBarManagerService中维系的一个StatusBarIconList对象mIcons 复制给传进来的iconList中
这里只需要注意mBar,这到底有啥用呢,我们知道mBar是IStatusBar的对象 ,也就是CommandQueue的对象,
而CommandQueue的接口的实现方法是在PhoneStatusBar.java(加载StatusBar的界面)实现的,
所以就很好来控制界面 ,只要获取了StatusBarManagerService这个IBinder的相应的Binder驱动(如何获取,上面源码中有) ,
发送指令 -> StatusBarManagerService extends IStatusBarService.Stub 调用CommandQueue extends IStatusBar.Stub的接口
->实现在PhoneStatusBar.java(加载Status Bar的界面)中,就可以达到控制界面的目的,所以这个mBar很关键 ,后面要用到 .
这里我们才把icon的名字初始化完成,图标还没有设置,因为我们的iconList 只是在StatusBarManagerService里面将mIcon的内容拷贝过来了,图标还没有设置,现在需要调用CommandQueue.CallBack的addIcon来设置图标,但是BaseStatusBar本身是个抽象类,没有实现这个方法,具体的实现是在子类PhoneStatusBar里面的.
接着到PhoneStatusBar里面分析start(),之前已经提到里面有个PhoneStatusBarPolicy.看注释就知道是他负责更新icon的.
// Lastly, call to the icon policy to install/update all the icons.
mIconPolicy = new PhoneStatusBarPolicy(mContext, mCastController, mHotspotController);
PhoneStatusBarPolicy其实是通过注册了很多广播接受器来实现及时的更新这些icon的.具体代码如下.
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Xlog.d(TAG, "onReceive:" + action);
if (action.equals(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)) {
updateAlarm();
}
else if (action.equals(Intent.ACTION_SYNC_STATE_CHANGED)) {
updateSyncState(intent);
}
else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED) ||
action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) {
updateBluetooth();
}
else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION) ||
action.equals(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)) {
updateVolumeZen();
}
else if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
updateSimState(intent);
}
else if (action.equals(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED)) {
int currentTtyMode = intent.getIntExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE,
TelecomManager.TTY_MODE_OFF);
updateTTY(currentTtyMode);
}
else if (action.equals(Intent.ACTION_USER_SWITCHED)) {
updateAlarm();
/// M: [Multi-User] register Alarm intent by user @{
int newUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
registerAlarmClockChanged(newUserId, true);
/// M: [Multi-User] register Alarm intent by user @}
}
/// M: [SystemUI] Support "Headset icon". @{
else if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
updateHeadSet(intent);
}
/// @}
}
};
public PhoneStatusBarPolicy(Context context, CastController cast, HotspotController hotspot) {
mContext = context;
mCast = cast;
mHotspot = hotspot;
mService = (StatusBarManager)context.getSystemService(Context.STATUS_BAR_SERVICE);
// listen for broadcasts
IntentFilter filter = new IntentFilter();
//filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
filter.addAction(Intent.ACTION_SYNC_STATE_CHANGED);
filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
filter.addAction(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
/// M: [SystemUI] Support "Headset icon". @{
filter.addAction(Intent.ACTION_HEADSET_PLUG);
/// @}
mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);
/// M: [Multi-User] register Alarm intent by user
registerAlarmClockChanged(UserHandle.USER_OWNER, false);
// TTY status
mService.setIcon(SLOT_TTY, R.drawable.stat_sys_tty_mode, 0, null);
mService.setIconVisibility(SLOT_TTY, false);
/// M: [ALPS01870707] Get the TTY status when power on @{
int settingsTtyMode = Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.TTY_MODE_ENABLED,
TelecomManager.TTY_MODE_OFF);
updateTTY(settingsTtyMode);
/// M: [ALPS01870707] Get the TTY status when power on @}
// Cdma Roaming Indicator, ERI
mService.setIcon(SLOT_CDMA_ERI, R.drawable.stat_sys_roaming_cdma_0, 0, null);
mService.setIconVisibility(SLOT_CDMA_ERI, false);
// bluetooth status
updateBluetooth();
// Alarm clock
mService.setIcon(SLOT_ALARM_CLOCK, R.drawable.stat_sys_alarm, 0, null);
mService.setIconVisibility(SLOT_ALARM_CLOCK, false);
// Sync state
mService.setIcon(SLOT_SYNC_ACTIVE, R.drawable.stat_sys_sync, 0, null);
mService.setIconVisibility(SLOT_SYNC_ACTIVE, false);
// "sync_failing" is obsolete: b/1297963
// zen
mService.setIcon(SLOT_ZEN, R.drawable.stat_sys_zen_important, 0, null);
mService.setIconVisibility(SLOT_ZEN, false);
// volume
mService.setIcon(SLOT_VOLUME, R.drawable.stat_sys_ringer_vibrate, 0, null);
mService.setIconVisibility(SLOT_VOLUME, false);
updateVolumeZen();
// cast
// M: Remove CastTile when WFD is not support in quicksetting
if (mCast != null) {
mService.setIcon(SLOT_CAST, R.drawable.stat_sys_cast, 0, null);
mService.setIconVisibility(SLOT_CAST, false);
mCast.addCallback(mCastCallback);
}
// hotspot
mService.setIcon(SLOT_HOTSPOT, R.drawable.stat_sys_hotspot, 0, null);
mService.setIconVisibility(SLOT_HOTSPOT, mHotspot.isHotspotEnabled());
mHotspot.addCallback(mHotspotCallback);
/// M: [SystemUI] Support "Headset icon". @{
mService.setIcon(SLOT_HEADSET, R.drawable.stat_sys_headset_with_mic, 0, null);
mService.setIconVisibility(SLOT_HEADSET, false);
/// @}
}
public void setIcon(String slot, int iconId, int iconLevel, String contentDescription) {
try {
final IStatusBarService svc = getService();
if (svc != null) {
svc.setIcon(slot, mContext.getPackageName(), iconId, iconLevel,
contentDescription);
}
} catch (RemoteException ex) {
// system process is dead anyway.
throw new RuntimeException(ex);
}
}
private synchronized IStatusBarService getService() {
if (mService == null) {
mService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
if (mService == null) {
Slog.w("StatusBarManager", "warning: no STATUS_BAR_SERVICE");
}
}
return mService;
}
我们可以看到在构造方法中,注册了一系列的广播,然后通过广播来更新icon, 并在构造方法中初始化了icon以及可见性
mService.setIcon 和 mService.setIconVisibility 设置了Icon和Visibility , 注意,这里icon出现了
其中svc.setIcon其实就是调用StatusBarManagerService.java中setIcon,
@Override
public void setIcon(String slot, String iconPackage, int iconId, int iconLevel,
String contentDescription) {
enforceStatusBar();
synchronized (mIcons) {
int index = mIcons.getSlotIndex(slot);
if (index < 0) {
throw new SecurityException("invalid status bar icon slot: " + slot);
}
StatusBarIcon icon = new StatusBarIcon(iconPackage, UserHandle.OWNER, iconId,
iconLevel, 0,
contentDescription);
//Slog.d(TAG, "setIcon slot=" + slot + " index=" + index + " icon=" + icon);
mIcons.setIcon(index, icon);
if (mBar != null) {
try {
mBar.setIcon(index, icon);
} catch (RemoteException ex) {
}
}
}
}
mBar.setIcon其实就是调用CommandQueue的setIcon.CommandQueue调用内部的Handler使用mCallBack来使用addIcon来将图标的图片设置到状态栏对应布局中去.而这个mCallBack其实就是实现了CommandQueue的CallBack接口.实际是PhoneStatusBar实现了这个addIcon方法.
PhoneStatusBar.java
public void addIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {
if (SPEW) Log.d(TAG, "addIcon slot=" + slot + " index=" + index + " viewIndex=" + viewIndex
+ " icon=" + icon);
StatusBarIconView view = new StatusBarIconView(mContext, slot, null);
view.set(icon);
//add in mStatusIcons
mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, mIconSize));
view = new StatusBarIconView(mContext, slot, null);
view.set(icon);
mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, mIconSize));
}
时序图如下:
首先我们要知道的是SystemUI 的状态栏再收起的时候,并不是说只有上面的状态栏那一部分才会调用相关的onTouchEvent方法的,实际上SystemUI 谁监听整个屏幕的,我们在桌面上面随便一个地方点击一下,com.android.systemui.statusbar.phone.StatusBarWindowView 都是会调用它的onTouchEvent方法的.这里先分析的是我们触碰一下状态栏哪里,状态栏是怎么样一步步的响应,最后拉下通知栏部分的.
在前面SystemUI 启动章节就提到系统是在PhoneStatusBar.java里面来加载SystemUI的视图的.那就makeStatusBarView() 从开始. super_status_bar其实就是SystemUI 状态栏的第一个layout配置文件. StatusBarWindowView设置了touch事件处理器,其实最后还是调用的自己的onTouchEvent方法.如果onTouch()返回的false,那么就会执行StatusBarWindowView 的onTouchEvent().
PhoneStatusBarView就是屏幕上面的状态栏那一部分.我们下滑之后快捷开关设置部分和通知部分就是NotificationPanelView.
protected PhoneStatusBarView makeStatusBarView() {
final Context context = mContext;
mStatusBarWindow = (StatusBarWindowView) View.inflate(context,
R.layout.super_status_bar, null);
mStatusBarWindow.setBackground(null);
mStatusBarWindow.mService = this;
mStatusBarWindow.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
checkUserAutohide(v, event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (mExpandedVisible) {
animateCollapsePanels();
}
}
return mStatusBarWindow.onTouchEvent(event);
}});
mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar);
mStatusBarView.setBar(this);
holder = (PanelHolder) mStatusBarWindow.findViewById(R.id.panel_holder);
mStatusBarView.setPanelHolder(holder);//
PhoneStatusBarView就是通过PanelHolder来获取NotificationPanelView对象的mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(
R.id.notification_panel);
mNotificationPanel.setStatusBar(this);
.....
}
从上面可以知道下滑动作的处理离不开PhoneStatusBarView 的.其实PhoneStatusBarview主要是把相关的MotionEvent时间传递给了NotificationPanelView来出来的.也就是直接调用NotificationPanelView的onTouchEvent().并没有执行NotificationPanelView的onInterceptTouchEvent().
这里需要特别说明的是PhoneStatusBarview的onInterceptTouchEvent()它始终返回的false,也就是说正常现象下是不应该执行本身的onTouchEvent()的,而是调用子视图的onTouch().当时这里却会调用PhoneStatusBarview本身的onTouchEvent().这是因为PhoneStatusBarview 所包含的child view视图并没有处理这些事件也就是是child view 的onTouchEvent() 都返回的是false.所以事件最终还是由PhoneStatusBarview来消费这个事件.
总结:在ViewGroup里面onInterceptTouchEvent里面如果反回false,触屏事件向下传递,但如果没有子View去消费这个事件,即子view的onTouchEvent没有返回True。
则最后还是由ViewGroup去消费此事件。也就又执行了自己的onTouchEvent.
PhoneStatusBarView就是通过PanelHolder来获取NotificationPanelView对象的,具体看上面的makeStatusBarView的红色部分.
因为NotificationPanelView本身就是PanelHolder的一个子视图.
在PhoneStatusBarView捕捉到ACTION_DWON事件的时候就已经将PanelHolder的子视图NotificationPanelView设置为自己的一个变量了.
PhoneStatusBarView.java 相关方法
public void addPanel(PanelView pv) {
mPanels.add(pv);
pv.setBar(this);
}
public void setPanelHolder(PanelHolder ph) {
if (ph == null) {
Log.e(TAG, "setPanelHolder: null PanelHolder", new Throwable());
return;
}
ph.setBar(this);
mPanelHolder = ph;
final int N = ph.getChildCount();
for (int i=0; i<N; i++) {
final View v = ph.getChildAt(i);
if (v != null && v instanceof PanelView) {
addPanel((PanelView) v);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// figure out which panel needs to be talked to here
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final PanelView panel = selectPanelForTouch(event);
if (panel == null) {
// panel is not there, so we'll eat the gesture
Log.v(TAG, String.format("onTouch: no panel for touch at (%d,%d)",
(int) event.getX(), (int) event.getY()));
mTouchingPanel = null;
return true;
}
boolean enabled = panel.isEnabled();
if (DEBUG) LOG("PanelBar.onTouch: state=%d ACTION_DOWN: panel %s %s", mState, panel,
(enabled ? "" : " (disabled)"));
if (!enabled) {
// panel is disabled, so we'll eat the gesture
Log.v(TAG, String.format(
"onTouch: panel (%s) is disabled, ignoring touch at (%d,%d)",
panel, (int) event.getX(), (int) event.getY()));
mTouchingPanel = null;
return true;
}
startOpeningPanel(panel);
}
final boolean result = mTouchingPanel != null
? mTouchingPanel.onTouchEvent(event)
: true;
return result;
}
上面的mTouchingPanel.onTouchEvent就是事件传递的过程.但是暂时没有搞明白为什么selectPanelForTouch方法里面为什么不是直接获取已经取到PanelView对象,因为实际就只有一个啊.不懂.
上面已经提到在下滑手指抬起的时候,就会调用fling()方法,Android L 原生的系统里面,下滑操作只会展开通知部分NotificationStackScrollLayout和StatusBarHeaderView部分,QSPanel部分默认是没有展开的.那就先看通知部分NotificationStackScrollLayout的展开流程.
关于在解锁页面和解锁之后想要滑动的时候默认直接展开通知栏和快捷设置QSPanel的修改方法和流程直接查看2.3节
在Panelview的ontouch方法里面判断是ACTION_UP之后,会根据滑动力度大小来调用fling,fling方法本身主要是设置并执行一个ValueAnimator.在ValueAnimator 变化过程中来实时调用setExpandedHeightInternal方法来展开NotificationStackScrollLayout.当然实际我们在判断是ACTION_MOVE也是会执行Panelview的setExpandedHeightInternal方法的
Panelview.java
public void setExpandedHeightInternal(float h) {
Log.d(TAG,"setExpandedHeightInternal finish() h=" + h + " mHeightAnimator=" + (mHeightAnimator==null));
float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount();
if (mHeightAnimator == null) {
float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion);
if (getOverExpansionPixels() != overExpansionPixels && mTracking) {
setOverExpansion(overExpansionPixels, true /* isPixels */);
}
mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount();
} else {
mExpandedHeight = h;
if (mOverExpandedBeforeFling) {
setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */);
}
}
mExpandedHeight = Math.max(0, mExpandedHeight);
mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0
? 0
: mExpandedHeight / fhWithoutOverExpansion);
onHeightUpdated(mExpandedHeight);
notifyBarPanelExpansionChanged();
}
Panelview 的setExpandedHeightInternal()会转调其子类(NotificationPanelView)的onHeightUpdated()方法实现对NotificationStackScrollLayout的高度的修改来实现通知部分逐渐下滑.同时在QSContainer的位置改变的时候也会影响到通知部分(NotificationStackScrollLayout)的位置的改变.因为QSContainer 在初始化的时候就设置了OnLayoutChangeListener,在调用setQsTranslation来改变快捷设置部分(QSPanel)的位置和高度的时候是会导致这个接口被回调.
NotificationPanelView.java
@Override protected void onHeightUpdated(float expandedHeight) { Log.d(TAG,"onHeightUpdated expandedHeight=" + expandedHeight ); if (!mQsExpanded || mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted) { positionClockAndNotifications(); } //部分代码去掉了....... mNotificationStackScroller.setStackHeight(expandedHeight); updateHeader(); updateUnlockIcon(); updateNotificationTranslucency(); }
其中NotificationStackScrollLayout和QSContainer 相关监听器都是在onFinishInflate()里面处理的.具体源码如下
@Override
protected void onFinishInflate() {
super.onFinishInflate();
.....
mNotificationStackScroller.setOnHeightChangedListener(this);
mNotificationStackScroller.setOverscrollTopChangedListener(this);
mNotificationStackScroller.setOnEmptySpaceClickListener(this);
mNotificationStackScroller.setScrollView(mScrollView);
....
// recompute internal state when qspanel height changes
mQsContainer.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight,
int oldBottom) {
final int height = bottom - top;
final int oldHeight = oldBottom - oldTop;
if (height != oldHeight) {
onScrollChanged();
}
}
});
}
@Override
public void onScrollChanged() {
if (mQsExpanded) {
requestScrollerTopPaddingUpdate(false /* animate */);
requestPanelHeightUpdate();
}
}
private void requestScrollerTopPaddingUpdate(boolean animate) {
mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
mScrollView.getScrollY(),
mAnimateNextTopPaddingChange || animate,
mKeyguardShowing
&& (mQsExpandImmediate || mIsExpanding && mQsExpandedWhenExpandingStarted));
mAnimateNextTopPaddingChange = false;
}
这一部分就是下拉菜单展开之后显示时间和设置按钮的靠上面那一部分,其实这一部分的出来代码就是上面onHeightUpdated里面的的updateHeader方法.
NotificationPanelView.java
/**
* Hides the header when notifications are colliding with it.
*/
private void updateHeader() {
if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
|| mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
Log.d(TAG,"updateHeader ------if ");
updateHeaderKeyguard();
} else {
Log.d(TAG,"updateHeader ------else ");
updateHeaderShade();
}
}
private void updateHeaderShade() {
Log.d(TAG,"updateHeaderShade mHeaderAnimatingIn=" + mHeaderAnimatingIn + " mQsExpansionHeight=" + mQsExpansionHeight +
" getHeaderTranslation=" + getHeaderTranslation());
if (!mHeaderAnimatingIn) {
mHeader.setTranslationY(getHeaderTranslation());
}
setQsTranslation(mQsExpansionHeight);
}
updateHeader()就是根据实际是否解锁来调用不同的,如果解锁了就是调用updateHeaderShade来移动位置
现在我们想讨论的是下滑状态栏的时候直接展开快捷设置QSPanel和通知部分,所以我们先分析下滑的时候,SystemUI是如何处理我们的滑动事件的.
这个分为2中情况,分为锁屏页面下滑和解锁之后下滑.(目前我分析项目在keyguard界面去掉了通知的显示)
解锁之后下滑状态栏的关键流程,前面有提到是从PhoneStatusBarview键touch事件由onInterceptTouchEvent返回false,而child view的onTouch又返回false,最近将事件传递给PhoneStatusBarview本身的onTouch来处理的.同样查看之前的流程图知道, PhoneStatusBarview的onTouch会调用NotificationPanelView的onTouch方法.
NotificationPanelView.java
public boolean onTouchEvent(MotionEvent event) {
if(DEBUG)Log.d(TAG , "onTouchEvent_npv action=" +event.getActionMasked());
if(getIsSuperSaveMode() || isSecurityMode()){//zhangle add
Log.d(TAG,"onTouchEvent IsSuperSaveMode" );
return false;
}
if (mBlockTouches) {
return false;
}
resetDownStates(event);
if ((!mIsExpanding || mHintAnimationRunning)
&& !mQsExpanded
&& mStatusBar.getBarState() != StatusBarState.SHADE) {
if(!doovDefaultLockView){
mAfforanceHelper.onTouchEvent(event);
}
}
if (mOnlyAffordanceInThisMotion) {
return true;
}
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
&& mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded
&& mQsExpansionEnabled) {
// Down in the empty area while fully expanded - go to QS.
mQsTracking = true;
mConflictingQsExpansionGesture = true;
onQsExpansionStarted();
mInitialHeightOnTouch = mQsExpansionHeight;
mInitialTouchY = event.getX();
mInitialTouchX = event.getY();
}
mIsDown = mExpandedHeight > mLastExpandedHeight;
mLastExpandedHeight = mExpandedHeight;
//zhangle add end
if (mExpandedHeight != 0) {
handleQsDown(event);
}
if (!mQsExpandImmediate && mQsTracking) {//
mQsTracking会在NotificationPanelView的onInterceptTouchEvent()里面处理Action_Move的时间变成true.onQsTouch(event);
if (!mConflictingQsExpansionGesture) {
return true;
}
}
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|| event.getActionMasked() == MotionEvent.ACTION_UP) {
mConflictingQsExpansionGesture = false;
}
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0
&& mQsExpansionEnabled) {
mTwoFingerQsExpandPossible = true;
}
//这一段代码是为了实现默认滑动下拉可以完全展开通知栏自己添加的,后面再分析
if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_MOVE
&& event.getY(event.getActionIndex()) >= mStatusBarMinHeight && mIsExpanding) {
mQsExpandImmediate = true;
requestPanelHeightUpdate();
setListening(true);
}
if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
&& event.getPointerCount() == 2
&& event.getY(event.getActionIndex()) < mStatusBarMinHeight) {
mQsExpandImmediate = true;
requestPanelHeightUpdate();
// Normally, we start listening when the panel is expanded, but here we need to start
// earlier so the state is already up to date when dragging down.
setListening(true);
}
super.onTouchEvent(event);//调用PanelView
return true;
}
查看以上代码可以看到,其实NotificationPanelView的onTouch()本身也是有处理.但是需要满足2个条件!mQsExpandImmediate&& mQsTracking.第一个变量先不说,直接说第2个,第2个mQsTracking需要在NotificationPanelView的onInterceptTouchEvent()里面的Move_Action才能是true.而我们目前讨论的情况是没有走onInterceptTouchEvent流程的,也就是说现在关键是super.onTouchEvent(event)这一行代码.这个也就是调用PanelView的onTouchEvent().
PanelView.java
public boolean onTouchEvent(MotionEvent event) {
if(DEBUG)Log.d(TAG , "onTouchEvent_pv action= " + event.getActionMasked() +(mInstantExpanding || mTouchDisabled));
if (mInstantExpanding || mTouchDisabled||(mStatusBar.getBarState() == StatusBarState.KEYGUARD)) {//解锁页面不处理相关事件
return false;
}
/*
* We capture touch events here and update the expand height here in case according to
* the users fingers. This also handles multi-touch.
*
* If the user just clicks shortly, we give him a quick peek of the shade.
*
* Flinging is also enabled in order to open or close the shade.
*/
int pointerIndex = event.findPointerIndex(mTrackingPointer);
if (pointerIndex < 0) {
pointerIndex = 0;
mTrackingPointer = event.getPointerId(pointerIndex);
}
final float y = event.getY(pointerIndex);
final float x = event.getX(pointerIndex);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mGestureWaitForTouchSlop = mExpandedHeight == 0f;
}
boolean waitForTouchSlop = hasConflictingGestures() || mGestureWaitForTouchSlop;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitialTouchY = y;
mInitialTouchX = x;
mInitialOffsetOnTouch = mExpandedHeight;
mTouchSlopExceeded = false;
mJustPeeked = false;
mPanelClosedOnDown = mExpandedHeight == 0.0f;
mHasLayoutedSinceDown = false;
mUpdateFlingOnLayout = false;
mPeekTouching = mPanelClosedOnDown;
mTouchAboveFalsingThreshold = false;
mDozingOnDown = isDozing();
if (mVelocityTracker == null) {
initVelocityTracker();
}
trackMovement(event);
if (!waitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) ||
mPeekPending || mPeekAnimator != null) {
cancelHeightAnimator();
cancelPeek();
mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)
|| mPeekPending || mPeekAnimator != null;
onTrackingStarted();
}
if (mExpandedHeight == 0) {
schedulePeek();
}
break;
case MotionEvent.ACTION_POINTER_UP:
final int upPointer = event.getPointerId(event.getActionIndex());
if (mTrackingPointer == upPointer) {
// gesture is ongoing, find a new pointer to track
final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
final float newY = event.getY(newIndex);
final float newX = event.getX(newIndex);
mTrackingPointer = event.getPointerId(newIndex);
mInitialOffsetOnTouch = mExpandedHeight;
mInitialTouchY = newY;
mInitialTouchX = newX;
}
break;
case MotionEvent.ACTION_MOVE:
float h = y - mInitialTouchY;
// If the panel was collapsed when touching, we only need to check for the
// y-component of the gesture, as we have no conflicting horizontal gesture.
if (Math.abs(h) > mTouchSlop
&& (Math.abs(h) > Math.abs(x - mInitialTouchX)
|| mInitialOffsetOnTouch == 0f)) {
mTouchSlopExceeded = true;
if (waitForTouchSlop && !mTracking) {
if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {
mInitialOffsetOnTouch = mExpandedHeight;
mInitialTouchX = x;
mInitialTouchY = y;
h = 0;
}
cancelHeightAnimator();
removeCallbacks(mPeekRunnable);
mPeekPending = false;
onTrackingStarted();
}
}
final float newHeight = Math.max(0, h + mInitialOffsetOnTouch);
if (newHeight > mPeekHeight) {
if (mPeekAnimator != null) {
mPeekAnimator.cancel();
}
mJustPeeked = false;
}
if (-h >= getFalsingThreshold()) {
mTouchAboveFalsingThreshold = true;
}
if (!mJustPeeked && (!waitForTouchSlop || mTracking) && !isTrackingBlocked()) {
Log.d(TAG , "onTouchEvent_move_newHeight="+newHeight);
setExpandedHeightInternal(newHeight);//手指在屏幕下移的时候调用的就是这里
}
trackMovement(event);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mTrackingPointer = -1;
trackMovement(event);
if ((mTracking && mTouchSlopExceeded)
|| Math.abs(x - mInitialTouchX) > mTouchSlop
|| Math.abs(y - mInitialTouchY) > mTouchSlop
|| event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
float vel = 0f;
float vectorVel = 0f;
if (mVelocityTracker != null) {
mVelocityTracker.computeCurrentVelocity(1000);
vel = mVelocityTracker.getYVelocity();
vectorVel = (float) Math.hypot(
mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
}
boolean expand = flingExpands(vel, vectorVel)
|| event.getActionMasked() == MotionEvent.ACTION_CANCEL;
onTrackingStopped(expand);
DozeLog.traceFling(expand, mTouchAboveFalsingThreshold,
mStatusBar.isFalsingThresholdNeeded(),
mStatusBar.isScreenOnComingFromTouch());
// Log collapse gesture if on lock screen.
if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
float displayDensity = mStatusBar.getDisplayDensity();
int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity);
int velocityDp = (int) Math.abs(vel / displayDensity);
EventLogTags.writeSysuiLockscreenGesture(
EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK,
heightDp, velocityDp);
}
fling(vel, expand);//滑动抬起手指时,自动下滑就是这里在处理
mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown;
if (mUpdateFlingOnLayout) {
mUpdateFlingVelocity = vel;
}
} else {
boolean expands = onEmptySpaceClick(mInitialTouchX);
onTrackingStopped(expands);
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mPeekTouching = false;
break;
}
return !waitForTouchSlop || mTracking;
}
上面这个方法我们需要关注就是返回值和Move,UP 事件.返回值是在3个事件(Action_Down,Action_Move,Action_UP)处理之后都是true,具体直接打log就可以看到.其中Action_Move的下拉实现是在setExpandedHeightInternal里面实现.Action_Up 的自动展开效果是在fling(vel, expand)里面实现的.但是fling(vel, expand)本身就是通过一个动画来实调用setExpandedHeightInternal.也就是ValueAnimator.AnimatorUpdateListener的onAnimationUpdate里面调用setExpandedHeightInternal.所以我们需要分析就是setExpandedHeightInternal这个方法, setExpandedHeightInternal 中需要我们注意的就是它调用了由子类实现的onHeightUpdated来转调setQsTranslation实现最终我们想要的视图位置的变动.
注意:
在fling(vel, expand)获取需要滑动的距离值target的时候,如果返回的值偏小就会出现默认不能自动展开的情况.具体为什么有时候获取的值偏小的根本原因暂时还没有搞明白.
实际分析下滑的log,很容易发现,具体走了NotificationPanelView的onInterceptTouchEvent(),也就是说和第一种情况是不一样的.
实际对于ViewGroup而已,每一个事件来之前,都是先经过dispatchTouchEvent.由于锁屏页面我们所看到的状态栏和解锁以后不一样.解锁之后的那个状态栏是layout/status_bar,而锁屏页面的是layout/keyguard_status_bar.
layout/keyguard_status_bar 是在layout/status_bar_expanded里面的.也就是NotificationPanelView的一个child view(KeyguardStatusBarView).
layout/status_bar 是PhoneStatusBarView也就是StatusBarWindowView的一个child view.
所以我们分析锁屏页面的下滑效果需要从NotificationPanelView 的dispatchTouchEvent开始.
NotificationPanelView 的dispatchTouchEvent只是调用了父类的dispatchTouchEvent的方法,所以最终还是要从NotificationPanelView 的onInterceptTouchEvent()来分析.
NotificationPanelView.java
public boolean onInterceptTouchEvent(MotionEvent event) {
if(DEBUG){
Log.d(TAG , "onInterceptTouchEvent_npv mBlockTouches=" + mBlockTouches + " event" + event.getAction());
}
if(getIsSuperSaveMode() || isSecurityMode()){//zhangle add
Log.d(TAG,"onInterceptTouchEvent IsSuperSaveMode" );
return false;
}
if (mBlockTouches) {
return false;
}
resetDownStates(event);
int pointerIndex = event.findPointerIndex(mTrackingPointer);
if (pointerIndex < 0) {
pointerIndex = 0;
mTrackingPointer = event.getPointerId(pointerIndex);
}
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mIntercepting = true;
mInitialTouchY = y;
mInitialTouchX = x;
initVelocityTracker();
trackMovement(event);
if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mQsExpansionAnimator != null) {
onQsExpansionStarted();
mInitialHeightOnTouch = mQsExpansionHeight;
mQsTracking = true;
mIntercepting = false;
mNotificationStackScroller.removeLongPressCallback();
}
break;
case MotionEvent.ACTION_POINTER_UP:
final int upPointer = event.getPointerId(event.getActionIndex());
if (mTrackingPointer == upPointer) {
// gesture is ongoing, find a new pointer to track
final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
mTrackingPointer = event.getPointerId(newIndex);
mInitialTouchX = event.getX(newIndex);
mInitialTouchY = event.getY(newIndex);
}
break;
case MotionEvent.ACTION_MOVE:
final float h = y - mInitialTouchY;
trackMovement(event);
if (mQsTracking) {
// Already tracking because onOverscrolled was called. We need to update here
// so we don't stop for a frame until the next touch event gets handled in
// onTouchEvent.
setQsExpansion(h + mInitialHeightOnTouch);
trackMovement(event);
mIntercepting = false;
return true;
}
if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
&& shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
//锁屏页面下滑时
shouldQuickSettingsIntercept会返回truemQsTracking = true;
onQsExpansionStarted();
mInitialHeightOnTouch = mQsExpansionHeight;
mInitialTouchY = y;
mInitialTouchX = x;
mIntercepting = false;
mNotificationStackScroller.removeLongPressCallback();
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
trackMovement(event);
if (mQsTracking) {
flingQsWithCurrentVelocity(
event.getActionMasked() == MotionEvent.ACTION_CANCEL);
mQsTracking = false;
}
mIntercepting = false;
break;
}
return super.onInterceptTouchEvent(event);
}
从Action_Down事件开始,但是好像没有什么特别,也没有看到返回值,但是需要注意的是其中对一个变量mQSTracking 赋值的条件,当时是mQsExpansionAnimator在我们正常下滑是null,也就是说Action_Down事件最终的返回值是false, KeyguardStatusBarView没有重写touch相关的处理方法.所以下一步还是看NotificationPanelView这个方法里面的Action_Move事件,这个事件会返回true,并修改mQSTracking为true.()
那接下来任何事件都是onTouchEvent()的工作了.由于之前一起贴出了这个方法全部代码,这里只贴出主要代码.
NotificationPanelView.java
public boolean onTouchEvent(MotionEvent event) {
........
if (!mQsExpandImmediate && mQsTracking) {//mQsTracking会在NotificationPanelView的onInterceptTouchEvent()里面处理Action_Move的时间变成true.
onQsTouch(event);
if (!mConflictingQsExpansionGesture) {
return true;
}
}
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
|| event.getActionMasked() == MotionEvent.ACTION_UP) {
mConflictingQsExpansionGesture = false;
}
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0
&& mQsExpansionEnabled) {
mTwoFingerQsExpandPossible = true;
}
//这一段代码是为了实现默认滑动下拉可以完全展开通知栏自己添加的,后面再分析
if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_MOVE
&& event.getY(event.getActionIndex()) >= mStatusBarMinHeight && mIsExpanding) {
mQsExpandImmediate = true;
requestPanelHeightUpdate();
setListening(true);
}
........
super.onTouchEvent(event);//调用PanelView
return true;
}
onTouchEvent()->Action_Move:下面的代码是去掉不需要关注的部分之后的代码,!mQsExpandImmediate&& mQsTracking是为true的, mConflictingQsExpansionGesture是false,所以在这个方法内部调用onQsTouch之后就返回true了.而且不光光是这个Action_Move,后续的Action_Up事件也是一样的.很显然onQsTouch方法是核心了.
NotificationPanelView.java
private void onQsTouch(MotionEvent event) {
if(DEBUG)Log.d(TAG,"onQsTouch action=" + event.getActionMasked());
int pointerIndex = event.findPointerIndex(mTrackingPointer);
if (pointerIndex < 0) {
pointerIndex = 0;
mTrackingPointer = event.getPointerId(pointerIndex);
}
final float y = event.getY(pointerIndex);
final float x = event.getX(pointerIndex);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
.....
case MotionEvent.ACTION_POINTER_UP:
.....
case MotionEvent.ACTION_MOVE:
final float h = y - mInitialTouchY;
setQsExpansion(h + mInitialHeightOnTouch);
if (h >= getFalsingThreshold()) {
mQsTouchAboveFalsingThreshold = true;
}
trackMovement(event);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mQsTracking = false;
mTrackingPointer = -1;
trackMovement(event);
float fraction = getQsExpansionFraction();
if ((fraction != 0f || y >= mInitialTouchY)
&& (fraction != 1f || y <= mInitialTouchY)) {
flingQsWithCurrentVelocity(
event.getActionMasked() == MotionEvent.ACTION_CANCEL);
} else {
mScrollYOverride = -1;
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
}
onQsTouch()里面我们需要关注的只有 ACTION_MOVE和ACTION_UP这2种事件,其他暂时不贴出来.其中ACTION_MOVE是转调setQsExpansion ACTION_UP是转调flingQsWithCurrentVelocity,其中经过flingSettings之后也是通过动画效果来调用setQsExpansion,也就是最终和ACTION_MOVE类似.现在我们分析了解锁前面2种情况下下滑状态栏的操作之后发现其实ACTION_UP事件之后,其实都是在指定的范围内重复多次调用ACTION_MOVE的处理.setQSExpansion本身也是调用setQsTranslation来实现视图的位移的.
注意:由于Android L 默认并没有在解锁之后单指下滑,默认展开通知和快捷设置部分的功能.这里主要公司内部的项目需要这个需求,具体在原生系统基础上修改如下:
下面只是记录主要的几个点,其实需要修改的代码不是很多,主要是要把流程梳理清楚,考虑到各种情况,避免引起其他故障,因为Android L的SystemUI 不仅是状态栏,其中锁屏首页也是在这里面的.而且其中事件的处理流程很复杂.
以下是我早期的改法:
在NotificationPanelView.java的public void fling(float vel, boolean expand) 调用父类方法之前调用下面的slideDownQS(),其中变量mISDown只要在onTouchEvent()里面根据坐标来判断是否是滑动操作就可以了.
NotificationPanelView.java
private void slideDownloadQS() {
if(mIsExpanding && mQsExpansionEnabled && !mQsExpanded && mExpandedHeight!=0 && mIsDown){
Log.d(TAG , "slideDownloadQS ---");
setListening(true);
onQsExpansionStarted();
flingSettings(0 /* vel */, true /* expand */);
}
}
其实方法有很多种,当然也可在参考2.2节里面的Header 展开的方式,直接调用setQsTranslation(int)来执行,只要传入的参数是正确的就可以了.
以上方法仅供参考,因为后期发现以上方法有bug,这里出现介绍其他修改方法.
后期改法:
参考Android L 2值滑动的效果,在NotificationPanelView的onTouchEvent里面添加如下代码:
if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_MOVE
&& event.getY(event.getActionIndex()) >= mStatusBarMinHeight && mIsExpanding) {
mQsExpandImmediate = true;
requestPanelHeightUpdate();
setListening(true);
}
如果需要实现快捷设置部分顶部在下滑的时候不变位置.需要修改setQsTranslation,具体参考如下:
主要是对mQsContainer的处理.
private void setQsTranslation(float height) {
.........
if(DEBUG) Log.d(TAG,"setQsTranslation mHeaderAnimatingIn=" + mHeaderAnimatingIn );
if (!mHeaderAnimatingIn) {
//zhangle delete at 20150521
//mQsContainer.setY(height - mQsContainer.getDesiredHeight() + getHeaderTranslation());
//zhangle add at 20150521 start
if(DEBUG)Log.d(TAG,"setQsTranslation height=" + height + " " + getHeaderTranslation() );
int heightOverride = (int) (mQsMaxExpansionHeight - height) ;
int y = mHeader.getCollapsedHeight()-heightOverride;
mQsContainer.setScrollY(-heightOverride);
mQsContainer.setY(y);
mQsContainer.invalidate();
if(DEBUG) Log.d(TAG,"setQsTranslation heightOverride=" + heightOverride + "y=" +y);
}
当我们按back键的时候,会调用StatusBarWindowView 的dispatchKeyEvent().接着调用PhoneStatusBar相关方法.
public boolean dispatchKeyEvent(KeyEvent event) {
boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BACK:
if (!down) {
mService.onBackPressed();//
mService就是PhoneStatusBar}
return true;
case KeyEvent.KEYCODE_MENU:
if (!down) {
return mService.onMenuPressed();
}
case KeyEvent.KEYCODE_SPACE:
if (!down) {
return mService.onSpacePressed();
}
break;
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
if (mService.isDozing()) {
MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, true);
return true;
}
break;
}
if (mService.interceptMediaKey(event)) {
return true;
}
return super.dispatchKeyEvent(event);
}
从上面的代码来看,实际还处理menu键和音量+ - 键.
home 键的监听应用层一般都是监听Intent.ACTION_CLOSE_SYSTEM_DIALOGS 这个广播来实现的.SystemUI 也是采用的这个方法.
PhoneStatusBar.java
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.v(TAG, "onReceive: " + intent);
if(DEBUG) Log.d(TAG, "onReceive:-------------- intent.getAction=" + intent.getAction());
String action = intent.getAction();
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {//监听home键
if (isCurrentProfile(getSendingUserId())) {
int flags = CommandQueue.FLAG_EXCLUDE_NONE;
String reason = intent.getStringExtra("reason");
if (reason != null && reason.equals(SYSTEM_DIALOG_REASON_RECENT_APPS)) {
flags |= CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL;
}
animateCollapsePanels(flags);
}
}
else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
mScreenOn = false;
notifyNavigationBarScreenOn(false);
notifyHeadsUpScreenOn(false);
finishBarAnimations();
resetUserExpandedStates();
}
else if (Intent.ACTION_SCREEN_ON.equals(action)) {
mScreenOn = true;
notifyNavigationBarScreenOn(true);
}
else if (ACTION_DEMO.equals(action)) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
String command = bundle.getString("command", "").trim().toLowerCase();
if (command.length() > 0) {
try {
dispatchDemoCommand(command, bundle);
} catch (Throwable t) {
Log.w(TAG, "Error running demo command, intent=" + intent, t);
}
}
}
} else if ("fake_artwork".equals(action)) {
if (DEBUG_MEDIA_FAKE_ARTWORK) {
updateMediaMetaData(true);
}
}else if(LEATHER_PATTERN.equals(action)){
mHandler.removeMessages(MSG_LEATHER_PATTERN);
mHandler.sendEmptyMessage(MSG_LEATHER_PATTERN);
}
}
};
从上面可以看到,这里连近期任务的Home键也是有在上面做处理的,只是flag 标志位不一样.
PhoneStatusBar.java
public void animateCollapsePanels(int flags) {
animateCollapsePanels(flags, false /* force */);
}
public void animateCollapsePanels(int flags, boolean force) {
if (!force &&
(mState == StatusBarState.KEYGUARD || mState == StatusBarState.SHADE_LOCKED)) {
runPostCollapseRunnables();
return;
}
if (SPEW) {
Log.d(TAG, "animateCollapse():"
+ " mExpandedVisible=" + mExpandedVisible
+ " flags=" + flags);
}
if ((flags & CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL) == 0) {
if (!mHandler.hasMessages(MSG_HIDE_RECENT_APPS)) {
mHandler.removeMessages(MSG_HIDE_RECENT_APPS);
mHandler.sendEmptyMessage(MSG_HIDE_RECENT_APPS);
}
}
if ((flags & CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL) == 0) {
mHandler.removeMessages(MSG_CLOSE_SEARCH_PANEL);
mHandler.sendEmptyMessage(MSG_CLOSE_SEARCH_PANEL);
}
if (mStatusBarWindow != null) {
// release focus immediately to kick off focus change transition
mStatusBarWindowManager.setStatusBarFocusable(false);
mStatusBarWindow.cancelExpandHelper();
mStatusBarView.collapseAllPanels(true);//TODO:1
}
}
上面代码中实际需要我们关注的就是最好一行,这个PhoneStatusBarView. collapseAllPanels(true)会调用PanelView 的collapse()来转调fling()来实现收起状态栏等相关的动作.但是并不是到这里就结束了.
PanelBar.java
public void collapseAllPanels(boolean animate) {//case1:press home key -> animate=true
boolean waiting = false;
for (PanelView pv : mPanels) {
if (animate && !pv.isFullyCollapsed()) {//TODO
pv.collapse(true /* delayed */);//第一次是执行的这里
waiting = true;
} else {//后面会再次调用这个方法的时时候会传入参数为false,就执行的这里
pv.resetViews();
pv.setExpandedFraction(0); // just in case
pv.setVisibility(View.GONE);
pv.cancelPeek();
}
}
if (DEBUG) LOG("collapseAllPanels: animate=%s waiting=%s", animate, waiting);
Log.d(TAG,"collapseAllPanels animate=" +animate + " waiting=" + waiting + " mState=" + mState);
if (/*!waiting &&*/ mState != STATE_CLOSED) {//zhangle update
// it's possible that nothing animated, so we replicate the termination
// conditions of panelExpansionChanged here
go(STATE_CLOSED);
onAllPanelsCollapsed();
}
}
注意根据log来看,上面先执行pv.collapse(true/* delayed */),再执行onAllPanelsCollapsed().由于这里只是记录流程,暂时不具体分析为什么是执行的这2种case.
PanelView.java
public void collapse(boolean delayed) {
if (DEBUG) logf("collapse: " + this);
if (mPeekPending || mPeekAnimator != null) {
mCollapseAfterPeek = true;
if (mPeekPending) {
// We know that the whole gesture is just a peek triggered by a simple click, so
// better start it now.
removeCallbacks(mPeekRunnable);
mPeekRunnable.run();
}
} else if (!isFullyCollapsed() && !mTracking && !mClosing) {
cancelHeightAnimator();
mClosing = true;
notifyExpandingStarted();
if (delayed) {
postDelayed(mFlingCollapseRunnable, 120);
} else {
fling(0, false /* expand */);
}
}
}
但是需要我们注意的是PanelView 的collapse()里面执行collapse 的时候是使用postDelayed(mFlingCollapseRunnable, 120);来执行的,也是就是说是使用Handler来异步执行的,还有120ms的延迟,那么这个有可能会执行PanelBar.java的onAllPanelsCollapsed(),但是PanelBar.java对于这个方法并没有具体的实现内容,具体的实现是在PhoneStatusBarView.java里面的.
@Override
public void onAllPanelsCollapsed() {
super.onAllPanelsCollapsed();
// Close the status bar in the next frame so we can show the end of the animation.
postOnAnimation(new Runnable() {
@Override
public void run() {
mBar.makeExpandedInvisible();
}
});
mLastFullyOpenedPanel = null;
}
上面代码中的注释很明确,就是为了在关闭状态栏之前有一个动画效果给我们看到.那么接着就是查看PhoneStatusBar的makeExpandedInvisible(),会发现这个方法内容会再次调用PhoneStatusBarView的collapseAllPanels(),但是这次和之前在PhoneStatusBar 的animateCollapsePanels()调用主要的不同之处在于参数是false.这次执行collapseAllPanels()会执行里面的pv.setExpandedFraction(0)从而将PanelView的mExpandedHeight设置为0,其中这个变量在前面提到的fling()里面会用到,导致下面代码的第2个if判断条件会是ture.从而执行notifyExpandingFinished(),而不是以动画的方式来收起状态栏.
protected void fling(float vel, boolean expand) {
cancelPeek();
float target = expand ? getMaxPanelHeight() : 0.0f;
// Hack to make the expand transition look nice when clear all button is visible - we make
// the animation only to the last notification, and then jump to the maximum panel height so
// clear all just fades in and the decelerating motion is towards the last notification.
final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible()
&& mExpandedHeight < getMaxPanelHeight() - getClearAllHeight()
&& !isClearAllVisible();
final boolean isExpand = expand;//zhangle add
if (clearAllExpandHack) {
target = getMaxPanelHeight() - getClearAllHeight();
}
if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) {//TODO
notifyExpandingFinished();
return;
}
//......
}
先从配置文件来分析,从super_status_bar.xml开始,这个是整个SystemUI应用的第一个布局配置文件.其中包括状态栏,下拉菜单,通知栏,幻灯片页面,锁屏页面.其中锁屏页面是在SystemUI 里面的,不是在Keyguard里面的,只有涉及到安全解锁比如密码相关等才在Keyguard里面的
super_status_bar.xml
xml version="1.0" encoding="utf-8"?>
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:id="@+id/backdrop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
>
android:id="@+id/backdrop_back"
android:layout_width="match_parent"
android:scaleType="centerCrop"
android:layout_height="match_parent" />
android:id="@+id/backdrop_front"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:visibility="invisible" />
android:id="@+id/scrim_behind"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no" />
layout="@layout/status_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/status_bar_height" />
android:id="@+id/brightness_mirror"
android:layout_width="@dimen/notification_panel_width"
android:layout_height="wrap_content"
android:layout_gravity="@integer/notification_panel_layout_gravity"
android:paddingLeft="@dimen/notification_side_padding"
android:paddingRight="@dimen/notification_side_padding"
android:visibility="gone">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="2dp"
android:background="@drawable/brightness_mirror_background">
layout="@layout/quick_settings_brightness_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:id="@+id/panel_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent" >
layout="@layout/status_bar_expanded"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
android:id="@+id/scrim_in_front"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no" />
显然我们感兴趣的是状态栏部分和状态栏下拉部分,但是这一节分析的是状态栏的添加图标,所以只分析第一个部分.其中重点是layout/system_icons,因为这个里面讲主要显示在状态栏右侧的图标包含在里面的.
status_bar.xml
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"
android:id="@+id/status_bar"
android:background="@drawable/system_bar_background"
android:orientation="vertical"
android:focusable="true"
android:descendantFocusability="afterDescendants"
>
android:id="@+id/notification_lights_out"
android:layout_width="@dimen/status_bar_icon_size"
android:layout_height="match_parent"
android:paddingStart="6dip"
android:paddingBottom="2dip"
android:src="@drawable/ic_sysbar_lights_out_dot_small"
android:scaleType="center"
android:visibility="gone"
/>
android:id="@+id/status_bar_contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="6dp"
android:paddingEnd="8dp"
android:orientation="horizontal"
>
android:id="@+id/notification_icon_area"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal"
>
android:id="@+id/notification_icon_area_inner"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
android:id="@+id/moreIcon"
android:layout_width="@dimen/status_bar_icon_size"
android:layout_height="match_parent"
android:src="@drawable/stat_notify_more"
android:visibility="gone"
/>
android:id="@+id/notificationIcons"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:gravity="center_vertical"
android:orientation="horizontal"/>
android:id="@+id/system_icon_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
>
layout="@layout/system_icons" />
android:id="@+id/clock"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:singleLine="true"
android:paddingStart="3dp"
android:gravity="center_vertical|start"
/>
android:id="@+id/ticker_stub"
android:inflatedId="@+id/ticker"
android:layout="@layout/status_bar_ticker"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
layout/system_icons
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/system_icons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical">
android:id="@+id/statusIcons"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"/>
layout="@layout/signal_cluster_view"
android:id="@+id/signal_cluster"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"/>
android:id="@+id/battery_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="5dp"
android:visibility="invisible"
android:textColor="@color/status_bar_clock_color"
android:textSize="@dimen/battery_level_text_size"
android:theme="@style/shadow_doov"
android:gravity="center"/>
android:id="@+id/battery"
android:layout_height="20dp"
android:layout_width="20dp"
android:rotation="-90"
android:gravity="center"
android:background="@color/shadow_color"
android:layout_gravity="center"/>
@layout/signal_cluster_view
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingEnd="@dimen/signal_cluster_battery_padding"
>
android:id="@+id/"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:paddingEnd="6dp"
android:src="@drawable/stat_sys__ic"
/>
android:id="@+id/wifi_combo"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
>
android:id="@+id/wifi_signal"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/>
android:id="@+id/wifi_signal_spacer"
android:layout_width="4dp"
android:layout_height="4dp"
android:visibility="gone"
/>
android:id="@+id/volte_icon"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:visibility="gone"
/>
android:id="@+id/sim_indicator_internet_or_alwaysask"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="3dip"
/>
android:id="@+id/mobile_signal_group"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
>
android:id="@+id/no_sims"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:src="@drawable/stat_sys_no_sims"
/>
android:id="@+id/wifi_airplane_spacer"
android:layout_width="4dp"
android:layout_height="4dp"
android:visibility="gone"
/>
android:id="@+id/airplane"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
/>
以上都是配置文件,实际代码是在PhoneStatusBar.java里面的.相关代码如下,由于这个方法实在太长了,这里只是保留了一部分代码,也是为了清晰思路起见.
protected PhoneStatusBarView makeStatusBarView() {
mStatusBarWindow = (StatusBarWindowView) View.inflate(context,
R.layout.super_status_bar, null);//加载第一个布局文件
mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar);//加载状态栏布局文件
mStatusBarView.setBar(this);
holder = (PanelHolder) mStatusBarWindow.findViewById(R.id.panel_holder);//状态栏下拉部分
mStatusBarView.setPanelHolder(holder);
mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(
R.id.notification_panel);//状态栏下拉部分
mNotificationPanel.setStatusBar(this);
mSystemIconArea = (LinearLayout) mStatusBarView.findViewById(R.id.system_icon_area);
mSystemIcons = (LinearLayout) mStatusBarView.findViewById(R.id.system_icons);//系统图标
mStatusIcons = (LinearLayout)mStatusBarView.findViewById(R.id.statusIcons);
mNotificationIconArea = mStatusBarView.findViewById(R.id.notification_icon_area_inner);
mNotificationIcons = (IconMerger)mStatusBarView.findViewById(R.id.notificationIcons);
mMoreIcon = mStatusBarView.findViewById(R.id.moreIcon);
mNotificationIcons.setOverflowIndicator(mMoreIcon);
mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents);
//zhangle add for show battery level start
battery_percent = (TextView)mStatusBarView.findViewById(R.id.battery_percent);//电池百分比
battery_percent.setVisibility(getShowBatteryLevelStatus()?View.VISIBLE:View.GONE);
mBatteryLevelContentObserver = new BatteryLevelContentObserver(new Handler());
mBatteryLevelContentObserver.startObserver();
//zhangle add for show battery level end
mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById(
R.id.notification_stack_scroller);//下拉菜单通知部分
mStackScroller.setLongPressListener(getNotificationLongClicker());
mStackScroller.setPhoneStatusBar(this);
mEmptyShadeView = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_no_notifications, mStackScroller, false);
mStackScroller.setEmptyShadeView(mEmptyShadeView);//下拉菜单无通知
mDismissView = (DismissView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_notification_dismiss_all, mStackScroller, false);//一键清除通知
mDismissView.setOnButtonClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clearAllNotifications();
}
});
mHeader = (StatusBarHeaderView) mStatusBarWindow.findViewById(R.id.header);//下拉菜单Header 部分,就是有设置图标和时间那个部分
mHeader.setActivityStarter(this);
mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header);//锁屏页面的状态栏
mStatusIconsKeyguard = (LinearLayout) mKeyguardStatusBar.findViewById(R.id.statusIcons);
mKeyguardStatusView = mStatusBarWindow.findViewById(R.id.keyguard_status_view);
mKeyguardBottomArea =
(KeyguardBottomAreaView) mStatusBarWindow.findViewById(R.id.keyguard_bottom_area);
mBatteryController = new BatteryController(mContext);//电池控制器
mBatteryController.addStateChangedCallback(new BatteryStateChangeCallback() {
@Override
public void onPowerSaveChanged() {
}
@Override
public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
battery_percent.setText("" + level + "%");//zhangle add
}
});
mNetworkController = new NetworkControllerImpl(mContext);//网络控制器
mHotspotController = new HotspotControllerImpl(mContext);//热点控制器
mBluetoothController = new BluetoothControllerImpl(mContext, mHandlerThread.getLooper());//蓝牙控制器
mSecurityController = new SecurityControllerImpl(mContext);//安全控制器
if (SIMHelper.isMtkHotKnotSupport()) {
mHotKnotController = new HotKnotControllerImpl(mContext);//HotKnont控制器.mtk 自己研发的功能
} else {
mHotKnotController = null;
}
if (SIMHelper.isMtkAudioProfilesSupport()) {
mAudioProfileController = new AudioProfileControllerImpl(mContext);//情景模式控制器
} else {
mAudioProfileController = null;
}
if(!SIMHelper.isWifiOnlyDevice()) {
mDataConnectionController = new DataConnectionControllerImpl(mContext);//数据连接控制器
} else {
mDataConnectionController = null;
}
if (mContext.getResources().getBoolean(R.bool.config_showRotationLock)) {
mRotationLockController = new RotationLockControllerImpl(mContext);//屏幕旋转
}
mUserInfoController = new UserInfoController(mContext);//个人用户账号信息
mVolumeComponent = getComponent(VolumeComponent.class);
//音量控制if (mVolumeComponent != null) {
mZenModeController = mVolumeComponent.getZenController();
//勿扰模式}
return mStatusBarView;
}
当前的需求是如果一个将状态栏的样式设置为了白色,而我们的状态栏图标和文字的颜色也是白色的,这个时候状态栏就是一片白,什么都看不见,为了看的见状态栏相关的视图,我们这个时候需要把状态栏的视图中的图标和文字颜色改成深色,比如黑色,就可以看见了.
至于怎么修改状态栏颜色可以参考下面的方法:
Android 5 沉浸式状态栏
item name="android:colorPrimaryDark">@android:color/holo_blue_bright
Android 6 修改状态栏背景颜色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
getWindow().setBackgroundDrawableResource(R.color.red_color);//页面背景色
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
getWindow().setStatusBarColor(Color.BLUE);//状态栏背景色
}
下面开始分析如何实现状态栏图标的自动变色和恢复,这里主要是记录修改方法和总结一下,以便后期继续完善这些笔记.
其实我们每次打开一个页面系统都会执行PhoneWindowManager的updateSystemUiVisibilityLw()方法(实际系统会执行很多次).
PhoneWindowManager.java
private int updateSystemUiVisibilityLw() {
// If there is no window focused, there will be nobody to handle the events
// anyway, so just hang on in whatever state we're in until things settle down.
final WindowState win = mFocusedWindow != null ? mFocusedWindow
: mTopFullscreenOpaqueWindowState;
if (win == null) {
return 0;
}
if ((win.getAttrs().privateFlags & PRIVATE_FLAG_KEYGUARD) != 0 && mHideLockScreen == true) {
// We are updating at a point where the keyguard has gotten
// focus, but we were last in a state where the top window is
// hiding it. This is probably because the keyguard as been
// shown while the top window was displayed, so we want to ignore
// it here because this is just a very transient change and it
// will quickly lose focus once it correctly gets hidden.
return 0;
}
int tmpVisibility = PolicyControl.getSystemUiVisibility(win, null)
& ~mResettingSystemUiFlags
& ~mForceClearedSystemUiFlags;
if (mForcingShowNavBar && win.getSurfaceLayer() < mForcingShowNavBarLayer) {
tmpVisibility &= ~PolicyControl.adjustClearableFlags(win, View.SYSTEM_UI_CLEARABLE_FLAGS);
}
/// M: BMW. If top window is a floating window, we should clear the low profile and only content flags@{
if (MultiWindowProxy.isFeatureSupport()
&& win.isFloatingWindow()) {
tmpVisibility = tmpVisibility
& ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
& ~View.SYSTEM_UI_FLAG_LOW_PROFILE
& ~View.SYSTEM_UI_FLAG_FULLSCREEN;
}
/// @}
final int visibility = updateSystemBarsLw(win, mLastSystemUiFlags, tmpVisibility);
final int diff = visibility ^ mLastSystemUiFlags;
final boolean needsMenu = win.getNeedsMenuLw(mTopFullscreenOpaqueWindowState);
if (diff == 0 && mLastFocusNeedsMenu == needsMenu
&& mFocusedApp == win.getAppToken()) {
return 0;
}
mLastSystemUiFlags = visibility;
mLastFocusNeedsMenu = needsMenu;
mFocusedApp = win.getAppToken();
mHandler.post(new Runnable() {
@Override
public void run() {
try {
IStatusBarService statusbar = getStatusBarService();
if (statusbar != null) {
statusbar.setSystemUiVisibility(visibility, 0xffffffff, win.toString());
/*if (DEBUG)*/ Slog.d(TAG, "setSystemUiVisibility = " + visibility);
statusbar.topAppWindowChanged(needsMenu);
}
} catch (RemoteException e) {
// re-acquire status bar service next time it is needed.
mStatusBarService = null;
}
}
});
return diff;
}
上面的代码总关键在于finalintdiff= visibility ^ mLastSystemUiFlags,也就是说和updateSystemBarsLw()有关,其实updateSystemBarsLw(win, mLastSystemUiFlags, tmpVisibility)主要就是获取当前Window设置了那些SYSTEM_UI_FLAG_xxx标志.如果和值的Window不一样diff就不等于0.这个时候就会执行statusbar.setSystemUiVisibility(visibility,0xffffffff, win.toString()),这行代码最终会导致PhoneStatusBar 中的setSystemUiVisibility()的执行(我就是在这里面做变化颜色的操作).所以我们只要在需要变化状态栏图标和文字颜色的页面设置一个不同的SYSTEM_UI_FLAG_xxx标志就可以达到目的.
在需要改变状态栏图标和文字颜色的activity里面使用如下的方法就可以了
Window window = getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(0x00000010);
//View.SYSTEM_UI_FLAG_TEXT_BLACK:0x00000010
}
上面提到过通过设置一个标志位flag,会导致PhoneStatusBar 中的setSystemUiVisibility()的执行, PhoneStatusBar 本身就是SystemUI中的一个重要的类,通过它就可以实现改变状态栏图标和文字颜色
下面记录一下PhoneStatusBar 中的修改.
private int mStatusBarBlackColor;
private boolean mBlack = false;
@Override // CommandQueue
public void setSystemUiVisibility(int vis, int mask) {
final int oldVal = mSystemUiVisibility;
final int newVal = (oldVal&~mask) | (vis&mask);
final int diff = newVal ^ oldVal;
if (DEBUG) Log.d(TAG, String.format(
"setSystemUiVisibility vis=%s mask=%s oldVal=%s newVal=%s diff=%s",
Integer.toHexString(vis), Integer.toHexString(mask),
Integer.toHexString(oldVal), Integer.toHexString(newVal),
Integer.toHexString(diff)));
if (diff != 0) {
// we never set the recents bit via this method, so save the prior state to prevent
// clobbering the bit below
final boolean wasRecentsVisible = (mSystemUiVisibility & View.RECENT_APPS_VISIBLE) > 0;
mSystemUiVisibility = newVal;
// update low profile
if ((diff & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) {
final boolean lightsOut = (vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0;
if (lightsOut) {
animateCollapsePanels();
if (mTicking) {
haltTicker();
}
}
setAreThereNotifications();
}
//zhangle add start
boolean black = mBlack;
mBlack = (vis & 0x00000010) != 0;
if(mBlack != black){
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
updateResColor(mBlack);
mBatteryController.setBlack(mBlack);
}
}, 200);
}
//zhangle add end
.............
}
}
上面的mBatteryController.setBlack(mBlack),就是修改电池图标的颜色,因为电池图标是重写draw()方法实现,并不是一个简单的ImagView和TextView.电池的修改,这里暂不做介绍,其实就是mBatteryController中通知电池显示的View更新一下画笔Paint的颜色,然后再调用postInvalidate()重新画一遍就ok.
/**
* @author zhangle
* @param black
*/
public void updateResColor(boolean black){
LogD.d(TAG,DEBUG,"updateResColor black=" +black);
int color = black ? mStatusBarBlackColor:Color.WHITE;
battery_percent.setTextColor(color);
mClock.setTextColor(color);
updateViewGroupColor(mSignalCluster, color);//wifi,sim
updateViewGroupColor(mNotificationIcons, color);//notifications icon
updateViewGroupColor(mStatusIcons, color);//zenMode,BT and other system status icon
}
public void updateViewGroupColor(ViewGroup vg,int color){
if(vg == null) return;
int counts = vg.getChildCount();
for (int i = 0; i < counts; i++) {
View child = vg.getChildAt(i);
if(child instanceof ImageView){
LogD.d(TAG,DEBUG,"updateViewGroupColor ImageView" );
((ImageView)child).setColorFilter(color);
}else if(child instanceof ViewGroup){
LogD.d(TAG,DEBUG,"updateViewGroupColor ViewGroup");
updateViewGroupColor((ViewGroup)child, color);
}else if(child instanceof TextView){
LogD.d(TAG,DEBUG,"updateViewGroupColor TextView");
((TextView)child).setTextColor(color);
}
}
}
以下修改是为了解决在通知来的时候在状态栏左边出现的动画中的Icon和文字看不清楚的bug.
private class MyTicker extends Ticker {
boolean black = false;//zhangle add
MyTicker(Context context, View sb) {
super(context, sb);
if (!mTickerEnabled) {
Log.w(TAG, "MyTicker instantiated with mTickerEnabled=false", new Throwable());
}
}
@Override
public void tickerStarting() {
if (!mTickerEnabled) return;
mTicking = true;
mStatusBarContents.setVisibility(View.GONE);
mTickerView.setVisibility(View.VISIBLE);
//zhangle add ,fix bug:notification ticker(left hand) icon not show clearly
if(black != mBlack){
updateViewGroupColor((ViewGroup)mTickerView, mBlack ? mStatusBarBlackColor:Color.WHITE);
black = mBlack;
}
mTickerView.startAnimation(loadAnim(com.android.internal.R.anim.push_up_in, null));
mStatusBarContents.startAnimation(loadAnim(com.android.internal.R.anim.push_up_out, null));
}
.......
}
同时为了解决在需要变化状态栏颜色的页面来了新通知,需要在状态栏动画完成之后在左边显示Icon的时候出现看不清楚的bug,需要在private void updateNotificationIcons() 的最后面添加以下代码:
updateViewGroupColor(mNotificationIcons, mBlack ? mStatusBarBlackColor:Color.WHITE);//notifications icon
到了这里好像工作就完成了,但是后来发现这个不好,为什么呢,因为这个需要人家app的配合,如果那个第3方的app把状态栏背景色弄出了白色,而我们的图标也是白色,这个时候第3方app怎么知道我们自己开发的功能,所以需要再优化,我们直接获取状态栏的颜色,然后来判断是否需要改变状态栏的图标颜色等.
这里也只是记录一下实现的方法,以便后续继续研究.
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) { //zhangle add start Log.d("zhangle","onApplyWindowInsets mStatusBarColor=" + mStatusBarColor ); /** * 进入非桌面应用时使用IPC:AIDL 来通知状态栏变色.桌面应用比较特殊,需要根据壁纸上方的颜色来适配 */ if(mLastStatusBarColor != mStatusBarColor){ //sysUiVisibility &= 0x00000010; //getDecorView().setSystemUiVisibility(getSystemUiVisibility() & 0x00000010); mHandler.post(new Runnable() { @Override public void run() { try { IStatusBarService statusbar = getStatusBarService(); if (statusbar != null) { statusbar.updateStatusBarColor(mStatusBarColor == -1); } } catch (RemoteException e) { // re-acquire status bar service next time it is needed. mStatusBarService = null; } } }); } mLastStatusBarColor = mStatusBarColor; //zhangle add end mFrameOffsets.set(insets.getSystemWindowInsets()); insets = updateColorViews(insets, true /* animate */); insets = updateStatusGuard(insets); updateNavigationGuard(insets); if (getForeground() != null) { drawableChanged(); } return insets; }
另外还添加以下代码
//zhangle add start
final Object mServiceAquireLock = new Object();
private IStatusBarService mStatusBarService;
private Handler mHandler = new Handler();
private static int mLastStatusBarColor ;
IStatusBarService getStatusBarService() {
synchronized (mServiceAquireLock) {
if (mStatusBarService == null) {
mStatusBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService("statusbar"));
}
return mStatusBarService;
}
}
//zhangle add end
上面我们调用StatusBarManagerService的 statusbar.updateStatusBarColor(mStatusBarColor == -1),其实这个方法不是Android自带的,是我们自己添加的.
frameworks/base/core/java/com/android/internal/statusbar/IStatusBarService.aidl
添加一个新方法
void updateStatusBarColor(boolean black);//zhangle add
frameworks/base/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
这里要实现上面AIDL的方法
//zhangle add
public void updateStatusBarColor(final boolean black){
mHandler.post(new Runnable() {
public void run() {
if (mBar != null) {
try {
mBar.updateStatusBarColor(black);
} catch (RemoteException ex) {
}
}
}
});
}
到了这里又发现调用了mBar.updateStatusBarColor(black);这个方法其实也是添加的,
frameworks/base/core/java/com/android/internal/statusbar/IStatusBar.aidl
void updateStatusBarColor(boolean black);//zhangle add
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
这里实现上面的IStatusBar.aidl的方法
/**
* zhangle add
* @param black
*/
public void updateStatusBarColor(boolean black) {
mCallbacks.updateStatusBarColor(black);
}
上面的mCallbacks变量其实就是CommandQueue的内部接口Callbacks.
public interface Callbacks{
....
public void updateStatusBarColor(boolean black) ;//zhangle add
....
}
那是谁实现Callbacks,实现它的有2个,分别是PhoneStatusBar和TvStatusBar.
这里我们关心的是手机, TvStatusBar只是空实现Callbacks的方法就好,不写具体的实现内容.
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
public void updateStatusBarColor(boolean black) {
if(mBlack != black){
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
updateResColor(mBlack);
mBatteryController.setBlack(mBlack);
}
}, 200);
}
mBlack = black;
}
其实这个部分的是实现和前面根据标志位flag是差不多的.
这里StatusBarManagerService和PhoneStatusBar的关联主要是在PhoneStatusBar的父类BaseStatusBar的start()方法里面的 mBarService.registerStatusBar(mCommandQueue, iconList, switches, binders).
public void start(){
....
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
....
mCommandQueue = new CommandQueue(this, iconList);
int[] switches = new int[8];
ArrayList<IBinder> binders = new ArrayList<IBinder>();
try {
mBarService.registerStatusBar(mCommandQueue, iconList, switches, binders);
} catch (RemoteException ex) {
// If the system process isn't there we're doomed anyway.
}
..
}
上面第2种方法其实仍有问题,因为虽然我们切换不同的应用都会执行PhoneWindow的onApplyWindowInsets()方法,但是重应用退到桌面和在同一个应用里面切换不同的Activity并不会再次执行这个方法,可以在PhoneWindwo的内部类DecorView里面的setVisibility()里面执行上面的onApplyWindowInsets里面的操作就可以了.同时去掉onApplyWindowInsets里面添加的代码.
public void setVisibility(int visibility) {
super.setVisibility(visibility);
Xlog.v("PhoneWindow", "DecorView setVisiblity: visibility = " + visibility
+ " ,Parent =" + getParent() + ", this =" + this);
//TODO
//zhangle add start
if(null != getParent() && visibility == View.VISIBLE){
Log.d(TAG,"setVisibility mStatusBarColor=" + mStatusBarColor );
/**
* 进入非桌面应用时使用IPC:AIDL 来通知状态栏变色.桌面应用比较特殊,需要根据壁纸上方的颜色来适配
*/
//sysUiVisibility &= 0x00000010;
//getDecorView().setSystemUiVisibility(getSystemUiVisibility() & 0x00000010);
mHandler.post(new Runnable() {
@Override
public void run() {
try {
IStatusBarService statusbar = getStatusBarService();
if (statusbar != null) {
statusbar.updateStatusBarColor(mStatusBarColor == -1);
}
} catch (RemoteException e) {
// re-acquire status bar service next time it is needed.
mStatusBarService = null;
}
}
});
//zhangle add end
}
}
Notification支持文字内容显示、震动、三色灯、铃声等多种提示形式,在默认情况下,Notification仅显示消息标题、消息内容、送达时间这3项内容。以下就是通知的基本布局。
在应用里面想发送一个通知Notification需要调用NotificationManager的notify()方法来实现. NotificationManager将主要的工作都交给了NotificationManagerService.framework层的NotificationManagerService和SystemUI之间的联系是通过NotificationListenerService和NotificationListeners来实现的.在SystemUI这个应用里面实现监听通知的增加和更新是通过NotificationListenerService调用registerAsSystemService方法将SystemUI创建的这个匿名内部类(NotificationListenerService)和framework层的Notification Manager 服务绑定到一起,framework在更新通知的时候会回调SystemUI中匿名内部类相关的方法,最终到达状态栏通知视图的变更.相关的时序图如下.
在前面的SystemUI启动流程中提到PhoneStatusBar,其实NotificationListenerService调用registerAsSystemService来绑定服务的操作也是在PhoneStatusBar的start()方法里面完成的,具体应该是它的父类BaseStatusBar的start()里面完成的.
BaseStatusBar.java
// Set up the initial notification state.
try {
mNotificationListener.registerAsSystemService(mContext,
new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
UserHandle.USER_ALL);
} catch (RemoteException e) {
Log.e(TAG, "Unable to register notification listener", e);
}
mNotificationListener 就是 匿名内部类(NotificationListenerService),当系统framework有通知消息需要处理的时候,
就会回调onNotificationPosted(),判断是更新Notification 还是新增,如果是更新的话调用BaseStatusBar.upateNotification(),当然最终还是要让子类PhoneStatusBar 来处理的.如果是新增的话,就直接调用之类PhoneStatusBar的addNotifcation()来处理.
PhoneStatusBar的addNotifcation()的会转调父类的addNotificationViews方法,再继续调到PhoneStatusBar 的updateNotifications().其中通知的排序工作就是在这里调用mNotificationData.filterAndSort()来实现.
这里需要注意的是在Android L里面有些通知来的时候不光会在下拉菜单显示,有的会显示为屏幕顶部弹窗形式,
也就是Heads UP Notification .2者之间的区别后续有时间再分析.
private final NotificationListenerService mNotificationListener =
new NotificationListenerService() {
@Override
public void onListenerConnected() {
if (DEBUG) Log.d(TAG, "onListenerConnected");
final StatusBarNotification[] notifications = getActiveNotifications();
final RankingMap currentRanking = getCurrentRanking();
mHandler.post(new Runnable() {
@Override
public void run() {
for (StatusBarNotification sbn : notifications) {
addNotification(sbn, currentRanking);
}
}
});
}
@Override
public void onNotificationPosted(final StatusBarNotification sbn,
final RankingMap rankingMap) {
if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn);
mHandler.post(new Runnable() {
@Override
public void run() {
Notification n = sbn.getNotification();
boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
|| isHeadsUp(sbn.getKey());
// Ignore children of notifications that have a summary, since we're not
// going to show them anyway. This is true also when the summary is canceled,
// because children are automatically canceled by NoMan in that case.
if (n.isGroupChild() &&
mNotificationData.isGroupWithSummary(sbn.getGroupKey())) {
//这里是处理Notification设置了GroupSummary属性的情况,因为这种情况下只显示一个,其他child 都不显示.
if (DEBUG) {
Log.d(TAG, "Ignoring group child due to existing summary: " + sbn);
}
// Remove existing notification to avoid stale data.
if (isUpdate) {
removeNotification(sbn.getKey(), rankingMap);
} else {
mNotificationData.updateRanking(rankingMap);
}
return;
}
if (isUpdate) {
updateNotification(sbn, rankingMap);
} else {
addNotification(sbn, rankingMap);
}
}
});
}
@Override
public void onNotificationRemoved(final StatusBarNotification sbn,
final RankingMap rankingMap) {
if (DEBUG) Log.d(TAG, "onNotificationRemoved: " + sbn);
mHandler.post(new Runnable() {
@Override
public void run() {
removeNotification(sbn.getKey(), rankingMap);
}
});
}
@Override
public void onNotificationRankingUpdate(final RankingMap rankingMap) {
if (DEBUG) Log.d(TAG, "onRankingUpdate");
mHandler.post(new Runnable() {
@Override
public void run() {
updateNotificationRanking(rankingMap);
}
});
}
};
PhoneStatusBar 最后要将Notification 相关的视图显示到下拉菜单区域(NotificationStackScrollLayout),只要直接调用NotificationStackScrollLayout 的addView()和changeViewPosition()方法就可以了.相关的部分代码如下.
PhoneStatusBar .java
private void updateNotificationShade() {
if (mStackScroller == null) return;
ArrayList<View> toRemove = new ArrayList<View>();
for (int i=0; i< mStackScroller.getChildCount(); i++) {
View child = mStackScroller.getChildAt(i);
if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
toRemove.add(child);
}
}
for (View remove : toRemove) {
mStackScroller.removeView(remove);
}
for (int i=0; i<toShow.size(); i++) {
View v = toShow.get(i);
if (v.getParent() == null) {
mStackScroller.addView(v);
}
}
// So after all this work notifications still aren't sorted correctly.
// Let's do that now by advancing through toShow and mStackScroller in
// lock-step, making sure mStackScroller matches what we see in toShow.
int j = 0;
for (int i = 0; i < mStackScroller.getChildCount(); i++) {
View child = mStackScroller.getChildAt(i);
if (!(child instanceof ExpandableNotificationRow)) {
// We don't care about non-notification views.
continue;
}
if (child == toShow.get(j)) {
// Everything is well, advance both lists.
j++;
continue;
}
// Oops, wrong notification at this position. Put the right one
// here and advance both lists.
mStackScroller.changeViewPosition(toShow.get(j), i);
j++;
}
//.........
}
备注:其中关于在Android 5.0 中添加的Notification GroupSummary的功能这里暂时不做分析.我们主要考虑简单的通知流程.
这里只是提供一个相关的demo,以便理解关于mNotificationListener的onNotificationPosted实现.
将下面的的Activity运行起来就可以看到相关的效果.
public class NotificationGroupSummaryActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_show_notification);
showNotifications();
}
private void showNotifications() {
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
showNotification1(notificationManager);
showNotification2(notificationManager);
showGroupSummaryNotification(notificationManager);
}
private void showNotification1(NotificationManager notificationManager) {
showSingleNotification(notificationManager, "title 1", "message 1", 1);
}
private void showNotification2(NotificationManager notificationManager) {
showSingleNotification(notificationManager, "title 2", "message 2", 2);
}
protected void showSingleNotification(NotificationManager notificationManager,
String title,
String message,
int notificationId) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentTitle(title)
.setContentText(message)
.setSmallIcon(R.drawable.ic_launcher)
.setGroupSummary(false)
.setGroup("group");
Notification notification = builder.build();
notificationManager.notify(notificationId, notification);
}
private void showGroupSummaryNotification(NotificationManager notificationManager) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentTitle("Dummy content title")
.setContentText("Dummy content text")
.setStyle(new NotificationCompat.InboxStyle()
.addLine("Line 1")
.addLine("Line 2")
.setSummaryText("Inbox summary text")
.setBigContentTitle("Big content title"))
.setNumber(2)
.setSmallIcon(R.drawable.ic_launcher)
.setCategory(Notification.CATEGORY_EVENT)
.setGroupSummary(true)
.setGroup("group");
Notification notification = builder.build();
notificationManager.notify(123456, notification);
}
}
通知的滑动先从NotificationStackScrollLayout 的onInterceptTouchEvent()开始的然后是调用onTouchEvent(),其中由于滑动清除是左右滑动,这个是通过SwipeHelper来处理的,近期任务的滑动也是使用它来处理的.
其中touch时间处理的顺序如下,具体如下面的log显示.
Line 4679: D/NotificationStackScrollLayout( 7677): onInterceptTouchEvent action=0
Line 4757: D/NotificationStackScrollLayout( 7677): onInterceptTouchEvent action=2
Line 4799: D/NotificationStackScrollLayout( 7677): onInterceptTouchEvent action=2
Line 4811: D/NotificationStackScrollLayout( 7677): onTouchEvent action=2
Line 4841: D/NotificationStackScrollLayout( 7677): onTouchEvent action=2
Line 4881: D/NotificationStackScrollLayout( 7677): onTouchEvent action=2
Line 4909: D/NotificationStackScrollLayout( 7677): onTouchEvent action=2
Line 4945: D/NotificationStackScrollLayout( 7677): onTouchEvent action=2
Line 4953: D/NotificationStackScrollLayout( 7677): onTouchEvent action=1
由于主要的任务是在SwipeHelper里面的处理的,这里主要分析SwipeHelper 的代码. onInterceptTouchEvent 里面主要是进行一些坐标设置和判断工作,主要的逻辑还是在onTouchEvent(),里面.
首先在NotificationStackScrollLayout 创建SwipeHelper 对象的时候就已经将自己作为参数传递给了额SwipeHelper,以便SwipeHelper 进行回调,更准确的说是NotificationStackScrollLayout实现了SwipeHelper内部接口类.在创建SwipeHelper 的时候还指定了滑动方向是X方向也就是水平方向.滑动事件处理时,会根据记录的坐标来获取当前需要获取的是那个通知的视图.得到视图的只是会判断该通知是否能够被清除,来修改最终能移动的水平距离范围,同样也要判断最终手指抬起是直接清除还是取消清除,将通知弹回原来的位置.
SwipeHelper创建过程如下.
NotificationStackScrollLayout.java
public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height);
mExpandHelper = new ExpandHelper(getContext(), this,
minHeight, maxHeight);
mExpandHelper.setEventSource(this);
mExpandHelper.setScrollAdapter(this);
mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext());//创建
SwipeHelper,并指定方向为X方向,设置回调对象为本身mSwipeHelper.setLongPressListener(mLongPressListener);
initView(context);
}
SwipeHelper.java
public boolean onTouchEvent(MotionEvent ev) {
if (mLongPressSent) {
return true;
}
Log.d(TAG, "onTouchEvent action=" +ev.getAction());
if (!mDragging) {//判断是否在拖动,mDragging 会在action=move 的时候在onInterceptTouchEvent里面变成true
if (mCallback.getChildAtPosition(ev) != null) {
// We are dragging directly over a card, make sure that we also catch the gesture
// even if nobody else wants the touch event.
onInterceptTouchEvent(ev);
return true;
} else {
// We are not doing anything, make sure the long press callback
// is not still ticking like a bomb waiting to go off.
removeLongPressCallback();
return false;
}
}
mVelocityTracker.addMovement(ev);//添加事件,以便监听力度大小
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_MOVE://手指移动中
if (mCurrView != null) {//mCurrView就是我们点击的通知,具体获取是在onInterceptTouchEvent里面的
float delta = getPos(ev) - mInitialTouchPos;//和我们按下手指时相比的距离
float absDelta = Math.abs(delta);
if (absDelta >= getFalsingThreshold()) {
mTouchAboveFalsingThreshold = true;
}
// don't let items that can't be dismissed be dragged more than
// maxScrollDistance
if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {//如果当前通知的视图不能被清除
float size = getSize(mCurrAnimView);//通知的宽度
float maxScrollDistance = 0.15f * size;//最大可以滑动距离,因为这里的通知不能清除,所以将其能移动的范围缩小为0.15倍
if (absDelta >= size) {//如果移动的具体大于通知的宽度
delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
} else {
delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));//修改实际将会移动的距离
}
}
setTranslation(mCurrAnimView, delta);//水平移动通知
updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
}
break;
case MotionEvent.ACTION_UP://手指抬起
case MotionEvent.ACTION_CANCEL:
if (mCurrView != null) {
float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
float velocity = getVelocity(mVelocityTracker);
float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
// Decide whether to dismiss the current view
boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
(Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
(velocity > 0) == (getTranslation(mCurrAnimView) > 0);
boolean falsingDetected = mCallback.isAntiFalsingNeeded()
&& !mTouchAboveFalsingThreshold;
boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
&& !falsingDetected && (childSwipedFastEnough || childSwipedFarEnough)
&& ev.getActionMasked() == MotionEvent.ACTION_UP;
if (dismissChild) {
// flingadingy
dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);//以动画清除通知,根据力度大小来定义动画效果
} else {
// snappity
mCallback.onDragCancelled(mCurrView); //不能清除动画,就取消
snapChild(mCurrView, velocity);//动画的形式将通知弹回
}
}
break;
}
return true;
}
具体判断通知是是否可以被清除调用的以下代码,其中主要和3个属性有关系,Notification.FLAG_ONGOING_EVENT,Notification.FLAG_NO_CLEAR,还有就是通知设置的publicLayout和privateLayout相关.关于这2个layout,暂时没有看具体逻辑,后续再分析.
NotificationStackScrollLayout.java
public boolean canChildBeDismissed(View v) {
final View veto = v.findViewById(R.id.veto);
return (veto != null && veto.getVisibility() != View.GONE);
}
其实上面主要是将选择的通知视图移动位置,最终将这个通知删除并刷新通知列表的并不是上面的代码.
其中真正的清除通知是在PhoneStatusBar 里面的mNotificationLocationsChangedListener.
private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
new OnChildLocationsChangedListener() {
@Override
public void onChildLocationsChanged(
NotificationStackScrollLayout stackScrollLayout) {
if (mHandler.hasCallbacks(mVisibilityReporter)) {
// Visibilities will be reported when the existing
// callback is executed.
return;
}
// Calculate when we're allowed to run the visibility
// reporter. Note that this timestamp might already have
// passed. That's OK, the callback will just be executed
// ASAP.
long nextReportUptimeMs =
mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
Log.d(TAG,"onChildLocationsChanged");
}
};
上面的代码中是mVisibilityReporter关键.在这个Runnable里面的logNotificationVisibilityChanges()的会通知NotificationManagerService来进行清除.
在下拉菜单判断有可以清除的通知的时候,在下方会出现一个清除的图标,也就是DismissView,点击这个DismissView,就会执行clearAllNotifications,接着转调performDismissAllAnimations来实现将需要隐藏的视图以动画的形式退出.
PhoneStatusBar.java
mDismissView = (DismissView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_notification_dismiss_all, mStackScroller, false);
mDismissView.setOnButtonClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clearAllNotifications();
}
});
private void clearAllNotifications() {
// animate-swipe all dismissable notifications, then animate the shade closed
int numChildren = mStackScroller.getChildCount();
final ArrayList<View> viewsToHide = new ArrayList<View>(numChildren);
for (int i = 0; i < numChildren; i++) {
final View child = mStackScroller.getChildAt(i);
if (mStackScroller.canChildBeDismissed(child)) {
if (child.getVisibility() == View.VISIBLE) {
viewsToHide.add(child);
}
}
}
if (viewsToHide.isEmpty()) {
animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE);
return;
}
addPostCollapseAction(new Runnable() {
@Override
public void run() {
try {
mBarService.onClearAllNotifications(mCurrentUserId);
} catch (Exception ex) { }
}
});
performDismissAllAnimations(viewsToHide);
}
前面已经提到 PhoneStatusBar 的updateNotifications().其中通知的排序工作就是在这里调用mNotificationData.filterAndSort()来实现.准确的来时是调用Collections.sort(mSortedAndFiltered, mRankingComparator)来实现,其中mRankingComparator比较器代码如下.
比较的时候主要还是以priority 和时间先后来的.先考虑Notification的priority值大小,如果相同就比较Notification的when值大小.
当然音乐播放器相关的比较数,在有音乐应用运行的时候,音乐应用的等级要高一些.如果需要让其他应用高于他,可以参考下面的代码进行修改.
NotificationData.java
public void filterAndSort() {
mSortedAndFiltered.clear();
mGroupsWithSummaries.clear();
final int N = mEntries.size();
for (int i = 0; i < N; i++) {
Entry entry = mEntries.valueAt(i);
StatusBarNotification sbn = entry.notification;
if (shouldFilterOut(sbn)) {
continue;
}
if (sbn.getNotification().isGroupSummary()) {
mGroupsWithSummaries.add(sbn.getGroupKey());
}
mSortedAndFiltered.add(entry);
}
// Second pass: Filter out group children with summary.
if (!mGroupsWithSummaries.isEmpty()) {
final int M = mSortedAndFiltered.size();
for (int i = M - 1; i >= 0; i--) {
Entry ent = mSortedAndFiltered.get(i);
StatusBarNotification sbn = ent.notification;
if (sbn.getNotification().isGroupChild() &&
mGroupsWithSummaries.contains(sbn.getGroupKey())) {
mSortedAndFiltered.remove(i);
}
}
}
Collections.sort(mSortedAndFiltered, mRankingComparator);
}
private final Comparator<Entry> mRankingComparator = new Comparator<Entry>() {
private final Ranking mRankingA = new Ranking();
private final Ranking mRankingB = new Ranking();
@Override
public int compare(Entry a, Entry b) {
// Upsort current media notification.
String mediaNotification = mEnvironment.getCurrentMediaNotificationKey();
boolean aMedia = a.key.equals(mediaNotification);
boolean bMedia = b.key.equals(mediaNotification);
//Doov zhangle add start:calendar can be higher than music apps
Log.d(TAG,"compare mediaNotification=" +mediaNotification + " a.key=" + a.key + " b.key=" + b.key);
if(aMedia || bMedia){
if(a.notification.getPackageName().equals("com.android.calendar") && a.notification.getNotification().priority >= Notification.PRIORITY_MAX ){
return -1;//b is media
}else if(b.notification.getPackageName().equals("com.android.calendar") && b.notification.getNotification().priority >= Notification.PRIORITY_MAX ){
return 1;//a is media
}
}//Doov zhangle add end:
if (aMedia != bMedia) {
return aMedia ? -1 : 1;
}
final StatusBarNotification na = a.notification;
final StatusBarNotification nb = b.notification;
// Upsort PRIORITY_MAX system notifications
boolean aSystemMax = na.getNotification().priority >= Notification.PRIORITY_MAX &&
isSystemNotification(na);
boolean bSystemMax = nb.getNotification().priority >= Notification.PRIORITY_MAX &&
isSystemNotification(nb);
if (aSystemMax != bSystemMax) {
return aSystemMax ? -1 : 1;
}
// RankingMap as received from NoMan.
if (mRankingMap != null) {
mRankingMap.getRanking(a.key, mRankingA);
mRankingMap.getRanking(b.key, mRankingB);
return mRankingA.getRank() - mRankingB.getRank();
}
int d = nb.getScore() - na.getScore();
if (a.interruption != b.interruption) {
return a.interruption ? -1 : 1;
} else if (d != 0) {
return d;
} else {
return (int) (nb.getNotification().when - na.getNotification().when);
}
}
};
Heads Up Notification 是Android L里面新加的功能,需要这个功能.
Android L以下的的软件版本使用如下方法(需要导入support.v4的jar包)
notification=new NotificationCompat.Builder(MainActivity.this)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_launcher)
.setFullScreenIntent(pendingIntent, false)
.setContentTitle("这是标题")
.setContentText("这是内容")
.addAction(R.drawable.ic_launcher, "菜单1", peddingIntent1)
.build();
notificationManager.notify(1, notification);
其中 Builder.setFullScreenIntent(pendingIntent, false) 是关键;
Notification.Builder builder = new Notification.Builder(this);
builder.setVisibility(Notification.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_launcher)
.setFullScreenIntent(contentIntent, false)
.setPriority(Notification.PRIORITY_MAX)
.setContentTitle("这是标题")
.setContentText("这是内容")
.setDeleteIntent(contentIntent)
.addAction(R.drawable.ic_launcher, "菜单1", contentIntent)
.build();
nm.notify(1, builder.getNotification());
在实际使用的时候发现系统应用如果设置了setFullScreenIntent属性才能停留的久一点(10s)
以下代码中的mHeadsUpNotificationView.showNotification(interruptionCandidate)就是调用HeadsupNotification 的. showNotification 会接着调用scheduleHeadsUpOpen方法来进行处理.
相关代码如下
PhoneStatusBar.java
@Override
public void addNotification(StatusBarNotification notification, RankingMap ranking) {
if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey());
if (mUseHeadsUp && shouldInterrupt(notification)) {
if (DEBUG) Log.d(TAG, "launching notification in heads up mode");
Entry interruptionCandidate = new Entry(notification, null);
ViewGroup holder = mHeadsUpNotificationView.getHolder();
if (inflateViewsForHeadsUp(interruptionCandidate, holder)) {
// 1. Populate mHeadsUpNotificationView
mHeadsUpNotificationView.showNotification(interruptionCandidate);
// do not show the notification in the shade, yet.
return;
}
}
}
mBar.scheduleHeadsUpOpen()表示弹窗操作,mBar.resetHeadsUpDecayTimer()表示关闭悬浮通知的操作.具体代码如下
public boolean showNotification(NotificationData.Entry headsUp) {
if (mHeadsUp != null && headsUp != null && !mHeadsUp.key.equals(headsUp.key)) {
// bump any previous heads up back to the shade
release();
}
mHeadsUp = headsUp;
if (mContentHolder != null) {
mContentHolder.removeAllViews();
}
if (mHeadsUp != null) {
mMostRecentPackageName = mHeadsUp.notification.getPackageName();
mHeadsUp.row.setSystemExpanded(true);
mHeadsUp.row.setSensitive(false);
mHeadsUp.row.setHeadsUp(true);
mHeadsUp.row.setHideSensitive(
false, false /* animated */, 0 /* delay */, 0 /* duration */);
if (mContentHolder == null) {
// too soon!
return false;
}
mContentHolder.setX(0);
mContentHolder.setVisibility(View.VISIBLE);
mContentHolder.setAlpha(mMaxAlpha);
mContentHolder.addView(mHeadsUp.row);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
mSwipeHelper.snapChild(mContentHolder, 1f);
mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay;
mHeadsUp.setInterruption();
mBar.scheduleHeadsUpOpen();
// 3. Set alarm to age the notification off
mBar.resetHeadsUpDecayTimer();
}
return true;
}
关于各种自定义的通知可以参考下面的链接:
http://www.itnose.net/detail/6169442.html
长按通知出现的动画效果是在PhoneStatusBar 里面添加的,在makeStatusBarView()里面设置了NotificationStackScrollLayout长按时的处理器getNotificationLongClicker().
在具体代码如下:
/*mStackScroller = (NotificationStackScrollLayout) mStatusBarWindow.findViewById(
R.id.notification_stack_scroller);*///zhangle delete
mStackScroller = mNotificationPanel.getmNotificationStackScroller();//zhangle add
mStackScroller.setLongPressListener(getNotificationLongClicker());
mStackScroller.setPhoneStatusBar(this);
mKeyguardIconOverflowContainer =
(NotificationOverflowContainer) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_notification_keyguard_overflow, mStackScroller, false);
mKeyguardIconOverflowContainer.setOnActivatedListener(this);
mKeyguardIconOverflowContainer.setOnClickListener(mOverflowClickListener);
mStackScroller.addView(mKeyguardIconOverflowContainer);
SpeedBumpView speedBump = (SpeedBumpView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_notification_speed_bump, mStackScroller, false);
mStackScroller.setSpeedBumpView(speedBump);
mEmptyShadeView = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_no_notifications, mStackScroller, false);
mStackScroller.setEmptyShadeView(mEmptyShadeView);
mDismissView = (DismissView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_notification_dismiss_all, mStackScroller, false);
mDismissView.setOnButtonClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clearAllNotifications();
}
});
mStackScroller.setDismissView(mDismissView);
mExpandedContents = mStackScroller;
从上面的代码可以看出,通知的一键清除和锁屏页面的通知的显示都在这个方法里面有所处理.
其中mKeyguardIconOverflowContainer就是锁屏通知很多的时候通知最后一行显示的更多通知集合的视图.
这里只是记录一下,具体动画效果的实现不做分析.
这里暂时只是记录一下关键的代码.
BaseStatusBar 里面有个内部类NotificationClicker,就是通过他来实现的点击打开通知和收起通知的工作.
在解锁的情况下长按Home键就可以直接调出近期使用过的应用,要清除一个应用退出,只要左滑或者右滑就可以.至于一键清除功能,google 默认是没有的,需要各家厂商自己去添加.
近期任务栏主要的几个类如下,需要注意的是AndroidL和AndroidKK在这一部分的代码路径是完全不同的.L保存了KK的代码目录,但是没有去使用,而是在com.android.systemui.recents里面重新添加了相关的代码.
com.android.systemui.recents下面相关的核心类
RecentsActivity.java
TaskStackView.java
RecentsView.java
TaskStackViewTouchHandler.java
SwipeHelper.java
还有一个主要的类是
frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
长按Home 键启动近期任务,肯定是有地方来判断长按这个动作的.具体代码是在PhoneWindowManager.java 里面
在interceptKeyBeforeDispatching 里面通过KeyEvent 来判断是长按事件之后就会调用handleLongPressOnHome() 方法,然后转调toggleRecentApps() 方法.
private void handleLongPressOnHome() {
Log.d(TAG,"handleLongPressOnHome mLongPressOnHomeBehavior=" + mLongPressOnHomeBehavior);
if (mLongPressOnHomeBehavior != LONG_PRESS_HOME_NOTHING) {
mHomeConsumed = true;
performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_RECENT_SYSTEM_UI) {
toggleRecentApps();
} else if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_ASSIST) {
launchAssistAction();
}
}
}
private void toggleRecentApps() {
Log.d(TAG,"toggleRecentApps");
mPreloadedRecentApps = false; // preloading no longer needs to be canceled
if(getIsSuperSaveMode()) return;//zhangle add
try {
IStatusBarService statusbar = getStatusBarService();
if (statusbar != null) {
statusbar.toggleRecentApps();
}
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException when toggling recent apps", e);
// re-acquire status bar service next time it is needed.
mStatusBarService = null;
}
}
其中mLongPressOnHomeBehavior 等于1 的情况下长按home 键就是打开近期任务等于2的情况就是打开搜索页面.
这个值是从com.android.internal.R.integer.config_longPressOnHomeBehavior里面读取的.
其中toggleRecentApps() 会调用StatusBarManagerService 的toggleRecentApps的方法,然后转调CommandQueue 的toggleRecentApps方法. CommandQueue通过回调BaseStatusBar 的toggleRecentApps方法,然后调用Recents的toggleRecents方法,到这里才会启动RecentsActivity.java.
上面提到的第7步实际会发现是4.4效果的近期任务视图.并不是卡片式的.5.0 默认使用的卡片式的任务管理.
4.4 的时序图步骤:1-7
5.0 的时序步骤:1-6 然后 8-11
在近期任务页面,左右滑动就可以进行清除单个应用.点击一下就可以重新进入到所选择的应用.
在RecentsActivity的onStart()方法里面调用自己的updateRecentsTasks方法,通过调用创建RecentsTaskLoader 实例来判断当前有未清除的任务.如果有的话,就将获取到的TaskStack 数组用来生成RecentsView视图,其实我们看到的近期任务真个视图几乎都是RecentsView.这个RecentsView视图是有多个TaskStackView组成的.而一个应用的视图就是一个TaskStackView.
在生成RecentsView 的视图的时候, RecentsView 会先清除之前保存的TaskStackView,再根据新传入的TaskStack 来生成TaskStackView来组建RecentsView视图.
每一个TaskStackView 在创建的时候,都会实例化一个TaskStackViewTouchHandler和TaskStackViewScroller对象.其中触摸相关的事件主要是由TaskStackViewTouchHandler 来处理的. TaskStackViewTouchHandler 在处理相关事件的时候将事件交给了SwipeHelper来处理.
SwipeHelper.java
public boolean onTouchEvent(MotionEvent ev) {
if (!mDragging) {
if (!onInterceptTouchEvent(ev)) {
return mCanCurrViewBeDimissed;
}
}
mVelocityTracker.addMovement(ev);
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_MOVE:
if (mCurrView != null) {
float delta = getPos(ev) - mInitialTouchPos;
setSwipeAmount(delta);
mCallback.onSwipeChanged(mCurrView, delta);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mCurrView != null) {
endSwipe(mVelocityTracker);
}
break;
}
return true;
}
SwipeHelper通过VelocityTracker来跟踪触屏事件,在检测到是手指抬起的的时候会调用endSwipe根据滑动的力度和距离及方向来决定是清除选择应用还是取消当前的清除动作.
private void endSwipe(VelocityTracker velocityTracker) {
velocityTracker.computeCurrentVelocity(1000 /* px/sec */);
float velocity = getVelocity(velocityTracker);
float perpendicularVelocity = getPerpendicularVelocity(velocityTracker);
float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
float translation = getTranslation(mCurrView);
// Decide whether to dismiss the current view
boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
Math.abs(translation) > 0.6 * getSize(mCurrView);
boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
(Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
(velocity > 0) == (translation > 0);
boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
&& isValidSwipeDirection(translation)
&& (childSwipedFastEnough || childSwipedFarEnough);
if (dismissChild) {
// flingadingy
dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
} else {
// snappity
mCallback.onDragCancelled(mCurrView);
snapChild(mCurrView, velocity);
}
}
上面代码中dismissChild()就是清除动作和效果动画效果的代码.最终SwipeHelper通过回调TaskStackViewTouchHandler的onChildDismissed,和TaskStackView 的onTaskViewDismissed来清除任务的.
以上分析了滑动清除的动作下面就来看看点击进入应用的动作相关的流程.看起来有点复杂,其实主要是利用内部接口类来进行回调.
由于Android L在手机重启之后还是会保留之前启动为手动清除的应用,所以在下面第10步的时候,是有种情况要判断的.
具体效果图看5.2节,功能就是点击清除按钮,任务视图依次右画退出,清除按钮播放转动.这里要主要是的有几个问题说一下.
1.为了避免任务过多的时候,执行时间过长,调用RecentsTaskLoader清除task 的时候最好是使用异步处理
2.从集合中循环去数据删除的时候需要转换成Iterator,不要直接使用List.List 不是线程安全的.
具体逻辑不复杂,具体如下.
RecentsView.java
public void removeAllTaskStacksAndNotify() {
if(myHandler == null){
myHandler = new MyHandler();
}
if(mWindowManager == null){
mWindowManager = (WindowManager) getContext()
.getSystemService(Context.WINDOW_SERVICE);
}
int childCount = getChildCount();
Log.d(TAG,"removeAllTaskStacksAndNotify childCount=" + childCount);
if(childCount>0){
mCb.onAnimationStart();
}
RemoveTask mRemoveTask = new RemoveTask();
mRemoveTask.execute();
long startTime = System.currentTimeMillis();
TaskStackView taskStackView = null;
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (null != child && child != mSearchBar && child instanceof TaskStackView ) {
taskStackView = (TaskStackView)child;
int tasks = taskStackView.getChildCount();
Log.d(TAG,"removeAllTaskStacksAndNotify tasks=" + tasks);
//将显示出来的近期任务视图依次右滑,从下到上
/*for (int j = tasks-1; j >= 0; j--) {
propertyValuesHolder(taskStackView.getChildAt(j),tasks-j,j==0);
}*/
//将显示出来的近期任务视图依次右滑,从上到下
for (int j = 0; j < tasks; j++) {
propertyValuesHolder(taskStackView.getChildAt(j),j,j==tasks-1);
}
}
}
long endTime = System.currentTimeMillis();
Log.d(TAG,"removeAllTaskStacksAndNotify time=" + (endTime-startTime));
if(childCount <= 0){
myHandler.sendMessageDelayed(myHandler.obtainMessage(myHandler.ANIMATIONEND), 0);
}
}
class MyHandler extends Handler{
final static int ANIMATIONEND = 10;
final static int TASKCLEANED = 11;
boolean isAnimationEnd = false;
boolean isTaskCleaned = false;
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case ANIMATIONEND:
isAnimationEnd = true;
break;
case TASKCLEANED:
isTaskCleaned = true;
break;
default:
break;
}
if(isAnimationEnd && isTaskCleaned){
mCb.onAllViewClean();
isTaskCleaned = isAnimationEnd = false;
}
}
}
private final int DURATION_TIME = 150;
private WindowManager mWindowManager = null;
public void propertyValuesHolder(View view,final int i,final boolean last){
float h = view.getHeight();
float w = view.getWidth();
float x = view.getX();
float y = view.getY();
float width_screen = mWindowManager.getDefaultDisplay().getWidth();
boolean ori = isScreenLand();
Log.d("zhangle","propertyValuesHolder x=" + x + " y=" +y + " w=" + w + " h=" +h + " width_screen=" +width_screen + " ori=" + ori);
/*PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1f, 0f, 1f);
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 0.5f, 1f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1f,0.5f, 1f);
PropertyValuesHolder y_tra = PropertyValuesHolder.ofFloat("y", y,-1500f);*/
PropertyValuesHolder x_tra = PropertyValuesHolder.ofFloat("x",width_screen);
ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view,x_tra).setDuration(DURATION_TIME*(ori?2:1));
oa.setInterpolator(new LinearInterpolator());
oa.addListener(new AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationRepeat(Animator animation) {
// TODO Auto-generated method stub
}
@Override
public void onAnimationEnd(Animator animation) {
if(last){
myHandler.sendMessageDelayed(myHandler.obtainMessage(myHandler.ANIMATIONEND), 0);
}
}
@Override
public void onAnimationCancel(Animator animation) {
// TODO Auto-generated method stub
}
} );
oa.setStartDelay((i+1)*70);
oa.start();
}
public boolean isScreenLand() {
Configuration mConfiguration = this.getResources().getConfiguration();
int ori = mConfiguration.orientation ; //获取屏幕方向
if(ori == mConfiguration.ORIENTATION_LANDSCAPE){//横屏
return true;
}else if(ori == mConfiguration.ORIENTATION_PORTRAIT){//竖屏
return false;
}
return false;
}
private void removeRecentsTask() {
if(null != mStacks){
try {
int mNum = mStacks.size();
Log.d("zhangle", "RecentsView removeAllTaskStacksAndNotify mNum=" + mNum);
for (int i = 0; i < mNum; i++) {
TaskStack taskStack = mStacks.get(i);
ArrayList<Task> list = taskStack.getTasks();
Iterator<Task> iterator = list.iterator();//fix bug java.util.ConcurrentModificationException
/* for (Task task : list) {*/
while(iterator.hasNext()){
Task t = iterator.next();
Log.d("zhangle", "RecentsView onTaskViewDismissed t="+ t.key.toString());
// Remove any stored data from the loader. We currently don't bother notifying the views
// that the data has been unloaded because at the point we call onTaskViewDismissed(), the views
// either don't need to be updated, or have already been removed.
RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
loader.deleteTaskData(t, false);
iterator.remove();
// Remove the old task from activity manager
RecentsTaskLoader.getInstance().getSystemServicesProxy().removeTask(t.key.id);
}
}
} catch (Exception e) {
Log.d("zhangle", "RecentsView removeAllTaskStacksAndNotify error" );
e.printStackTrace();
}
}
}
class RemoveTask extends AsyncTask{
long startTime, endTime;
@Override
protected Object doInBackground(Object... params) {
Log.d(TAG,"doInBackground");
removeRecentsTask();
return null;
}
@Override
protected void onPreExecute() {
Log.d(TAG,"onPreExecute");
startTime = System.currentTimeMillis();
super.onPreExecute();
}
@Override
protected void onPostExecute(Object result) {
endTime = System.currentTimeMillis();
Log.d(TAG,"RemoveTask time=" + (endTime-startTime));
super.onPostExecute(result);
myHandler.sendMessageDelayed(myHandler.obtainMessage(myHandler.TASKCLEANED), 0);
}
}
在长按home 键的时候会出现任务列表重下到上的动画.这个动画的最终结果是我们看到的就是一个类似卡片的列表.效果图5.2节有.
现在就记录一下这个动画的流程.
RecentsActivity.java
protected void onStart() {
super.onStart();
RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
SystemServicesProxy ssp = loader.getSystemServicesProxy();
AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, true);
// Register the broadcast receiver to handle messages from our service
IntentFilter filter = new IntentFilter();
filter.addAction(AlternateRecentsComponent.ACTION_HIDE_RECENTS_ACTIVITY);
filter.addAction(AlternateRecentsComponent.ACTION_TOGGLE_RECENTS_ACTIVITY);
filter.addAction(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION);
registerReceiver(mServiceBroadcastReceiver, filter);
// Register any broadcast receivers for the task loader
loader.registerReceivers(this, mRecentsView);
// Update the recent tasks
updateRecentsTasks(getIntent());
// If this is a new instance from a configuration change, then we have to manually trigger
// the enter animation state
if (mConfig.launchedHasConfigurationChanged) {//这里是false
onEnterAnimationTriggered();
}
}
上面又2个地方需要注意.
AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, true);
filter.addAction(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION);
a.通知系统状态栏需要改变:
这个就是通知SystemUI的其他地方Recents 发生了改变.下面也记录一下相关流程.
AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, true)会转调自己的visibilityChanged()方法.
static void visibilityChanged(boolean visible) {
if (sRecentsComponentCallbacks != null) {
sRecentsComponentCallbacks.onVisibilityChanged(visible);
} else {
Xlog.w(TAG, "notifyVisibilityChanged : callback is null");
}
}
在visibilityChanged内部会判断sRecentsComponentCallbacks是否不为null,并调用其onVisibilityChanged()方法,当然这些方法的参数值都是true.那么这个sRecentsComponentCallbacks 是怎么来的,搜索之后发现是在Recents.java里面的一个setCallback()方法里面设置的,在这里需要注意Recents 的类结构:public class Recents extends SystemUI implements RecentsComponent.
所以Recents是实现了接口RecentsComponent的setCallback(Callbacks cb)方法.继续反向跟踪会发现调用这个方法的地方是在BaseStatusBar 的start()方法里面.
private RecentsComponent mRecents;
...
public void start() {.....
mRecents = getComponent(RecentsComponent.class);
mRecents.setCallback(this);//BaseStatusBar 实现了RecentsComponent.Callbacks接口
RecentsComponent只是一个接口,所以mRecents就是一个实现了RecentsComponent接口的实体类的对象,SystemUI 里面搜索就会发现是Recents实现了RecentsComponent这个接口.所以这就是BaseStatusBar在它的start()方法里面调用一个Recents对象的setCallback(方法),并将自己传递给了Recents.以便回调执行已经实现RecentsComponent.Callbacks接口的方法public void onVisibilityChanged(boolean visible),其实这个方法真正的实现是在PhoneStatusBar里面. BaseStatusBar只是一个空实现.
到这里我们就可以知道,在AlternateRecentsComponent.notifyVisibilityChanged(this, ssp, true)里面调用的sRecentsComponentCallbacks其实就是一个PhoneStatusBar对象.最终的结果也是执行PhoneStatusBar的onVisibilityChanged()方法(BaseStatusBar继承了RecentsComponent.Callbacks接口).
@Override
public void onVisibilityChanged(boolean visible) {
// Update the recents visibility flag
if (visible) {
mSystemUiVisibility |= View.RECENT_APPS_VISIBLE;
} else {
mSystemUiVisibility &= ~View.RECENT_APPS_VISIBLE;
}
notifyUiVisibilityChanged(mSystemUiVisibility);
}
private void notifyUiVisibilityChanged(int vis) {
try {
mWindowManagerService.statusBarVisibilityChanged(vis);
} catch (RemoteException ex) {
}
}
查看上面的2个方法的源码可以发现是在PhoneStatusBar里面通过AIDL调用WindowManagerService对象的statusBarVisibilityChanged().
b.注册广播接收器接受相关消息执行动画操作.
filter.addAction(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION)(RecentsActivity.java)
这个就是给广播监听器注册一个新的Action:AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION,后面也正是通过监听这广播来执行动画操作.
final BroadcastReceiver mServiceBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(AlternateRecentsComponent.ACTION_HIDE_RECENTS_ACTIVITY)) {
} else if (action.equals(AlternateRecentsComponent.ACTION_TOGGLE_RECENTS_ACTIVITY)) {
// If we are toggling Recents, then first unfilter any filtered stacks first
dismissRecentsToFocusedTaskOrHome(true);
} else if (action.equals(AlternateRecentsComponent.ACTION_START_ENTER_ANIMATION)) {
// Trigger the enter animation
onEnterAnimationTriggered();
// Notify the fallback receiver that we have successfully got the broadcast
// See AlternateRecentsComponent.onAnimationStarted()
setResultCode(Activity.RESULT_OK);
}
}
};
就是在这个广播接收器里面执行的onEnterAnimationTriggered,这个方法内部转调RecentsView的startEnterRecentsAnimation方法.在这个方法里面RecentsView找到child view:TaskStackView,并调
用TaskStackView的startEnterRecentsAnimation().
RecentsView.java
public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
// We have to increment/decrement the post animation trigger in case there are no children
// to ensure that it runs
ctx.postAnimationTrigger.increment();
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child != mSearchBar) {
TaskStackView stackView = (TaskStackView) child;
stackView.startEnterRecentsAnimation(ctx);
}
}
ctx.postAnimationTrigger.decrement();
}
TaskStackView调用startEnterRecentsAnimation就是获取自己的child view:TaskView,并遍历执行startEnterRecentsAnimation()
TaskView.java
void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) {
final TaskViewTransform transform = ctx.currentTaskTransform;
int startDelay = 0;
if (mConfig.launchedFromAppWithThumbnail) {
} else if (mConfig.launchedFromHome) {
// Animate the tasks up
int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1);
int delay = mConfig.transitionEnterFromHomeDelay +
frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay;
setScaleX(transform.scale);
setScaleY(transform.scale);
if (!mConfig.fakeShadows) {
animate().translationZ(transform.translationZ);
}
animate()
.translationY(transform.translationY)
.setStartDelay(delay)
.setUpdateListener(ctx.updateListener)
.setInterpolator(mConfig.quintOutInterpolator)
.setDuration(mConfig.taskViewEnterFromHomeDuration +
frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay)
.withEndAction(new Runnable() {
@Override
public void run() {
// Decrement the post animation trigger
ctx.postAnimationTrigger.decrement();
}
})
.start();
ctx.postAnimationTrigger.increment();
startDelay = delay;
}
// Enable the focus animations from this point onwards so that they aren't affected by the
// window transitions
postDelayed(new Runnable() {
@Override
public void run() {
enableFocusAnimations();
}
}, startDelay);
}
通过上面的代码很明显可以看到就是使用animte()获取ViewPropertyAnimator来执行Y方法的平移和X与Y方向的缩放.
备注:暂时只知道是在AlternateRecentsComponent的onAnimationStarted里面发送广播来触发RecentsActivity的广播接收器的mServiceBroadcastReceiver的的下一步处理进而执行动画相关的流程,但是还没有找到为什么AlternateRecentsComponent的onAnimationStarted会被执行. onAnimationStarted是ActivityOptions.OnAnimationStartedListener的方法,暂时没有搞懂这之间的关系.
在实际操作上下滑动的时候就会发现,页面中始终不会超过7个视图,即使有很多任务视图,在滑动的时候也是会根据手指的滑动来获取新的视图和去掉已经不需要显示的视图.
关于页面中不会超过7个任务的可以查看TaskStackView 的onMeasure()中调用的synchronizeStackViewsWithModel()方法.
根据5.4节已经知道卡片式任务栏就是TaskStackView.每一个任务视图就是它的child view.那么这里主要分析TaskStackView 的onInterceptTouchEvent()和onTouchEvent().查看代码就可以发现TaskStackView 实际是调用的TaskStackViewTouchHandler来处理的.
TaskStackViewTouchHandler的onInterceptTouchEvent()在action=move的时候,当滑动的距离大于configuration.getScaledTouchSlop()的时候, 也就是mIsScrolling=true,onInterceptTouchEvent就会返回true,那么后续事件都是由它的onTouchEvent()来处理的.
case MotionEvent.ACTION_MOVE: {
if (mActivePointerId == INACTIVE_POINTER_ID) break;
mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
Log.d(TAG,"onTouchEvent_move mIsScrolling=" + mIsScrolling);
int activePointerIndex = ev.findPointerIndex(mActivePointerId);
int x = (int) ev.getX(activePointerIndex);
int y = (int) ev.getY(activePointerIndex);
int yTotal = Math.abs(y - mInitialMotionY);
float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y);
float deltaP = mLastP - curP;
if (!mIsScrolling) {
if (yTotal > mScrollTouchSlop) {
mIsScrolling = true;
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
if (mIsScrolling) {
float curStackScroll = mScroller.getStackScroll();
float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
if (Float.compare(overScrollAmount, 0f) != 0) {
// Bound the overscroll to a fixed amount, and inversely scale the y-movement
// relative to how close we are to the max overscroll
float maxOverScroll = mConfig.taskStackOverscrollPct;
deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
/ maxOverScroll));
}
Log.d(TAG,"onTouchEvent_move (curStackScroll + deltaP)=" + (curStackScroll + deltaP));
mScroller.setStackScroll(curStackScroll + deltaP);//TaskStackView ->invalidate
}
mLastMotionX = x;
mLastMotionY = y;
Log.d(TAG,"onTouchEvent_move mLastMotionY=" + mLastMotionY);
mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
mTotalPMotion += Math.abs(deltaP);
break;
}
这里只是贴出了onTouchEvent里面对action=move的处理部分的代码.因为mIsScrolling=true,所以会执行mScroller.setStackScroll(curStackScroll+ deltaP),这个方法会导致TaskStackView 执行postInvalidateOnAnimation(),这就导致TaskStackView 执行draw()方法,由于在draw()方法内部会执行computeScroll(),而TaskStackView 又重写了这个方法.
@Override
public void computeScroll() {
Log.d(TAG,"computeScroll");
mStackScroller.computeScroll();
// Synchronize the views
synchronizeStackViewsWithModel();
clipTaskViews();
// Notify accessibility
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
}
这个方法有2个地方需要注意:
mStackScroller.computeScroll():判断滑动是否完成,没有完成就继续回调TaskStackView的onScrollChange()
synchronizeStackViewsWithModel():这个方法就是根据我们的滑动从ViewPool中获取新的数据和将暂时不显示的视图放到ViewPool里面.
这里只是记录大体流程,关于滑动处理的细节这里暂时不分析.
这一部分的笔记是考虑在要在下一个项目上面要把锁屏页面的通知改成和解锁之后的状态栏那种效果,也就是显示在快捷设置的项目,并随之下滑.
原生的系统锁屏通知是显示在时间下面的.
这里可以暂时把SystemUI 分为2个部分,显示系统图标的状态栏(status_bar.xml),和下来菜单及锁屏页面(status_bar_expanded.xml)
这里我们需要分析的就是status_bar_expanded.xml部分.
打开这个这个文件发现是一个自定义的视图:com.android.systemui.statusbar.phone.NotificationPanelView
之前已经在View的工作流程的笔记中提到,View 的显示离不开measure,layout,draw.
这里NotificationPanelView并没有重写onMeasure和onDraw,只是重写了onLayout.
为了更好的理解和分析,保存一下由熄屏到按power键的时的部分log
C:\Users\zy1373.DOOV>adb logcat -c && adb logcat NotificationStackScrollLayout:D NotificationPanelView:D *:S
--------- beginning of main
--------- beginning of system
D/NotificationPanelView( 1145): onScreenTurnedOn , mWrapper=true,isScreenOff=0
D/NotificationPanelView( 1145): onScreenTurnedOn , mLockscreenView.getXY=[0.0,0.0]
D/NotificationPanelView( 1145): onScreenTurnedOn ,ibimuyuId.getXY=[0.0,0.0],ibimuyuId=1
D/NotificationPanelView( 1145): getMaxPanelHeight mQsExpandImmediate=false mQsExpanded=false mIsExpanding=false mQsExpandedWhenExpandingStarted=false min=75
D/NotificationPanelView( 1145): getMaxPanelHeight maxHeight=1151 min=75
D/NotificationPanelView( 1145): onLayout mQsExpanded=false mQsFullyExpanded=false oldMaxHeight=1200 mQsMaxExpansionHeight=1200 getExpandedHeight=1151.0 mQsMinExpans
ionHeight=0
D/NotificationPanelView( 1145): positionClockAndNotifications ------
D/NotificationPanelView( 1145): getMaxPanelHeight mQsExpandImmediate=false mQsExpanded=false mIsExpanding=false mQsExpandedWhenExpandingStarted=false min=75
D/NotificationPanelView( 1145): getMaxPanelHeight maxHeight=1151 min=75
D/NotificationPanelView( 1145): updateClock ------
D/NotificationPanelView( 1145): calculateQsTopPadding mKeyguardShowing=true mQsSizeChangeAnimator=nulltrue mScrollYOverride=-1 mQsExpansionHeigh=t0.0
D/NotificationStackScrollLayout( 1145): updateTopPadding qsHeight=0.0 scrollY=0 animate=false mNotificationTopPadding=0 ignoreIntrinsicPadding=false
D/NotificationStackScrollLayout( 1145): updateTopPadding start=0.0 clampPadding((int) start=503 mLastSetStackHeight=1151.0
D/NotificationPanelView( 1145): setQsExpansion ------ height=0.0 mQsExpanded=false mStackScrollerOverscrolling=false
D/NotificationPanelView( 1145): isSecurityMode mode=None
D/NotificationPanelView( 1145): setQsTranslation mHeaderAnimatingIn=false
D/NotificationPanelView( 1145): setQsTranslation height=0.0 0.0
D/NotificationPanelView( 1145): setQsTranslation heightOverride=1200y=-993
D/NotificationPanelView( 1145): calculateQsTopPadding mKeyguardShowing=true mQsSizeChangeAnimator=nulltrue mScrollYOverride=-1 mQsExpansionHeigh=t0.0
D/NotificationStackScrollLayout( 1145): updateTopPadding qsHeight=0.0 scrollY=0 animate=false mNotificationTopPadding=0 ignoreIntrinsicPadding=false
D/NotificationStackScrollLayout( 1145): updateTopPadding start=0.0 clampPadding((int) start=503 mLastSetStackHeight=1151.0
NotificationPanelView.java
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// Update Clock Pivot
mKeyguardStatusView.setPivotX(getWidth() / 2);
/// M: We use "getHeight()" instead of "getTextSize()" since the ClockView is implemented by our own and does not have "getTextSize()".
/// Our own ClockView will display AM/PM by default.
mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getHeight());
// Calculate quick setting heights.
int oldMaxHeight = mQsMaxExpansionHeight;
mQsMinExpansionHeight = mKeyguardShowing ? 0 : mHeader.getCollapsedHeight() + mQsPeekHeight;
//mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight(); //zhangle delete for status bar
mQsMaxExpansionHeight = mHeader.getCollapsedHeight() + mQsContainer.getHeight();//zhangle add for status bar
if(DEBUG){
Log.d(TAG,"onLayout mQsExpanded=" + mQsExpanded +
" mQsFullyExpanded=" +mQsFullyExpanded +
" oldMaxHeight=" + oldMaxHeight+
" mQsMaxExpansionHeight=" + mQsMaxExpansionHeight+
" getExpandedHeight=" + getExpandedHeight()+
" mQsMinExpansionHeight=" + mQsMinExpansionHeight);
}
positionClockAndNotifications();
if (mQsExpanded && mQsFullyExpanded) {
mQsExpansionHeight = mQsMaxExpansionHeight;
requestScrollerTopPaddingUpdate(false /* animate */);
requestPanelHeightUpdate();
// Size has changed, start an animation.
if (mQsMaxExpansionHeight != oldMaxHeight) {
startQsSizeChangeAnimation(oldMaxHeight, mQsMaxExpansionHeight);
}
} else if (!mQsExpanded) {
setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
}
mNotificationStackScroller.setStackHeight(getExpandedHeight());
updateHeader();
mNotificationStackScroller.updateIsSmallScreen(
mHeader.getCollapsedHeight() + mQsPeekHeight);
// If we are running a size change animation, the animation takes care of the height of
// the container. However, if we are not animating, we always need to make the QS container
// the desired height so when closing the QS detail, it stays smaller after the size change
// animation is finished but the detail view is still being animated away (this animation
// takes longer than the size change animation).
if (mQsSizeChangeAnimator == null) {
mQsContainer.setHeightOverride(mQsContainer.getDesiredHeight());
}
}
由于暂时只关注锁屏通知的显示逻辑,就从上面onLayout的24行positionClockAndNotifications开始分析,
NotificationPanelView.java
private void positionClockAndNotifications() {
boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
if(DEBUG){
Log.d(TAG , "positionClockAndNotifications ------ ");
}
int stackScrollerPadding;
if (mStatusBarState != StatusBarState.KEYGUARD) {
int bottom = mHeader.getCollapsedHeight();
stackScrollerPadding = mStatusBarState == StatusBarState.SHADE
? bottom + mQsPeekHeight + mNotificationTopPadding
: mKeyguardStatusBar.getHeight() + mNotificationTopPadding;
mTopPaddingAdjustment = 0;
} else {
mClockPositionAlgorithm.setup(
mStatusBar.getMaxKeyguardNotifications(),
getMaxPanelHeight(),
getExpandedHeight(),
mNotificationStackScroller.getNotGoneChildCount(),
getHeight(),
mKeyguardStatusView.getHeight(),
mEmptyDragAmount);
mClockPositionAlgorithm.run(mClockPositionResult);
if (animate || mClockAnimator != null) {
startClockAnimation(mClockPositionResult.clockY);
} else {
mKeyguardStatusView.setY(mClockPositionResult.clockY);
}
updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
}
mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
requestScrollerTopPaddingUpdate(animate);
}
我们需要注意的就是以下2行代码:
mNotificationStackScroller.(stackScrollerPadding);
requestScrollerTopPaddingUpdate(animate);
其中第一行的就是mNotificationStackScroller就是:id/notification_stack_scroller,也就是我们看到那个通知列表.它调用的是用来设置内部padding的.
第2行代码会转调mNotificationStackScroller的updateTopPadding(),
NotificationStackScroller.java
public void updateTopPadding(float qsHeight, int scrollY, boolean animate,
boolean ignoreIntrinsicPadding) {
Log.d(TAG,"updateTopPadding qsHeight=" + qsHeight +
" scrollY=" + scrollY + " animate=" + animate +
" mNotificationTopPadding=" + mNotificationTopPadding +
" ignoreIntrinsicPadding=" + ignoreIntrinsicPadding);
float start = qsHeight - scrollY + mNotificationTopPadding;
float stackHeight = getHeight() - start;
int minStackHeight = getMinStackHeight();
if (stackHeight <= minStackHeight) {
float overflow = minStackHeight - stackHeight;
stackHeight = minStackHeight;
start = getHeight() - stackHeight;
mTopPaddingOverflow = overflow;
} else {
mTopPaddingOverflow = 0;
}
Log.d(TAG,"updateTopPadding start=" + start + " clampPadding((int) start=" +clampPadding((int) start) +
" mLastSetStackHeight=" + mLastSetStackHeight);
setTopPadding(ignoreIntrinsicPadding ? (int) start : clampPadding((int) start),
animate);
setStackHeight(mLastSetStackHeight);
}
updateTopPadding()其实关键在后面2行代码,一行是设置顶部的padding距离,一行是设置stack height,这个stack height值会影响通知实际显示的时候高度.它会调用setTranslationY来改变自己的位置.
继续回到NotificationPanelView的onLayout方法,根据log可以知道接着会执行35行的setQsExpansion(mQsMinExpansionHeight+ mLastOverscroll),
这个方法需要我们关注的就是它调用requestScrollerTopPaddingUpdate(false /* animate */),其实这个方法和之前positionClockAndNotifications里面调用的是一样的.
继续往onLayout方法下一步看就是它的37行调用mNotificationStackScroller.setStackHeight(getExpandedHeight()).也就是NotificationStackScroller里面执行过的setStackHeight().
总结:锁屏页面的通知之所以默认显示在中部主要是和2个方法有关,分别是setTopPadding和setStackHeight,其中根据log 来看,传给setTopPading的参数是503,传给setStackHeight的值是1151.
后面如果想要锁屏页面的通知不显示,并下来状态栏的时候随之下滑,是需要调用上面的这2个方法,
锁屏页面的通知的显示的数量会和解锁之后的不一样,具体流程是在BaseStatusBar.java的updateRowStates()里面.
锁屏的通知显示偏小,这个通过StackScrollAlgorithm 的DIMMED_SCALE来决定的.可以在updateDimmedActivatedHideSensitive()方法里面看到相关的代码
这里只是记录一下大致的流程,以便后续整理
从BaseStatusBar的visibilityChanged()开始,具体时序图如下.
锁屏有一个Service来通过AIDL 来和系统交互的.
这个Service就是KeyguardService.KeyguardService 的创建是在PhoneWindowManager的systemReady() 方法中,启动是在systemBooted()中.
PhoneWindowManager.java
public void systemReady() {
mKeyguardDelegate = new KeyguardServiceDelegate(mContext);
mKeyguardDelegate.onSystemReady();
.....
}
上面实际看到的是KeyguardServiceDelegate ,其实KeyguardServiceDelegate 内部有个变量是KeyguardServiceWrapper对象,这个KeyguardServiceWrapper 实现了IKeyguardService这个AIDL接口.
而KeyguardService 的onBind()返回的就是IKeyguardService.Stub() 对象.
@Override
public void systemBooted() {
boolean bindKeyguardNow = false;
synchronized (mLock) {
// Time to bind Keyguard; take care to only bind it once, either here if ready or
// in systemReady if not.
if (mKeyguardDelegate != null) {
bindKeyguardNow = true;
} else {
// Because mKeyguardDelegate is null, we know that the synchronized block in
// systemReady didn't run yet and setting this will actually have an effect.
mDeferBindKeyguard = true;
}
}
if (bindKeyguardNow) {
mKeyguardDelegate.bindService(mContext);
mKeyguardDelegate.onBootCompleted();
}
synchronized (mLock) {
mSystemBooted = true;
}
startedWakingUp();
screenTurningOn(null);
}
通过mKeyguardDelegate.bindService(mContext)以bind的方式来连接KeyguardService
KeyguardService.java 的onCreate()
@Override
public void onCreate() {
((SystemUIApplication) getApplication()).startServicesIfNeeded();
mKeyguardViewMediator =
((SystemUIApplication) getApplication()).getComponent(KeyguardViewMediator.class);
}
startServicesIfNeeded定义在SystemUIApplication 里面,是初始化SystemUI各个模块的入口,这个方法再前面1.3节其实有提到.就是启动各个实现了SystemUI这个抽象类的各个模块,其中KeyguardViewMediator也在其中,也实现这个抽象类.在后面就是通过KeyguardViewMediator的handleShow来调用StatusBarKeyguardViewManager 的reset()来决定是否显示锁屏了.
锁屏就这样和系统服务PhoneWindowManager绑定了,系统可以控制锁屏的显示.在来电和其他应用窗口需要隐藏系统锁屏时,就可以进行响应了.
以下时序图是系统启动时锁屏的启动流程.
上面有调用StatusBarKeyguardViewManager的hide() 有转调KeyguardBouncer的hide(boolean destroyView),这个destroyView参数就是决定是真正的删除这个视图还是仅仅设置为invisible
public void hide(boolean destroyView) {
cancelShowRunnable();
if (mKeyguardView != null) {
mKeyguardView.setOnDismissAction(null);
mKeyguardView.cleanUp();
}
if (destroyView) {
if (DEBUG) Log.d(TAG, "call removeView()") ;
removeView();
} else if (mRoot != null) {
if (DEBUG) Log.d(TAG, "just set keyguard Invisible.") ;
mRoot.setVisibility(View.INVISIBLE);
}
}
KeyguardBouncer的(boolean destroyView)参数true 的情况:
KeyguardViweMediator.keyguardDone() -> StatusBarKeyguardViewManager的hide()
KeyguardBouncer的(boolean destroyView)参数为false的情况:
StatusBarKeyguardViewManager的 reset()
StatusBarKeyguardViewManager的 showBouncerOrKeyguard()
这里我们就Notifiter.java 的handleWakefulnessChange()开始分析.
亮屏时:mPolicy.wakingUp();
熄屏时:mPolicy.goingToSleep(why);
其实这里最终是调用的PhoneWindowManager.java 中的对应方法.
frameworks/base/services/core/java/com/android/server/power/Notifier.java
private void handleWakefulnessChange(final int wakefulness, boolean interactive,
final int reason) {
// Tell the activity manager about changes in wakefulness, not just interactivity.
// It needs more granularity than other components.
mHandler.post(new Runnable() {
@Override
public void run() {
mActivityManagerInternal.onWakefulnessChanged(wakefulness);
}
});
// Handle changes in the overall interactive state.
boolean interactiveChanged = false;
synchronized (mLock) {
// Broadcast interactive state changes.
if (interactive) {
// Waking up...
interactiveChanged = (mActualInteractiveState != INTERACTIVE_STATE_AWAKE);
if (interactiveChanged) {
mActualInteractiveState = INTERACTIVE_STATE_AWAKE;
mPendingWakeUpBroadcast = true;
mHandler.post(new Runnable() {
@Override
public void run() {
EventLog.writeEvent(EventLogTags.POWER_SCREEN_STATE, 1, 0, 0, 0);
if (DEBUG) {
Slog.d(TAG, "onInteractiveChangeStarted: mPolicy.wakingUp()");
}
mPolicy.wakingUp();//亮屏
}
});
updatePendingBroadcastLocked();
}
} else {
// Going to sleep...
// This is a good time to make transitions that we don't want the user to see,
// such as bringing the key guard to focus. There's no guarantee for this,
// however because the user could turn the device on again at any time.
// Some things may need to be protected by other mechanisms that defer screen on.
interactiveChanged = (mActualInteractiveState != INTERACTIVE_STATE_ASLEEP);
if (interactiveChanged) {
mActualInteractiveState = INTERACTIVE_STATE_ASLEEP;
mPendingGoToSleepBroadcast = true;
if (mUserActivityPending) {
mUserActivityPending = false;
mHandler.removeMessages(MSG_USER_ACTIVITY);
}
mHandler.post(new Runnable() {
@Override
public void run() {
int why = WindowManagerPolicy.OFF_BECAUSE_OF_USER;
switch (reason) {
case PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN:
why = WindowManagerPolicy.OFF_BECAUSE_OF_ADMIN;