//处理焦点
* @param focusedRect The starting point of the search.
* @param direction Direction to look.
* @return The next focusable view, or null if none exists.
*/
public View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) {
mFocusedRect.set(focusedRect);
return findNextFocus(root, null, mFocusedRect, direction);
}
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
//Log.d(TAG,Log.getStackTraceString(new Throwable()));
if (JoyarHelper.DBG) {
Log.d(TAG, "findNextFocus root:" + root + " focused:" + focused);
}
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
ViewGroup rv = JoyarHelper.getInstance().FocusFinderFindNextFocus(this, root, focused, focusedRect, direction);
if (rv != null) {
effectiveRoot = rv;
}
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
if (JoyarHelper.DBG) {
Log.d(TAG, "######## findNextFocus focusables start size:" + focusables.size() + " ########");
for (View f : focusables) {
Log.d(TAG, f.toString());
}
Log.d(TAG, "######## findNextFocus focusables end ########");
}
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
if (JoyarHelper.DBG) {
Log.d("FocusFinder", "find next1:" + next + " focused:" + focused);
}
if (next == null && focused != null) {
next = JoyarHelper.getInstance().FocusFinderFindSmallFocusView(focusables, focused);
}
if (JoyarHelper.DBG) {
Log.d("FocusFinder", "find next2:" + next);
}
}
} finally {
focusables.clear();
}
return next;
}
boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
// if rect1 isn't exclusively in the src beam, it doesn't win
if (rect2InSrcBeam || !rect1InSrcBeam) {
return false;
}
if (JoyarHelper.getInstance().FocusFinderBeamBeats(this, direction, source, rect1, rect2)) {
return true;
}
// we know rect1 is in the beam, and rect2 is not
// if rect1 is to the direction of, and rect2 is not, rect1 wins.
// for example, for direction left, if rect1 is to the left of the source
// and rect2 is below, then we always prefer the in beam rect1, since rect2
// could be reached by going down.
if (!isToDirectionOf(direction, source, rect2)) {
return true;
}
/**
* Is destRect a candidate for the next focus given the direction? This
* checks whether the dest is at least partially to the direction of (e.g left of)
* from source.
*
* Includes an edge case for an empty rect (which is used in some cases when
* searching from a point on the screen).
*/
boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
if (JoyarHelper.getInstance().FocusFinderIsCaredCandidate(this, srcRect, destRect, direction)) {
return JoyarHelper.getInstance().FocusFinderIsCandidate(this, srcRect, destRect, direction);
}
switch (direction) {
case View.FOCUS_LEFT:
return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
&& srcRect.left > destRect.left;
case View.FOCUS_RIGHT:
return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
&& srcRect.right < destRect.right;
case View.FOCUS_UP:
if (JoyarHelper.DBG) {
Log.d(TAG, "isCandidate bottom:" + (srcRect.bottom > destRect.bottom)
+ "top and bottom:" + (srcRect.top >= destRect.bottom)
+ " top and top:" + (srcRect.top > destRect.top));
}
return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
&& srcRect.top > destRect.top;
case View.FOCUS_DOWN:
return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
&& srcRect.bottom < destRect.bottom;
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P;
sCanFocusZeroSized = targetSdkVersion < Build.VERSION_CODES.P;
sAlwaysAssignFocus = targetSdkVersion < Build.VERSION_CODES.P;
sAcceptZeroSizeDragShadow = targetSdkVersion < Build.VERSION_CODES.P;
sCompatibilityDone = true;
}
JoyarHelper.getInstance().ViewAddPerformClickViews(this);
}
public void setOnClickListener(@Nullable OnClickListener l) {
boolean handle = JoyarHelper.getInstance().ViewSetOnClickListener(this, l);
if (!handle && !isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public boolean performClick() {
if (JoyarHelper.getInstance().ViewPerformClick(this)) {
return false;
}
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
JoyarHelper.getInstance().ViewOnClick(this);
result = true;
} else if (JoyarHelper.getInstance().ViewOnPerformClick(this)) {
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
JoyarHelper.getInstance().ViewSetOnTouchListener(this, l);
getListenerInfo().mOnTouchListener = l;
}
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
JoyarHelper.getInstance().ViewRequestChildFocusPre(this, mParent);
if (JoyarHelper.getInstance().isInNf()) {
JoyarHelper.getInstance().ViewGroupRequestChildFocus(mParent, this, this);
} else {
mParent.requestChildFocus(this, this);
}
JoyarHelper.getInstance().ViewRequestChildFocusSuper(this, mParent, this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
invalidate(true);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnFocusChangeListener != null) {
li.mOnFocusChangeListener.onFocusChange(this, gainFocus);
}
if (mAttachInfo != null) {
mAttachInfo.mKeyDispatchState.reset(this);
}
JoyarHelper.getInstance().ViewOnFocusChanged(this, gainFocus, direction);
notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
}
@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
JoyarHelper.getInstance().ViewSetVisibility(this, visibility);
setFlags(visibility, VISIBILITY_MASK);
}
* @param focusable If true, this view can receive the focus.
*
* @see #setFocusableInTouchMode(boolean)
* @see #setFocusable(int)
* @attr ref android.R.styleable#View_focusable
*/
public void setFocusable(boolean focusable) {
if (JoyarHelper.getInstance().ViewSetFocusable(this, focusable)) {
return;
}
setFocusable(focusable ? FOCUSABLE : NOT_FOCUSABLE);
}
public void setFocusableInTouchMode(boolean focusableInTouchMode) {
if (JoyarHelper.getInstance().ViewSetFocusableInTouchMode(this, focusableInTouchMode)) {
return;
}
// Focusable in touch mode should always be set before the focusable flag
// otherwise, setting the focusable flag will trigger a focusableViewAvailable()
// which, in touch mode, will not successfully request focus on this view
// because the focusable in touch mode flag is not set
setFlags(focusableInTouchMode ? FOCUSABLE_IN_TOUCH_MODE : 0, FOCUSABLE_IN_TOUCH_MODE);
// Clear FOCUSABLE_AUTO if set.
if (focusableInTouchMode) {
// Clears FOCUSABLE_AUTO if set.
setFlags(FOCUSABLE, FOCUSABLE_MASK);
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
//wangny todo
JoyarHelper.getInstance().ViewFocusSearch(this, direction);
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if (JoyarHelper.DBG) {
Log.d(VIEW_LOG_TAG, "requestFocusNoSearch view:" + this + " int parent:" + mParent);
}
if (!canTakeFocus()) {
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
if (JoyarHelper.getInstance().isInNf() && JoyarHelper.getInstance().ViewIsFullscreenView(this)) {
if (JoyarHelper.DBG) {
Log.d(VIEW_LOG_TAG, "cause in nf and fullscreen view, skip it!");
}
return false;
}
if (!isLayoutValid()) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
} else {
clearParentsWantFocus();
}
if (JoyarHelper.DBG) {
Log.d(VIEW_LOG_TAG, "finally gain focus view:" + this + " parent:" + mParent);
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
Rect tmpRect = new Rect(mScrollX, mScrollY, mScrollX + mRight - mLeft, mScrollY + mBottom - mTop);
JoyarHelper.getInstance().ViewOnDraw(this, canvas, tmpRect);
if (debugDraw()) {
debugDrawFocus(canvas);
}
} else {
draw(canvas);
}
}
} finally {
renderNode.end(canvas);
setDisplayListProperties(renderNode);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
Rect tmpRect = new Rect(mScrollX, mScrollY, mScrollX + mRight - mLeft, mScrollY + mBottom - mTop);
JoyarHelper.getInstance().ViewOnDraw(this, canvas, tmpRect);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
Rect tmpRect = new Rect(mScrollX, mScrollY, mScrollX + mRight - mLeft, mScrollY + mBottom - mTop);
JoyarHelper.getInstance().ViewOnDraw(this, canvas, tmpRect);
if (debugDraw()) {
debugDrawFocus(canvas);
}
}
ViewGroup.java
java复制代码
}
if (mParent != null) {
JoyarHelper.getInstance().ViewRequestChildFocusPre(this, getParent());
if (JoyarHelper.getInstance().isInNf()) {
JoyarHelper.getInstance().ViewGroupRequestChildFocus(mParent, this, focused);
} else {
mParent.requestChildFocus(this, focused);
}
JoyarHelper.getInstance().ViewRequestChildFocusSuper(this, getParent(), this, focused);
}
}
void setDefaultFocus(View child) {
// Stop at any higher view which is explicitly focused-by-default
if (mDefaultFocus != null && mDefaultFocus.isFocusedByDefault()) {
return;
}
mDefaultFocus = child;
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).setDefaultFocus(this);
}
}
/**
* Clears the default-focus chain from {@param child} up to the first parent which has another
* default-focusable branch below it or until there is no default-focus chain.
*
* @param child
*/
void clearDefaultFocus(View child) {
// Stop at any higher view which is explicitly focused-by-default
if (mDefaultFocus != child && mDefaultFocus != null
&& mDefaultFocus.isFocusedByDefault()) {
return;
}
mDefaultFocus = null;
// Search child siblings for default focusables.
for (int i = 0; i < mChildrenCount; ++i) {
View sibling = mChildren[i];
if (sibling.isFocusedByDefault()) {
mDefaultFocus = sibling;
return;
} else if (mDefaultFocus == null && sibling.hasDefaultFocus()) {
mDefaultFocus = sibling;
}
}
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).clearDefaultFocus(this);
}
}
@Override
boolean hasDefaultFocus() {
return mDefaultFocus != null || super.hasDefaultFocus();
}
/**
* Removes {@code child} (and associated focusedInCluster chain) from the cluster containing
* it.
* <br>
* This is intended to be run on {@code child}'s immediate parent. This is necessary because
* the chain is sometimes cleared after {@code child} has been detached.
*/
void clearFocusedInCluster(View child) {
if (mFocusedInCluster != child) {
return;
}
clearFocusedInCluster();
}
/**
* Removes the focusedInCluster chain from this up to the cluster containing it.
*/
void clearFocusedInCluster() {
View top = findKeyboardNavigationCluster();
ViewParent parent = this;
do {
((ViewGroup) parent).mFocusedInCluster = null;
if (parent == top) {
break;
}
parent = parent.getParent();
} while (parent instanceof ViewGroup);
}
@Override
public void focusableViewAvailable(View v) {
if (mParent != null
// shortcut: don't report a new focusable view if we block our descendants from
// getting focus or if we're not visible
&& (getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS)
&& ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
&& (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())
// shortcut: don't report a new focusable view if we already are focused
// (and we don't prefer our descendants)
//
// note: knowing that mFocused is non-null is not a good enough reason
// to break the traversal since in that case we'd actually have to find
// the focused view and make sure it wasn't FOCUS_AFTER_DESCENDANTS and
// an ancestor of v; this will get checked for at ViewAncestor
&& !(isFocused() && getDescendantFocusability() != FOCUS_AFTER_DESCENDANTS)) {
mParent.focusableViewAvailable(v);
}
}
@Override
public boolean showContextMenuForChild(View originalView) {
if (isShowingContextMenuWithCoords()) {
// We're being called for compatibility. Return false and let the version
// with coordinates recurse up.
return false;
}
return mParent != null && mParent.showContextMenuForChild(originalView);
}
/**
* @hide used internally for compatibility with existing app code only
*/
public final boolean isShowingContextMenuWithCoords() {
return (mGroupFlags & FLAG_SHOW_CONTEXT_MENU_WITH_COORDS) != 0;
}
@Override
public boolean showContextMenuForChild(View originalView, float x, float y) {
try {
mGroupFlags |= FLAG_SHOW_CONTEXT_MENU_WITH_COORDS;
if (showContextMenuForChild(originalView)) {
return true;
}
} finally {
mGroupFlags &= ~FLAG_SHOW_CONTEXT_MENU_WITH_COORDS;
}
return mParent != null && mParent.showContextMenuForChild(originalView, x, y);
}
@Override
public ActionMode startActionModeForChild(View originalView, ActionMode.Callback callback) {
if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED) == 0) {
// This is the original call.
try {
mGroupFlags |= FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED;
return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY);
} finally {
mGroupFlags &= ~FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED;
}
} else {
// We are being called from the new method with type.
return SENTINEL_ACTION_MODE;
}
}
@Override
public ActionMode startActionModeForChild(
View originalView, ActionMode.Callback callback, int type) {
if ((mGroupFlags & FLAG_START_ACTION_MODE_FOR_CHILD_IS_NOT_TYPED) == 0
&& type == ActionMode.TYPE_PRIMARY) {
ActionMode mode;
try {
mGroupFlags |= FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED;
mode = startActionModeForChild(originalView, callback);
} finally {
mGroupFlags &= ~FLAG_START_ACTION_MODE_FOR_CHILD_IS_TYPED;
}
if (mode != SENTINEL_ACTION_MODE) {
return mode;
}
}
if (mParent != null) {
try {
return mParent.startActionModeForChild(originalView, callback, type);
} catch (AbstractMethodError ame) {
// Custom view parents might not implement this method.
return mParent.startActionModeForChild(originalView, callback);
}
}
return null;
}
/**
* @hide
*/
@Override
public boolean dispatchActivityResult(
String who, int requestCode, int resultCode, Intent data) {
if (super.dispatchActivityResult(who, requestCode, resultCode, data)) {
return true;
}
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.dispatchActivityResult(who, requestCode, resultCode, data)) {
return true;
}
}
return false;
}
/**
* Find the nearest view in the specified direction that wants to take
* focus.
*
* @param focused The view that currently has focus
* @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
* FOCUS_RIGHT, or 0 for not applicable.
*/
@Override
public View focusSearch(View focused, int direction) {
//wangny todo
JoyarHelper.getInstance().ViewGroupFocusSearch(this, focused, direction);
if (isRootNamespace()) {
// root namespace means we should consider ourselves the top of the
// tree for focus searching; otherwise we could be focus searching
// into other tabs. see LocalActivityManager and TabHost for more info.
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
return mParent.focusSearch(focused, direction);
}
return null;
}
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
return false;
}
@Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
ViewParent parent = mParent;
if (parent == null) {
return false;
}
final boolean propagate = onRequestSendAccessibilityEvent(child, event);
if (!propagate) {
return false;
}
return parent.requestSendAccessibilityEvent(this, event);
}
/**
* Called when a child has requested sending an {@link AccessibilityEvent} and
* gives an opportunity to its parent to augment the event.
* <p>
* If an {@link android.view.View.AccessibilityDelegate} has been specified via calling
* {@link android.view.View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its
* {@link android.view.View.AccessibilityDelegate#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)}
* is responsible for handling this call.
* </p>
*
* @param child The child which requests sending the event.
* @param event The event to be sent.
* @return True if the event should be sent.
*
* @see #requestSendAccessibilityEvent(View, AccessibilityEvent)
*/
public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
if (mAccessibilityDelegate != null) {
return mAccessibilityDelegate.onRequestSendAccessibilityEvent(this, child, event);
} else {
return onRequestSendAccessibilityEventInternal(child, event);
}
}
/**
* @see #onRequestSendAccessibilityEvent(View, AccessibilityEvent)
*
* Note: Called from the default {@link View.AccessibilityDelegate}.
*
* @hide
*/
public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
return true;
}
/**
* Called when a child view has changed whether or not it is tracking transient state.
*/
@Override
public void childHasTransientStateChanged(View child, boolean childHasTransientState) {
final boolean oldHasTransientState = hasTransientState();
if (childHasTransientState) {
mChildCountWithTransientState++;
} else {
mChildCountWithTransientState--;
}
final boolean newHasTransientState = hasTransientState();
if (mParent != null && oldHasTransientState != newHasTransientState) {
try {
mParent.childHasTransientStateChanged(this, newHasTransientState);
} catch (AbstractMethodError e) {
Log.e(TAG, mParent.getClass().getSimpleName() +
" does not fully implement ViewParent", e);
}
}
}
@Override
public boolean hasTransientState() {
return mChildCountWithTransientState > 0 || super.hasTransientState();
}
@Override
public boolean dispatchUnhandledMove(View focused, int direction) {
return mFocused != null &&
mFocused.dispatchUnhandledMove(focused, direction);
}
@Override
public void clearChildFocus(View child) {
if (DBG) {
System.out.println(this + " clearChildFocus()");
}
mFocused = null;
if (mParent != null) {
mParent.clearChildFocus(this);
}
}
@Override
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
}
if (mFocused == null) {
super.clearFocus();
} else {
View focused = mFocused;
mFocused = null;
focused.clearFocus();
}
}
@Override
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
if (mFocused == null) {
super.unFocus(focused);
} else {
mFocused.unFocus(focused);
mFocused = null;
}
}
/**
* Returns the focused child of this view, if any. The child may have focus
* or contain focus.
*
* @return the focused child or null.
*/
public View getFocusedChild() {
return mFocused;
}
View getDeepestFocusedChild() {
View v = this;
while (v != null) {
if (v.isFocused()) {
return v;
}
v = v instanceof ViewGroup ? ((ViewGroup) v).getFocusedChild() : null;
}
return null;
}
/**
* Returns true if this view has or contains focus
*
* @return true if this view has or contains focus
*/
@Override
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
}
/*
* (non-Javadoc)
*
* @see android.view.View#findFocus()
*/
@Override
public View findFocus() {
if (DBG) {
System.out.println("Find focus in " + this + ": flags="
+ isFocused() + ", child=" + mFocused);
}
if (isFocused()) {
return this;
}
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}
@Override
boolean hasFocusable(boolean allowAutoFocus, boolean dispatchExplicit) {
// This should probably be super.hasFocusable, but that would change
// behavior. Historically, we have not checked the ancestor views for
// shouldBlockFocusForTouchscreen() in ViewGroup.hasFocusable.
// Invisible and gone views are never focusable.
if ((mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
// Only use effective focusable value when allowed.
if ((allowAutoFocus || getFocusable() != FOCUSABLE_AUTO) && isFocusable()) {
return true;
}
// Determine whether we have a focused descendant.
final int descendantFocusability = getDescendantFocusability();
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
return hasFocusableChild(dispatchExplicit);
}
return false;
}
boolean hasFocusableChild(boolean dispatchExplicit) {
// Determine whether we have a focusable descendant.
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
// In case the subclass has overridden has[Explicit]Focusable, dispatch
// to the expected one for each child even though we share logic here.
if ((dispatchExplicit && child.hasExplicitFocusable())
|| (!dispatchExplicit && child.hasFocusable())) {
return true;
}
}
return false;
}
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);
if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {
if (focusSelf) {
super.addFocusables(views, direction, focusableMode);
}
return;
}
if (blockFocusForTouchscreen) {
focusableMode |= FOCUSABLES_TOUCH_MODE;
}
if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
super.addFocusables(views, direction, focusableMode);
}
int count = 0;
final View[] children = new View[mChildrenCount];
for (int i = 0; i < mChildrenCount; ++i) {
View child = mChildren[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
children[count++] = child;
}
}
FocusFinder.sort(children, 0, count, this, isLayoutRtl());
for (int i = 0; i < count; ++i) {
if (!JoyarHelper.getInstance().ViewGroupAddFocusables(this, children[i], views, direction, focusableMode)) {
children[i].addFocusables(views, direction, focusableMode);
}
}
// When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
// there aren't any focusable descendants. this is
// to avoid the focus search finding layouts when a more precise search
// among the focusable children would be more interesting.
if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
&& focusableCount == views.size()) {
super.addFocusables(views, direction, focusableMode);
}
ViewRootImpl.java
java复制代码
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = JoyarHelper.getInstance().ViewRootImplPerformFocusNavigation(event, View.FOCUS_UP);
//direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = JoyarHelper.getInstance().ViewRootImplPerformFocusNavigation(event, View.FOCUS_DOWN);
//direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
JoyarHelper.getInstance().ViewRootImplOnPerformFocusNavigation(event, direction);
if (direction != 0) {
View focused = mView.findFocus();
if (JoyarHelper.DBG) {
Log.d(TAG, "mView findFocus:" + focused);
}
if (focused != null) {
View v = focused.focusSearch(direction);
if (JoyarHelper.DBG) {
Log.d(TAG, "focused focusSearch:" + v);
}
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return true;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
private boolean performKeyboardGroupNavigation(int direction) {
final View focused = mView.findFocus();
if (focused == null && mView.restoreDefaultFocus()) {
return true;
}
View cluster = focused == null ? keyboardNavigationClusterSearch(null, direction)
: focused.keyboardNavigationClusterSearch(null, direction);
// Since requestFocus only takes "real" focus directions (and therefore also
// restoreFocusInCluster), convert forward/backward focus into FOCUS_DOWN.
int realDirection = direction;
if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
realDirection = View.FOCUS_DOWN;
}
if (cluster != null && cluster.isRootNamespace()) {
// the default cluster. Try to find a non-clustered view to focus.
if (cluster.restoreFocusNotInCluster()) {
playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
return true;
}
// otherwise skip to next actual cluster
cluster = keyboardNavigationClusterSearch(null, direction);
}
if (cluster != null && cluster.restoreFocusInCluster(realDirection)) {
playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
return true;
}
return false;
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
if (mUnhandledKeyManager.preViewDispatch(event)) {
return FINISH_HANDLED;
}
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event) && !JoyarHelper.getInstance().ViewRootImplProcessKeyEvent(mView, event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// This dispatch is for windows that don't have a Window.Callback. Otherwise,
// the Window.Callback usually will have already called this (see
// DecorView.superDispatchKeyEvent) leaving this call a no-op.
if (mUnhandledKeyManager.dispatch(mView, event)) {
return FINISH_HANDLED;
}
int groupNavigationDirection = 0;
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
groupNavigationDirection = View.FOCUS_FORWARD;
} else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
groupNavigationDirection = View.FOCUS_BACKWARD;
}
}
// If a modifier is held, try to interpret the key as a shortcut.
if (event.getAction() == KeyEvent.ACTION_DOWN
&& !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())
&& groupNavigationDirection == 0) {
if (mView.dispatchKeyShortcutEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
}
// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (groupNavigationDirection != 0) {
if (performKeyboardGroupNavigation(groupNavigationDirection)) {
return FINISH_HANDLED;
}
} else {
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
return FORWARD;
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
if (mConsumeBatchedInputScheduled) {
scheduleConsumeBatchedInputImmediately();
}
}
return handled ? FINISH_HANDLED : FORWARD;
}
private void maybeUpdatePointerIcon(MotionEvent event) {
if (event.getPointerCount() == 1 && event.isFromSource(InputDevice.SOURCE_MOUSE)) {
if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
|| event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
// Other apps or the window manager may change the icon type outside of
// this app, therefore the icon type has to be reset on enter/exit event.
mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
}
if (event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) {
if (!updatePointerIcon(event) &&
event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
}
}
}
}
private int processTrackballEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
if (event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE)) {
if (!hasPointerCapture() || mView.dispatchCapturedPointerEvent(event)) {
return FINISH_HANDLED;
}
}
if (mView.dispatchTrackballEvent(event)) {
return FINISH_HANDLED;
}
return FORWARD;
}
private int processGenericMotionEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
// Deliver the event to the view.
if (mView.dispatchGenericMotionEvent(event)) {
return FINISH_HANDLED;
}
return FORWARD;
}
}
private void resetPointerIcon(MotionEvent event) {
mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
updatePointerIcon(event);
}
private boolean updatePointerIcon(MotionEvent event) {
final int pointerIndex = 0;
final float x = event.getX(pointerIndex);
final float y = event.getY(pointerIndex);
if (mView == null) {
// E.g. click outside a popup to dismiss it
Slog.d(mTag, "updatePointerIcon called after view was removed");
return false;
}
if (x < 0 || x >= mView.getWidth() || y < 0 || y >= mView.getHeight()) {
// E.g. when moving window divider with mouse
Slog.d(mTag, "updatePointerIcon called with position out of bounds");
return false;
}
final PointerIcon pointerIcon = mView.onResolvePointerIcon(event, pointerIndex);
final int pointerType = (pointerIcon != null) ?
pointerIcon.getType() : PointerIcon.TYPE_DEFAULT;
if (mPointerIconType != pointerType) {
mPointerIconType = pointerType;
mCustomPointerIcon = null;
if (mPointerIconType != PointerIcon.TYPE_CUSTOM) {
InputManager.getInstance().setPointerIconType(pointerType);
return true;
}
}
if (mPointerIconType == PointerIcon.TYPE_CUSTOM &&
!pointerIcon.equals(mCustomPointerIcon)) {
mCustomPointerIcon = pointerIcon;
InputManager.getInstance().setCustomPointerIcon(mCustomPointerIcon);
}
return true;
}
private void maybeUpdateTooltip(MotionEvent event) {
if (event.getPointerCount() != 1) {
return;
}
final int action = event.getActionMasked();
if (action != MotionEvent.ACTION_HOVER_ENTER
&& action != MotionEvent.ACTION_HOVER_MOVE
&& action != MotionEvent.ACTION_HOVER_EXIT) {
return;
}
AccessibilityManager manager = AccessibilityManager.getInstance(mContext);
if (manager.isEnabled() && manager.isTouchExplorationEnabled()) {
return;
}
if (mView == null) {
Slog.d(mTag, "maybeUpdateTooltip called after view was removed");
return;
}
mView.dispatchTooltipHoverEvent(event);
}
/**
* Performs synthesis of new input events from unhandled input events.
*/
final class SyntheticInputStage extends InputStage {
private final SyntheticTrackballHandler mTrackball = new SyntheticTrackballHandler();
private final SyntheticJoystickHandler mJoystick = new SyntheticJoystickHandler();
private final SyntheticTouchNavigationHandler mTouchNavigation =
new SyntheticTouchNavigationHandler();
private final SyntheticKeyboardHandler mKeyboard = new SyntheticKeyboardHandler();
public SyntheticInputStage() {
super(null);
}
@Override
protected int onProcess(QueuedInputEvent q) {
q.mFlags |= QueuedInputEvent.FLAG_RESYNTHESIZED;
if (q.mEvent instanceof MotionEvent) {
final MotionEvent event = (MotionEvent)q.mEvent;
final int source = event.getSource();
if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
mTrackball.process(event);
return FINISH_HANDLED;
} else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
mJoystick.process(event);
return FINISH_HANDLED;
} else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION)
== InputDevice.SOURCE_TOUCH_NAVIGATION) {
mTouchNavigation.process(event);
return FINISH_HANDLED;
}
} else if ((q.mFlags & QueuedInputEvent.FLAG_UNHANDLED) != 0) {
mKeyboard.process((KeyEvent)q.mEvent);
return FINISH_HANDLED;
}
return FORWARD;
}
@Override
protected void onDeliverToNext(QueuedInputEvent q) {
if ((q.mFlags & QueuedInputEvent.FLAG_RESYNTHESIZED) == 0) {
// Cancel related synthetic events if any prior stage has handled the event.
if (q.mEvent instanceof MotionEvent) {
final MotionEvent event = (MotionEvent)q.mEvent;
final int source = event.getSource();
if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
mTrackball.cancel();
} else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
mJoystick.cancel();
} else if ((source & InputDevice.SOURCE_TOUCH_NAVIGATION)
== InputDevice.SOURCE_TOUCH_NAVIGATION) {
mTouchNavigation.cancel(event);
}
}
}
super.onDeliverToNext(q);
}
@Override
protected void onWindowFocusChanged(boolean hasWindowFocus) {
if (!hasWindowFocus) {
mJoystick.cancel();
}
}
@Override
protected void onDetachedFromWindow() {
mJoystick.cancel();
}
}
/**
* Creates dpad events from unhandled trackball movements.
*/
final class SyntheticTrackballHandler {
private final TrackballAxis mX = new TrackballAxis();
private final TrackballAxis mY = new TrackballAxis();
private long mLastTime;
public void process(MotionEvent event) {
// Translate the trackball event into DPAD keys and try to deliver those.
long curTime = SystemClock.uptimeMillis();
if ((mLastTime + MAX_TRACKBALL_DELAY) < curTime) {
// It has been too long since the last movement,
// so restart at the beginning.
mX.reset(0);
mY.reset(0);
mLastTime = curTime;
}
final int action = event.getAction();
final int metaState = event.getMetaState();
switch (action) {
case MotionEvent.ACTION_DOWN:
mX.reset(2);
mY.reset(2);
enqueueInputEvent(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
InputDevice.SOURCE_KEYBOARD));
break;
case MotionEvent.ACTION_UP:
mX.reset(2);
mY.reset(2);
enqueueInputEvent(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
InputDevice.SOURCE_KEYBOARD));
break;
}
if (DEBUG_TRACKBALL) Log.v(mTag, "TB X=" + mX.position + " step="
+ mX.step + " dir=" + mX.dir + " acc=" + mX.acceleration
+ " move=" + event.getX()
+ " / Y=" + mY.position + " step="
+ mY.step + " dir=" + mY.dir + " acc=" + mY.acceleration
+ " move=" + event.getY());
final float xOff = mX.collect(event.getX(), event.getEventTime(), "X");
final float yOff = mY.collect(event.getY(), event.getEventTime(), "Y");
// Generate DPAD events based on the trackball movement.
// We pick the axis that has moved the most as the direction of
// the DPAD. When we generate DPAD events for one axis, then the
// other axis is reset -- we don't want to perform DPAD jumps due
// to slight movements in the trackball when making major movements
// along the other axis.
int keycode = 0;
int movement = 0;
float accel = 1;
if (xOff > yOff) {
movement = mX.generate();
if (movement != 0) {
keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_RIGHT
: KeyEvent.KEYCODE_DPAD_LEFT;
accel = mX.acceleration;
mY.reset(2);
}
} else if (yOff > 0) {
movement = mY.generate();
if (movement != 0) {
keycode = movement > 0 ? KeyEvent.KEYCODE_DPAD_DOWN
: KeyEvent.KEYCODE_DPAD_UP;
accel = mY.acceleration;
mX.reset(2);
}
}
if (keycode != 0) {
if (movement < 0) movement = -movement;
int accelMovement = (int)(movement * accel);
if (DEBUG_TRACKBALL) Log.v(mTag, "Move: movement=" + movement
+ " accelMovement=" + accelMovement
+ " accel=" + accel);
if (accelMovement > movement) {
if (DEBUG_TRACKBALL) Log.v(mTag, "Delivering fake DPAD: "
+ keycode);
movement--;
int repeatCount = accelMovement - movement;
enqueueInputEvent(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_MULTIPLE, keycode, repeatCount, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
InputDevice.SOURCE_KEYBOARD));
}
while (movement > 0) {
if (DEBUG_TRACKBALL) Log.v(mTag, "Delivering fake DPAD: "
+ keycode);
movement--;
curTime = SystemClock.uptimeMillis();
enqueueInputEvent(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_DOWN, keycode, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
InputDevice.SOURCE_KEYBOARD));
enqueueInputEvent(new KeyEvent(curTime, curTime,
KeyEvent.ACTION_UP, keycode, 0, metaState,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_FALLBACK,
InputDevice.SOURCE_KEYBOARD));
}
mLastTime = curTime;
}
}
public void cancel() {
mLastTime = Integer.MIN_VALUE;
// If we reach this, we consumed a trackball event.
// Because we will not translate the trackball event into a key event,
// touch mode will not exit, so we exit touch mode here.
if (mView != null && mAdded) {
ensureTouchMode(false);
}
}
}
/**
* Maintains state information for a single trackball axis, generating
* discrete (DPAD) movements based on raw trackball motion.
*/
static final class TrackballAxis {
/**
* The maximum amount of acceleration we will apply.
*/
static final float MAX_ACCELERATION = 20;
/**
* The maximum amount of time (in milliseconds) between events in order
* for us to consider the user to be doing fast trackball movements,
* and thus apply an acceleration.
*/
static final long FAST_MOVE_TIME = 150;
/**
* Scaling factor to the time (in milliseconds) between events to how
* much to multiple/divide the current acceleration. When movement
* is < FAST_MOVE_TIME this multiplies the acceleration; when >
* FAST_MOVE_TIME it divides it.
*/
static final float ACCEL_MOVE_SCALING_FACTOR = (1.0f/40);
static final float FIRST_MOVEMENT_THRESHOLD = 0.5f;
static final float SECOND_CUMULATIVE_MOVEMENT_THRESHOLD = 2.0f;
static final float SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD = 1.0f;
float position;
float acceleration = 1;
long lastMoveTime = 0;
int step;
int dir;
int nonAccelMovement;
void reset(int _step) {
position = 0;
acceleration = 1;
lastMoveTime = 0;
step = _step;
dir = 0;
}
/**
* Add trackball movement into the state. If the direction of movement
* has been reversed, the state is reset before adding the
* movement (so that you don't have to compensate for any previously
* collected movement before see the result of the movement in the
* new direction).
*
* @return Returns the absolute value of the amount of movement
* collected so far.
*/
float collect(float off, long time, String axis) {
long normTime;
if (off > 0) {
normTime = (long)(off * FAST_MOVE_TIME);
if (dir < 0) {
if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to positive!");
position = 0;
step = 0;
acceleration = 1;
lastMoveTime = 0;
}
dir = 1;
} else if (off < 0) {
normTime = (long)((-off) * FAST_MOVE_TIME);
if (dir > 0) {
if (DEBUG_TRACKBALL) Log.v(TAG, axis + " reversed to negative!");
position = 0;
step = 0;
acceleration = 1;
lastMoveTime = 0;
}
dir = -1;
} else {
normTime = 0;
}
// The number of milliseconds between each movement that is
// considered "normal" and will not result in any acceleration
// or deceleration, scaled by the offset we have here.
if (normTime > 0) {
long delta = time - lastMoveTime;
lastMoveTime = time;
float acc = acceleration;
if (delta < normTime) {
// The user is scrolling rapidly, so increase acceleration.
float scale = (normTime-delta) * ACCEL_MOVE_SCALING_FACTOR;
if (scale > 1) acc *= scale;
if (DEBUG_TRACKBALL) Log.v(TAG, axis + " accelerate: off="
+ off + " normTime=" + normTime + " delta=" + delta
+ " scale=" + scale + " acc=" + acc);
acceleration = acc < MAX_ACCELERATION ? acc : MAX_ACCELERATION;
} else {
// The user is scrolling slowly, so decrease acceleration.
float scale = (delta-normTime) * ACCEL_MOVE_SCALING_FACTOR;
if (scale > 1) acc /= scale;
if (DEBUG_TRACKBALL) Log.v(TAG, axis + " deccelerate: off="
+ off + " normTime=" + normTime + " delta=" + delta
+ " scale=" + scale + " acc=" + acc);
acceleration = acc > 1 ? acc : 1;
}
}
position += off;
return Math.abs(position);
}
/**
* Generate the number of discrete movement events appropriate for
* the currently collected trackball movement.
*
* @return Returns the number of discrete movements, either positive
* or negative, or 0 if there is not enough trackball movement yet
* for a discrete movement.
*/
int generate() {
int movement = 0;
nonAccelMovement = 0;
do {
final int dir = position >= 0 ? 1 : -1;
switch (step) {
// If we are going to execute the first step, then we want
// to do this as soon as possible instead of waiting for
// a full movement, in order to make things look responsive.
case 0:
if (Math.abs(position) < FIRST_MOVEMENT_THRESHOLD) {
return movement;
}
movement += dir;
nonAccelMovement += dir;
step = 1;
break;
// If we have generated the first movement, then we need
// to wait for the second complete trackball motion before
// generating the second discrete movement.
case 1:
if (Math.abs(position) < SECOND_CUMULATIVE_MOVEMENT_THRESHOLD) {
return movement;
}
movement += dir;
nonAccelMovement += dir;
position -= SECOND_CUMULATIVE_MOVEMENT_THRESHOLD * dir;
step = 2;
break;
// After the first two, we generate discrete movements
// consistently with the trackball, applying an acceleration
// if the trackball is moving quickly. This is a simple
// acceleration on top of what we already compute based
// on how quickly the wheel is being turned, to apply
// a longer increasing acceleration to continuous movement
// in one direction.
default:
if (Math.abs(position) < SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD) {
return movement;
}
movement += dir;
position -= dir * SUBSEQUENT_INCREMENTAL_MOVEMENT_THRESHOLD;
float acc = acceleration;
acc *= 1.1f;
acceleration = acc < MAX_ACCELERATION ? acc : acceleration;
break;
}
} while (true);
}
}
/**
* Creates dpad events from unhandled joystick movements.
*/
final class SyntheticJoystickHandler extends Handler {
private final static int MSG_ENQUEUE_X_AXIS_KEY_REPEAT = 1;
private final static int MSG_ENQUEUE_Y_AXIS_KEY_REPEAT = 2;
private final JoystickAxesState mJoystickAxesState = new JoystickAxesState();
private final SparseArray<KeyEvent> mDeviceKeyEvents = new SparseArray<>();
public SyntheticJoystickHandler() {
super(true);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ENQUEUE_X_AXIS_KEY_REPEAT:
case MSG_ENQUEUE_Y_AXIS_KEY_REPEAT: {
if (mAttachInfo.mHasWindowFocus) {
KeyEvent oldEvent = (KeyEvent) msg.obj;
KeyEvent e = KeyEvent.changeTimeRepeat(oldEvent,
SystemClock.uptimeMillis(), oldEvent.getRepeatCount() + 1);
enqueueInputEvent(e);
Message m = obtainMessage(msg.what, e);
m.setAsynchronous(true);
sendMessageDelayed(m, ViewConfiguration.getKeyRepeatDelay());
}
} break;
}
}
public void process(MotionEvent event) {
switch(event.getActionMasked()) {
case MotionEvent.ACTION_CANCEL:
cancel();
break;
case MotionEvent.ACTION_MOVE:
update(event);
break;
default:
Log.w(mTag, "Unexpected action: " + event.getActionMasked());
}
}
private void cancel() {
removeMessages(MSG_ENQUEUE_X_AXIS_KEY_REPEAT);
removeMessages(MSG_ENQUEUE_Y_AXIS_KEY_REPEAT);
for (int i = 0; i < mDeviceKeyEvents.size(); i++) {
final KeyEvent keyEvent = mDeviceKeyEvents.valueAt(i);
if (keyEvent != null) {
enqueueInputEvent(KeyEvent.changeTimeRepeat(keyEvent,
SystemClock.uptimeMillis(), 0));
}
}
mDeviceKeyEvents.clear();
mJoystickAxesState.resetState();
}
private void update(MotionEvent event) {
final int historySize = event.getHistorySize();
for (int h = 0; h < historySize; h++) {
final long time = event.getHistoricalEventTime(h);
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_X,
event.getHistoricalAxisValue(MotionEvent.AXIS_X, 0, h));
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_Y,
event.getHistoricalAxisValue(MotionEvent.AXIS_Y, 0, h));
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_X,
event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, 0, h));
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_Y,
event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, 0, h));
}
final long time = event.getEventTime();
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_X,
event.getAxisValue(MotionEvent.AXIS_X));
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_Y,
event.getAxisValue(MotionEvent.AXIS_Y));
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_X,
event.getAxisValue(MotionEvent.AXIS_HAT_X));
mJoystickAxesState.updateStateForAxis(event, time, MotionEvent.AXIS_HAT_Y,
event.getAxisValue(MotionEvent.AXIS_HAT_Y));
}
final class JoystickAxesState {
// State machine: from neutral state (no button press) can go into
// button STATE_UP_OR_LEFT or STATE_DOWN_OR_RIGHT state, emitting an ACTION_DOWN event.
// From STATE_UP_OR_LEFT or STATE_DOWN_OR_RIGHT state can go into neutral state,
// emitting an ACTION_UP event.
private static final int STATE_UP_OR_LEFT = -1;
private static final int STATE_NEUTRAL = 0;
private static final int STATE_DOWN_OR_RIGHT = 1;
final int[] mAxisStatesHat = {STATE_NEUTRAL, STATE_NEUTRAL}; // {AXIS_HAT_X, AXIS_HAT_Y}
final int[] mAxisStatesStick = {STATE_NEUTRAL, STATE_NEUTRAL}; // {AXIS_X, AXIS_Y}
void resetState() {
mAxisStatesHat[0] = STATE_NEUTRAL;
mAxisStatesHat[1] = STATE_NEUTRAL;
mAxisStatesStick[0] = STATE_NEUTRAL;
mAxisStatesStick[1] = STATE_NEUTRAL;
}
void updateStateForAxis(MotionEvent event, long time, int axis, float value) {
// Emit KeyEvent if necessary
// axis can be AXIS_X, AXIS_Y, AXIS_HAT_X, AXIS_HAT_Y
final int axisStateIndex;
final int repeatMessage;
if (isXAxis(axis)) {
axisStateIndex = 0;
repeatMessage = MSG_ENQUEUE_X_AXIS_KEY_REPEAT;
} else if (isYAxis(axis)) {
axisStateIndex = 1;
repeatMessage = MSG_ENQUEUE_Y_AXIS_KEY_REPEAT;
} else {
Log.e(mTag, "Unexpected axis " + axis + " in updateStateForAxis!");
return;
}
final int newState = joystickAxisValueToState(value);
final int currentState;
if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y) {
currentState = mAxisStatesStick[axisStateIndex];
} else {
currentState = mAxisStatesHat[axisStateIndex];
}
if (currentState == newState) {
return;
}
final int metaState = event.getMetaState();
final int deviceId = event.getDeviceId();
final int source = event.getSource();
if (currentState == STATE_DOWN_OR_RIGHT || currentState == STATE_UP_OR_LEFT) {
// send a button release event
final int keyCode = joystickAxisAndStateToKeycode(axis, currentState);
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
enqueueInputEvent(new KeyEvent(time, time, KeyEvent.ACTION_UP, keyCode,
0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source));
// remove the corresponding pending UP event if focus lost/view detached
mDeviceKeyEvents.put(deviceId, null);
}
removeMessages(repeatMessage);
}
if (newState == STATE_DOWN_OR_RIGHT || newState == STATE_UP_OR_LEFT) {
// send a button down event
final int keyCode = joystickAxisAndStateToKeycode(axis, newState);
if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
KeyEvent keyEvent = new KeyEvent(time, time, KeyEvent.ACTION_DOWN, keyCode,
0, metaState, deviceId, 0, KeyEvent.FLAG_FALLBACK, source);
enqueueInputEvent(keyEvent);
Message m = obtainMessage(repeatMessage, keyEvent);
m.setAsynchronous(true);
sendMessageDelayed(m, ViewConfiguration.getKeyRepeatTimeout());
// store the corresponding ACTION_UP event so that it can be sent
// if focus is lost or root view is removed
mDeviceKeyEvents.put(deviceId,
new KeyEvent(time, time, KeyEvent.ACTION_UP, keyCode,
0, metaState, deviceId, 0,
KeyEvent.FLAG_FALLBACK | KeyEvent.FLAG_CANCELED,
source));
}
}
if (axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_Y) {
mAxisStatesStick[axisStateIndex] = newState;
} else {
mAxisStatesHat[axisStateIndex] = newState;
}
}
private boolean isXAxis(int axis) {
return axis == MotionEvent.AXIS_X || axis == MotionEvent.AXIS_HAT_X;
}
private boolean isYAxis(int axis) {
return axis == MotionEvent.AXIS_Y || axis == MotionEvent.AXIS_HAT_Y;
}
private int joystickAxisAndStateToKeycode(int axis, int state) {
if (isXAxis(axis) && state == STATE_UP_OR_LEFT) {
return KeyEvent.KEYCODE_DPAD_LEFT;
}
if (isXAxis(axis) && state == STATE_DOWN_OR_RIGHT) {
return KeyEvent.KEYCODE_DPAD_RIGHT;
}
if (isYAxis(axis) && state == STATE_UP_OR_LEFT) {
return KeyEvent.KEYCODE_DPAD_UP;
}
if (isYAxis(axis) && state == STATE_DOWN_OR_RIGHT) {
return KeyEvent.KEYCODE_DPAD_DOWN;
}
Log.e(mTag, "Unknown axis " + axis + " or direction " + state);
return KeyEvent.KEYCODE_UNKNOWN; // should never happen
}
private int joystickAxisValueToState(float value) {
if (value >= 0.5f) {
return STATE_DOWN_OR_RIGHT;
} else if (value <= -0.5f) {
return STATE_UP_OR_LEFT;
} else {
return STATE_NEUTRAL;
}
}
}
}
/**
* Creates dpad events from unhandled touch navigation movements.
*/
final class SyntheticTouchNavigationHandler extends Handler {
private static final String LOCAL_TAG = "SyntheticTouchNavigationHandler";
private static final boolean LOCAL_DEBUG = false;
// Assumed nominal width and height in millimeters of a touch navigation pad,
// if no resolution information is available from the input system.
private static final float DEFAULT_WIDTH_MILLIMETERS = 48;
private static final float DEFAULT_HEIGHT_MILLIMETERS = 48;
/* TODO: These constants should eventually be moved to ViewConfiguration. */
// The nominal distance traveled to move by one unit.
private static final int TICK_DISTANCE_MILLIMETERS = 12;
// Minimum and maximum fling velocity in ticks per second.
// The minimum velocity should be set such that we perform enough ticks per
// second that the fling appears to be fluid. For example, if we set the minimum
// to 2 ticks per second, then there may be up to half a second delay between the next
// to last and last ticks which is noticeably discrete and jerky. This value should
// probably not be set to anything less than about 4.
// If fling accuracy is a problem then consider tuning the tick distance instead.
private static final float MIN_FLING_VELOCITY_TICKS_PER_SECOND = 6f;
private static final float MAX_FLING_VELOCITY_TICKS_PER_SECOND = 20f;
// Fling velocity decay factor applied after each new key is emitted.
// This parameter controls the deceleration and overall duration of the fling.
// The fling stops automatically when its velocity drops below the minimum
// fling velocity defined above.
private static final float FLING_TICK_DECAY = 0.8f;
/* The input device that we are tracking. */
private int mCurrentDeviceId = -1;
private int mCurrentSource;
private boolean mCurrentDeviceSupported;
/* Configuration for the current input device. */
// The scaled tick distance. A movement of this amount should generally translate
// into a single dpad event in a given direction.
private float mConfigTickDistance;
// The minimum and maximum scaled fling velocity.
private float mConfigMinFlingVelocity;
private float mConfigMaxFlingVelocity;
/* Tracking state. */
// The velocity tracker for detecting flings.
private VelocityTracker mVelocityTracker;
// The active pointer id, or -1 if none.
private int mActivePointerId = -1;
// Location where tracking started.
private float mStartX;
private float mStartY;
// Most recently observed position.
private float mLastX;
private float mLastY;
// Accumulated movement delta since the last direction key was sent.
private float mAccumulatedX;
private float mAccumulatedY;
// Set to true if any movement was delivered to the app.
// Implies that tap slop was exceeded.
private boolean mConsumedMovement;
// The most recently sent key down event.
// The keycode remains set until the direction changes or a fling ends
// so that repeated key events may be generated as required.
private long mPendingKeyDownTime;
private int mPendingKeyCode = KeyEvent.KEYCODE_UNKNOWN;
private int mPendingKeyRepeatCount;
private int mPendingKeyMetaState;
// The current fling velocity while a fling is in progress.
private boolean mFlinging;
private float mFlingVelocity;
public SyntheticTouchNavigationHandler() {
super(true);
}
public void process(MotionEvent event) {
// Update the current device information.
final long time = event.getEventTime();
final int deviceId = event.getDeviceId();
final int source = event.getSource();
if (mCurrentDeviceId != deviceId || mCurrentSource != source) {
finishKeys(time);
finishTracking(time);
mCurrentDeviceId = deviceId;
mCurrentSource = source;
mCurrentDeviceSupported = false;
InputDevice device = event.getDevice();
if (device != null) {
// In order to support an input device, we must know certain
// characteristics about it, such as its size and resolution.
InputDevice.MotionRange xRange = device.getMotionRange(MotionEvent.AXIS_X);
InputDevice.MotionRange yRange = device.getMotionRange(MotionEvent.AXIS_Y);
if (xRange != null && yRange != null) {
mCurrentDeviceSupported = true;
// Infer the resolution if it not actually known.
float xRes = xRange.getResolution();
if (xRes <= 0) {
xRes = xRange.getRange() / DEFAULT_WIDTH_MILLIMETERS;
}
float yRes = yRange.getResolution();
if (yRes <= 0) {
yRes = yRange.getRange() / DEFAULT_HEIGHT_MILLIMETERS;
}
float nominalRes = (xRes + yRes) * 0.5f;
// Precompute all of the configuration thresholds we will need.
mConfigTickDistance = TICK_DISTANCE_MILLIMETERS * nominalRes;
mConfigMinFlingVelocity =
MIN_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance;
mConfigMaxFlingVelocity =
MAX_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance;
if (LOCAL_DEBUG) {
Log.d(LOCAL_TAG, "Configured device " + mCurrentDeviceId
+ " (" + Integer.toHexString(mCurrentSource) + "): "
+ ", mConfigTickDistance=" + mConfigTickDistance
+ ", mConfigMinFlingVelocity=" + mConfigMinFlingVelocity
+ ", mConfigMaxFlingVelocity=" + mConfigMaxFlingVelocity);
}
}
}
}
if (!mCurrentDeviceSupported) {
return;
}
// Handle the event.
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN: {
boolean caughtFling = mFlinging;
finishKeys(time);
finishTracking(time);
mActivePointerId = event.getPointerId(0);
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(event);
mStartX = event.getX();
mStartY = event.getY();
mLastX = mStartX;
mLastY = mStartY;
mAccumulatedX = 0;
mAccumulatedY = 0;
// If we caught a fling, then pretend that the tap slop has already
// been exceeded to suppress taps whose only purpose is to stop the fling.
mConsumedMovement = caughtFling;
break;
}
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP: {
if (mActivePointerId < 0) {
break;
}
final int index = event.findPointerIndex(mActivePointerId);
if (index < 0) {
finishKeys(time);
finishTracking(time);
break;
}
mVelocityTracker.addMovement(event);
final float x = event.getX(index);
final float y = event.getY(index);
mAccumulatedX += x - mLastX;
mAccumulatedY += y - mLastY;
mLastX = x;
mLastY = y;
// Consume any accumulated movement so far.
final int metaState = event.getMetaState();
consumeAccumulatedMovement(time, metaState);
// Detect taps and flings.
if (action == MotionEvent.ACTION_UP) {
if (mConsumedMovement && mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
// It might be a fling.
mVelocityTracker.computeCurrentVelocity(1000, mConfigMaxFlingVelocity);
final float vx = mVelocityTracker.getXVelocity(mActivePointerId);
final float vy = mVelocityTracker.getYVelocity(mActivePointerId);
if (!startFling(time, vx, vy)) {
finishKeys(time);
}
}
finishTracking(time);
}
break;
}
case MotionEvent.ACTION_CANCEL: {
finishKeys(time);
finishTracking(time);
break;
}
}
}
public void cancel(MotionEvent event) {
if (mCurrentDeviceId == event.getDeviceId()
&& mCurrentSource == event.getSource()) {
final long time = event.getEventTime();
finishKeys(time);
finishTracking(time);
}
}
private void finishKeys(long time) {
cancelFling();
sendKeyUp(time);
}
private void finishTracking(long time) {
if (mActivePointerId >= 0) {
mActivePointerId = -1;
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
private void consumeAccumulatedMovement(long time, int metaState) {
final float absX = Math.abs(mAccumulatedX);
final float absY = Math.abs(mAccumulatedY);
if (absX >= absY) {
if (absX >= mConfigTickDistance) {
mAccumulatedX = consumeAccumulatedMovement(time, metaState, mAccumulatedX,
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT);
mAccumulatedY = 0;
mConsumedMovement = true;
}
} else {
if (absY >= mConfigTickDistance) {
mAccumulatedY = consumeAccumulatedMovement(time, metaState, mAccumulatedY,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN);
mAccumulatedX = 0;
mConsumedMovement = true;
}
}
}
private float consumeAccumulatedMovement(long time, int metaState,
float accumulator, int negativeKeyCode, int positiveKeyCode) {
while (accumulator <= -mConfigTickDistance) {
sendKeyDownOrRepeat(time, negativeKeyCode, metaState);
accumulator += mConfigTickDistance;
}
while (accumulator >= mConfigTickDistance) {
sendKeyDownOrRepeat(time, positiveKeyCode, metaState);
accumulator -= mConfigTickDistance;
}
return accumulator;
}
private void sendKeyDownOrRepeat(long time, int keyCode, int metaState) {
if (mPendingKeyCode != keyCode) {
sendKeyUp(time);
mPendingKeyDownTime = time;
mPendingKeyCode = keyCode;
mPendingKeyRepeatCount = 0;
} else {
mPendingKeyRepeatCount += 1;
}
mPendingKeyMetaState = metaState;
// Note: Normally we would pass FLAG_LONG_PRESS when the repeat count is 1
// but it doesn't quite make sense when simulating the events in this way.
if (LOCAL_DEBUG) {
Log.d(LOCAL_TAG, "Sending key down: keyCode=" + mPendingKeyCode
+ ", repeatCount=" + mPendingKeyRepeatCount
+ ", metaState=" + Integer.toHexString(mPendingKeyMetaState));
}
enqueueInputEvent(new KeyEvent(mPendingKeyDownTime, time,
KeyEvent.ACTION_DOWN, mPendingKeyCode, mPendingKeyRepeatCount,
mPendingKeyMetaState, mCurrentDeviceId,
KeyEvent.FLAG_FALLBACK, mCurrentSource));
}
private void sendKeyUp(long time) {
if (mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
if (LOCAL_DEBUG) {
Log.d(LOCAL_TAG, "Sending key up: keyCode=" + mPendingKeyCode
+ ", metaState=" + Integer.toHexString(mPendingKeyMetaState));
}
enqueueInputEvent(new KeyEvent(mPendingKeyDownTime, time,
KeyEvent.ACTION_UP, mPendingKeyCode, 0, mPendingKeyMetaState,
mCurrentDeviceId, 0, KeyEvent.FLAG_FALLBACK,
mCurrentSource));
mPendingKeyCode = KeyEvent.KEYCODE_UNKNOWN;
}
}
private boolean startFling(long time, float vx, float vy) {
if (LOCAL_DEBUG) {
Log.d(LOCAL_TAG, "Considering fling: vx=" + vx + ", vy=" + vy
+ ", min=" + mConfigMinFlingVelocity);
}
// Flings must be oriented in the same direction as the preceding movements.
switch (mPendingKeyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (-vx >= mConfigMinFlingVelocity
&& Math.abs(vy) < mConfigMinFlingVelocity) {
mFlingVelocity = -vx;
break;
}
return false;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (vx >= mConfigMinFlingVelocity
&& Math.abs(vy) < mConfigMinFlingVelocity) {
mFlingVelocity = vx;
break;
}
return false;
case KeyEvent.KEYCODE_DPAD_UP:
if (-vy >= mConfigMinFlingVelocity
&& Math.abs(vx) < mConfigMinFlingVelocity) {
mFlingVelocity = -vy;
break;
}
return false;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (vy >= mConfigMinFlingVelocity
&& Math.abs(vx) < mConfigMinFlingVelocity) {
mFlingVelocity = vy;
break;
}
return false;
}
// Post the first fling event.
mFlinging = postFling(time);
return mFlinging;
}
private boolean postFling(long time) {
// The idea here is to estimate the time when the pointer would have
// traveled one tick distance unit given the current fling velocity.
// This effect creates continuity of motion.
if (mFlingVelocity >= mConfigMinFlingVelocity) {
long delay = (long)(mConfigTickDistance / mFlingVelocity * 1000);
postAtTime(mFlingRunnable, time + delay);
if (LOCAL_DEBUG) {
Log.d(LOCAL_TAG, "Posted fling: velocity="
+ mFlingVelocity + ", delay=" + delay
+ ", keyCode=" + mPendingKeyCode);
}
return true;
}
return false;
}
private void cancelFling() {
if (mFlinging) {
removeCallbacks(mFlingRunnable);
mFlinging = false;
}
}
private final Runnable mFlingRunnable = new Runnable() {
@Override
public void run() {
final long time = SystemClock.uptimeMillis();
sendKeyDownOrRepeat(time, mPendingKeyCode, mPendingKeyMetaState);
mFlingVelocity *= FLING_TICK_DECAY;
if (!postFling(time)) {
mFlinging = false;
finishKeys(time);
}
}
};
}
final class SyntheticKeyboardHandler {
public void process(KeyEvent event) {
if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
return;
}
final KeyCharacterMap kcm = event.getKeyCharacterMap();
final int keyCode = event.getKeyCode();
final int metaState = event.getMetaState();
// Check for fallback actions specified by the key character map.
KeyCharacterMap.FallbackAction fallbackAction =
kcm.getFallbackAction(keyCode, metaState);
if (fallbackAction != null) {
final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
KeyEvent fallbackEvent = KeyEvent.obtain(
event.getDownTime(), event.getEventTime(),
event.getAction(), fallbackAction.keyCode,
event.getRepeatCount(), fallbackAction.metaState,
event.getDeviceId(), event.getScanCode(),
flags, event.getSource(), null);
fallbackAction.recycle();
enqueueInputEvent(fallbackEvent);
}
}
}
/**
* Returns true if the key is used for keyboard navigation.
* @param keyEvent The key event.
* @return True if the key is used for keyboard navigation.
*/
private static boolean isNavigationKey(KeyEvent keyEvent) {
switch (keyEvent.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_PAGE_UP:
case KeyEvent.KEYCODE_PAGE_DOWN:
case KeyEvent.KEYCODE_MOVE_HOME:
case KeyEvent.KEYCODE_MOVE_END:
case KeyEvent.KEYCODE_TAB:
case KeyEvent.KEYCODE_SPACE:
case KeyEvent.KEYCODE_ENTER:
return true;
}
return false;
}
/**
* Returns true if the key is used for typing.
* @param keyEvent The key event.
* @return True if the key is used for typing.
*/
private static boolean isTypingKey(KeyEvent keyEvent) {
return keyEvent.getUnicodeChar() > 0;
}
/**
* See if the key event means we should leave touch mode (and leave touch mode if so).
* @param event The key event.
* @return Whether this key event should be consumed (meaning the act of
* leaving touch mode alone is considered the event).
*/
private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
// Only relevant in touch mode.
if (!mAttachInfo.mInTouchMode) {
return false;
}
// Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
final int action = event.getAction();
if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
return false;
}
// Don't leave touch mode if the IME told us not to.
if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
return false;
}
// If the key can be used for keyboard navigation then leave touch mode
// and select a focused view if needed (in ensureTouchMode).
// When a new focused view is selected, we consume the navigation key because
// navigation doesn't make much sense unless a view already has focus so
// the key's purpose is to set focus.
if (isNavigationKey(event)) {
return ensureTouchMode(false);
}
// If the key can be used for typing then leave touch mode
// and select a focused view if needed (in ensureTouchMode).
// Always allow the view to process the typing key.
if (isTypingKey(event)) {
ensureTouchMode(false);
return false;
}
return false;
}
/* drag/drop */
void setLocalDragState(Object obj) {
mLocalDragState = obj;
}
private void handleDragEvent(DragEvent event) {
// From the root, only drag start/end/location are dispatched. entered/exited
// are determined and dispatched by the viewgroup hierarchy, who then report
// that back here for ultimate reporting back to the framework.
if (mView != null && mAdded) {
final int what = event.mAction;
// Cache the drag description when the operation starts, then fill it in
// on subsequent calls as a convenience
if (what == DragEvent.ACTION_DRAG_STARTED) {
mCurrentDragView = null; // Start the current-recipient tracking
mDragDescription = event.mClipDescription;
} else {
if (what == DragEvent.ACTION_DRAG_ENDED) {
mDragDescription = null;
}
event.mClipDescription = mDragDescription;
}
if (what == DragEvent.ACTION_DRAG_EXITED) {
// A direct EXITED event means that the window manager knows we've just crossed
// a window boundary, so the current drag target within this one must have
// just been exited. Send the EXITED notification to the current drag view, if any.
if (View.sCascadedDragDrop) {
mView.dispatchDragEnterExitInPreN(event);
}
setDragFocus(null, event);
} else {
// For events with a [screen] location, translate into window coordinates
if ((what == DragEvent.ACTION_DRAG_LOCATION) || (what == DragEvent.ACTION_DROP)) {
mDragPoint.set(event.mX, event.mY);
if (mTranslator != null) {
mTranslator.translatePointInScreenToAppWindow(mDragPoint);
}
if (mCurScrollY != 0) {
mDragPoint.offset(0, mCurScrollY);
}
event.mX = mDragPoint.x;
event.mY = mDragPoint.y;
}
// Remember who the current drag target is pre-dispatch
final View prevDragView = mCurrentDragView;
if (what == DragEvent.ACTION_DROP && event.mClipData != null) {
event.mClipData.prepareToEnterProcess();
}
// Now dispatch the drag/drop event
boolean result = mView.dispatchDragEvent(event);
if (what == DragEvent.ACTION_DRAG_LOCATION && !event.mEventHandlerWasCalled) {
// If the LOCATION event wasn't delivered to any handler, no view now has a drag
// focus.
setDragFocus(null, event);
}
// If we changed apparent drag target, tell the OS about it
if (prevDragView != mCurrentDragView) {
try {
if (prevDragView != null) {
mWindowSession.dragRecipientExited(mWindow);
}
if (mCurrentDragView != null) {
mWindowSession.dragRecipientEntered(mWindow);
}
} catch (RemoteException e) {
Slog.e(mTag, "Unable to note drag target change");
}
}
// Report the drop result when we're done
if (what == DragEvent.ACTION_DROP) {
try {
Log.i(mTag, "Reporting drop result: " + result);
mWindowSession.reportDropResult(mWindow, result);
} catch (RemoteException e) {
Log.e(mTag, "Unable to report drop result");
}
}
// When the drag operation ends, reset drag-related state
if (what == DragEvent.ACTION_DRAG_ENDED) {
mCurrentDragView = null;
setLocalDragState(null);
mAttachInfo.mDragToken = null;
if (mAttachInfo.mDragSurface != null) {
mAttachInfo.mDragSurface.release();
mAttachInfo.mDragSurface = null;
}
}
}
}
event.recycle();
}
public void handleDispatchSystemUiVisibilityChanged(SystemUiVisibilityInfo args) {
if (mSeq != args.seq) {
// The sequence has changed, so we need to update our value and make
// sure to do a traversal afterward so the window manager is given our
// most recent data.
mSeq = args.seq;
mAttachInfo.mForceReportNewAttributes = true;
scheduleTraversals();
}
if (mView == null) return;
if (args.localChanges != 0) {
mView.updateLocalSystemUiVisibility(args.localValue, args.localChanges);
}
int visibility = args.globalVisibility&View.SYSTEM_UI_CLEARABLE_FLAGS;
if (visibility != mAttachInfo.mGlobalSystemUiVisibility) {
mAttachInfo.mGlobalSystemUiVisibility = visibility;
mView.dispatchSystemUiVisibilityChanged(visibility);
}
}
/**
* Notify that the window title changed
*/
public void onWindowTitleChanged() {
mAttachInfo.mForceReportNewAttributes = true;
}
public void handleDispatchWindowShown() {
mAttachInfo.mTreeObserver.dispatchOnWindowShown();
}
public void handleRequestKeyboardShortcuts(IResultReceiver receiver, int deviceId) {
Bundle data = new Bundle();
ArrayList<KeyboardShortcutGroup> list = new ArrayList<>();
if (mView != null) {
mView.requestKeyboardShortcuts(list, deviceId);
}
data.putParcelableArrayList(WindowManager.PARCEL_KEY_SHORTCUTS_ARRAY, list);
try {
receiver.send(0, data);
} catch (RemoteException e) {
}
}
public void getLastTouchPoint(Point outLocation) {
outLocation.x = (int) mLastTouchPoint.x;
outLocation.y = (int) mLastTouchPoint.y;
}
public int getLastTouchSource() {
return mLastTouchSource;
}
public void setDragFocus(View newDragTarget, DragEvent event) {
if (mCurrentDragView != newDragTarget && !View.sCascadedDragDrop) {
// Send EXITED and ENTERED notifications to the old and new drag focus views.
final float tx = event.mX;
final float ty = event.mY;
final int action = event.mAction;
final ClipData td = event.mClipData;
// Position should not be available for ACTION_DRAG_ENTERED and ACTION_DRAG_EXITED.
event.mX = 0;
event.mY = 0;
event.mClipData = null;
if (mCurrentDragView != null) {
event.mAction = DragEvent.ACTION_DRAG_EXITED;
mCurrentDragView.callDragEventHandler(event);
}
if (newDragTarget != null) {
event.mAction = DragEvent.ACTION_DRAG_ENTERED;
newDragTarget.callDragEventHandler(event);
}
event.mAction = action;
event.mX = tx;
event.mY = ty;
event.mClipData = td;
}
mCurrentDragView = newDragTarget;
}
private AudioManager getAudioManager() {
if (mView == null) {
throw new IllegalStateException("getAudioManager called when there is no mView");
}
if (mAudioManager == null) {
mAudioManager = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE);
}
return mAudioManager;
}
private @Nullable AutofillManager getAutofillManager() {
if (mView instanceof ViewGroup) {
ViewGroup decorView = (ViewGroup) mView;
if (decorView.getChildCount() > 0) {
// We cannot use decorView's Context for querying AutofillManager: DecorView's
// context is based on Application Context, it would allocate a different
// AutofillManager instance.
return decorView.getChildAt(0).getContext()
.getSystemService(AutofillManager.class);
}
}
return null;
}
private boolean isAutofillUiShowing() {
AutofillManager afm = getAutofillManager();
if (afm == null) {
return false;
}
return afm.isAutofillUiShowing();
}
public AccessibilityInteractionController getAccessibilityInteractionController() {
if (mView == null) {
throw new IllegalStateException("getAccessibilityInteractionController"
+ " called when there is no mView");
}
if (mAccessibilityInteractionController == null) {
mAccessibilityInteractionController = new AccessibilityInteractionController(this);
}
return mAccessibilityInteractionController;
}
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
float appScale = mAttachInfo.mApplicationScale;
boolean restore = false;
if (params != null && mTranslator != null) {
restore = true;
params.backup();
mTranslator.translateWindowLayout(params);
}
if (params != null) {
if (DBG) Log.d(mTag, "WindowLayout in layoutWindow:" + params);
if (mOrigWindowType != params.type) {
// For compatibility with old apps, don't crash here.
if (mTargetSdkVersion < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
Slog.w(mTag, "Window type can not be changed after "
+ "the window is added; ignoring change of " + mView);
params.type = mOrigWindowType;
}
}
}
long frameNumber = -1;
if (mSurface.isValid()) {
frameNumber = mSurface.getNextFrameNumber();
}
int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurface);
mPendingAlwaysConsumeNavBar =
(relayoutResult & WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_NAV_BAR) != 0;
if (restore) {
params.restore();
}
if (mTranslator != null) {
mTranslator.translateRectInScreenToAppWinFrame(mWinFrame);
mTranslator.translateRectInScreenToAppWindow(mPendingOverscanInsets);
mTranslator.translateRectInScreenToAppWindow(mPendingContentInsets);
mTranslator.translateRectInScreenToAppWindow(mPendingVisibleInsets);
mTranslator.translateRectInScreenToAppWindow(mPendingStableInsets);
}
return relayoutResult;
}
/**
* {@inheritDoc}
*/
@Override
public void playSoundEffect(int effectId) {
checkThread();
try {
final AudioManager audioManager = getAudioManager();
switch (effectId) {
case SoundEffectConstants.CLICK:
audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
return;
case SoundEffectConstants.NAVIGATION_DOWN:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
return;
case SoundEffectConstants.NAVIGATION_LEFT:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
return;
case SoundEffectConstants.NAVIGATION_RIGHT:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
return;
case SoundEffectConstants.NAVIGATION_UP:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
return;
default:
throw new IllegalArgumentException("unknown effect id " + effectId +
" not defined in " + SoundEffectConstants.class.getCanonicalName());
}
} catch (IllegalStateException e) {
// Exception thrown by getAudioManager() when mView is null
Log.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);
e.printStackTrace();
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean performHapticFeedback(int effectId, boolean always) {
try {
return mWindowSession.performHapticFeedback(mWindow, effectId, always);
} catch (RemoteException e) {
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public View focusSearch(View focused, int direction) {
checkThread();
if (!(mView instanceof ViewGroup)) {
return null;
}
if (JoyarHelper.DBG) {
Log.d(TAG, "focusSearch findNextFocus root:" + mView + " focused:" + focused);
}
return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
}
AdapterView.java
java复制代码
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked.
*
* @param listener The callback that will be invoked.
*/
public void setOnItemClickListener(@Nullable OnItemClickListener listener) {
JoyarHelper.getInstance().AdapterViewSetOnItemClickListener(this, listener);
mOnItemClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked, or null id no callback has been set.
*/
@Nullable
public final OnItemClickListener getOnItemClickListener() {
return mOnItemClickListener;
}
/**
* Call the OnItemClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @param view The view within the AdapterView that was clicked.
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
* @return True if there was an assigned OnItemClickListener that was
* called, false otherwise is returned.
*/
public boolean performItemClick(View view, int position, long id) {
final boolean result;
if (mOnItemClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
if (!JoyarHelper.getInstance().AdapterViewOnItemClick(this, view, position, id)) {
mOnItemClickListener.onItemClick(this, view, position, id);
}
result = true;
} else if (JoyarHelper.getInstance().AdapterViewPerformItemClick(this, view, position, id)) {
result = true;
} else {
result = false;
}
if (view != null) {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
return result;
}
/**
* Interface definition for a callback to be invoked when an item in this
* view has been clicked and held.
*/
public interface OnItemLongClickListener {
/**
* Callback method to be invoked when an item in this view has been
* clicked and held.
*
* Implementers can call getItemAtPosition(position) if they need to access
* the data associated with the selected item.
*
* @param parent The AbsListView where the click happened
* @param view The view within the AbsListView that was clicked
* @param position The position of the view in the list
* @param id The row id of the item that was clicked
*
* @return true if the callback consumed the long click, false otherwise
*/
boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked and held
*
* @param listener The callback that will run
*/
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
if (!isLongClickable()) {
setLongClickable(true);
}
mOnItemLongClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked and held, or null id no callback as been set.
*/
public final OnItemLongClickListener getOnItemLongClickListener() {
return mOnItemLongClickListener;
}
/**
* Interface definition for a callback to be invoked when
* an item in this view has been selected.
*/
public interface OnItemSelectedListener {
/**
* <p>Callback method to be invoked when an item in this view has been
* selected. This callback is invoked only when the newly selected
* position is different from the previously selected position or if
* there was no selected item.</p>
*
* Implementers can call getItemAtPosition(position) if they need to access the
* data associated with the selected item.
*
* @param parent The AdapterView where the selection happened
* @param view The view within the AdapterView that was clicked
* @param position The position of the view in the adapter
* @param id The row id of the item that is selected
*/
void onItemSelected(AdapterView<?> parent, View view, int position, long id);
/**
* Callback method to be invoked when the selection disappears from this
* view. The selection can disappear for instance when touch is activated
* or when the adapter becomes empty.
*
* @param parent The AdapterView that now contains no selected item.
*/
void onNothingSelected(AdapterView<?> parent);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been selected.
*
* @param listener The callback that will run
*/
public void setOnItemSelectedListener(@Nullable OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
}
@Nullable
public final OnItemSelectedListener getOnItemSelectedListener() {
return mOnItemSelectedListener;
}
/**
* Extra menu information provided to the
* {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
* callback when a context menu is brought up for this AdapterView.
*
*/
public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
public AdapterContextMenuInfo(View targetView, int position, long id) {
this.targetView = targetView;
this.position = position;
this.id = id;
}
/**
* The child view for which the context menu is being displayed. This
* will be one of the children of this AdapterView.
*/
public View targetView;
/**
* The position in the adapter for which the context menu is being
* displayed.
*/
public int position;
/**
* The row id of the item for which the context menu is being displayed.
*/
public long id;
}
/**
* Returns the adapter currently associated with this widget.
*
* @return The adapter used to provide this view's content.
*/
public abstract T getAdapter();
/**
* Sets the adapter that provides the data and the views to represent the data
* in this widget.
*
* @param adapter The adapter to use to create this view's content.
*/
public abstract void setAdapter(T adapter);
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child) {
throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param index Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, int index) {
throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param params Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, LayoutParams) "
+ "is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param index Ignored.
* @param params Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, int index, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
+ "is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeView(View child) {
throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param index Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeViewAt(int index) {
throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeAllViews() {
throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mLayoutHeight = getHeight();
}
/**
* Return the position of the currently selected item within the adapter's data set
*
* @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
*/
@ViewDebug.CapturedViewProperty
public int getSelectedItemPosition() {
return mNextSelectedPosition;
}
/**
* @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
* if nothing is selected.
*/
@ViewDebug.CapturedViewProperty
public long getSelectedItemId() {
return mNextSelectedRowId;
}
/**
* @return The view corresponding to the currently selected item, or null
* if nothing is selected
*/
public abstract View getSelectedView();
/**
* @return The data corresponding to the currently selected item, or
* null if there is nothing selected.
*/
public Object getSelectedItem() {
T adapter = getAdapter();
int selection = getSelectedItemPosition();
if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
return adapter.getItem(selection);
} else {
return null;
}
}
/**
* @return The number of items owned by the Adapter associated with this
* AdapterView. (This is the number of data items, which may be
* larger than the number of visible views.)
*/
@ViewDebug.CapturedViewProperty
public int getCount() {
return mItemCount;
}
/**
* Returns the position within the adapter's data set for the view, where
* view is a an adapter item or a descendant of an adapter item.
* <p>
* <strong>Note:</strong> The result of this method only reflects the
* position of the data bound to <var>view</var> during the most recent
* layout pass. If the adapter's data set has changed without a subsequent
* layout pass, the position returned by this method may not match the
* current position of the data within the adapter.
*
* @param view an adapter item, or a descendant of an adapter item. This
* must be visible in this AdapterView at the time of the call.
* @return the position within the adapter's data set of the view, or
* {@link #INVALID_POSITION} if the view does not correspond to a
* list item (or it is not currently visible)
*/
public int getPositionForView(View view) {
View listItem = view;
try {
View v;
while ((v = (View) listItem.getParent()) != null && !v.equals(this)) {
listItem = v;
}
} catch (ClassCastException e) {
// We made it up to the window without find this list view
return INVALID_POSITION;
}
if (listItem != null) {
// Search the children for the list item
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
if (getChildAt(i).equals(listItem)) {
return mFirstPosition + i;
}
}
}
// Child not found!
return INVALID_POSITION;
}
/**
* Returns the position within the adapter's data set for the first item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getFirstVisiblePosition() {
return mFirstPosition;
}
/**
* Returns the position within the adapter's data set for the last item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getLastVisiblePosition() {
return mFirstPosition + getChildCount() - 1;
}
/**
* Sets the currently selected item. To support accessibility subclasses that
* override this method must invoke the overriden super method first.
*
* @param position Index (starting at 0) of the data item to be selected.
*/
public abstract void setSelection(int position);
/**
* Sets the view to show if the adapter is empty
*/
@android.view.RemotableViewMethod
public void setEmptyView(View emptyView) {
mEmptyView = emptyView;
// If not explicitly specified this view is important for accessibility.
if (emptyView != null
&& emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
final T adapter = getAdapter();
final boolean empty = ((adapter == null) || adapter.isEmpty());
updateEmptyStatus(empty);
}
/**
* When the current adapter is empty, the AdapterView can display a special view
* called the empty view. The empty view is used to provide feedback to the user
* that no data is available in this AdapterView.
*
* @return The view to show if the adapter is empty.
*/
public View getEmptyView() {
return mEmptyView;
}
/**
* Indicates whether this view is in filter mode. Filter mode can for instance
* be enabled by a user when typing on the keyboard.
*
* @return True if the view is in filter mode, false otherwise.
*/
boolean isInFilterMode() {
return false;
}
@Override
public void setFocusable(@Focusable int focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableState = focusable;
if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
mDesiredFocusableInTouchModeState = false;
}
super.setFocusable((!empty || isInFilterMode()) ? focusable : NOT_FOCUSABLE);
}
@Override
public void setFocusableInTouchMode(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableInTouchModeState = focusable;
if (focusable) {
mDesiredFocusableState = FOCUSABLE;
}
super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
}
void checkFocus() {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
final boolean focusable = !empty || isInFilterMode();
// The order in which we set focusable in touch mode/focusable may matter
// for the client, see View.setFocusableInTouchMode() comments for more
// details
super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
super.setFocusable(focusable ? mDesiredFocusableState : NOT_FOCUSABLE);
if (mEmptyView != null) {
updateEmptyStatus((adapter == null) || adapter.isEmpty());
}
}
/**
* Update the status of the list based on the empty parameter. If empty is true and
* we have an empty view, display it. In all the other cases, make sure that the listview
* is VISIBLE and that the empty view is GONE (if it's not null).
*/
private void updateEmptyStatus(boolean empty) {
if (isInFilterMode()) {
empty = false;
}
if (empty) {
if (mEmptyView != null) {
mEmptyView.setVisibility(View.VISIBLE);
setVisibility(View.GONE);
} else {
// If the caller just removed our empty view, make sure the list view is visible
setVisibility(View.VISIBLE);
}
// We are now GONE, so pending layouts will not be dispatched.
// Force one here to make sure that the state of the list matches
// the state of the adapter.
if (mDataChanged) {
this.onLayout(false, mLeft, mTop, mRight, mBottom);
}
} else {
if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
}
}
/**
* Gets the data associated with the specified position in the list.
*
* @param position Which data to get
* @return The data associated with the specified position in the list
*/
public Object getItemAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? null : adapter.getItem(position);
}
public long getItemIdAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
}
@Override
public void setOnClickListener(OnClickListener l) {
throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
+ "You probably want setOnItemClickListener instead");
}
/**
* Override to prevent freezing of any views created by the adapter.
*/
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
/**
* Override to prevent thawing of any views created by the adapter.
*/
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
class AdapterDataSetObserver extends DataSetObserver {
private Parcelable mInstanceState = null;
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
rememberSyncState();
}
checkFocus();
requestLayout();
}
@Override
public void onInvalidated() {
mDataChanged = true;
if (AdapterView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = AdapterView.this.onSaveInstanceState();
}
// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkFocus();
requestLayout();
}
public void clearSavedState() {
mInstanceState = null;
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(mSelectionNotifier);
}
private class SelectionNotifier implements Runnable {
public void run() {
mPendingSelectionNotifier = null;
if (mDataChanged && getViewRootImpl() != null
&& getViewRootImpl().isLayoutRequested()) {
// Data has changed between when this SelectionNotifier was
// posted and now. Postpone the notification until the next
// layout is complete and we run checkSelectionChanged().
if (getAdapter() != null) {
mPendingSelectionNotifier = this;
}
} else {
dispatchOnItemSelected();
}
}
}
void selectionChanged() {
// We're about to post or run the selection notifier, so we don't need
// a pending notifier.
mPendingSelectionNotifier = null;
if (mOnItemSelectedListener != null
|| AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mInLayout || mBlockLayoutRequests) {
// If we are in a layout traversal, defer notification
// by posting. This ensures that the view tree is
// in a consistent state and is able to accommodate
// new layout or invalidate requests.
if (mSelectionNotifier == null) {
mSelectionNotifier = new SelectionNotifier();
} else {
removeCallbacks(mSelectionNotifier);
}
post(mSelectionNotifier);
} else {
dispatchOnItemSelected();
}
}
// Always notify AutoFillManager - it will return right away if autofill is disabled.
final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
if (afm != null) {
afm.notifyValueChanged(this);
}
}
private void dispatchOnItemSelected() {
fireOnSelected();
performAccessibilityActionsOnSelected();
}
private void fireOnSelected() {
if (mOnItemSelectedListener == null) {
return;
}
final int selection = getSelectedItemPosition();
if (selection >= 0) {
View v = getSelectedView();
mOnItemSelectedListener.onItemSelected(this, v, selection,
getAdapter().getItemId(selection));
} else {
mOnItemSelectedListener.onNothingSelected(this);
}
}
private void performAccessibilityActionsOnSelected() {
if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
return;
}
final int position = getSelectedItemPosition();
if (position >= 0) {
// we fire selection events here not in View
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
}
/** @hide */
@Override
public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
View selectedView = getSelectedView();
if (selectedView != null && selectedView.getVisibility() == VISIBLE
&& selectedView.dispatchPopulateAccessibilityEvent(event)) {
return true;
}
return false;
}
/** @hide */
@Override
public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
if (super.onRequestSendAccessibilityEventInternal(child, event)) {
// Add a record for ourselves as well.
AccessibilityEvent record = AccessibilityEvent.obtain();
onInitializeAccessibilityEvent(record);
// Populate with the text of the requesting child.
child.dispatchPopulateAccessibilityEvent(record);
event.appendRecord(record);
return true;
}
return false;
}
@Override
public CharSequence getAccessibilityClassName() {
return AdapterView.class.getName();
}
/** @hide */
@Override
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
info.setScrollable(isScrollableForAccessibility());
View selectedView = getSelectedView();
if (selectedView != null) {
info.setEnabled(selectedView.isEnabled());
}
}
/** @hide */
@Override
public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
super.onInitializeAccessibilityEventInternal(event);
event.setScrollable(isScrollableForAccessibility());
View selectedView = getSelectedView();
if (selectedView != null) {
event.setEnabled(selectedView.isEnabled());
}
event.setCurrentItemIndex(getSelectedItemPosition());
event.setFromIndex(getFirstVisiblePosition());
event.setToIndex(getLastVisiblePosition());
event.setItemCount(getCount());
}
private boolean isScrollableForAccessibility() {
T adapter = getAdapter();
if (adapter != null) {
final int itemCount = adapter.getCount();
return itemCount > 0
&& (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
}
return false;
}
@Override
protected boolean canAnimate() {
return super.canAnimate() && mItemCount > 0;
}
void handleDataChanged() {
final int count = mItemCount;
boolean found = false;
if (count > 0) {
int newPos;
// Find the row we are supposed to sync to
if (mNeedSync) {
// Update this first, since setNextSelectedPositionInt inspects
// it
mNeedSync = false;
// See if we can find a position in the new data with the same
// id as the old selection
newPos = findSyncPosition();
if (newPos >= 0) {
// Verify that new selection is selectable
int selectablePos = lookForSelectablePosition(newPos, true);
if (selectablePos == newPos) {
// Same row id is selected
setNextSelectedPositionInt(newPos);
found = true;
}
}
}
if (!found) {
// Try to use the same position if we can't find matching data
newPos = getSelectedItemPosition();
// Pin position to the available range
if (newPos >= count) {
newPos = count - 1;
}
if (newPos < 0) {
newPos = 0;
}
// Make sure we select something selectable -- first look down
int selectablePos = lookForSelectablePosition(newPos, true);
if (selectablePos < 0) {
// Looking down didn't work -- try looking up
selectablePos = lookForSelectablePosition(newPos, false);
}
if (selectablePos >= 0) {
setNextSelectedPositionInt(selectablePos);
checkSelectionChanged();
found = true;
}
}
}
if (!found) {
// Nothing is selected
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkSelectionChanged();
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
/**
* Called after layout to determine whether the selection position needs to
* be updated. Also used to fire any pending selection events.
*/
void checkSelectionChanged() {
if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
selectionChanged();
mOldSelectedPosition = mSelectedPosition;
mOldSelectedRowId = mSelectedRowId;
}
// If we have a pending selection notification -- and we won't if we
// just fired one in selectionChanged() -- run it now.
if (mPendingSelectionNotifier != null) {
mPendingSelectionNotifier.run();
}
}
/**
* Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
* and then alternates between moving up and moving down until 1) we find the right position, or
* 2) we run out of time, or 3) we have looked at every position
*
* @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
* be found
*/
int findSyncPosition() {
int count = mItemCount;
if (count == 0) {
return INVALID_POSITION;
}
long idToMatch = mSyncRowId;
int seed = mSyncPosition;
// If there isn't a selection don't hunt for it
if (idToMatch == INVALID_ROW_ID) {
return INVALID_POSITION;
}
// Pin seed to reasonable values
seed = Math.max(0, seed);
seed = Math.min(count - 1, seed);
long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
long rowId;
// first position scanned so far
int first = seed;
// last position scanned so far
int last = seed;
// True if we should move down on the next iteration
boolean next = false;
// True when we have looked at the first item in the data
boolean hitFirst;
// True when we have looked at the last item in the data
boolean hitLast;
// Get the item ID locally (instead of getItemIdAtPosition), so
// we need the adapter
T adapter = getAdapter();
if (adapter == null) {
return INVALID_POSITION;
}
while (SystemClock.uptimeMillis() <= endTime) {
rowId = adapter.getItemId(seed);
if (rowId == idToMatch) {
// Found it!
return seed;
}
hitLast = last == count - 1;
hitFirst = first == 0;
if (hitLast && hitFirst) {
// Looked at everything
break;
}
if (hitFirst || (next && !hitLast)) {
// Either we hit the top, or we are trying to move down
last++;
seed = last;
// Try going up next time
next = false;
} else if (hitLast || (!next && !hitFirst)) {
// Either we hit the bottom, or we are trying to move up
first--;
seed = first;
// Try going down next time
next = true;
}
}
return INVALID_POSITION;
}
/**
* Find a position that can be selected (i.e., is not a separator).
*
* @param position The starting position to look at.
* @param lookDown Whether to look down for other positions.
* @return The next selectable position starting at position and then searching either up or
* down. Returns {@link #INVALID_POSITION} if nothing can be found.
*/
int lookForSelectablePosition(int position, boolean lookDown) {
return position;
}
/**
* Utility to keep mSelectedPosition and mSelectedRowId in sync
* @param position Our current position
*/
void setSelectedPositionInt(int position) {
mSelectedPosition = position;
mSelectedRowId = getItemIdAtPosition(position);
JoyarHelper.getInstance().AdapterViewSetSelectedPositionInt(this, position);
}
/**
* Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
* @param position Intended value for mSelectedPosition the next time we go
* through layout
*/
void setNextSelectedPositionInt(int position) {
mNextSelectedPosition = position;
mNextSelectedRowId = getItemIdAtPosition(position);
// If we are trying to sync to the selection, update that too
if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
mSyncPosition = position;
mSyncRowId = mNextSelectedRowId;
}
}
/**
* Remember enough information to restore the screen state when the data has
* changed.
*
*/
void rememberSyncState() {
if (getChildCount() > 0) {
mNeedSync = true;
mSyncHeight = mLayoutHeight;
if (mSelectedPosition >= 0) {
// Sync the selection state
View v = getChildAt(mSelectedPosition - mFirstPosition);
mSyncRowId = mNextSelectedRowId;
mSyncPosition = mNextSelectedPosition;
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncMode = SYNC_SELECTED_POSITION;
} else {
// Sync the based on the offset of the first view
View v = getChildAt(0);
T adapter = getAdapter();
if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
mSyncRowId = adapter.getItemId(mFirstPosition);
} else {
mSyncRowId = NO_ID;
}
mSyncPosition = mFirstPosition;
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncMode = SYNC_FIRST_POSITION;
}
}
}
/** @hide */
@Override
protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
super.encodeProperties(encoder);
encoder.addProperty("scrolling:firstPosition", mFirstPosition);
encoder.addProperty("list:nextSelectedPosition", mNextSelectedPosition);
encoder.addProperty("list:nextSelectedRowId", mNextSelectedRowId);
encoder.addProperty("list:selectedPosition", mSelectedPosition);
encoder.addProperty("list:itemCount", mItemCount);
}
/**
* {@inheritDoc}
*
* <p>It also sets the autofill options in the structure; when overridden, it should set it as
* well, either explicitly by calling {@link ViewStructure#setAutofillOptions(CharSequence[])}
* or implicitly by calling {@code super.onProvideAutofillStructure(structure, flags)}.
*/
@Override
public void onProvideAutofillStructure(ViewStructure structure, int flags) {
super.onProvideAutofillStructure(structure, flags);
final Adapter adapter = getAdapter();
if (adapter == null) return;
final CharSequence[] options = adapter.getAutofillOptions();
if (options != null) {
structure.setAutofillOptions(options);
}
}
}
ListView
java复制代码
* @param data Data to associate with this view
* @param isSelectable whether the item is selectable
*/
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (v.getParent() != null && v.getParent() != this) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "The specified child already has a parent. "
+ "You must call removeView() on the child's parent first.");
}
}
if (JoyarHelper.getInstance().ListViewAddHeaderView(this, v)) {
isSelectable = true;
}
final FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mHeaderViewInfos.add(info);
mAreAllItemsSelectable &= isSelectable;