亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android中的Notification機制深入理解

 更新時間:2019年02月24日 14:40:15   作者:張朝旭  
這篇文章主要給大家介紹了關(guān)于Android中Notification機制的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧

本文需要解決的問題

筆者最近正在做一個項目,里面需要用到 Android Notification 機制來實現(xiàn)某些特定需求。我正好通過這個機會研究一下 Android Notification 相關(guān)的發(fā)送邏輯和接收邏輯,以及整理相關(guān)的筆記。我研究 Notification 機制的目的是解決以下我在使用過程中所思考的問題:

  • 我們創(chuàng)建的 Notification 實例最終以什么樣的方式發(fā)送給系統(tǒng)?
  • 系統(tǒng)是如何接收到 Notification 實例并顯示的?
  • 我們是否能攔截其他 app 的 Notification 并獲取其中的信息?

什么是 Android Notification 機制?

Notification,中文名翻譯為通知,每個 app 可以自定義通知的樣式和內(nèi)容等,它會顯示在系統(tǒng)的通知欄等區(qū)域。用戶可以打開抽屜式通知欄查看通知的詳細信息。在實際生活中,Android Notification 機制有很廣泛的應(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 新建了一個 Notification 對象,最后通過 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();
 }
}

我們可以看到,到最后會調(diào)用 service.enqueueNotificationWithTag() 方法,這里的是 service 是 INotificationManager 接口。如果熟悉 AIDL 等系統(tǒng)相關(guān)運行機制的話,就可以看出這里是代理類調(diào)用了代理接口的方法,實際方法實現(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;
}

這里代碼比較多,但通過注釋可以清晰地理清整個邏輯:

  • 首先檢查通知發(fā)起者是系統(tǒng)進程或者是查看發(fā)起者發(fā)送的是否是同個 app 的通知信息,否則拋出異常;
  • 除了系統(tǒng)的通知和已注冊的監(jiān)聽器允許入隊列外,其他 app 的通知都會限制通知數(shù)上限和通知頻率上限;
  • 將 notification 的 PendingIntent 加入到白名單;
  • 將之前的 notification 進一步封裝為 StatusBarNotification 和 NotificationRecord,最后封裝到一個異步線程 EnqueueNotificationRunnable 中

這里有一個點,就是 mHandler,涉及到切換線程,我們先跟蹤一下 mHandler 是在哪個線程被創(chuàng)建。

mHandler 是 WorkerHandler 類的一個實例,在 NotificationManagerService#onStart() 方法中被創(chuàng)建,而 NotificationManagerService 是系統(tǒng) Service,所以 EnqueueNotificationRunnable 的 run 方法會運行在 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);
 }
}
  1. 省略的代碼主要的工作是提取 notification 相關(guān)的屬性,同時通知 notification ranking service,有新的 notification 進來,然后對所有 notification 進行重新排序;
  2. 然后到最后會調(diào)用 mListeners.notifyPostedLocked() 方法。這里 mListeners 是 NotificationListeners 類的一個實例。

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)用到最后會執(zhí)行 listener.onNotificationPosted() 方法。通過全局搜索得知,listener 類型是 NotificationListenerService.NotificationListenerWrapper 的代理對象。

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();
  }
 }
}

這里在一開始會從 sbnHolder 中獲取到 sbn 對象,sbn 隸屬于 StatusBarNotificationHolder 類,繼承于 IStatusBarNotificationHolder.Stub 對象。注意到這里捕獲了一個 RemoteException,猜測涉及到跨進程調(diào)用,但我們不知道這段代碼是在哪個進程中執(zhí)行的,所以在這里暫停跟蹤代碼。

筆者之前是通過向系統(tǒng)發(fā)送通知的方式跟蹤源碼,發(fā)現(xiàn)走不通。故個人嘗試從另一個角度入手,即系統(tǒng)接收我們發(fā)過來的通知并顯示到通知欄這個方式入手跟蹤代碼。

系統(tǒng)如何顯示 Notification,即對于系統(tǒng)端來說,Notification 的接收邏輯

系統(tǒng)顯示 Notification 的過程,猜測是在 PhoneStatusBar.java 中,因為系統(tǒng)啟動的過程中,會啟動 SystemUI 進程,初始化整個 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);
 }
 // 省略代碼
}

這段代碼中,會調(diào)用 NotificationListenerService#registerAsSystemService() 方法,涉及到我們之前跟蹤代碼的類。我們繼續(xù)跟進去看一下。

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);
}

這里會初始化一個 NotificationListenerWrapper 和 mHandler。由于這是在 SystemUI 進程中去調(diào)用此方法將 NotificationListenerService 注冊為系統(tǒng)服務(wù),所以在前面分析的那里:

NotificationListenerService.NotificationListenerWrapper#onNotificationPosted(),這段代碼是運行在 SystemUI 進程,而 mHandler 則是運行在 SystemUI 主線程上的 Handler。所以,onNotificationPosted() 是運行在 SystemUI 進程中,它通過 sbn 從 system_server 進程中獲取到 sbn 對象。下一步是通過 mHandler 處理消息,查看 NotificationListenerService.MyHandler#handleMessage() 方法,得知當(dāng) message.what 為 MSG_ON_NOTIFICATION_POSTED 時,調(diào)用的是 onNotificationPosted() 方法。

但是,NotificationListenerService 是一個抽象類,onNotificationPosted() 為空方法,真正的實現(xiàn)是它的實例類。

觀察到之前 BaseStatusBar#start() 中,是調(diào)用了 mNotificationListener.registerAsSystemService() 方法。那么,mNotificationListener 是在哪里進行初始化呢?

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 的實例對象,實現(xiàn)了 onNotificationPost() 這個抽象方法;

onNotificationPost() 中,通過 handler 進行消息處理,最終調(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();
}

在這個方法中,最關(guān)鍵的方法是最后的 addNotificationViews() 方法。調(diào)用這個方法之后,你創(chuàng)建的 Notification 才會被添加到系統(tǒng)通知欄上。

總結(jié)

跟蹤完整個過程中,之前提到的問題也可以一一解決了:

Q:我們創(chuàng)建的 Notification 實例最終以什么樣的方式發(fā)送給系統(tǒng)?

A:首先,我們在 app 進程創(chuàng)建 Notification 實例,通過跨進程調(diào)用,傳遞到 system_server 進程的 NotificationManagerService 中進行處理,經(jīng)過兩次異步調(diào)用,最后傳遞給在 NotificationManagerService 中已經(jīng)注冊的 NotificationListenerWrapper。而 android 系統(tǒng)在初始化 systemui 進程的時候,會往 NotificationManagerService 中注冊監(jiān)聽器(這里指的就是 NotificationListenerWrapper)。這種實現(xiàn)方法就是基于我們熟悉的一種設(shè)計模式:監(jiān)聽者模式。

Q:系統(tǒng)是如何獲取到 Notification 實例并顯示的?

A:上面提到,由于初始化的時候已經(jīng)往 NotificationManagerService 注冊監(jiān)聽器,所以系統(tǒng) SystemUI 進程會接收到 Notification 實例之后經(jīng)過進一步解析,然后構(gòu)造出 Notification Views 并最終顯示在系統(tǒng)通知欄上。

Q:我們是否能攔截 Notification 并獲取其中的信息?

A:通過上面的流程,我個人認為可以通過 Xposed 等框架去 hook 其中幾個重要的方法去捕獲 Notification 實例,例如 hook NotificationManager#notify() 方法去獲取 Notification 實例。

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

相關(guān)文章

  • listView的item中有checkbox,導(dǎo)致setOnItemClick失效的原因及解決辦法

    listView的item中有checkbox,導(dǎo)致setOnItemClick失效的原因及解決辦法

    這篇文章主要介紹了listView的item中有checkbox,導(dǎo)致setOnItemClick失效的原因及解決辦法,需要的朋友可以參考下
    2017-01-01
  • Android切圓角的幾種常見方式總結(jié)

    Android切圓角的幾種常見方式總結(jié)

    這篇文章主要給大家介紹了關(guān)于Android切圓角的常見方式,文中通過示例代碼介紹的非常詳細,對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-12-12
  • Android實現(xiàn)彈窗進度條效果

    Android實現(xiàn)彈窗進度條效果

    這篇文章主要為大家詳細介紹了Android實現(xiàn)彈窗進度條效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解

    Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解

    這篇文章主要介紹了Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • Android?shape與selector標簽使用詳解

    Android?shape與selector標簽使用詳解

    Android中提供一種xml的方式,讓我們可以自由地定義背景,比較常用的就是shape標簽和selector標簽,這篇文章主要介紹了Android?shape與selector標簽使用,需要的朋友可以參考下
    2022-05-05
  • 實例講解Android中的AutoCompleteTextView自動補全組件

    實例講解Android中的AutoCompleteTextView自動補全組件

    AutoCompleteTextView組件被用在輸入框中能實現(xiàn)輸入內(nèi)容自動補全的功能,類似于大家平時用Google時的輸入聯(lián)想,這里我們來用實例講解Android中的AutoCompleteTextView自動補全組件,特別是實現(xiàn)郵箱地址補全的例子,非常實用
    2016-05-05
  • Android 實現(xiàn)滑動方法總結(jié)

    Android 實現(xiàn)滑動方法總結(jié)

    這篇文章主要介紹了Android 實現(xiàn)滑動方法總結(jié)的相關(guān)資料,需要的朋友可以參考下
    2017-07-07
  • Flutter實戰(zhàn)之自定義日志打印組件詳解

    Flutter實戰(zhàn)之自定義日志打印組件詳解

    這篇文章主要介紹了Flutter實戰(zhàn)之自定義日志打印組件詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-03-03
  • Android實現(xiàn)讀取NFC卡的編號

    Android實現(xiàn)讀取NFC卡的編號

    這篇文章主要為大家詳細介紹了Android實現(xiàn)讀取NFC卡的編號,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • Android-Okhttp的使用解析

    Android-Okhttp的使用解析

    okhttp是Android6.0推出的網(wǎng)絡(luò)框架。這篇文章主要介紹了Android-Okhttp的使用解析,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-03-03

最新評論