Android?AccessibilityService?事件分發(fā)原理分析總結(jié)
前言:
在了解了無障礙服務(wù)基礎(chǔ)使用之后,我們來探究一下 AccessibilityService 的事件接收方法回調(diào)的時機(jī)和它深層次的實(shí)現(xiàn)邏輯。
AccessibilityService 監(jiān)聽事件的調(diào)用邏輯
AccessibilityService
有很多用來接收外部調(diào)用事件變化的方法,這些方法封裝在內(nèi)部接口 Callbacks
中:
public interface Callbacks { void onAccessibilityEvent(AccessibilityEvent event); void onInterrupt(); void onServiceConnected(); void init(int connectionId, IBinder windowToken); boolean onGesture(AccessibilityGestureEvent gestureInfo); boolean onKeyEvent(KeyEvent event); void onMagnificationChanged(int displayId, @NonNull Region region, float scale, float centerX, float centerY); void onSoftKeyboardShowModeChanged(int showMode); void onPerformGestureResult(int sequence, boolean completedSuccessfully); void onFingerprintCapturingGesturesChanged(boolean active); void onFingerprintGesture(int gesture); void onAccessibilityButtonClicked(int displayId); void onAccessibilityButtonAvailabilityChanged(boolean available); void onSystemActionsChanged(); }
以最常用的 onAccessibilityEvent
為例,介紹一下調(diào)用流程。
onAccessibilityEvent
在 AccessibilityService
中,AccessibilityService
在 onBind
生命周期中,返回了一個IAccessibilityServiceClientWrapper
對象,它是一個 Binder ,所以外部實(shí)際上通過 Binder 機(jī)制跨進(jìn)程調(diào)用到無障礙服務(wù)的。
外部通過 Binder 調(diào)用到 Service 具體的實(shí)現(xiàn)方法的調(diào)用棧是:
- frameworks/base/core/java/android/accessibilityservice/AccessibilityService.java#IAccessibilityServiceClientWrapper#onAccessibilityEvent - frameworks/base/core/java/com/android/internal/os/HandlerCaller.java#sendMessage - frameworks/base/core/java/com/android/internal/os/HandlerCaller.java#Callback#executeMessage - frameworks/base/core/java/android/accessibilityservice/AccessibilityService.java#IAccessibilityServiceClientWrapper#executeMessage - frameworks/base/core/java/android/accessibilityservice/AccessibilityService.java#Callbacks#onAccessibilityEvent - frameworks/base/core/java/android/accessibilityservice/AccessibilityService.java#onAccessibilityEvent
首先是外部調(diào)用到 IAccessibilityServiceClientWrapper
的 onAccessibilityEvent 方法:
// IAccessibilityServiceClientWrapper public void onAccessibilityEvent(AccessibilityEvent event, boolean serviceWantsEvent) { Message message = mCaller.obtainMessageBO( DO_ON_ACCESSIBILITY_EVENT, serviceWantsEvent, event); mCaller.sendMessage(message); }
在這個方法中通過 HandlerCaller
切換到主線程,然后發(fā)送了一個消息。
這里的 HandlerCaller
的源碼是:
public class HandlerCaller { final Looper mMainLooper; final Handler mH; final Callback mCallback; class MyHandler extends Handler { MyHandler(Looper looper, boolean async) { super(looper, null, async); } @Override public void handleMessage(Message msg) { mCallback.executeMessage(msg); } } public interface Callback { public void executeMessage(Message msg); } public HandlerCaller(Context context, Looper looper, Callback callback, boolean asyncHandler) { mMainLooper = looper != null ? looper : context.getMainLooper(); mH = new MyHandler(mMainLooper, asyncHandler); mCallback = callback; } ... }
從它的源碼中可以看出,這是一個向主線程發(fā)消息的 Handler 。 在主線程中執(zhí)行它的內(nèi)部類 Callback
的 executeMessage
方法。
而IAccessibilityServiceClientWrapper
實(shí)現(xiàn)了 AccessibilityService.Callback
接口,所以調(diào)用到了IAccessibilityServiceClientWrapper.executeMessage
方法中。IAccessibilityServiceClientWrapper
對象的創(chuàng)建是在 onBind
生命周期中。它接收一個 AccessibilityService.Callback
對象作為 IAccessibilityServiceClientWrapper
的構(gòu)造參數(shù):
@Override public final IBinder onBind(Intent intent) { return new IAccessibilityServiceClientWrapper(this, getMainLooper(), new Callbacks() { @Override public void onServiceConnected() { AccessibilityService.this.dispatchServiceConnected(); } @Override public void onInterrupt() { AccessibilityService.this.onInterrupt(); } @Override public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityService.this.onAccessibilityEvent(event); } ... });
IAccessibilityServiceClientWrapper
中的 executeMessage
中,根據(jù)不同的 Handler 消息調(diào)用了 AccessibilityService.Callback
的對應(yīng)方法:
public static class IAccessibilityServiceClientWrapper extends IAccessibilityServiceClient.Stub implements HandlerCaller.Callback { private final HandlerCaller mCaller; public IAccessibilityServiceClientWrapper(Context context, Looper looper, Callbacks callback) { // ... mCaller = new HandlerCaller(context, looper, this, true); } @Override public void executeMessage(Message message) { switch (message.what) { case DO_ON_ACCESSIBILITY_EVENT: { // ... mCallback.onAccessibilityEvent(event); return; } case DO_ON_INTERRUPT: { // ... mCallback.onInterrupt(); return; } default : Log.w(LOG_TAG, "Unknown message type " + message.what); } } }
而剛才傳入的 AccessibilityService.Callback
方法的實(shí)現(xiàn)中,調(diào)用了AccessibilityService
的 onAccessibilityEvent
方法:
@Override public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityService.this.onAccessibilityEvent(event); }
這樣整個調(diào)用鏈就清晰了:
- 外部通過 Binder 機(jī)制調(diào)用到
AccessibilityService
的內(nèi)部 Binder 代理實(shí)現(xiàn)IAccessibilityServiceClientWrapper
對象 IAccessibilityServiceClientWrapper
對象內(nèi)部通過 Handler 機(jī)制切換到主線程執(zhí)行AccessibilityService.Callback
中對應(yīng)的方法。AccessibilityService.Callback
中的方法調(diào)用到了AccessibilityService
對應(yīng)的生命周期方法。
接下來關(guān)注一下一些重要的事件接收方法。
onAccessibilityEvent
Handler 中的 DO_ON_ACCESSIBILITY_EVENT
事件會調(diào)用到 onAccessibilityEvent
。 在 executeMessage(Message message)
方法中的邏輯是:
case DO_ON_ACCESSIBILITY_EVENT: { AccessibilityEvent event = (AccessibilityEvent) message.obj; boolean serviceWantsEvent = message.arg1 != 0; if (event != null) { // Send the event to AccessibilityCache via AccessibilityInteractionClient AccessibilityInteractionClient.getInstance(mContext).onAccessibilityEvent( event); if (serviceWantsEvent && (mConnectionId != AccessibilityInteractionClient.NO_ID)) { // Send the event to AccessibilityService mCallback.onAccessibilityEvent(event); } // Make sure the event is recycled. try { event.recycle(); } catch (IllegalStateException ise) { /* ignore - best effort */ } } return; }
- 取出
message.obj
轉(zhuǎn)換為AccessibilityEvent
,并根據(jù)message.arg1
檢查 Service 是否想要處理這個事件。 - 檢查
AccessibilityEvent
對象是否為 null,為空直接 return - 將
AccessibilityEvent
對象通過AccessibilityInteractionClient
加入到 AccessibilityCache 緩存中,然后根據(jù) service 是否要處理事件和AccessibilityInteractionClient
連接狀態(tài),決定是否要將事件發(fā)送給AccessibilityService
- 最后回收事件對象。
這里的AccessibilityInteractionClient
連接狀態(tài)檢查時通過 mConnectionId
屬性來判斷的,在IAccessibilityServiceClientWrapper
的 init 時被賦值,init 也是通過 Handler 傳遞來的消息切換到主線程進(jìn)行的:
case DO_INIT: { mConnectionId = message.arg1; SomeArgs args = (SomeArgs) message.obj; IAccessibilityServiceConnection connection = (IAccessibilityServiceConnection) args.arg1; IBinder windowToken = (IBinder) args.arg2; args.recycle(); if (connection != null) { AccessibilityInteractionClient.getInstance(mContext).addConnection( mConnectionId, connection); mCallback.init(mConnectionId, windowToken); mCallback.onServiceConnected(); } else { AccessibilityInteractionClient.getInstance(mContext).removeConnection( mConnectionId); mConnectionId = AccessibilityInteractionClient.NO_ID; AccessibilityInteractionClient.getInstance(mContext).clearCache(); mCallback.init(AccessibilityInteractionClient.NO_ID, null); } return; }
onIntercept
與onAccessibilityEvent
事件一樣都是通過 Handler 機(jī)制進(jìn)行處理的:
case DO_ON_INTERRUPT: { if (mConnectionId != AccessibilityInteractionClient.NO_ID) { mCallback.onInterrupt(); } return; }
只檢查了與AccessibilityInteractionClient
的連接狀態(tài)。
AccessibilityService 事件的外部來源
經(jīng)過上面的分析,我們知道外部通過 binder 機(jī)制觸發(fā)了 AccessibilityService
的事件監(jiān)聽方法,那么它們來自哪里呢? 接下來從 AccessibilityServiceInfo
開始,分析系統(tǒng)事件是如何傳遞到無障礙服務(wù)的。
AccessibilityServiceInfo
AccessibilityServiceInfo
用來描述 AccessibilityService 。系統(tǒng)根據(jù)這個類中的信息,將 AccessibilityEvents
通知給一個 AccessibilityService。
AccessibilityServiceInfo
中定義了一些屬性,用來控制無障礙服務(wù)的一些權(quán)限和能力。我們在 AndroidManifest.xml
中為無障礙服務(wù)指定的meta-data
標(biāo)簽中,指定的配置文件中的配置,和AccessibilityServiceInfo
中的屬性一一對應(yīng)。
AccessibilityServiceInfo
的引用:
查看 AccessibilityServiceInfo
的引用棧,發(fā)現(xiàn)有很多地方都用到了這個類,關(guān)于無障礙的重點(diǎn)看 AccessibilityManagerService
和 AccessibilityManager
這一套邏輯。 從名稱上看,無障礙功能提供了類似 AMS 一樣的系統(tǒng)服務(wù),并通過一個 Manager 類來進(jìn)行調(diào)用。
AccessibilityManager
AccessibilityManager 是一個系統(tǒng)服務(wù)管理器,用來分發(fā) AccessibilityEvent 事件。當(dāng)用戶界面中發(fā)生一些值得注意的事件時,例如焦點(diǎn)變化和 Activity 啟動等,會生成這些事件。
AccessibilityManager 內(nèi)部有一個看起來與發(fā)送消息有關(guān)的方法:
public void sendAccessibilityEvent(AccessibilityEvent event) { final IAccessibilityManager service; final int userId; final AccessibilityEvent dispatchedEvent; synchronized (mLock) { service = getServiceLocked(); if (service == null) return; event.setEventTime(SystemClock.uptimeMillis()); if (event.getAction() == 0) { event.setAction(mPerformingAction); } if (mAccessibilityPolicy != null) { dispatchedEvent = mAccessibilityPolicy.onAccessibilityEvent(event, mIsEnabled, mRelevantEventTypes); if (dispatchedEvent == null) return; } else { dispatchedEvent = event; } if (!isEnabled()) { Looper myLooper = Looper.myLooper(); if (myLooper == Looper.getMainLooper()) { throw new IllegalStateException("Accessibility off. Did you forget to check that?"); } else { // 當(dāng)不是在主線程(mainLooper)運(yùn)行時,調(diào)用檢查無障礙開啟狀態(tài)可能會異常。因此直接拋出異常 Log.e(LOG_TAG, "AccessibilityEvent sent with accessibility disabled"); return; } } userId = mUserId; } try { final long identityToken = Binder.clearCallingIdentity(); try { service.sendAccessibilityEvent(dispatchedEvent, userId); } finally { Binder.restoreCallingIdentity(identityToken); } if (DEBUG) { Log.i(LOG_TAG, dispatchedEvent + " sent"); } } catch (RemoteException re) { Log.e(LOG_TAG, "Error during sending " + dispatchedEvent + " ", re); } finally { if (event != dispatchedEvent) { event.recycle(); } dispatchedEvent.recycle(); } }
這個方法是用來發(fā)送一個 AccessibilityEvent
事件的,簡化里面的邏輯:
- 加鎖 - getServiceLocked() 獲取 service 對象,service 獲取不到直接 return - event 設(shè)置一個時間,然后設(shè)置 action - 檢查 AccessibilityPolicy 對象是否為 null - 不為空,dispatchedEvent 根據(jù) AccessibilityPolicy 的 onAccessibilityEvent(event) 賦值,賦值后仍為空直接 return - 為空, dispatchedEvent = event - 檢查系統(tǒng)是否開啟無障礙功能 - 解鎖 - try - try - service.sendAccessibilityEvent(dispatchedEvent, userId); 通過 AccessibilityManagerService 的 sendAccessibilityEvent 發(fā)送事件 - finally - Binder.restoreCallingIdentity(identityToken); - finally - 回收 event 和 dispatchedEvent 對象
這里 getServiceLocked()
內(nèi)部調(diào)用了 tryConnectToServiceLocked
方法:
private void tryConnectToServiceLocked(IAccessibilityManager service) { if (service == null) { IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); if (iBinder == null) { return; } service = IAccessibilityManager.Stub.asInterface(iBinder); } // ... }
而 AccessibilityManagerService
實(shí)現(xiàn)了 IAccessibilityManager.Stub
,所以這里的 service 時 AccessibilityManagerService
。 這里調(diào)用了 service.sendAccessibilityEvent(dispatchedEvent, userId);
通過 Binder 機(jī)制,調(diào)用到的是 AccessibilityManagerService
里的 sendAccessibilityEvent
方法。
AccessibilityManager.sendAccessibilityEvent
的調(diào)用位置有很多,其中比較顯眼的是在 ViewRootImpl 中的,因?yàn)?ViewRootImpl 是 View 添加到 Window 的重要實(shí)現(xiàn)類。sendAccessibilityEvent
在 ViewRootImpl 的內(nèi)部類中存在調(diào)用:
@Override public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { // ... final int eventType = event.getEventType(); final View source = getSourceForAccessibilityEvent(event); switch (eventType) { case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { if (source != null) { AccessibilityNodeProvider provider = source.getAccessibilityNodeProvider(); if (provider != null) { final int virtualNodeId = AccessibilityNodeInfo.getVirtualDescendantId(event.getSourceNodeId()); final AccessibilityNodeInfo node; node = provider.createAccessibilityNodeInfo(virtualNodeId); setAccessibilityFocus(source, node); } } } break; case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: { if (source != null && source.getAccessibilityNodeProvider() != null) { setAccessibilityFocus(null, null); } } break; case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { handleWindowContentChangedEvent(event); } break; } mAccessibilityManager.sendAccessibilityEvent(event); return true; }
這是一個 override 方法,它的定義在接口 ViewParent
中。 這個方法的調(diào)用棧很多:
可以跟到 View 中存在的同名方法 :
public void sendAccessibilityEvent(int eventType) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else { sendAccessibilityEventInternal(eventType); } }
它在 View 中的調(diào)用:
可以看出,常見的 View 的事件,包括:點(diǎn)擊、長按、焦點(diǎn)變化等,都會調(diào)用sendAccessibilityEvent
方法。
在 ViewRootImpl
中,AccessibilityManager
也存在很多處調(diào)用邏輯:
可以看出在 Android 在 View 體系中,提供了很多對無障礙能力的支持。所有的 View 的事件都會被系統(tǒng)的無障礙服務(wù)捕獲到。
回到調(diào)用邏輯,AccessibilityManager
內(nèi)部調(diào)用到的是AccessibilityManagerService
里的sendAccessibilityEvent
方法。下面介紹 AccessibilityManagerService
里面的流程。
AccessibilityManagerService
sendAccessibilityEvent
偽代碼邏輯:
synchronized { 1. 解析配置文件中的屬性,并對其進(jìn)行配置 2. 設(shè)置配置的包名范圍 } if (dispatchEvent) { 3. 確保接收此事件的 client 能夠獲取 window 當(dāng)前的狀態(tài),因?yàn)?Window Manager 可能會出于性能原因延遲計(jì)算,通過配置 shouldComputeWindows = true/false if (shouldComputeWindows) { 4. 獲取 WindowManagerInternal wm 5. wm.computeWindowsForAccessibility(displayId); } synchoronized { notifyAccessibilityServicesDelayedLocked(event, false) notifyAccessibilityServicesDelayedLocked(event, true) mUiAutomationManager.sendAccessibilityEventLocked(event); } } ...
最后的關(guān)鍵三行代碼中,調(diào)用了兩個方法:
notifyAccessibilityServicesDelayedLocked :
private void notifyAccessibilityServicesDelayedLocked(AccessibilityEvent event, boolean isDefault) { try { AccessibilityUserState state = getCurrentUserStateLocked(); for (int i = 0, count = state.mBoundServices.size(); i < count; i++) { AccessibilityServiceConnection service = state.mBoundServices.get(i); if (service.mIsDefault == isDefault) { service.notifyAccessibilityEvent(event); } } } catch (IndexOutOfBoundsException oobe) {} }
AccessibilityManagerService
從這個方法中,調(diào)用 AccessibilityServiceConnection
的同名方法notifyAccessibilityEvent
。
這個意思是,先通知service.mIsDefault = false
的無障礙服務(wù)連接發(fā)送事件,然后再通知 等于 true 的無障礙服務(wù)連接發(fā)送事件。
mUiAutomationManager.sendAccessibilityEventLocked(event):
mUiAutomationManager
的類型是UiAutomationManager
,它的sendAccessibilityEventLocked
方法實(shí)現(xiàn)是:
void sendAccessibilityEventLocked(AccessibilityEvent event) { if (mUiAutomationService != null) { mUiAutomationService.notifyAccessibilityEvent(event); } }
mUiAutomationService
的類型是 UiAutomationService
,它是UiAutomationManager
的內(nèi)部類,繼承自AbstractAccessibilityServiceConnection
,內(nèi)部操作都是切換到主線程進(jìn)行的,notifyAccessibilityEvent
方法的實(shí)現(xiàn)在父類中,后續(xù)會和上面的AccessibilityServiceConnection
一起說明。
AccessibilityServiceConnection
此類用來表示一個無障礙服務(wù)。 它存儲著服務(wù)管理所需的所有每個服務(wù)數(shù)據(jù),提供用于啟動/停止服務(wù)的 API,并負(fù)責(zé)在服務(wù)管理的數(shù)據(jù)結(jié)構(gòu)中添加/刪除服務(wù)。 該類還公開了配置接口,該接口在綁定后立即傳遞給它所代表的服務(wù)。 它還用作服務(wù)的連接。
AccessibilityServiceConnection
與UiAutomationService
一樣,繼承自AbstractAccessibilityServiceConnection
。
它們的notifyAccessibilityEvent
方法,在AbstractAccessibilityServiceConnection
中:
public void notifyAccessibilityEvent(AccessibilityEvent event) { synchronized (mLock) { ... // copy 一個副本,因?yàn)樵谡{(diào)度期間,如果接收的服務(wù)沒有訪問窗口內(nèi)容的權(quán)限,則可能會修改并刪除事件。 AccessibilityEvent newEvent = AccessibilityEvent.obtain(event); Message message; if ((mNotificationTimeout > 0) && (eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)) { // 最多允許一個待處理事件 final AccessibilityEvent oldEvent = mPendingEvents.get(eventType); mPendingEvents.put(eventType, newEvent); if (oldEvent != null) { mEventDispatchHandler.removeMessages(eventType); oldEvent.recycle(); } message = mEventDispatchHandler.obtainMessage(eventType); } else { // 發(fā)送所有消息,繞過 mPendingEvents message = mEventDispatchHandler.obtainMessage(eventType, newEvent); } message.arg1 = serviceWantsEvent ? 1 : 0; mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout); } }
這個方法中,通過一個 Handler 來處理和發(fā)送消息。
mEventDispatchHandler = new Handler(mainHandler.getLooper()) { @Override public void handleMessage(Message message) { final int eventType = message.what; AccessibilityEvent event = (AccessibilityEvent) message.obj; boolean serviceWantsEvent = message.arg1 != 0; notifyAccessibilityEventInternal(eventType, event, serviceWantsEvent); } };
內(nèi)部調(diào)用notifyAccessibilityEventInternal
:
private void notifyAccessibilityEventInternal(int eventType, AccessibilityEvent event, boolean serviceWantsEvent) { IAccessibilityServiceClient listener; synchronized (mLock) { listener = mServiceInterface; // 如果在消息分發(fā)無障礙事件時 service die 或 關(guān)閉,listener 可能為空 if (listener == null) return; // 我們有兩種通知事件的方式,節(jié)流和非節(jié)流。 如果我們不進(jìn)行節(jié)流,那么消息會隨事件一起出現(xiàn),我們會毫不費(fèi)力地處理這些事件。 if (event == null) { // 我們正在限制事件,所以只要它為空,我們就會在 mPendingEvents 中發(fā)送這種類型的事件。 由于競爭條件,它只能為空: // 1) 一個 binder 線程調(diào)用 notifyAccessibilityServiceDelayedLocked,它發(fā)布一條用于調(diào)度事件的消息并將該事件存儲在 mPendingEvents 中。 // 2) 消息由服務(wù)線程上的處理程序從隊(duì)列中拉出,此方法即將獲取鎖。 // 3) 另一個 binder 線程在 notifyAccessibilityEvent 中獲取鎖 // 4) notifyAccessibilityEvent 回收該方法即將處理的事件,替換為新的,并發(fā)布第二條消息 // 5) 此方法抓取新事件,對其進(jìn)行處理,然后將其從 mPendingEvents 中刪除 // 6) (4) 中發(fā)送的第二條消息到達(dá),但事件已在 (5) 中刪除。 event = mPendingEvents.get(eventType); if (event == null) { return; } mPendingEvents.remove(eventType); } if (mSecurityPolicy.canRetrieveWindowContentLocked(this)) { event.setConnectionId(mId); } else { event.setSource((View) null); } event.setSealed(true); } try { listener.onAccessibilityEvent(event, serviceWantsEvent); } catch (RemoteException re) {} finally { event.recycle(); } }
備注中說明了消息處理和分發(fā)邏輯,但我們這里只需要關(guān)注最后的:
listener.onAccessibilityEvent(event, serviceWantsEvent);
這里的 listener 是一個IAccessibilityServiceClient
,是個 AIDL 文件。這個 AIDL 在我們的 AccessibilityService 中有實(shí)現(xiàn)類!
調(diào)用到這個IAccessibilityServiceClient
, 就能調(diào)用到AccessibilityService
中的代碼了。
至此無障礙服務(wù)的調(diào)用,從系統(tǒng)的 View 和其他事件位置,經(jīng)過AccessibilityManager.notifyAccessibilityEvent
用 Binder 機(jī)制調(diào)用AccessibilityManagerService.notifyAccessibilityEvent
;然后AccessibilityManagerService
內(nèi)部通過調(diào)用AccessibilityServiceConnection.notifyAccessibilityEvent
來調(diào)用我們可以實(shí)現(xiàn)的AccessibilityService
中接收事件。
到此這篇關(guān)于Android AccessibilityService 事件分發(fā)原理分析總結(jié)的文章就介紹到這了,更多相關(guān)Android AccessibilityService 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android實(shí)現(xiàn)瘋狂連連看游戲之游戲效果預(yù)覽(一)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)瘋狂連連看游戲之游戲的效果預(yù)覽,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03android教程之intent的action屬性使用示例(intent發(fā)短信)
這篇文章主要介紹了android中intent的action屬性使用示例,提供了使用intent撥打電話、發(fā)送短信、播放mp3的代碼2014-01-01Android Studio 3.6 正式版終于發(fā)布了,快來圍觀
Android Studio 3.6 正式版終于發(fā)布了,值得興奮呀,畢竟 3.5 大版本更新也已經(jīng)差不多半年了,撒花撒花!這次更新又更新了什么呢?快來跟隨小編一起看看吧2020-02-02Android 實(shí)現(xiàn)界面刷新的幾種方法
這篇文章主要介紹了Android 實(shí)現(xiàn)界面刷新的相關(guān)資料,這里提供了幾種方法及實(shí)例代碼,具有一定的參考價(jià)值,需要的朋友可以參考下2016-11-11Android App中ViewPager與Fragment結(jié)合的一些問題解決
這篇文章主要介紹了Android App中ViewPager與Fragment結(jié)合的一些問題解決,重點(diǎn)講解了如何更新及替換ViewPager中的Fragment,需要的朋友可以參考下2016-03-03android 下載時文件名是中文和空格會報(bào)錯解決方案
項(xiàng)目中遇到了下載文件文件名是中文而且還有空格如果不對連接進(jìn)行處理下載就會報(bào)錯要想解決這個問題只需對你的url進(jìn)行編碼然后替換空格用編碼表示,感興趣的朋友可以詳細(xì)了解下2013-01-01