編輯:關於Android編程
在開始講述touch事件流程之前,還簡單介紹下TouchEvent,View和ViewGroup。
ACTION_DOWN
(1次) -> ACTION_MOVE
(N次) -> ACTION_UP
(1次),Pointer
會有一個id和index。對於多指操作,通過pointerindex來獲取指定Pointer的觸屏位置。比如,對於單點操作時獲取x坐標通過getX()
,而多點操作獲取x坐標通過getX(pointerindex)
Drawable.Callback
, 按鍵相關的接口KeyEvent.Callback
, 交互相關的接口AccessibilityEventSource
。比如Button繼承自View。abstract
類,一組View的集合,可以包含View和ViewGroup,是所有布局的父類或間接父類。繼承了View
,實現了ViewParent
(用於與父視圖交互的接口), ViewManager
(用於添加、刪除、更新子視圖到Activity的接口)。比如常用的LinearLayout,RelativeLayout都是繼承自ViewGroup。InputEventReceiver
中的dispatchInputEvent
方法。經過層層調用,交由Activity的dispatchTouchEvent
方法來處理。當用戶觸摸屏幕時會產生一系列MotionEvent,那麼Activity,ViewGroup,View之間是如何處理呢?先通過一張圖來從全局性描述整個流程,後面再詳細介紹。
下面先給出通過對源碼分析的一些結論,並在後面段落的源碼分析中,詳細分析了整個過程,並解釋了結論。
onInterceptTouchEvent
返回值true表示事件攔截, onTouch/onTouchEvent
返回值true表示事件消費。
觸摸事件先交由Activity.dispatchTouchEvent
。再一層層往下分發,當中間的ViewGroup都不攔截時,進入最底層的View後,開始由最底層的OnTouchEvent
來處理,如果一直不消費,則最後返回到Activity.OnTouchEvent
。
ViewGroup才有onInterceptTouchEvent
攔截方法。在分發過程中,中間任何一層ViewGroup都可以直接攔截,則不再往下分發,而是交由發生攔截操作的ViewGroup的OnTouchEvent
來處理。
子View可調用requestDisallowInterceptTouchEvent
方法,來設置disallowIntercept=true
,從而阻止父ViewGroup的onInterceptTouchEvent
攔截操作。
OnTouchEvent由下往上冒泡時,當中間任何一層的OnTouchEvent消費該事件,則不再往上傳遞,表示事件已處理。
如果View沒有消費ACTION_DOWN事件,則之後的ACTION_MOVE等事件都不會再接收。
只要View.onTouchEvent
是可點擊或可長按,則消費該事件.
onTouch
優先於onTouchEvent
執行,上面流程圖中省略,onTouch
的位置在onTouchEvent
前面。當onTouch
返回true,則不執行onTouchEvent
,否則會執行onTouchEvent。onTouch
只有View設置了OnTouchListener
,且是enable的才執行該方法。
####1.1 dispatchTouchEvent
這是Touch事件傳遞到上層的第一個處理方法,是由觸屏事件調用。如果重寫該方法,則會在分發事件之前攔截所有的觸摸事件,源碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//第一次按下操作時,用戶希望能與設備進行交互,可通過實現該方法
onUserInteraction();
}
//獲取當前Activity的頂層窗口是PhoneWindow
if (getWindow().superDispatchTouchEvent(ev)) { //見代碼(1-1-1)
return true;
}
//當沒有任何view處理時,交由activity的onTouchEvent處理
return onTouchEvent(ev); // 見代碼(1.2)
}
(1-1-1)
public boolean superDispatchTouchEvent(KeyEvent event) {
return mDecor.superDispatcTouchEvent(event); // 見代碼(2.1)
}
PhoneWindow的最頂View是DecorView,再交由DecorView處理。而DecorView的間距父類是ViewGroup,接著調用 ViewGroup.dispatchTouchEvent()方法。為了精簡篇幅,有些中間函數調用不涉及關鍵邏輯,可能會直接跳過。
####1.2 onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
//當窗口需要關閉時,消費掉當前event
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
####2.1 dispatchTouchEvent
這是整個touch事件流程中,最核心的方法,涵蓋了大部分的事件分發的邏輯,這個方法由200多行(這麼冗長的方法,閱讀起來很費勁,下面通過分割線把整個方法劃分幾個區域),言歸正傳,下面重點講解。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
//用於Debug,通知verifier來檢查Touch事件
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
//根據隱私策略而來決定是否過濾本次觸摸事件
if (onFilterTouchEventForSecurity(ev)) { //見代碼(2-1-1)
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//發生ACTION_DOWN事件
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 取消並清除之前所有的觸摸targets,可能是由於ANR或者其他狀態改變
cancelAndClearTouchTargets(ev);
// 重置觸摸狀態
resetTouchState();
}
//---------------------------------start--------------------------------------
// 發生ACTION_DOWN事件或者已經發生過ACTION_DOWN;才進入此區域,主要功能是攔截器
//只有發生過ACTION_DOWN事件,則mFirstTouchTarget != null;
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//可通過調用requestDisallowInterceptTouchEvent,不讓父View攔截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//判斷是否允許調用攔截器
if (!disallowIntercept) {
//調用攔截方法
intercepted = onInterceptTouchEvent(ev); //見代碼(2.2)
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 當沒有觸摸targets,且不是down事件時,開始持續攔截觸摸。
intercepted = true;
}
//----------------------------------end-----------------------------------------
// 如果已攔截或者某個view正在處理gesture時,開始正常的事件分發
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 檢測取消器.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//更新觸摸targets列表
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//---------------------------------start--------------------------------------
//不取消事件,同時不攔截事件, 並且是Down事件才進入該區域
if (!canceled && !intercepted) {
//把事件分發給所有的子視圖,尋找可以獲取焦點的視圖。
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // down事件等於0
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign); //清空早先的觸摸對象
final int childrenCount = mChildrenCount;
//第一次down事件,同時子視圖不會空時
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 從視圖最上層到下層,獲取所有能接收該事件的子視圖
final ArrayList<View> preorderedList = buildOrderedChildList(); //見代碼(2-1-2)
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
/* 從最底層的父視圖開始遍歷,
** 找尋newTouchTarget,並賦予view與 pointerIdBits;
** 如果已經存在找尋newTouchTarget,說明正在接收觸摸事件,則跳出循環。
*/
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// 如果當前視圖無法獲取用戶焦點,則跳過本次循環
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//如果view不可見,或者觸摸的坐標點不在view的范圍內,則跳過本次循環
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
// 已經開始接收觸摸事件,並退出整個循環。
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
//重置取消或抬起標志位
resetCancelNextUpFlag(child);
//如果觸摸位置在child的區域內,則把事件分發給子View或ViewGroup
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { //見代碼(2-1-3)
// 獲取TouchDown的時間點
mLastTouchDownTime = ev.getDownTime();
// 獲取TouchDown的Index
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
//獲取TouchDown的x,y坐標
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//添加TouchTarget,則mFirstTouchTarget != null。
newTouchTarget = addTouchTarget(child, idBitsToAssign); //見代碼(2-1-4)
//表示以及分發給NewTouchTarget
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
// 清除視圖列表
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
//將mFirstTouchTarget的鏈表最後的touchTarget賦給newTouchTarget
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//----------------------------------end-----------------------------------------
//---------------------------------start--------------------------------------
// mFirstTouchTarget賦值是在通過addTouchTarget方法獲取的;
// 只有處理ACTION_DOWN事件,才會進入addTouchTarget方法。
// 這也正是當View沒有消費ACTION_DOWN事件,則不會接收其他MOVE,UP等事件的原因
if (mFirstTouchTarget == null) {
//沒有觸摸target,則由當前ViewGroup來處理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS); //見代碼(2-1-3)
} else {
//如果View消費ACTION_DOWN事件,那麼MOVE,UP等事件相繼開始執行
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) { //見代碼(2-1-3)
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//----------------------------------end-----------------------------------------
//當發生抬起或取消事件,更新觸摸targets
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
} //此處大括號,是if (onFilterTouchEventForSecurity(ev))的結尾
//通知verifier由於當前時間未處理,那麼該事件其余的都將被忽略
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
(2-1-1)
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
} 根據隱私策略來過濾觸摸事件。當返回true,表示繼續分發事件;當返回flase,表示該事件應該被過濾掉,不再進行任何分發。
(2-1-2)
ArrayList<View> buildOrderedChildList() {
final int count = mChildrenCount;
if (count <= 1 || !hasChildWithZ()) return null;
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<View>(count);
} else {
mPreSortedChildren.ensureCapacity(count);
}
final boolean useCustomOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < mChildrenCount; i++) {
// 添加下一個子視圖到列表
int childIndex = useCustomOrder ? getChildDrawingOrder(mChildrenCount, i) : i;
View nextChild = mChildren[childIndex];
float currentZ = nextChild.getZ(); //見代碼(2-1-2-1)
int insertIndex = i;
//按Z軸,從小到大排序所有的子視圖
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
} 獲取一個視圖組的先序列表,通過虛擬的Z軸來排序。
(2-1-2-1)
public float getZ() {
return getElevation() + getTranslationZ();
}
getZ()
用於獲取Z軸坐標。屏幕只有x,y坐標,而Z是虛擬的,可通過setElevation()
,setTranslationZ()
或者setZ()
方法來修改Z軸的坐標值。
(2-1-3)
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 發生取消操作時,不再執行後續的任何操作
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
//由於某些原因,發生不一致的操作,那麼將拋棄該事件
if (newPointerIdBits == 0) {
return false;
}
//---------------------------------start--------------------------------------
//分發的主要區域
final MotionEvent transformedEvent;
//判斷預期的pointer id與事件的pointer id是否相等
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//不存在子視圖時,ViewGroup調用View.dispatchTouchEvent分發事件,再調用ViewGroup.onTouchEvent來處理事件
handled = super.dispatchTouchEvent(event); // 見代碼(3.1)
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
//將觸摸事件分發給子ViewGroup或View;
//如果是ViewGroup,則調用代碼(2.1);
//如果是View,則調用代碼(3.1);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY); //調整該事件的位置
}
return handled;
}
transformedEvent = MotionEvent.obtain(event); //拷貝該事件,來創建一個新的MotionEvent
} else {
//分離事件,獲取包含newPointerIdBits的MotionEvent
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
//不存在子視圖時,ViewGroup調用View.dispatchTouchEvent分發事件,再調用ViewGroup.onTouchEvent來處理事件
handled = super.dispatchTouchEvent(transformedEvent); //見代碼(3.1)
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
//將該視圖的矩陣進行轉換
transformedEvent.transform(child.getInverseMatrix());
}
//將觸摸事件分發給子ViewGroup或View;
/如果是ViewGroup,則調用代碼(2.1);
//如果是View,則調用代碼(3.1);
handled = child.dispatchTouchEvent(transformedEvent);
}
//----------------------------------end-----------------------------------------
//回收transformedEvent
transformedEvent.recycle();
return handled;
}
該方法是ViewGroup真正處理事件的地方,分發子View來處理事件,過濾掉不相干的pointer ids。當子視圖為null時,MotionEvent將會發送給該ViewGroup。最終調用View.dispatchTouchEvent方法來分發事件。
(2-1-4)
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
調用該方法,獲取了TouchTarget,同時mFirstTouchTarget不再為null。
####2.2 onInterceptTouchEvent
當返回true,表示該事件被當前視圖攔截;當返回false,繼續執行事件分發。如果在Activity中直接攔截了,那麼將事件不會再往其他View分發,而是由Activity.onTouchEvent()方法處理。
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
#### 3.1 dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
//在Down事件之前,如果存在滾動操作則停止。不存在則不進行操作
stopNestedScroll();
}
//---------------------------------start--------------------------------------
// 該區域說明了mOnTouchListener.onTouch優先於onTouchEvent。
if (onFilterTouchEventForSecurity(event)) {
//當存在OnTouchListener,且視圖狀態為ENABLED時,調用onTouch()方法
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//如果已經消費事件,則返回True
result = true;
}
//如果OnTouch()沒有消費Touch事件則調用OnTouchEvent()
if (!result && onTouchEvent(event)) { // 見代碼(2.6)
//如果已經消費事件,則返回True
result = true;
}
}
//----------------------------------end-----------------------------------------
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// 處理取消或抬起操作
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
//---------------------------------start--------------------------------------
// 當View狀態為DISABLED,如果可點擊或可長按,則返回True,即消費事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
//----------------------------------end-----------------------------------------
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//---------------------------------start--------------------------------------
//當View狀態為ENABLED,如果可點擊或可長按,則返回True,即消費事件;
//與前面的的結合,可得出結論:只要view是可點擊或可長按,則消費該事件.
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress) {
//這是Tap操作,移除長按回調方法
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//調用View.OnClickListener
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
//獲取是否處於可滾動的視圖內
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//當處於可滾動視圖內,則延遲TAP_TIMEOUT,再反饋按壓狀態,用來判斷用戶是否想要滾動。默認延時為100ms
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
//當不再滾動視圖內,則立刻反饋按壓狀態
setPressed(true, x, y);
//檢測是否是長按
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
//----------------------------------end-----------------------------------------
return false;
}
從官網下載了ndk,可是運行ndk-build竟然提示錯誤E:\android-ndk-r10d>ndk-build‘”E:\
很久前也寫過一篇Android數據庫操作相關內容。在正式項目中,我們通常會使用數據庫開源框架如GreenDao來對數據庫進行操作。感覺很久沒有直接使用Sql語句了,這幾天
1、為什麼需要異步加載。因為我們都知道在Android中的是單線程模型,不允許其他的子線程來更新UI,只允許UI線程(主線程更新UI),否則會多個線程都去更新UI會造成U
思路分析:1、自定義View實現字母導航欄2、ListView實現聯系人列表3、字母導航欄滑動事件處理4、字母導航欄與中間字母的聯動5、字母導航欄與ListView的聯動