從"Show?tabs"了解Android?Input系統(tǒng)
Input源碼解讀——從"Show tabs"開始
本文基于Android T版本源碼,梳理當(dāng)用戶在開發(fā)者選項(xiàng)中開啟Show tabs功能后顯示第點(diǎn)按操作的視覺反饋的原理,來(lái)進(jìn)一步了解Android Input系統(tǒng)

Settings 寫入設(shè)置
首先是設(shè)置應(yīng)用(Settings)提供的開發(fā)者選項(xiàng)畫面響應(yīng)點(diǎn)擊,將Show taps選項(xiàng)對(duì)應(yīng)的設(shè)置Key SHOW_TOUCHES的 ON 值通過(guò)android.provder.Settings接口寫入到保存系統(tǒng)設(shè)置數(shù)據(jù)的SettingsProvier中。
// packages/apps/Settings/src/com/android/settings/development/ShowTapsPreferenceController.java
public class ShowTapsPreferenceController extends DeveloperOptionsPreferenceController ... {
...
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isEnabled = (Boolean) newValue;
Settings.System.putInt(mContext.getContentResolver(),
Settings.System.SHOW_TOUCHES, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
return true;
}
...
}
InputManagerService監(jiān)聽設(shè)置
負(fù)責(zé)管理輸入的系統(tǒng)服務(wù)InputManagerService在啟動(dòng)之際,會(huì)監(jiān)聽設(shè)置中的 SHOW_TOUCHES字段的變化,在設(shè)置產(chǎn)生變化的時(shí)候調(diào)用native側(cè)的代碼進(jìn)行處理。
// frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public class InputManagerService extends IInputManager.Stub... {
...
public void start() {
...
registerShowTouchesSettingObserver();
...
}
private void registerShowTouchesSettingObserver() {
mContext.getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.SHOW_TOUCHES), true,
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
updateShowTouchesFromSettings();
}
}, UserHandle.USER_ALL);
}
private void updateShowTouchesFromSettings() {
int setting = getShowTouchesSetting(0);
mNative.setShowTouches(setting != 0);
}
...
// frameworks/base/services/core/java/com/android/server/input/NativeInputManagerService.java
public interface NativeInputManagerService {
...
void setShowTouches(boolean enabled);
...
}
// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
class NativeInputManager : public virtual RefBase, ...{
...
void setShowTouches(bool enabled);
...
}
void NativeInputManager::setShowTouches(bool enabled) {
{ // acquire lock
AutoMutex _l(mLock);
if (mLocked.showTouches == enabled) {
return;
}
ALOGI("Setting show touches feature to %s.", enabled ? "enabled" : "disabled");
mLocked.showTouches = enabled;
} // release lock
mInputManager->getReader().requestRefreshConfiguration(
InputReaderConfiguration::CHANGE_SHOW_TOUCHES);
}
這里的mInputManager是InputManagerInterface對(duì)象實(shí)例,InputManager是InputManagerInterface和子類,所以通過(guò)mInputManager可以連接NativeInputManager和InputReader。
這里向負(fù)責(zé)讀取事件的InputReader發(fā)出更新配置的請(qǐng)求,配置變更的Type為 CHANGE_SHOW_TOUCHES。
通過(guò) InputReader 請(qǐng)求刷新配置
InputReader接收到配置變化的Type之后,會(huì)根據(jù)記錄待刷新配置的變量 mConfigurationChangesToRefresh判斷當(dāng)前是否已經(jīng)在刷新過(guò)程中。
如果尚未處于刷新中,則更新mConfigurationChangesToRefresh的值,并喚醒EventHub進(jìn)行配置刷新。
// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::requestRefreshConfiguration(uint32_t changes) {
std::scoped_lock _l(mLock);
if (changes) {
bool needWake = !mConfigurationChangesToRefresh;
mConfigurationChangesToRefresh |= changes;
if (needWake) {
mEventHub->wake();
}
}
}
EventHub 喚醒 InputReader 線程
InputManagerService過(guò)來(lái)的刷新請(qǐng)求最終需要InputReader線程來(lái)處理。
可是 InputReader 線程處在從EventHub中讀取事件和沒有事件時(shí)便調(diào)用epoll_wait進(jìn)入等待狀態(tài)的循環(huán)當(dāng)中。
所以為了讓其立即處理配置變化,需要EventHub的手動(dòng)喚醒。
// frameworks/native/services/inputflinger/reader/EventHub.cpp
void EventHub::wake() {
ALOGV("wake() called");
ssize_t nWrite;
do {
nWrite = write(mWakeWritePipeFd, "W", 1);
} while (nWrite == -1 && errno == EINTR);
if (nWrite != 1 && errno != EAGAIN) {
ALOGW("Could not write wake signal: %s", strerror(errno));
}
}
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
...
for (;;) {
...
int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
}
...
}
InputReader線程刷新配置
EventHub喚醒后處于等待狀態(tài)的getEvents會(huì)結(jié)束,之后InputReader線程會(huì)進(jìn)入下次循環(huán)即loopOnce。
其首先將檢查是否存在待刷新的配置變化changes,存在的話調(diào)用refreshConfigurationLocked讓InputDevice去重新適配變化。
// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::loopOnce() {
...
std::vector<InputDeviceInfo> inputDevices;
{ // acquire lock
...
uint32_t changes = mConfigurationChangesToRefresh;
if (changes) {
mConfigurationChangesToRefresh = 0;
timeoutMillis = 0;
refreshConfigurationLocked(changes);
} else if (mNextTimeout != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
}
} // release lock
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
...
}
需要留意,refreshConfigurationLocked在調(diào)用InputDevice進(jìn)一步處理之前需要先獲取配置的變化放入mConfig中。
// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::refreshConfigurationLocked(uint32_t changes) {
mPolicy->getReaderConfiguration(&mConfig);
...
if (changes & InputReaderConfiguration::CHANGE_MUST_REOPEN) {
mEventHub->requestReopenDevices();
} else {
for (auto& devicePair : mDevices) {
std::shared_ptr<InputDevice>& device = devicePair.second;
device->configure(now, &mConfig, changes);
}
}
...
}
InputDevice配置變化
InputDevice的configure需要處理很多配置變化,比如鍵盤布局、麥克風(fēng)等。對(duì)于Show taps的變化關(guān)注調(diào)用 InputMapper的congfigure即可。
// frameworks/native/services/inputflinger/reader/InputDevice.cpp
void InputDevice::configure(nsecs_t when, const InputReaderConfiguration* config,
uint32_t changes) {
...
if (!isIgnored()) {
...
for_each_mapper([this, when, config, changes](InputMapper& mapper) {
mapper.configure(when, config, changes);
mSources |= mapper.getSources();
});
...
}
}
TouchInputMapper 進(jìn)一步處理
眾多輸入事件的物理數(shù)據(jù)需要對(duì)應(yīng)的InputMapper來(lái)轉(zhuǎn)化為上層能識(shí)別的事件類型。比如識(shí)別鍵盤輸入的 KeyboardInputMapper、識(shí)別震動(dòng)的VibratorInputMapper等等。
現(xiàn)在的觸摸屏都支持多點(diǎn)觸控,所以是MultiTouchInputMapper來(lái)處理的。可MultiTouchInputMapper沒有復(fù)寫 configure(),而是沿用由父類TouchInputMapper的共通處理。
// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::configure(nsecs_t when, const InputReaderConfiguration* config,
uint32_t changes) {
...
bool resetNeeded = false;
if (!changes ||
(changes &
(InputReaderConfiguration::CHANGE_DISPLAY_INFO |
InputReaderConfiguration::CHANGE_POINTER_CAPTURE |
InputReaderConfiguration::CHANGE_POINTER_GESTURE_ENABLEMENT |
InputReaderConfiguration::CHANGE_SHOW_TOUCHES |
InputReaderConfiguration::CHANGE_EXTERNAL_STYLUS_PRESENCE))) {
// Configure device sources, display dimensions, orientation and
// scaling factors.
configureInputDevice(when, &resetNeeded);
}
...
}
TouchInputMapper會(huì)依據(jù)changes的類型進(jìn)行對(duì)應(yīng)處理,對(duì)于SHOW_TOUCHES的變化需要調(diào)用configureInputDevice進(jìn)一步處理。
創(chuàng)建和初始化 PointerController
configureInputDevice進(jìn)行多個(gè)參數(shù)的測(cè)量和配置,其中和Show taps相關(guān)的是PointerController的創(chuàng)建,該類是 Mouse、Taps、Pointer location 等系統(tǒng) Touch 顯示的專用類。
// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::configureInputDevice(nsecs_t when, bool* outResetNeeded) {
...
// Create pointer controller if needed, and keep it around if Pointer Capture is enabled to
// preserve the cursor position.
if (mDeviceMode == DeviceMode::POINTER ||
(mDeviceMode == DeviceMode::DIRECT && mConfig.showTouches) ||
(mParameters.deviceType == Parameters::DeviceType::POINTER &&
mConfig.pointerCaptureRequest.enable)) {
if (mPointerController == nullptr) {
mPointerController = getContext()->getPointerController(getDeviceId());
}
if (mConfig.pointerCaptureRequest.enable) {
mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
}
} else {
mPointerController.reset();
}
...
}
這里調(diào)用InputReaderContext#getPointerController,InputReader::ContextImpl是InputReaderContext的子類,所以會(huì)回調(diào)到InputReader開啟PointerController的創(chuàng)建和初始化。
// frameworks/native/services/inputflinger/reader/InputReader.cpp
std::shared_ptr<PointerControllerInterface> InputReader::ContextImpl::getPointerController(
int32_t deviceId) {
// lock is already held by the input loop
return mReader->getPointerControllerLocked(deviceId);
}
std::shared_ptr<PointerControllerInterface> InputReader::getPointerControllerLocked(
int32_t deviceId) {
std::shared_ptr<PointerControllerInterface> controller = mPointerController.lock();
if (controller == nullptr) {
controller = mPolicy->obtainPointerController(deviceId);
mPointerController = controller;
updatePointerDisplayLocked();
}
return controller;
}
這里調(diào)用InputReaderPolicyInterface#obtainPointerController,而NativeInputManager是InputReaderPolicyInterface的子類。
// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController(
int32_t /* deviceId */) {
...
std::shared_ptr<PointerController> controller = mLocked.pointerController.lock();
if (controller == nullptr) {
ensureSpriteControllerLocked();
controller = PointerController::create(this, mLooper, mLocked.spriteController);
mLocked.pointerController = controller;
updateInactivityTimeoutLocked();
}
return controller;
}
PointerController 構(gòu)建的同時(shí)需要構(gòu)建持有的 MouseCursorController。
// frameworks/base/libs/input/PointerController.cpp
std::shared_ptr<PointerController> PointerController::create( ... ) {
std::shared_ptr<PointerController> controller = std::shared_ptr<PointerController>(
new PointerController(policy, looper, spriteController));
...
return controller;
}
PointerController::PointerController( ... )
: mContext(policy, looper, spriteController, *this), mCursorController(mContext) {
std::scoped_lock lock(mLock);
mLocked.presentation = Presentation::SPOT;
...
}
obtainPointerController執(zhí)行完之后調(diào)用updatePointerDisplayLocked執(zhí)行PointerController的初始化。
初始化 PointerController
調(diào)用PointerController的setDisplayViewport傳入顯示用的DisplayViewPort。
// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::updatePointerDisplayLocked() {
...
std::optional<DisplayViewport> viewport =
mConfig.getDisplayViewportById(mConfig.defaultPointerDisplayId);
if (!viewport) {
...
viewport = mConfig.getDisplayViewportById(ADISPLAY_ID_DEFAULT);
}
...
controller->setDisplayViewport(*viewport);
}
setDisplayViewport需要持有的MouseCursorController進(jìn)一步初始化。
// frameworks/base/libs/input/PointerController.cpp
void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
...
mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
}
MouseCursorController需要獲取Display相關(guān)的參數(shù),并執(zhí)行兩個(gè)重要步驟:loadResourcesLocked/updatePointerLocked
// frameworks/base/libs/input/MouseCursorController.cpp
void MouseCursorController::setDisplayViewport(const DisplayViewport& viewport,
bool getAdditionalMouseResources) {
...
// Reset cursor position to center if size or display changed.
if (oldViewport.displayId != viewport.displayId || oldDisplayWidth != newDisplayWidth ||
oldDisplayHeight != newDisplayHeight) {
float minX, minY, maxX, maxY;
if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
mLocked.pointerX = (minX + maxX) * 0.5f;
mLocked.pointerY = (minY + maxY) * 0.5f;
// Reload icon resources for density may be changed.
loadResourcesLocked(getAdditionalMouseResources);
...
}
} else if (oldViewport.orientation != viewport.orientation) {
...
}
updatePointerLocked();
}
加載 Pointer 相關(guān)資源
// frameworks/base/libs/input/MouseCursorController.cpp
void MouseCursorController::loadResourcesLocked(bool getAdditionalMouseResources) REQUIRES(mLock) {
...
policy->loadPointerResources(&mResources, mLocked.viewport.displayId);
policy->loadPointerIcon(&mLocked.pointerIcon, mLocked.viewport.displayId);
...
}
省略諸多細(xì)節(jié),loadPointerResources將通過(guò)InputManagerService的JNI端以及PointerIcon的JNI端創(chuàng)建PointerIcon實(shí)例,并讀取顯示的資源。
getSystemIcon則是負(fù)責(zé)的函數(shù),其將讀取系統(tǒng)資源里名為Pointer的Style,并讀取指針對(duì)應(yīng)的資源 ID。
// frameworks/base/core/java/android/view/PointerIcon.java
public static PointerIcon getSystemIcon(@NonNull Context context, int type) {
...
int typeIndex = getSystemIconTypeIndex(type);
if (typeIndex == 0) {
typeIndex = getSystemIconTypeIndex(TYPE_DEFAULT);
}
int defStyle = sUseLargeIcons ?
com.android.internal.R.style.LargePointer : com.android.internal.R.style.Pointer;
TypedArray a = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.Pointer,
0, defStyle);
int resourceId = a.getResourceId(typeIndex, -1);
...
icon = new PointerIcon(type);
if ((resourceId & 0xff000000) == 0x01000000) {
icon.mSystemIconResourceId = resourceId;
} else {
icon.loadResource(context, context.getResources(), resourceId);
}
systemIcons.append(type, icon);
return icon;
}
private static int getSystemIconTypeIndex(int type) {
switch (type) {
...
case TYPE_SPOT_TOUCH:
return com.android.internal.R.styleable.Pointer_pointerIconSpotTouch;
...
default:
return 0;
}
}
資源 ID 為 pointer_spot_touch_icon。
<!-- frameworks/base/core/res/res/drawable/pointer_spot_touch_icon.xml -->
<?xml version="1.0" encoding="utf-8"?>
<pointer-icon xmlns:android="http://schemas.android.com/apk/res/android"
android:bitmap="@drawable/pointer_spot_touch"
android:hotSpotX="16dp"
android:hotSpotY="16dp" />
其指向的圖片就是如下熟悉的 Spot png:pointer_spot_touch.png。之后的loadPointerIcon階段會(huì)將該圖片解析成 Bitmap 并被管理在SpriteIcon中。
而SpriteIcon在updatePointerLocked階段會(huì)被存放到SpriteController中,等待顯示的調(diào)度。
// frameworks/base/libs/input/MouseCursorController.cpp
void MouseCursorController::updatePointerLocked() REQUIRES(mLock) {
if (!mLocked.viewport.isValid()) {
return;
}
sp<SpriteController> spriteController = mContext.getSpriteController();
spriteController->openTransaction();
...
if (mLocked.updatePointerIcon) {
if (mLocked.requestedPointerType == mContext.getPolicy()->getDefaultPointerIconId()) {
mLocked.pointerSprite->setIcon(mLocked.pointerIcon);
...
}
mLocked.updatePointerIcon = false;
}
spriteController->closeTransaction();
}
顯示Tap
點(diǎn)擊的時(shí)候EventHub#getEvents會(huì)產(chǎn)生事件,InputReader#loopOnce會(huì)調(diào)用processEventsLocked處理事件。
// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::loopOnce() {
...
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
{ // acquire lock
...
if (count) {
processEventsLocked(mEventBuffer, count);
}
....
} // release lock
...
}
之后調(diào)用InputMapper開始加工事件,并在TouchInputMapper#cookAndDispatch的時(shí)候調(diào)用updateTouchSpots更新 PointerController的一些參數(shù)。
// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::updateTouchSpots() {
...
mPointerController->setPresentation(PointerControllerInterface::Presentation::SPOT);
mPointerController->fade(PointerControllerInterface::Transition::GRADUAL);
mPointerController->setButtonState(mCurrentRawState.buttonState);
setTouchSpots(mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex,
mCurrentCookedState.cookedPointerData.touchingIdBits, mViewport.displayId);
}
其中比較關(guān)鍵的setTouchSpots是顯示Taps的關(guān)鍵步驟,準(zhǔn)備 x、y 坐標(biāo)和壓力值。
在 Reader 而不是 Dispatch、更不是 ViewRootImpl 的時(shí)候處理的原因在于:Read 到事件即顯示可以更早地響,同時(shí)不用占用 App 進(jìn)程。
// frameworks/native/services/inputflinger/reader/mapper/TouchInputMapper.cpp
void TouchInputMapper::setTouchSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
BitSet32 spotIdBits, int32_t displayId) {
std::array<PointerCoords, MAX_POINTERS> outSpotCoords{};
for (BitSet32 idBits(spotIdBits); !idBits.isEmpty();) {
const uint32_t index = spotIdToIndex[idBits.clearFirstMarkedBit()];
float x = spotCoords[index].getX();
float y = spotCoords[index].getY();
float pressure = spotCoords[index].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE);
...
}
mPointerController->setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits, displayId);
}
其后PointerController會(huì)通過(guò)TouchSpotController創(chuàng)建Spot實(shí)例向其發(fā)送updateSprite請(qǐng)求。最后回調(diào) SpriteController調(diào)用setIcon處理。
// frameworks/base/libs/input/TouchSpotController.cpp
void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float x, float y,
int32_t displayId) {
sprite->setLayer(Sprite::BASE_LAYER_SPOT + id);
...
if (icon != mLastIcon) {
mLastIcon = icon;
if (icon) {
sprite->setIcon(*icon);
sprite->setVisible(true);
} else {
sprite->setVisible(false);
}
}
}
// frameworks/base/libs/input/SpriteController.cpp
void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) {
AutoMutex _l(mController->mLock);
...
invalidateLocked(dirty);
}
void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
...
if (!wasDirty) {
mController->invalidateSpriteLocked(this);
}
}
void SpriteController::invalidateSpriteLocked(const sp<SpriteImpl>& sprite) {
bool wasEmpty = mLocked.invalidatedSprites.isEmpty();
mLocked.invalidatedSprites.push(sprite);
if (wasEmpty) {
if (mLocked.transactionNestingCount != 0) {
mLocked.deferredSpriteUpdate = true;
} else {
mLooper->sendMessage(mHandler, Message(MSG_UPDATE_SPRITES));
}
}
}
MSG_UPDATE_SPRITES經(jīng)過(guò) Handler 回調(diào)doUpdateSprites,將取出封裝在SpriteUpdate中的SpriteIcon并執(zhí)行 draw。
// frameworks/base/libs/input/SpriteController.cpp
void SpriteController::doUpdateSprites() {
...
for (size_t i = 0; i < numSprites; i++) {
SpriteUpdate& update = updates.editItemAt(i);
if ((update.state.dirty & DIRTY_BITMAP) && update.state.surfaceDrawn) {
update.state.surfaceDrawn = false;
update.surfaceChanged = surfaceChanged = true;
}
if (update.state.surfaceControl != NULL && !update.state.surfaceDrawn
&& update.state.wantSurfaceVisible()) {
sp<Surface> surface = update.state.surfaceControl->getSurface();
if (update.state.icon.draw(surface)) {
update.state.surfaceDrawn = true;
update.surfaceChanged = surfaceChanged = true;
}
}
}
...
updates.clear();
}
最后,SpriteIcon將取出Bitmap描畫到Surface的Canvas上去。
// frameworks/base/libs/input/SpriteIcon.cpp
bool SpriteIcon::draw(sp<Surface> surface) const {
...
graphics::Paint paint;
paint.setBlendMode(ABLEND_MODE_SRC);
graphics::Canvas canvas(outBuffer, (int32_t)surface->getBuffersDataSpace());
canvas.drawBitmap(bitmap, 0, 0, &paint);
...
status = surface->unlockAndPost();
if (status) {
ALOGE("Error %d unlocking and posting sprite surface after drawing.", status);
}
return !status;
}總體流程
通過(guò)一個(gè)框圖簡(jiǎn)單回顧一下整個(gè)流程。

可以看到,簡(jiǎn)簡(jiǎn)單單的 Show taps 功能,從設(shè)置、配置、刷新再到顯示,經(jīng)歷了多個(gè)進(jìn)程、多個(gè)模塊的協(xié)力。
涉及的Input核心邏輯框圖

到此這篇關(guān)于從"Show tabs"了解Android Input系統(tǒng)的文章就介紹到這了,更多相關(guān)Android Input系統(tǒng) Show tabs內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android IPC機(jī)制利用Messenger實(shí)現(xiàn)跨進(jìn)程通信
這篇文章主要介紹了Android IPC機(jī)制中 Messager 實(shí)現(xiàn)跨進(jìn)程通信的知識(shí),對(duì)Android學(xué)習(xí)通信知識(shí)非常重要,需要的同學(xué)可以參考下2016-07-07
android實(shí)現(xiàn)添加耳機(jī)狀態(tài)圖標(biāo)的方法
這篇文章主要介紹了android實(shí)現(xiàn)添加耳機(jī)狀態(tài)圖標(biāo)的方法,較為詳細(xì)的分析了Android實(shí)現(xiàn)添加耳機(jī)圖標(biāo)的原理與相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Android編程實(shí)現(xiàn)輸入框動(dòng)態(tài)自動(dòng)提示功能
這篇文章主要介紹了Android編程實(shí)現(xiàn)輸入框動(dòng)態(tài)自動(dòng)提示功能,結(jié)合實(shí)例形式分析了AutoCompleteTextView相關(guān)使用技巧,需要的朋友可以參考下2017-03-03
android讀寫sd卡操作寫入數(shù)據(jù)讀取數(shù)據(jù)示例
這篇文章主要介紹了android讀寫sd卡操作,示例實(shí)現(xiàn)了寫入數(shù)據(jù)讀取數(shù)據(jù)的功能,大家參考使用吧2014-01-01
Android Studio進(jìn)行APP圖標(biāo)更改的兩種方式總結(jié)
這篇文章主要介紹了Android Studio進(jìn)行APP圖標(biāo)更改的兩種方式總結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
去掉RecycleView或者ListView上下滑動(dòng)陰影的方法
下面小編就為大家分享一篇去掉RecycleView或者ListView上下滑動(dòng)陰影的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01

