前言

上一篇我们具体分析了状态栏上状态图标,例如 wifi、蓝牙等图标的控制流程,本篇文章我们继续来分析下状态栏上通知图标的控制流程。主要分析当一个新通知来临时,新通知的图标是如何一步步显示到状态栏上的。

一、通知图标控制器

1、通过上一篇系统状态栏图标控制可知,状态栏图标是由一个叫StatusBarIconController接口控制显示的,而通知图标区域也有一个控制器,叫NotificationIconAreaController(它不是一个接口)。

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java

public class NotificationIconAreaController implements DarkReceiver {...public NotificationIconAreaController(Context context, StatusBar statusBar) {mStatusBar = statusBar;mNotificationColorUtil = NotificationColorUtil.getInstance(context);//通知栏颜色工具mContext = context;mEntryManager = Dependency.get(NotificationEntryManager.class);initializeNotificationAreaViews(context);//初始化通知栏区域视图}...
}

2、在NotificationIconAreaController的构造函数中会调用如下方法来创建通知图标的容器

public class NotificationIconAreaController implements DarkReceiver {.../*** 初始化化通知图标区域视图*/protected void initializeNotificationAreaViews(Context context) {reloadDimens(context);LayoutInflater layoutInflater = LayoutInflater.from(context);// 实例化通知图标区域视图mNotificationIconArea = inflateIconArea(layoutInflater);// mNotificationIcons才是真正存放通知图标的父容器mNotificationIcons = (NotificationIconContainer) mNotificationIconArea.findViewById(R.id.notificationIcons);mNotificationScrollLayout = mStatusBar.getNotificationScrollLayout();}...protected View inflateIconArea(LayoutInflater inflater) {// 实例化通知图标区域视图return inflater.inflate(R.layout.notification_icon_area, null);}...
}

3、在NotificationIconAreaController的inflateIconArea方法中会加载了R.layout.notification_icon_are.xml布局,来看下这个布局:

frameworks/base/packages/SystemUI/res/layout/notification_icon_area.xml

<com.android.keyguard.AlphaOptimizedLinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/notification_icon_area_inner"android:layout_width="match_parent"android:layout_height="match_parent"android:clipChildren="false"><com.android.systemui.statusbar.phone.NotificationIconContainerandroid: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:clipChildren="false"/>
</com.android.keyguard.AlphaOptimizedLinearLayout>

id为notificationIcons的控件就是状态栏通知图标容器,对应于上面代码的mNotificationIcons变量。

二、初始化通知图标区域

1、既然是NotificationIconAreaController自己创建了通知图标容器,那么通知图标是如何被添加到状态栏视图中的呢?这个过程主要是在StatusBar的makeStatusBarView方法中实现的,关键代码如下所示:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java

    protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) {...// 创建状态栏视图FragmentHostManager.get(mStatusBarWindow).addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {CollapsedStatusBarFragment statusBarFragment = (CollapsedStatusBarFragment) fragment;// 初始化了通知图标区域statusBarFragment.initNotificationIconArea(mNotificationIconAreaController);...}).getFragmentManager().beginTransaction()// CollapsedStatusBarFragment实现了状态栏的添加.replace(R.id.status_bar_container, new CollapsedStatusBarFragment(), CollapsedStatusBarFragment.TAG).commit();     ...         }

2、CollapsedStatusBarFragment的initNotificationIconArea方法如下所示:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java

@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,Bundle savedInstanceState) {//CollapsedStatusBarFragment加载布局文件return inflater.inflate(R.layout.status_bar, container, false);}@Overridepublic void onViewCreated(View view, @Nullable Bundle savedInstanceState) {super.onViewCreated(view, savedInstanceState);mStatusBar = (PhoneStatusBarView) view;}// 初始化通知图标区域public void initNotificationIconArea(NotificationIconAreaControllernotificationIconAreaController) {// 通知图标区域ViewGroup notificationIconArea = mStatusBar.findViewById(R.id.notification_icon_area);// 这个才是通知图标的父容器mNotificationIconAreaInner = notificationIconAreaController.getNotificationInnerAreaView();if (mNotificationIconAreaInner.getParent() != null) {((ViewGroup) mNotificationIconAreaInner.getParent()).removeView(mNotificationIconAreaInner);}// 把通知图标父容器添加到通知图标区域里notificationIconArea.addView(mNotificationIconAreaInner);// 这里其实显示了通知图标区域和中心图标区域showNotificationIconArea(false);}

上面代码的主要目的就是将通知图标控制器所构建的视图mNotificationIconAreaInner添加到布局status_bar.xml中id等于notification_icon_area的容器中,notification_icon_area容器在status_bar.xml布局中的位置如下所示:

frameworks/base/packages/SystemUI/res/layout/status_bar.xml

<?xml version="1.0" encoding="utf-8"?>
<com.android.systemui.statusbar.phone.PhoneStatusBarViewxmlns:android="http://schemas.android.com/apk/res/android"xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"android:layout_width="match_parent"android:layout_height="@dimen/status_bar_height"android:id="@+id/status_bar"android:background="@drawable/system_bar_background"android:orientation="vertical"android:focusable="false"android:descendantFocusability="afterDescendants"android:accessibilityPaneTitle="@string/status_bar">...<LinearLayout android:id="@+id/status_bar_contents"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingStart="@dimen/status_bar_padding_start"android:paddingEnd="@dimen/status_bar_padding_end"android:orientation="horizontal">...<FrameLayoutandroid:layout_height="match_parent"android:layout_width="0dp"android:layout_weight="1">...  <LinearLayoutandroid:id="@+id/status_bar_left_side"android:layout_height="match_parent"android:layout_width="match_parent"android:clipChildren="false">...<com.android.systemui.statusbar.AlphaOptimizedFrameLayoutandroid:id="@+id/notification_icon_area"android:layout_width="0dp"android:layout_height="match_parent"android:layout_weight="1"android:orientation="horizontal"android:clipChildren="false"/></LinearLayout></FrameLayout></LinearLayout></com.android.systemui.statusbar.phone.PhoneStatusBarView>

三、监听通知的服务端

1、当一条新通知发送后,它会存储到通知服务端,也就是NotificationManagerService,那SystemUI是如何知道新通知来临的?这就需要SystemUI向NotificationManagerService注册一个"服务"(一个Binder)。
这个"服务"就相当于客户端SystemUI在服务端NotificationManagerService注册的一个回调。当有通知来临的时候,就会通过这个"服务"通知SystemUI。这个注册是在StatusBar#start()中完成的:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java

public class StatusBar extends SystemUI implements DemoMode,DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,OnHeadsUpChangedListener, CommandQueue.Callbacks, ZenModeController.Callback,ColorExtractor.OnColorsChangedListener, ConfigurationListener, NotificationPresenter {...protected NotificationListener mNotificationListener;protected NotificationEntryManager mEntryManager;...public void start() {...// 向通知服务端注册一个"服务",用于接收通知信息的回调mNotificationListener =  Dependency.get(NotificationListener.class);mNotificationListener.setUpWithPresenter(this, mEntryManager);...        }...
}

2、继续来看NotificationListener的setUpWithPresenter方法,该方法会调用父类的NotificationListenerWithPlugins的registerAsSystemService方法:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener .java

  public class NotificationListener extends NotificationListenerWithPlugins {...    public void setUpWithPresenter(NotificationPresenter presenter, NotificationEntryManager entryManager) {mPresenter = presenter;mEntryManager = entryManager;try {registerAsSystemService(mContext, new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()), UserHandle.USER_ALL);} catch (RemoteException e) {Log.e(TAG, "Unable to register notification listener", e);}}//连接成功@Overridepublic void onListenerConnected() {...}//收到新的通知@Overridepublic void onNotificationPosted(final StatusBarNotification sbn,final RankingMap rankingMap) {...}//移除通知@Overridepublic void onNotificationRemoved(StatusBarNotification sbn,final RankingMap rankingMap) {...}//更新通知@Overridepublic void onNotificationRankingUpdate(final RankingMap rankingMap) {...}}

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationListenerWithPlugins.java

public class NotificationListenerWithPlugins extends NotificationListenerService implementsPluginListener<NotificationListenerController> {...@Overridepublic void registerAsSystemService(Context context, ComponentName componentName,int currentUser) throws RemoteException {super.registerAsSystemService(context, componentName, currentUser);//获取PluginManager的具体实现类PluginManagerImpl,并将自身添加到PluginManagerImpl实例对象中Dependency.get(PluginManager.class).addPluginListener(this, NotificationListenerController.class);}...
}

四、服务端接收到新的通知消息

1、当一条新的通知来临的时候,会触发NotificationListener的onNotificationPosted方法,该方法会调用状态栏条目管理者的addNotification方法,关键代码如下:

  public class NotificationListener extends NotificationListenerWithPlugins {...protected NotificationEntryManager mEntryManager;//状态栏条目管理者@Overridepublic void onNotificationPosted(final StatusBarNotification sbn,final RankingMap rankingMap) {if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {// 在主线程中进行更新mPresenter.getHandler().post(() -> {processForRemoteInput(sbn.getNotification(), mContext);String key = sbn.getKey();mEntryManager.removeKeyKeptForRemoteInput(key);//我们第一次创建通知的时候getNotificationData返回的一定是null,因为还没有放进去,所以isUpdate = false,走添加通知的流程boolean isUpdate = mEntryManager.getNotificationData().get(key) != null;...//判断到来的通知是需要更新还是添加                if (isUpdate) {// 更新通知操作mEntryManager.updateNotification(sbn, rankingMap);} else {// 添加新通知操作mEntryManager.addNotification(sbn, rankingMap);}});}}...
}

2、NotificationEntryManager的addNotification方法又进一步调用addNotificationInternal方法

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...@Overridepublic void addNotification(StatusBarNotification notification,NotificationListenerService.RankingMap ranking) {...addNotificationInternal(notification, ranking);...}...private void addNotificationInternal(StatusBarNotification notification,NotificationListenerService.RankingMap ranking) throws InflationException {//获取唯一标识key,key打印出来是这样的:0|com.example.app3|110|null|10161String key = notification.getKey();mNotificationData.updateRanking(ranking);//创建Entry实例对象NotificationData.Entry shadeEntry = createNotificationViews(notification);...}
}

3、addNotificationInternal方法进一步调用createNotificationViews方法,该方法会继续调用inflateViews来构建视图对象

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)throws InflationException {NotificationData.Entry entry = new NotificationData.Entry(sbn);Dependency.get(LeakDetector.class).trackInstance(entry);entry.createIcons(mContext, sbn);//构建视图inflateViews(entry, mListContainer.getViewParentForNotification(entry));return entry;}...
}

4、inflateViews方法会创建RowInflaterTask对象并调用inflate方法:

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...private void inflateViews(NotificationData.Entry entry, ViewGroup parent) {PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,entry.notification.getUser().getIdentifier());final StatusBarNotification sbn = entry.notification;if (entry.row != null) {entry.reset();updateNotification(entry, pmUser, sbn, entry.row);} else {//创建构建视图的任务对象new RowInflaterTask().inflate(mContext, parent, entry,// 加载完成的回调,这里的加载指的仅仅是一个空视图row -> {// 绑定监听事件和回调bindRow(entry, pmUser, sbn, row);// 在视图上更新通知信息                        updateNotification(entry, pmUser, sbn, row);});}}
}

5、RowInflaterTask的inflate方法会创建异步构建视图任务对象AsyncLayoutInflater并调用他的inflate方法:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/RowInflaterTask.java

public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInflateFinishedListener {/*** 构建一个新的通知栏视图*/public void inflate(Context context, ViewGroup parent, NotificationData.Entry entry,RowInflationFinishedListener listener) {...//使用异步方式加载视图的InflaterAsyncLayoutInflater inflater = new AsyncLayoutInflater(context);...inflater.inflate(R.layout.status_bar_notification_row, parent, this);}}

6、AsyncLayoutInflater对象调用inflate将status_bar_notification_row.xml布局文件转化为视图对象,该布局文件的具体内容如下所示:

frameworks/base/packages/SystemUI/res/layout/status_bar_notification_row.xml

<?xml version="1.0" encoding="utf-8"?>
<com.android.systemui.statusbar.ExpandableNotificationRowxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"android:focusable="true"android:clickable="true"><com.android.systemui.statusbar.NotificationBackgroundViewandroid:id="@+id/backgroundNormal"android:layout_width="match_parent"android:layout_height="match_parent" /><com.android.systemui.statusbar.NotificationBackgroundViewandroid:id="@+id/backgroundDimmed"android:layout_width="match_parent"android:layout_height="match_parent" /><com.android.systemui.statusbar.NotificationContentView android:id="@+id/expanded"android:layout_width="match_parent"android:layout_height="wrap_content" /><com.android.systemui.statusbar.NotificationContentView android:id="@+id/expandedPublic"android:layout_width="match_parent"android:layout_height="wrap_content" /><Buttonandroid:id="@+id/veto"android:layout_width="48dp"android:layout_height="0dp"android:gravity="end"android:layout_marginEnd="-80dp"android:background="@null"android:paddingEnd="8dp"android:paddingStart="8dp"/><ViewStubandroid:layout="@layout/notification_children_container"android:id="@+id/child_container_stub"android:inflatedId="@+id/notification_children_container"android:layout_width="match_parent"android:layout_height="wrap_content"/><ViewStubandroid:layout="@layout/notification_guts"android:id="@+id/notification_guts_stub"android:inflatedId="@+id/notification_guts"android:layout_width="match_parent"android:layout_height="wrap_content"/><com.android.systemui.statusbar.notification.FakeShadowViewandroid:id="@+id/fake_shadow"android:layout_width="match_parent"android:layout_height="match_parent" />
</com.android.systemui.statusbar.ExpandableNotificationRow>

7、从视图树可以看到这整个布局文件status_bar_notification_row.xml就是一条通知ExpandableNotificationRow:

8、AsyncLayoutInflater是采用异步方式来加载布局文件的,待布局文件加载完之后会调用之前在RowInflaterTask中设置的onInflateFinished回调方法:

frameworks/support/asynclayoutinflater/src/main/java/androidx/asynclayoutinflater/view/AsyncLayoutInflater.java

public final class AsyncLayoutInflater {...private Callback mHandlerCallback = new Callback() {@Overridepublic boolean handleMessage(Message msg) {InflateRequest request = (InflateRequest) msg.obj;if (request.view == null) {request.view = mInflater.inflate(request.resid, request.parent, false);}//布局加载完毕,调用之前在RowInflaterTask中设置的回调方法request.callback.onInflateFinished(request.view, request.resid, request.parent);mInflateThread.releaseRequest(request);return true;}};...
}

9、RowInflaterTask的onInflateFinished方法会继续回调之前在NotificationEntryManager的inflateViews方法中使用lamb表达式设置的回调方法:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...private void inflateViews(NotificationData.Entry entry, ViewGroup parent) {PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,entry.notification.getUser().getIdentifier());...new RowInflaterTask().inflate(mContext, parent, entry,//回调方法row -> {//填充数据,绑定监听事件和回调bindRow(entry, pmUser, sbn, row);// 在视图上更新通知信息                        updateNotification(entry, pmUser, sbn, row);});...}...
}

10、bindRow方法会填充ExpandableNotificationRow的数据,并且再将ExpandableNotificationRow数据绑定到StatusBar:

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...//填充数据并绑定到StatusBarNotificationPresenter中private void bindRow(NotificationData.Entry entry, PackageManager pmUser,StatusBarNotification sbn, ExpandableNotificationRow row) {row.setExpansionLogger(this, entry.notification.getKey());row.setGroupManager(mGroupManager);row.setHeadsUpManager(mHeadsUpManager);row.setOnExpandClickListener(mPresenter);row.setInflationCallback(this);//这个回调很重要row.setLongPressListener(getNotificationLongClicker());mListContainer.bindRow(row);mRemoteInputManager.bindRow(row);final String pkg = sbn.getPackageName();String appname = pkg;try {final ApplicationInfo info = pmUser.getApplicationInfo(pkg,PackageManager.MATCH_UNINSTALLED_PACKAGES| PackageManager.MATCH_DISABLED_COMPONENTS);if (info != null) {appname = String.valueOf(pmUser.getApplicationLabel(info));}} catch (PackageManager.NameNotFoundException e) {// Do nothing}row.setAppName(appname);row.setOnDismissRunnable(() ->performRemoveNotification(row.getStatusBarNotification()));row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);if (ENABLE_REMOTE_INPUT) {row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);}row.setAppOpsOnClickListener(mOnAppOpsClickListener);//StatusBar实现了mCallbackmCallback.onBindRow(entry, pmUser, sbn, row);}...
}

11、updateNotification方法来更新ExpandableNotificationRow的视图:

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...protected void updateNotification(NotificationData.Entry entry, PackageManager pmUser,StatusBarNotification sbn, ExpandableNotificationRow row) {row.setNeedsRedaction(mLockscreenUserManager.needsRedaction(entry));boolean isLowPriority = mNotificationData.isAmbient(sbn.getKey());boolean isUpdate = mNotificationData.get(entry.key) != null;boolean wasLowPriority = row.isLowPriority();row.setIsLowPriority(isLowPriority);row.setLowPriorityStateUpdated(isUpdate && (wasLowPriority != isLowPriority));// bind the click event to the content areamNotificationClicker.register(row, sbn);// Extract target SDK version.try {ApplicationInfo info = pmUser.getApplicationInfo(sbn.getPackageName(), 0);entry.targetSdk = info.targetSdkVersion;} catch (PackageManager.NameNotFoundException ex) {Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex);}row.setLegacy(entry.targetSdk >= Build.VERSION_CODES.GINGERBREAD&& entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);entry.setIconTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP);entry.autoRedacted = entry.notification.getNotification().publicVersion == null;entry.row = row;entry.row.setOnActivatedListener(mPresenter);boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(sbn,mNotificationData.getImportance(sbn.getKey()));boolean useIncreasedHeadsUp = useIncreasedCollapsedHeight&& !mPresenter.isPresenterFullyCollapsed();row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);row.updateNotification(entry);//更新通知视图}...
}

12 ExpandableNotificationRow的updateNotification方法如下所示:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java

public class ExpandableNotificationRow extends ActivatableNotificationViewimplements PluginListener<NotificationMenuRowPlugin> {...private final NotificationInflater mNotificationInflater;   ...public void updateNotification(NotificationData.Entry entry) {mEntry = entry;mStatusBarNotification = entry.notification;//调用NotificationInflater的inflateNotificationViews方法mNotificationInflater.inflateNotificationViews();cacheIsSystemNotification();}...
}

13、继续来看下NotificationInflater的inflateNotificationViews方法:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java

public class NotificationInflater {...public void inflateNotificationViews() {inflateNotificationViews(FLAG_REINFLATE_ALL);}void inflateNotificationViews(int reInflateFlags) {//如果通知已经被删除,直接返回if (mRow.isRemoved()) {// We don't want to reinflate anything for removed notifications. Otherwise views might// be readded to the stack, leading to leaks. This may happen with low-priority groups// where the removal of already removed children can lead to a reinflation.return;}StatusBarNotification sbn = mRow.getEntry().notification;AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mRow,mIsLowPriority,mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,mCallback, mRemoteViewClickHandler);if (mCallback != null && mCallback.doInflateSynchronous()) {task.onPostExecute(task.doInBackground());} else {task.execute();}}...
}

14、通过上面的代码可以知道,通知视图是通过异步进行加载的,来看这个AsyncInflationTask,首先看下doInBackground方法:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java

public class NotificationInflater {...public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>implements InflationCallback, InflationTask {...@Overrideprotected InflationProgress doInBackground(Void... params) {try {final Notification.Builder recoveredBuilder= Notification.Builder.recoverBuilder(mContext,mSbn.getNotification());Context packageContext = mSbn.getPackageContext(mContext);Notification notification = mSbn.getNotification();//如果是多媒体类型的通知if (notification.isMediaNotification()) {MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,packageContext);processor.processNotification(notification, recoveredBuilder);}//构造通知Viewreturn createRemoteViews(mReInflateFlags,recoveredBuilder, mIsLowPriority, mIsChildInGroup,mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,packageContext);} catch (Exception e) {mError = e;return null;}}...//InflationProgress是NotificationContentInflater的静态内部类,用于构造通知View  static class InflationProgress {//RemoteViews类型的视图主要用于跨进程显示视图private RemoteViews newContentView;private RemoteViews newHeadsUpView;private RemoteViews newExpandedView;private RemoteViews newAmbientView;private RemoteViews newPublicView;@VisibleForTestingContext packageContext;private View inflatedContentView;private View inflatedHeadsUpView;private View inflatedExpandedView;private View inflatedAmbientView;private View inflatedPublicView;private CharSequence headsUpStatusBarText;private CharSequence headsUpStatusBarTextPublic;}  ...        }
}

15、doInBackground方法会继续调用createRemoteViews方法,此方法是构造状态栏通知View的核心方法,会返回通知布局文:

public class NotificationInflater {...private static InflationProgress createRemoteViews(int reInflateFlags,Notification.Builder builder, boolean isLowPriority, boolean isChildInGroup,boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,Context packageContext) {InflationProgress result = new InflationProgress();isLowPriority = isLowPriority && !isChildInGroup;if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {//默认布局 result.newContentView = createContentView(builder, isLowPriority, usesIncreasedHeight);}if ((reInflateFlags & FLAG_REINFLATE_EXPANDED_VIEW) != 0) {result.newExpandedView = createExpandedView(builder, isLowPriority);}if ((reInflateFlags & FLAG_REINFLATE_HEADS_UP_VIEW) != 0) {result.newHeadsUpView = builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight);}if ((reInflateFlags & FLAG_REINFLATE_PUBLIC_VIEW) != 0) {result.newPublicView = builder.makePublicContentView();}if ((reInflateFlags & FLAG_REINFLATE_AMBIENT_VIEW) != 0) {result.newAmbientView = redactAmbient ? builder.makePublicAmbientNotification(): builder.makeAmbientNotification();}result.packageContext = packageContext;result.headsUpStatusBarText = builder.getHeadsUpStatusBarText(false /* showingPublic */);result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText(true /* showingPublic */);return result;}...
}

16、通过上面的代码可以看出,系统会根据不同的flag(不同通知类型)来加载不同的通知布局,我们这里以最简单的默认布局(createContentView)来分析,可以看到直接通过Notification.Builder来创建通知View:

public class NotificationInflater {...private static RemoteViews createContentView(Notification.Builder builder,boolean isLowPriority, boolean useLarge) {if (isLowPriority) {return builder.makeLowPriorityContentView(false /* useRegularSubtext */);}return builder.createContentView(useLarge);}...
}

17、继续来看Notification#Builder的createContentView方法:

frameworks/base/core/java/android/app/Notification.java

public class Notification implements Parcelable
{...//构造最终的通知UI布局public RemoteViews createContentView() {return createContentView(false);}public RemoteViews createContentView(boolean increasedHeight) {//mN.contentView其实就是我们需要自定义通知View时调用setContent方法设置RemoteView,如果没有设置就加载系统默认布局if (mN.contentView != null && useExistingRemoteView()) {return mN.contentView;} else if (mStyle != null) {final RemoteViews styleView = mStyle.makeContentView(increasedHeight);if (styleView != null) {return styleView;}}//加载标准的默认模板return applyStandardTemplate(getBaseLayoutResource(), null /* result */);}//获取布局文件,默认加载布局就是R.layout.notification_template_material_baseprivate int getBaseLayoutResource() {return R.layout.notification_template_material_base;}...}

18、applyStandardTemplate方法会给布局设置寬高颜色字体等属性:

public class Notification implements Parcelable
{...private RemoteViews applyStandardTemplate(int resId, TemplateBindResult result) {return applyStandardTemplate(resId, mParams.reset().fillTextsFrom(this), result);}private RemoteViews applyStandardTemplate(int resId, boolean hasProgress,TemplateBindResult result) {return applyStandardTemplate(resId, mParams.reset().hasProgress(hasProgress).fillTextsFrom(this), result);}private RemoteViews applyStandardTemplate(int resId, StandardTemplateParams p,TemplateBindResult result) {RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId);resetStandardTemplate(contentView);final Bundle ex = mN.extras;updateBackgroundColor(contentView);bindNotificationHeader(contentView, p.ambient, p.headerTextSecondary);bindLargeIconAndReply(contentView, p, result);boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex);if (p.title != null) {contentView.setViewVisibility(R.id.title, View.VISIBLE);contentView.setTextViewText(R.id.title, processTextSpans(p.title));if (!p.ambient) {setTextViewColorPrimary(contentView, R.id.title);}contentView.setViewLayoutWidth(R.id.title, showProgress? ViewGroup.LayoutParams.WRAP_CONTENT: ViewGroup.LayoutParams.MATCH_PARENT);}if (p.text != null) {int textId = showProgress ? com.android.internal.R.id.text_line_1: com.android.internal.R.id.text;contentView.setTextViewText(textId, processTextSpans(p.text));if (!p.ambient) {setTextViewColorSecondary(contentView, textId);}contentView.setViewVisibility(textId, View.VISIBLE);}setContentMinHeight(contentView, showProgress || mN.hasLargeIcon());return contentView;}...
}

19、经过15-18步的分析,我们已经知道了通知栏视图的创建流程,现在让我们重新回到第14步,在AsyncInflationTask的doInBackground方法执行完毕,接着就会执行onPostExecute方法:

public class NotificationInflater {...public static class AsyncInflationTask extends AsyncTask<Void, Void, InflationProgress>implements InflationCallback, InflationTask {...@Overrideprotected InflationProgress doInBackground(Void... params) {...}@Overrideprotected void onPostExecute(InflationProgress result) {if (mError == null) {mCancellationSignal = apply(result, mReInflateFlags, mRow, mRedactAmbient,mRemoteViewClickHandler, this);} else {handleError(mError);}}...}
}

20、AsyncInflationTask的onPostExecute方法会进一步调用NotificationInflater的apply方法,该方法会根据不同的通知类型做不同的事,这里选一个最普通的类型FLAG_REINFLATE_CONTENT_VIEW来看一下这个方法:

public class NotificationInflater {...public static CancellationSignal apply(InflationProgress result, int reInflateFlags,ExpandableNotificationRow row, boolean redactAmbient,RemoteViews.OnClickHandler remoteViewClickHandler,@Nullable InflationCallback callback) {NotificationData.Entry entry = row.getEntry();NotificationContentView privateLayout = row.getPrivateLayout();//获取id为R.id.expanded的视图ViewNotificationContentView publicLayout = row.getPublicLayout();//获取id为R.id.expandedPublic的视图Viewfinal HashMap<Integer, CancellationSignal> runningInflations = new HashMap<>();int flag = FLAG_REINFLATE_CONTENT_VIEW;if ((reInflateFlags & flag) != 0) {//通知栏默认布局//是否是新增加的Viewboolean isNewView = !canReapplyRemoteView(result.newContentView, entry.cachedContentView);//创建回调对象ApplyCallback applyCallback = new ApplyCallback() {@Overridepublic void setResultView(View v) {result.inflatedContentView = v;}@Overridepublic RemoteViews getRemoteView() {return result.newContentView;}};applyRemoteView(result, reInflateFlags, flag, row, redactAmbient,isNewView, remoteViewClickHandler, callback, entry, privateLayout,privateLayout.getContractedChild(), privateLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED),runningInflations, applyCallback);}  ...}...
}

21、继续看applyRemoteView方法:

public class NotificationInflater {...static void applyRemoteView(final InflationProgress result,final int reInflateFlags, int inflationId,final ExpandableNotificationRow row,final boolean redactAmbient, boolean isNewView,RemoteViews.OnClickHandler remoteViewClickHandler,@Nullable final InflationCallback callback, NotificationData.Entry entry,NotificationContentView parentLayout, View existingView,NotificationViewWrapper existingWrapper,final HashMap<Integer, CancellationSignal> runningInflations,ApplyCallback applyCallback) {RemoteViews newContentView = applyCallback.getRemoteView();if (callback != null && callback.doInflateSynchronous()) {try {//新增通知的情况if (isNewView) {View v = newContentView.apply(result.packageContext,parentLayout,remoteViewClickHandler);v.setIsRootNamespace(true);applyCallback.setResultView(v);} else {newContentView.reapply(result.packageContext,existingView,remoteViewClickHandler);existingWrapper.onReinflated();}} ...}...}
}

22、重新回到第20步的apply方法,继续向下看,会继续调用一个关键方法finishIfDone,

public class NotificationInflater {...public static CancellationSignal apply(InflationProgress result, int reInflateFlags,ExpandableNotificationRow row, boolean redactAmbient,RemoteViews.OnClickHandler remoteViewClickHandler,@Nullable InflationCallback callback) {...finishIfDone(result, reInflateFlags, runningInflations, callback, row,redactAmbient);CancellationSignal cancellationSignal = new CancellationSignal();cancellationSignal.setOnCancelListener(() -> runningInflations.values().forEach(CancellationSignal::cancel));return cancellationSignal;}...
}

23、finishIfDone方法中比较关键的代码点在于,会通过调用privateLayout的setContractedChild方法将remouteViews添加到privateLayout视图中:

public class NotificationInflater {...private static boolean finishIfDone(InflationProgress result, int reInflateFlags,HashMap<Integer, CancellationSignal> runningInflations,@Nullable InflationCallback endListener, ExpandableNotificationRow row,boolean redactAmbient) {Assert.isMainThread();NotificationData.Entry entry = row.getEntry();NotificationContentView privateLayout = row.getPrivateLayout();NotificationContentView publicLayout = row.getPublicLayout();if (runningInflations.isEmpty()) {if ((reInflateFlags & FLAG_REINFLATE_CONTENT_VIEW) != 0) {if (result.inflatedContentView != null) {//result.inflatedContentView就是RemoteViews//这里将result.inflatedContentView添加到privateLayout(R.id.expanded)privateLayout.setContractedChild(result.inflatedContentView);}entry.cachedContentView = result.newContentView;}...if (endListener != null) {//RemoteView加载到NotificationContentView之后会回调endListener的onAsyncInflationFinished方法endListener.onAsyncInflationFinished(row.getEntry());}return true;}return false;}...
}

23、NotificationContentView的setContractedChild方法代码如下:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java

public class NotificationContentView extends FrameLayout {...public void setContractedChild(View child) {if (mContractedChild != null) {mContractedChild.animate().cancel();removeView(mContractedChild);}addView(child);mContractedChild = child;mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,mContainingNotification);}...
}

上面23个步骤具体讲述了系统通知管理服务端,从接收到通知消息到创建对应通知消息视图View,并将RemoteView加载到NotificationContentView的过程。

五、显示新的通知图标

1、接下来我们继续来看一下NotificationContentView创建完毕之后,是如何更新到系统状态栏视图上的。
继续看上面提到过的NotificationInflater的finishIfDone方法,该方法的最后会调用endListener的onAsyncInflationFinished方法:

public class NotificationInflater {...private static boolean finishIfDone(InflationProgress result, int reInflateFlags,HashMap<Integer, CancellationSignal> runningInflations,@Nullable InflationCallback endListener, ExpandableNotificationRow row,boolean redactAmbient) {...if (endListener != null) {//RemoteView加载到NotificationContentView之后会回调endListener的onAsyncInflationFinished方法endListener.onAsyncInflationFinished(row.getEntry());}return true;}return false;}...
}
 //回调接口public interface InflationCallback {void handleInflationException(StatusBarNotification notification, Exception e);void onAsyncInflationFinished(NotificationData.Entry entry);default boolean doInflateSynchronous() {return false;}}

2、endListener的回调很关键,该回调最早是在NotificationEntryManager的bindRow方法中设置的,也因此这里会层层回调,最终回调回NotificationEntryManager的onAsyncInflationFinished方法,该方法首先会判断是否是 新的通知,如果是则调用addEntry方法,该方法继续调用addNotificationViews方法,addNotificationViews方法在添加新的通知栏视图数据之后,会继续调用updateNotifications方法,updateNotifications方法先是对通知栏视图数据进行过滤和排序,然后再用mPresenter层的updateNotificationViews方法来刷新通知栏视图:

public class NotificationEntryManager implements Dumpable, NotificationInflater.InflationCallback,ExpandableNotificationRow.ExpansionLogger, NotificationUpdateHandler,VisualStabilityManager.Callback {...//填充数据并绑定到StatusBarNotificationPresenter中private void bindRow(NotificationData.Entry entry, PackageManager pmUser,StatusBarNotification sbn, ExpandableNotificationRow row) {row.setExpansionLogger(this, entry.notification.getKey());row.setGroupManager(mGroupManager);row.setHeadsUpManager(mHeadsUpManager);row.setOnExpandClickListener(mPresenter);row.setInflationCallback(this);//这里会设置回调方法,这个回调很关键...}...//最终的回调方法@Overridepublic void onAsyncInflationFinished(NotificationData.Entry entry) {mPendingNotifications.remove(entry.key);boolean isNew = mNotificationData.get(entry.key) == null;if (isNew && !entry.row.isRemoved()) {//1如果是新的通知,调用addEntry方法addEntry(entry);} else if (!isNew && entry.row.hasLowPriorityStateUpdated()) {mVisualStabilityManager.onLowPriorityUpdated(entry);mPresenter.updateNotificationViews();}entry.row.setLowPriorityStateUpdated(false);}private void addEntry(NotificationData.Entry shadeEntry) {...//2继续调用addNotificationViews方法来增加通知栏视图addNotificationViews(shadeEntry);...}protected void addNotificationViews(NotificationData.Entry entry) {if (entry == null) {return;}//添加通知视图数据对象mNotificationData.add(entry);tagForeground(entry.notification);//3继续调用updateNotifications方法updateNotifications();}public void updateNotifications() {//对通知进行过滤和排序mNotificationData.filterAndSort();//4继续调用P层的updateNotificationViews方法刷新通知栏视图mPresenter.updateNotificationViews();}
}

3、NotificationEntryManager的mPresenter的实现者是在StatusBar,来看下状态栏对象的updateNotificationViews方法,在该方法中,首先是将通知视图添加到通知栏视图中,然后再更新通知栏视图和和视图图标:

public class StatusBar extends SystemUI implements DemoMode,DragDownHelper.DragDownCallback, ActivityStarter, OnUnlockMethodChangedListener,OnHeadsUpChangedListener, CommandQueue.Callbacks, ZenModeController.Callback,ColorExtractor.OnColorsChangedListener, ConfigurationListener, NotificationPresenter {...@Overridepublic void updateNotificationViews() {...    // 将通知视图添加到通知栏视图中mViewHierarchyManager.updateNotificationViews();updateSpeedBumpIndex();updateFooter();updateEmptyShadeView();updateQsExpansionEnabled();  // 这里不仅仅更新了通知面版的通知视图,也更新了状态栏的通知图标mNotificationIconAreaController.updateNotificationIcons();}...
}

4、将通知视图添加到通知栏视图中的具体方法在NotificationViewHierarchyManager的updateNotificationViews方法中:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java

public class NotificationViewHierarchyManager {...public void updateNotificationViews() {//拿到所有的通知条目,条目中封装了整个通知的信息ArrayList<NotificationData.Entry> activeNotifications = mEntryManager.getNotificationData().getActiveNotifications();//创建ExpandableNotificationRow,用来承载每条通知RemoteViewArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());final int N = activeNotifications.size();//遍历全部通知for (int i = 0; i < N; i++) {NotificationData.Entry ent = activeNotifications.get(i);...//经过过滤之后全部添加到toShowtoShow.add(ent.row);...}}...//移除通知子条目removeNotificationChildren();//遍历需要显示的通知,添加到NotificationStackScrollLayoutfor (int i = 0; i < toShow.size(); i++) {View v = toShow.get(i);//如果是新增通知则需要添加,addContainerViewif (v.getParent() == null) {mVisualStabilityManager.notifyViewAddition(v);//将通知条目视图添加到通知栏视图NotificationStackScrollLayout中mListContainer.addContainerView(v);}}//后面会对通知视图进行排序等操作}...
}

5、NotificationStackScrollLayout的addContainerView方法如下所示:

frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java

public class NotificationStackScrollLayout extends ViewGroupimplements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,ExpandableView.OnHeightChangedListener, NotificationGroupManager.OnGroupChangeListener,NotificationMenuRowPlugin.OnMenuEventListener, VisibilityLocationProvider,NotificationListContainer {...@Overridepublic void addContainerView(View v) {addView(v);}...
}

6、分析完通知视图添加到通知栏视图的过程,接下来我们继续来看下是如何更新通知栏和状态栏视图的,该过程主要是在NotificationIconAreaController的updateNotificationIcons方法中实现的:

public class NotificationIconAreaController implements DarkReceiver {...public void updateNotificationIcons() {//更新状态栏图标updateStatusBarIcons();//向状态栏添加通知图标updateIconsForLayout(entry -> entry.expandedIcon, mShelfIcons,NotificationShelf.SHOW_AMBIENT_ICONS, false /* hideDismissed */,false /* hideRepliedMessages */);applyNotificationIconsTint();}public void updateStatusBarIcons() {updateIconsForLayout(entry -> entry.icon, mNotificationIcons,false /* showAmbient */, true /* hideDismissed */, true /* hideRepliedMessages */);}    ...
}

7、可以发现updateIconsForLayout方法:

public class NotificationIconAreaController implements DarkReceiver {...private void updateIconsForLayout(Function<NotificationData.Entry, StatusBarIconView> function,NotificationIconContainer hostLayout, boolean showAmbient, boolean hideDismissed,boolean hideRepliedMessages) {// toShow保存即将显示的图标ArrayList<StatusBarIconView> toShow = new ArrayList<>(mNotificationScrollLayout.getChildCount());// 过滤通知,并保存需要显示的通知图标for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {// 获取一个通知视图View view = mNotificationScrollLayout.getChildAt(i);if (view instanceof ExpandableNotificationRow) {NotificationData.Entry ent = ((ExpandableNotificationRow) view).getEntry();if (shouldShowNotificationIcon(ent, showAmbient, hideDismissed,hideRepliedMessages)) {//添加图标toShow.add(function.apply(ent));}}}ArrayMap<String, ArrayList<StatusBarIcon>> replacingIcons = new ArrayMap<>();ArrayList<View> toRemove = new ArrayList<>();for (int i = 0; i < hostLayout.getChildCount(); i++) {View child = hostLayout.getChildAt(i);if (!(child instanceof StatusBarIconView)) {continue;}if (!toShow.contains(child)) {boolean iconWasReplaced = false;StatusBarIconView removedIcon = (StatusBarIconView) child;String removedGroupKey = removedIcon.getNotification().getGroupKey();for (int j = 0; j < toShow.size(); j++) {StatusBarIconView candidate = toShow.get(j);if (candidate.getSourceIcon().sameAs((removedIcon.getSourceIcon()))&& candidate.getNotification().getGroupKey().equals(removedGroupKey)) {if (!iconWasReplaced) {iconWasReplaced = true;} else {iconWasReplaced = false;break;}}}if (iconWasReplaced) {ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(removedGroupKey);if (statusBarIcons == null) {statusBarIcons = new ArrayList<>();replacingIcons.put(removedGroupKey, statusBarIcons);}statusBarIcons.add(removedIcon.getStatusBarIcon());}toRemove.add(removedIcon);}}// removing all duplicatesArrayList<String> duplicates = new ArrayList<>();for (String key : replacingIcons.keySet()) {ArrayList<StatusBarIcon> statusBarIcons = replacingIcons.get(key);if (statusBarIcons.size() != 1) {duplicates.add(key);}}replacingIcons.removeAll(duplicates);hostLayout.setReplacingIcons(replacingIcons);final int toRemoveCount = toRemove.size();for (int i = 0; i < toRemoveCount; i++) {hostLayout.removeView(toRemove.get(i));}final FrameLayout.LayoutParams params = generateIconLayoutParams();for (int i = 0; i < toShow.size(); i++) {StatusBarIconView v = toShow.get(i);// The view might still be transiently added if it was just removed and added againhostLayout.removeTransientView(v);if (v.getParent() == null) {if (hideDismissed) {v.setOnDismissListener(mUpdateStatusBarIcons);}hostLayout.addView(v, i, params);}}hostLayout.setChangingViewPositions(true);// Re-sort notification iconsfinal int childCount = hostLayout.getChildCount();for (int i = 0; i < childCount; i++) {View actual = hostLayout.getChildAt(i);StatusBarIconView expected = toShow.get(i);if (actual == expected) {continue;}hostLayout.removeView(expected);hostLayout.addView(expected, i);}hostLayout.setChangingViewPositions(false);hostLayout.setReplacingIcons(null);}...
}

到此通知已经全部的创建,加载以及添加完成。

五、总结

本篇文章大概梳理了通知布局的加载,数据填充以及添加和显示,整个流程大概就是从framework拿到StatusBarNotification,封装成NotificationEntry,之后再根据不同通知类型加载不同布局,如果没有自定义布局则加载默认布局,创建RemoteView,添加到ExpandableNotificationRow的中的id为R.id.expanded的NotificationContentView,之后再将ExpandableNotificationRow添加到NotificationStackScrollLayout,整个通知布局如下图:

Android 9.0系统源码_SystemUI(四)通知图标控制器相关推荐

  1. Android 9.0系统源码_SystemUI(一)SystemUI的启动流程

    一.SystemUI 介绍 1.初步认识SystemUI Android 的 SystemUI 其实就是 Android 的系统界面,它包括了界面上方的状态栏 status bar,下方的导航栏Nav ...

  2. Android 9.0系统源码_SystemUI(二)StatusBar系统状态栏的创建流程

    前言 上一篇我们具体分析了SystemUI的启动流程,在SystemServer的startOtherServices方法中,会启动SystemUIService服务,SystemUIService服 ...

  3. Android 9.0系统源码_SystemUI(九)PhoneWindowManager构建状态栏和导航栏视图窗口区域坐标的流程解析

    前言 NavigationBar 和 StatusBar 都属于 SystemBar,也叫做 decor,就是说给 App 装饰的意思.一般的 window 的布局是在 PhoneWindowMana ...

  4. Android 9.0系统源码_SystemUI(三)系统状态图标控制

    前言 上一篇我们具体分析了系统状态栏StatusBar的创建过程,其中状态栏视图就存储在CollapsedStatusBarFragment中,这个视图被添加到id为status_bar_contai ...

  5. Android 9.0系统源码_SystemUI(六)滑动锁屏的创建

    一.前言 前面几篇文章大致介绍了SystemUI的两个模块,StatusBar和QuickSetting,这篇文章开始分析Keyguard模块. 对于锁屏呢,需要有个基本认知,它分为两类,一是滑动锁屏 ...

  6. Android 8.0系统源码分析--Camera processCaptureResult结果回传源码分析

    相机,从上到下概览一下,真是太大了,上面的APP->Framework->CameraServer->CameraHAL,HAL进程中Pipeline.接各种算法的Node.再往下的 ...

  7. android 系统源码调试 局部变量值_如何方便快速的整编Android 9.0系统源码?

    点击上方"刘望舒",选择"星标" 多点在看,就是真爱! 作者 :  刘望舒  |  来源 :刘望舒的博客地址:http://liuwangshu.cn/fram ...

  8. Android 8.0系统源码分析--开篇

    个人分类: Android框架总结Android源码解析android framework 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sinat ...

  9. Ubuntu16.04编译Android 6.0系统源码过程简要记录总结

    一,安装VMware Workstation,百度网盘下载(内含注册机) 链接: https://pan.baidu.com/s/1wz4hdNQBikTvyUMNokSVYg 提取码: yed7 V ...

最新文章

  1. HDU-4059 The Boss on Mars 容斥定理
  2. 二十一、文本情感分类二
  3. SqlAlchemy初探
  4. 某法院HP-P4500存储数据恢复案例
  5. ScrollReveal.js – 帮助你实现超炫的元素运动效果
  6. cgdb 调试_在MacOS上使用gdb(cgdb)调试Golang程序
  7. 年回报60%!孙正义如何经营“沉迷AI”的愿景基金?
  8. matlab拼接tiff文件_ImageJ实用技巧——自动图片拼接(基本功能篇)
  9. Excel文件批量删除指定行或列
  10. c语言程序设计教学工作总结,c语言教学的工作总结.docx
  11. 河南学业水平计算机,河南高中学业水平考试查询系统
  12. 010-lissajous(二)
  13. 圆形相交,相切,相离,包含
  14. 2016 版 Laravel 系列入门教程(二)【最适合中国人的 Laravel 教程】
  15. 数据库可移植性重要吗?
  16. Latex中插入.eps图片遇到的问题 (Unknown graphics extension:.eps)
  17. mysql删除表中所有数据
  18. ”舌上有龙泉,杀人不见血,生而为人,需得择善而行”
  19. iOS7人机界面指南 – ISUX原创翻译
  20. 中文网页设计分享之《banner设计篇》

热门文章

  1. html css做一个简历表,HTML table制做我的简历
  2. 上交大计算机科学与技术,上海交通大学计算机科学与工程系(CSE)
  3. 人民银行新闻发布会:详细解读2020年上半年金融统计数据
  4. oracle重建inventory,Oracle中Inventory目录作用以及如何重建此目录-Oracle
  5. 截止频率计算公式wc_计算截止频率Wc的快速方法
  6. 黎曼和 Riemann Sum ,黎曼积分Riemann Integral,正态分布normal distribution
  7. Qt示例解析 【Callout】
  8. Spring aop报错:com.sun.proxy.$Proxyxxx cannot be cast to yyy
  9. B. Disturbed People(模拟) Codeforces Round #521 (Div. 3)
  10. 你想知道的NB-IoT知识都在这里了!