Android中的Notification機(jī)制深入理解
本文需要解決的問題
筆者最近正在做一個(gè)項(xiàng)目,里面需要用到 Android Notification 機(jī)制來實(shí)現(xiàn)某些特定需求。我正好通過這個(gè)機(jī)會(huì)研究一下 Android Notification 相關(guān)的發(fā)送邏輯和接收邏輯,以及整理相關(guān)的筆記。我研究 Notification 機(jī)制的目的是解決以下我在使用過程中所思考的問題:
- 我們創(chuàng)建的 Notification 實(shí)例最終以什么樣的方式發(fā)送給系統(tǒng)?
- 系統(tǒng)是如何接收到 Notification 實(shí)例并顯示的?
- 我們是否能攔截其他 app 的 Notification 并獲取其中的信息?
什么是 Android Notification 機(jī)制?
Notification,中文名翻譯為通知,每個(gè) app 可以自定義通知的樣式和內(nèi)容等,它會(huì)顯示在系統(tǒng)的通知欄等區(qū)域。用戶可以打開抽屜式通知欄查看通知的詳細(xì)信息。在實(shí)際生活中,Android Notification 機(jī)制有很廣泛的應(yīng)用,例如 IM app 的新消息通知,資訊 app 的新聞推送等等。
源碼分析
本文的源碼基于 Android 7.0。
Notification 的發(fā)送邏輯
一般來說,如果我們自己的 app 想發(fā)送一條新的 Notification,可以參照以下代碼:
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.notification_icon) .setWhen(System.currentTimeMillis()) .setContentTitle("Test Notification Title") .setContentText("Test Notification Content!"); Intent resultIntent = new Intent(this, ResultActivity.class); PendingIntent contentIntent = PendingIntent.getActivity( this, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT ); mBuilder.setContentIntent(resultPendingIntent); NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); // mId allows you to update the notification later on. mNotificationManager.notify(mId, mBuilder.build());
可以看到,我們通過 NotificationCompat.Builder
新建了一個(gè) Notification 對(duì)象,最后通過 NotificationManager#notify()
方法將 Notification 發(fā)送出去。
NotificationManager#notify()
public void notify(int id, Notification notification) { notify(null, id, notification); } // 省略部分注釋 public void notify(String tag, int id, Notification notification) { notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId())); } /** * @hide */ public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) { int[] idOut = new int[1]; INotificationManager service = getService(); String pkg = mContext.getPackageName(); // Fix the notification as best we can. Notification.addFieldsFromContext(mContext, notification); if (notification.sound != null) { notification.sound = notification.sound.getCanonicalUri(); if (StrictMode.vmFileUriExposureEnabled()) { notification.sound.checkFileUriExposed("Notification.sound"); } } fixLegacySmallIcon(notification, pkg); if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { if (notification.getSmallIcon() == null) { throw new IllegalArgumentException("Invalid notification (no valid small icon): " + notification); } } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); final Notification copy = Builder.maybeCloneStrippedForDelivery(notification); try { // !!! service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, idOut, user.getIdentifier()); if (localLOGV && id != idOut[0]) { Log.v(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
我們可以看到,到最后會(huì)調(diào)用 service.enqueueNotificationWithTag()
方法,這里的是 service 是 INotificationManager 接口。如果熟悉 AIDL 等系統(tǒng)相關(guān)運(yùn)行機(jī)制的話,就可以看出這里是代理類調(diào)用了代理接口的方法,實(shí)際方法實(shí)現(xiàn)是在 NotificationManagerService 當(dāng)中。
NotificationManagerService#enqueueNotificationWithTag()
@Override public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, Notification notification, int[] idOut, int userId) throws RemoteException { enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(), tag, id, notification, idOut, userId); } void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int[] idOut, int incomingUserId) { if (DBG) { Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification); } checkCallerIsSystemOrSameApp(pkg); final boolean isSystemNotification = isUidSystem(callingUid) || ("android".equals(pkg)); final boolean isNotificationFromListener = mListeners.isListenerPackage(pkg); final int userId = ActivityManager.handleIncomingUser(callingPid, callingUid, incomingUserId, true, false, "enqueueNotification", pkg); final UserHandle user = new UserHandle(userId); // Fix the notification as best we can. try { final ApplicationInfo ai = getContext().getPackageManager().getApplicationInfoAsUser( pkg, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, (userId == UserHandle.USER_ALL) ? UserHandle.USER_SYSTEM : userId); Notification.addFieldsFromContext(ai, userId, notification); } catch (NameNotFoundException e) { Slog.e(TAG, "Cannot create a context for sending app", e); return; } mUsageStats.registerEnqueuedByApp(pkg); if (pkg == null || notification == null) { throw new IllegalArgumentException("null not allowed: pkg=" + pkg + " id=" + id + " notification=" + notification); } final StatusBarNotification n = new StatusBarNotification( pkg, opPkg, id, tag, callingUid, callingPid, 0, notification, user); // Limit the number of notifications that any given package except the android // package or a registered listener can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemNotification && !isNotificationFromListener) { synchronized (mNotificationList) { if(mNotificationsByKey.get(n.getKey()) != null) { // this is an update, rate limit updates only final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg); if (appEnqueueRate > mMaxPackageEnqueueRate) { mUsageStats.registerOverRateQuota(pkg); final long now = SystemClock.elapsedRealtime(); if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) { Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate + ". Shedding events. package=" + pkg); mLastOverRateLogTime = now; } return; } } int count = 0; final int N = mNotificationList.size(); for (int i=0; i<N; i++) { final NotificationRecord r = mNotificationList.get(i); if (r.sbn.getPackageName().equals(pkg) && r.sbn.getUserId() == userId) { if (r.sbn.getId() == id && TextUtils.equals(r.sbn.getTag(), tag)) { break; // Allow updating existing notification } count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { mUsageStats.registerOverCountQuota(pkg); Slog.e(TAG, "Package has already posted " + count + " notifications. Not showing more. package=" + pkg); return; } } } } } // Whitelist pending intents. if (notification.allPendingIntents != null) { final int intentCount = notification.allPendingIntents.size(); if (intentCount > 0) { final ActivityManagerInternal am = LocalServices .getService(ActivityManagerInternal.class); final long duration = LocalServices.getService( DeviceIdleController.LocalService.class).getNotificationWhitelistDuration(); for (int i = 0; i < intentCount; i++) { PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i); if (pendingIntent != null) { am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(), duration); } } } } // Sanitize inputs notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN, Notification.PRIORITY_MAX); // setup local book-keeping final NotificationRecord r = new NotificationRecord(getContext(), n); mHandler.post(new EnqueueNotificationRunnable(userId, r)); idOut[0] = id; }
這里代碼比較多,但通過注釋可以清晰地理清整個(gè)邏輯:
- 首先檢查通知發(fā)起者是系統(tǒng)進(jìn)程或者是查看發(fā)起者發(fā)送的是否是同個(gè) app 的通知信息,否則拋出異常;
- 除了系統(tǒng)的通知和已注冊(cè)的監(jiān)聽器允許入隊(duì)列外,其他 app 的通知都會(huì)限制通知數(shù)上限和通知頻率上限;
- 將 notification 的 PendingIntent 加入到白名單;
- 將之前的 notification 進(jìn)一步封裝為 StatusBarNotification 和 NotificationRecord,最后封裝到一個(gè)異步線程 EnqueueNotificationRunnable 中
這里有一個(gè)點(diǎn),就是 mHandler,涉及到切換線程,我們先跟蹤一下 mHandler 是在哪個(gè)線程被創(chuàng)建。
mHandler 是 WorkerHandler 類的一個(gè)實(shí)例,在 NotificationManagerService#onStart()
方法中被創(chuàng)建,而 NotificationManagerService 是系統(tǒng) Service,所以 EnqueueNotificationRunnable 的 run 方法會(huì)運(yùn)行在 system_server 的主線程。
NotificationManagerService.EnqueueNotificationRunnable#run()
@Override public void run() { synchronized(mNotificationList) { // 省略代碼 if (notification.getSmallIcon() != null) { StatusBarNotification oldSbn = (old != null) ? old.sbn : null; mListeners.notifyPostedLocked(n, oldSbn); } else { Slog.e(TAG, "Not posting notification without small icon: " + notification); if (old != null && !old.isCanceled) { mListeners.notifyRemovedLocked(n); } // ATTENTION: in a future release we will bail out here // so that we do not play sounds, show lights, etc. for invalid // notifications Slog.e(TAG, "WARNING: In a future release this will crash the app: " + n.getPackageName()); } buzzBeepBlinkLocked(r); } }
- 省略的代碼主要的工作是提取 notification 相關(guān)的屬性,同時(shí)通知 notification ranking service,有新的 notification 進(jìn)來,然后對(duì)所有 notification 進(jìn)行重新排序;
- 然后到最后會(huì)調(diào)用
mListeners.notifyPostedLocked()
方法。這里 mListeners 是 NotificationListeners 類的一個(gè)實(shí)例。
NotificationManagerService.NotificationListeners#notifyPostedLocked() -> NotificationManagerService.NotificationListeners#notifyPosted()
public void notifyPostedLocked(StatusBarNotification sbn, StatusBarNotification oldSbn) { // Lazily initialized snapshots of the notification. TrimCache trimCache = new TrimCache(sbn); for (final ManagedServiceInfo info: mServices) { boolean sbnVisible = isVisibleToListener(sbn, info); boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false; // This notification hasn't been and still isn't visible -> ignore. if (!oldSbnVisible && !sbnVisible) { continue; } final NotificationRankingUpdate update = makeRankingUpdateLocked(info); // This notification became invisible -> remove the old one. if (oldSbnVisible && !sbnVisible) { final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight(); mHandler.post(new Runnable() { @Override public void run() { notifyRemoved(info, oldSbnLightClone, update); } }); continue; } final StatusBarNotification sbnToPost = trimCache.ForListener(info); mHandler.post(new Runnable() { @Override public void run() { notifyPosted(info, sbnToPost, update); } }); } } private void notifyPosted(final ManagedServiceInfo info, final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) { final INotificationListener listener = (INotificationListener) info.service; StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn); try { listener.onNotificationPosted(sbnHolder, rankingUpdate); } catch (RemoteException ex) { Log.e(TAG, "unable to notify listener (posted): " + listener, ex); } }
調(diào)用到最后會(huì)執(zhí)行 listener.onNotificationPosted()
方法。通過全局搜索得知,listener 類型是 NotificationListenerService.NotificationListenerWrapper
的代理對(duì)象。
NotificationListenerService.NotificationListenerWrapper#onNotificationPosted()
public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder, NotificationRankingUpdate update) { StatusBarNotification sbn; try { sbn = sbnHolder.get(); } catch (RemoteException e) { Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e); return; } try { // convert icon metadata to legacy format for older clients createLegacyIconExtras(sbn.getNotification()); maybePopulateRemoteViews(sbn.getNotification()); } catch (IllegalArgumentException e) { // warn and drop corrupt notification Log.w(TAG, "onNotificationPosted: can't rebuild notification from " + sbn.getPackageName()); sbn = null; } // protect subclass from concurrent modifications of (@link mNotificationKeys}. synchronized(mLock) { applyUpdateLocked(update); if (sbn != null) { SomeArgs args = SomeArgs.obtain(); args.arg1 = sbn; args.arg2 = mRankingMap; mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED, args).sendToTarget(); } else { // still pass along the ranking map, it may contain other information mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE, mRankingMap).sendToTarget(); } } }
這里在一開始會(huì)從 sbnHolder 中獲取到 sbn 對(duì)象,sbn 隸屬于 StatusBarNotificationHolder 類,繼承于 IStatusBarNotificationHolder.Stub
對(duì)象。注意到這里捕獲了一個(gè) RemoteException,猜測(cè)涉及到跨進(jìn)程調(diào)用,但我們不知道這段代碼是在哪個(gè)進(jìn)程中執(zhí)行的,所以在這里暫停跟蹤代碼。
筆者之前是通過向系統(tǒng)發(fā)送通知的方式跟蹤源碼,發(fā)現(xiàn)走不通。故個(gè)人嘗試從另一個(gè)角度入手,即系統(tǒng)接收我們發(fā)過來的通知并顯示到通知欄這個(gè)方式入手跟蹤代碼。
系統(tǒng)如何顯示 Notification,即對(duì)于系統(tǒng)端來說,Notification 的接收邏輯
系統(tǒng)顯示 Notification 的過程,猜測(cè)是在 PhoneStatusBar.java 中,因?yàn)橄到y(tǒng)啟動(dòng)的過程中,會(huì)啟動(dòng) SystemUI 進(jìn)程,初始化整個(gè) Android 顯示的界面,包括系統(tǒng)通知欄。
PhoneStatusBar#start() -> BaseStatusBar#start()
public void start() { // 省略代碼 // 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); } // 省略代碼 }
這段代碼中,會(huì)調(diào)用 NotificationListenerService#registerAsSystemService()
方法,涉及到我們之前跟蹤代碼的類。我們繼續(xù)跟進(jìn)去看一下。
NotificationListenerService#registerAsSystemService()
public void registerAsSystemService(Context context, ComponentName componentName, int currentUser) throws RemoteException { if (mWrapper == null) { mWrapper = new NotificationListenerWrapper(); } mSystemContext = context; INotificationManager noMan = getNotificationInterface(); mHandler = new MyHandler(context.getMainLooper()); mCurrentUser = currentUser; noMan.registerListener(mWrapper, componentName, currentUser); }
這里會(huì)初始化一個(gè) NotificationListenerWrapper 和 mHandler。由于這是在 SystemUI 進(jìn)程中去調(diào)用此方法將 NotificationListenerService 注冊(cè)為系統(tǒng)服務(wù),所以在前面分析的那里:
NotificationListenerService.NotificationListenerWrapper#onNotificationPosted(),
這段代碼是運(yùn)行在 SystemUI 進(jìn)程,而 mHandler 則是運(yùn)行在 SystemUI 主線程上的 Handler。所以,onNotificationPosted()
是運(yùn)行在 SystemUI 進(jìn)程中,它通過 sbn 從 system_server 進(jìn)程中獲取到 sbn 對(duì)象。下一步是通過 mHandler 處理消息,查看 NotificationListenerService.MyHandler#handleMessage()
方法,得知當(dāng) message.what 為 MSG_ON_NOTIFICATION_POSTED 時(shí),調(diào)用的是 onNotificationPosted()
方法。
但是,NotificationListenerService 是一個(gè)抽象類,onNotificationPosted()
為空方法,真正的實(shí)現(xiàn)是它的實(shí)例類。
觀察到之前 BaseStatusBar#start()
中,是調(diào)用了 mNotificationListener.registerAsSystemService()
方法。那么,mNotificationListener 是在哪里進(jìn)行初始化呢?
BaseStatusBar.mNotificationListener#onNotificationPosted
private final NotificationListenerService mNotificationListener = new NotificationListenerService() { // 省略代碼 @Override public void onNotificationPosted(final StatusBarNotification sbn, final RankingMap rankingMap) { if (DEBUG) Log.d(TAG, "onNotificationPosted: " + sbn); if (sbn != null) { mHandler.post(new Runnable() { @Override public void run() { processForRemoteInput(sbn.getNotification()); String key = sbn.getKey(); mKeysKeptForRemoteInput.remove(key); boolean isUpdate = mNotificationData.get(key) != null; // In case we don't allow child notifications, we 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 (!ENABLE_CHILD_NOTIFICATIONS && mGroupManager.isChildInGroupWithSummary(sbn)) { if (DEBUG) { Log.d(TAG, "Ignoring group child due to existing summary: " + sbn); } // Remove existing notification to avoid stale data. if (isUpdate) { removeNotification(key, rankingMap); } else { mNotificationData.updateRanking(rankingMap); } return; } if (isUpdate) { updateNotification(sbn, rankingMap); } else { addNotification(sbn, rankingMap, null /* oldEntry */ ); } } }); } } // 省略代碼 }
通過上述代碼,我們知道了在 BaseStatusBar.java 中,創(chuàng)建了 NotificationListenerService 的實(shí)例對(duì)象,實(shí)現(xiàn)了 onNotificationPost()
這個(gè)抽象方法;
在 onNotificationPost()
中,通過 handler 進(jìn)行消息處理,最終調(diào)用 addNotification()
方法
PhoneStatusBar#addNotification()
@Override public void addNotification(StatusBarNotification notification, RankingMap ranking, Entry oldEntry) { if (DEBUG) Log.d(TAG, "addNotification key=" + notification.getKey()); mNotificationData.updateRanking(ranking); Entry shadeEntry = createNotificationViews(notification); if (shadeEntry == null) { return; } boolean isHeadsUped = shouldPeek(shadeEntry); if (isHeadsUped) { mHeadsUpManager.showNotification(shadeEntry); // Mark as seen immediately setNotificationShown(notification); } if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) { if (shouldSuppressFullScreenIntent(notification.getKey())) { if (DEBUG) { Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + notification.getKey()); } } else if (mNotificationData.getImportance(notification.getKey()) < NotificationListenerService.Ranking.IMPORTANCE_MAX) { if (DEBUG) { Log.d(TAG, "No Fullscreen intent: not important enough: " + notification.getKey()); } } else { // Stop screensaver if the notification has a full-screen intent. // (like an incoming phone call) awakenDreams(); // not immersive & a full-screen alert should be shown if (DEBUG) Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); try { EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, notification.getKey()); notification.getNotification().fullScreenIntent.send(); shadeEntry.notifyFullScreenIntentLaunched(); MetricsLogger.count(mContext, "note_fullscreen", 1); } catch (PendingIntent.CanceledException e) {} } } // !!! addNotificationViews(shadeEntry, ranking); // Recalculate the position of the sliding windows and the titles. setAreThereNotifications(); }
在這個(gè)方法中,最關(guān)鍵的方法是最后的 addNotificationViews()
方法。調(diào)用這個(gè)方法之后,你創(chuàng)建的 Notification 才會(huì)被添加到系統(tǒng)通知欄上。
總結(jié)
跟蹤完整個(gè)過程中,之前提到的問題也可以一一解決了:
Q:我們創(chuàng)建的 Notification 實(shí)例最終以什么樣的方式發(fā)送給系統(tǒng)?
A:首先,我們?cè)?app 進(jìn)程創(chuàng)建 Notification 實(shí)例,通過跨進(jìn)程調(diào)用,傳遞到 system_server 進(jìn)程的 NotificationManagerService 中進(jìn)行處理,經(jīng)過兩次異步調(diào)用,最后傳遞給在 NotificationManagerService 中已經(jīng)注冊(cè)的 NotificationListenerWrapper。而 android 系統(tǒng)在初始化 systemui 進(jìn)程的時(shí)候,會(huì)往 NotificationManagerService 中注冊(cè)監(jiān)聽器(這里指的就是 NotificationListenerWrapper)。這種實(shí)現(xiàn)方法就是基于我們熟悉的一種設(shè)計(jì)模式:監(jiān)聽者模式。
Q:系統(tǒng)是如何獲取到 Notification 實(shí)例并顯示的?
A:上面提到,由于初始化的時(shí)候已經(jīng)往 NotificationManagerService 注冊(cè)監(jiān)聽器,所以系統(tǒng) SystemUI 進(jìn)程會(huì)接收到 Notification 實(shí)例之后經(jīng)過進(jìn)一步解析,然后構(gòu)造出 Notification Views 并最終顯示在系統(tǒng)通知欄上。
Q:我們是否能攔截 Notification 并獲取其中的信息?
A:通過上面的流程,我個(gè)人認(rèn)為可以通過 Xposed 等框架去 hook 其中幾個(gè)重要的方法去捕獲 Notification 實(shí)例,例如 hook NotificationManager#notify()
方法去獲取 Notification 實(shí)例。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android中通知Notification使用實(shí)例(振動(dòng)、燈光、聲音)
- Android中通過Notification&NotificationManager實(shí)現(xiàn)消息通知
- android使用NotificationListenerService監(jiān)聽通知欄消息
- Android開發(fā) -- 狀態(tài)欄通知Notification、NotificationManager詳解
- android中創(chuàng)建通知欄Notification代碼實(shí)例
- Android 中Notification彈出通知實(shí)現(xiàn)代碼
- Android中AlarmManager+Notification實(shí)現(xiàn)定時(shí)通知提醒功能
- Android 通知使用權(quán)(NotificationListenerService)的使用
- 詳解Android中Notification通知提醒
- Android種使用Notification實(shí)現(xiàn)通知管理以及自定義通知欄實(shí)例(示例四)
相關(guān)文章
listView的item中有checkbox,導(dǎo)致setOnItemClick失效的原因及解決辦法
這篇文章主要介紹了listView的item中有checkbox,導(dǎo)致setOnItemClick失效的原因及解決辦法,需要的朋友可以參考下2017-01-01Android實(shí)現(xiàn)彈窗進(jìn)度條效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)彈窗進(jìn)度條效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解
這篇文章主要介紹了Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Android?shape與selector標(biāo)簽使用詳解
Android中提供一種xml的方式,讓我們可以自由地定義背景,比較常用的就是shape標(biāo)簽和selector標(biāo)簽,這篇文章主要介紹了Android?shape與selector標(biāo)簽使用,需要的朋友可以參考下2022-05-05實(shí)例講解Android中的AutoCompleteTextView自動(dòng)補(bǔ)全組件
AutoCompleteTextView組件被用在輸入框中能實(shí)現(xiàn)輸入內(nèi)容自動(dòng)補(bǔ)全的功能,類似于大家平時(shí)用Google時(shí)的輸入聯(lián)想,這里我們來用實(shí)例講解Android中的AutoCompleteTextView自動(dòng)補(bǔ)全組件,特別是實(shí)現(xiàn)郵箱地址補(bǔ)全的例子,非常實(shí)用2016-05-05Android 實(shí)現(xiàn)滑動(dòng)方法總結(jié)
這篇文章主要介紹了Android 實(shí)現(xiàn)滑動(dòng)方法總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-07-07Flutter實(shí)戰(zhàn)之自定義日志打印組件詳解
這篇文章主要介紹了Flutter實(shí)戰(zhàn)之自定義日志打印組件詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-03-03Android實(shí)現(xiàn)讀取NFC卡的編號(hào)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)讀取NFC卡的編號(hào),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09