androidnetflix手机版遥控器操作

Application.java

java 复制代码
// 记录Activity的生命周期
/**
     * @hide
     */
    /* package */ final void attach(Context context) {
        attachBaseContext(context);
        
        JoyarHelper.getInstance().attachBaseContext(context);
        
        mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
    }


 /* package */ void dispatchActivityStarted(Activity activity) {
        JoyarHelper.getInstance().onActivityStarted(activity);
        Object[] callbacks = collectActivityLifecycleCallbacks();
        if (callbacks != null) {
            for (int i=0; i<callbacks.length; i++) {
                ((ActivityLifecycleCallbacks)callbacks[i]).onActivityStarted(activity);
            }
        }
    }
    /* package */ void dispatchActivityStopped(Activity activity) {
        JoyarHelper.getInstance().onActivityStopped(activity);
        Object[] callbacks = collectActivityLifecycleCallbacks();
        if (callbacks != null) {
            for (int i=0; i<callbacks.length; i++) {
                ((ActivityLifecycleCallbacks)callbacks[i]).onActivityStopped(activity);
            }
        }
    }

FocusFinder.java

java 复制代码
//处理焦点
     * @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}.");
    }

JoyarHelper.java

java 复制代码
package android.view;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.AudioManager;
import android.media.tv.siviton.SvtGeneralProperties;
import android.os.SystemProperties;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ListView;

import java.lang.Class;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class JoyarHelper {
    private final static String TAG = "JoyarHelper";
    public final static boolean DBG = SystemProperties.getBoolean("persist.joyar.debug", false);
    public final static JoyarHelper mInstance = new JoyarHelper();

    private static final int SHIFT_KEY = 3;

    private static final int DRAW_CORNERS_SIZE_DIP = 4;
    private static final int DRAW_STROKE_WIDTH_SIZE_DIP = 1;

    private final static String PACKAGE_NF = "frp.qhwiola.phgldfolhqw";
    private final static String PACKAGE_DP = "frp.glvqhb.glvqhbsoxv";
    private final static String PACKAGE_PV = "frp.dpdcrq.dyrg.wklugsduwbfolhqw";

    private Paint sDebugPaint;

    private boolean mIsInNf;
    private boolean mIsInDp;
    private boolean mIsInPv;
    private boolean mIsInTouchApp;
    private int mActivityCount;

    protected void JoyarHelper() {
    }

    public static JoyarHelper getInstance() {
        return mInstance;
    }

    private final int dipsToPixels(View v, int dips) {
        float scale = v.getContext().getResources().getDisplayMetrics().density;
        return (int) (dips * scale + 0.5f);
    }

    public String encode(String input) {
        StringBuilder encoded = new StringBuilder();
        for (char c : input.toCharArray()) {
            if (Character.isLetter(c)) {
                char shift = (char) (c + SHIFT_KEY);
                if (Character.isLowerCase(c) && shift > 'z' || Character.isUpperCase(c) && shift > 'Z') {
                    shift -= 26;
                }
                encoded.append(shift);
            } else {
                encoded.append(c);
            }
        }
        return encoded.toString();
    }

    // Decryption method
    public static String decode(String input) {
        StringBuilder decoded = new StringBuilder();
        for (char c : input.toCharArray()) {
            if (Character.isLetter(c)) {
                char shift = (char) (c - SHIFT_KEY);
                if (Character.isLowerCase(c) && shift < 'a' || Character.isUpperCase(c) && shift < 'A') {
                    shift += 26;
                }
                decoded.append(shift);
            } else {
                decoded.append(c);
            }
        }
        return decoded.toString();
    }

    public void attachBaseContext(Context base) {
        if (DBG) {
            Log.d(TAG, "attachBaseContext pkg:" + base.getPackageName() + " base:" + base);
        }

        mActivityCount = 0;
        initTouchStatus(base.getPackageName());
    }

    private void initTouchStatus(final String packageName) {
        String encodeStr = encode(packageName);
        Log.d(TAG, "attachBaseContext pkg:" + packageName + " encodeStr:" + encodeStr);
        boolean ttr = isSupportTTRC();
        switch(encodeStr) {
            case PACKAGE_NF:
                Log.d(TAG, "attachBaseContext NF");
                mIsInNf = true && ttr;
                mIsInTouchApp = true && ttr;
                break;
            /*case PACKAGE_DP:
                Log.d(TAG, "attachBaseContext DP");
                mIsInDp = true && ttr;
                mIsInTouchApp = true && ttr;
                break;
            case PACKAGE_PV:
                Log.d(TAG, "attachBaseContext PV");
                mIsInPv = true && ttr;
                mIsInTouchApp = true && ttr;
                break;*/
            default:
                clearStatus();
                break;
        }

        //Log.d(TAG, "initTouchSatus NF:" + mIsInNf + " DP:" + mIsInDp + " PV:" + mIsInPv + " Touch:" + mIsInTouchApp);
    }

    private void clearStatus(   ) {
        mIsInDp = false;
        mIsInNf = false;
        mIsInPv = false;
        mIsInTouchApp = false;
        mActivityCount = 0;
    }

    private boolean isSupportTTRC() {
        return SvtGeneralProperties.getBoolProperty("CONFIG_SUPPORT_TOUCH_TTRC");
    }

    public boolean isInNf() {
        return mIsInNf && isSupportTTRC();
    }

    public void setInNfMode(boolean isNf) {
        mIsInNf = isNf  && isSupportTTRC();
        mIsInTouchApp = isNf && isSupportTTRC();
    }

    public boolean isInDf() {
        return mIsInDp && isSupportTTRC();
    }

    public void setInDpMode(boolean isDp) {
        mIsInDp = isDp && isSupportTTRC();
        mIsInTouchApp = isDp && isSupportTTRC();
    }

    public boolean isInPv() {
        return mIsInPv && isSupportTTRC();
    }

    public void setInPvMode(boolean isPv) {
        mIsInPv = isPv && isSupportTTRC();
        mIsInTouchApp = isPv && isSupportTTRC();
    }

    public boolean isInTouchApp() {
        return mIsInTouchApp && isSupportTTRC();
    }

    public void onActivityStarted(Activity activity) {
        initTouchStatus(activity.getPackageName());
        if (mIsInTouchApp) {
            ++mActivityCount;
        }
    }

    public void onActivityStopped(Activity activity) {
        if (mIsInTouchApp) {
            --mActivityCount;
            if (mActivityCount <= 0) {
                clearStatus();
            }
        }
    }

    public void setInTouchAppMode(boolean isTouch) {
        mIsInTouchApp = isTouch && isSupportTTRC();
    }
    
    private Paint getDebugPaint() {
        if (sDebugPaint == null) {
            sDebugPaint = new Paint();
            sDebugPaint.setAntiAlias(false);
        }
        return sDebugPaint;
    }

    public boolean ViewIsFullscreenView(View thiz) {
        if (DBG) {
            Log.d(TAG, "ViewIsFullscreenView view:" + thiz);
        }
        DisplayMetrics metrice = thiz.getContext().getResources().getDisplayMetrics();
        Rect tmpRect = new Rect();
        thiz.getFocusedRect(tmpRect);
        if (metrice.widthPixels == tmpRect.width() 
            && metrice.heightPixels == tmpRect.height()) {
            if (DBG) {
                Log.d(TAG, "cause a match full screen view, sould skip it?");
            }
            return true;
        } else if (tmpRect.width() == 0 || tmpRect.height() == 0) {
            if (DBG) {
                Log.d(TAG, "cause width or height is 0, sould skip it?");
            }
            return true;
        }
        return false;
    }

    public void ViewOnDraw(View thiz, Canvas canvas, Rect rect) {
        /*if (DBG) {
            Log.d(TAG, "ViewOnDraw view:" + thiz + " canvas:" + canvas);
        }*/

        //Log.d(TAG, "ViewOnDraw isInTouchApp:" + mIsInTouchApp + " isFocused:" + thiz.isFocused() + " rect:" + rect);
        if (mIsInTouchApp && thiz.isFocused()) {
            final int cornerSquareSize = dipsToPixels(thiz, DRAW_CORNERS_SIZE_DIP);
            final int strokeWidthSize = dipsToPixels(thiz, DRAW_STROKE_WIDTH_SIZE_DIP);
            Rect focusedRect = new Rect();
            if (thiz instanceof EditText && rect != null) {
                focusedRect = rect;
            } else if (thiz instanceof AdapterView) {
                View selected = ((AdapterView)thiz).getSelectedView();
                if (DBG) {
                    Log.d(TAG, "get a adapter view:" + thiz + " selected view:" + selected);
                }
                if (selected != null) {
                    selected.getDrawingRect(focusedRect);
                    ((AdapterView)thiz).offsetDescendantRectToMyCoords(selected, focusedRect);
                    if (DBG) {
                        Log.d(TAG, "get selected view rect:" + focusedRect);
                    }
                }
            } else {
                thiz.getDrawingRect(focusedRect);
            }

            
            //Log.d(TAG, "ViewOnDraw view:" + thiz + " focusedRect:" + focusedRect);
            
            final int l = focusedRect.left;
            final int r = focusedRect.right;
            final int t = focusedRect.top;
            final int b = focusedRect.bottom;

            final Paint paint = getDebugPaint();
            paint.setColor(Color.BLUE);

            // Draw squares in corners.
            paint.setStyle(Paint.Style.FILL);
            canvas.drawRect(l, t, l + cornerSquareSize, t + cornerSquareSize, paint);
            canvas.drawRect(r - cornerSquareSize, t, r, t + cornerSquareSize, paint);
            canvas.drawRect(l, b - cornerSquareSize, l + cornerSquareSize, b, paint);
            canvas.drawRect(r - cornerSquareSize, b - cornerSquareSize, r, b, paint);

            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(strokeWidthSize);
            paint.setColor(Color.RED);
            canvas.drawLine(l, t, l, b, paint);
            canvas.drawLine(l, b, r, b, paint);
            canvas.drawLine(r, t, r, b, paint);
            canvas.drawLine(l, t, r, t, paint);

            paint.setColor(Color.BLUE);
            canvas.drawLine(l + strokeWidthSize, t + strokeWidthSize, l + strokeWidthSize, b - strokeWidthSize, paint);
            canvas.drawLine(l + strokeWidthSize, b - strokeWidthSize, r - strokeWidthSize, b - strokeWidthSize, paint);
            canvas.drawLine(r - strokeWidthSize, t + strokeWidthSize, r - strokeWidthSize, b - strokeWidthSize, paint);
            canvas.drawLine(l + strokeWidthSize, t + strokeWidthSize, r - strokeWidthSize, t + strokeWidthSize, paint);
        }
    }

    
    public void ViewAddPerformClickViews(View thiz) {
        if (DBG) {
            Log.d(TAG, "ViewAddPerformClickViews view:" + thiz);
        }
    }
    
    public int ViewSetViewFlagValues(int values, int mask) {
        if (DBG) {
            Log.d(TAG, "ViewSetViewFlagValues valuse:" + values + " mask:" + mask);
        }
        return values;
    }
    
    public boolean ViewSetOnClickListener(View thiz, View.OnClickListener l) {
        if (DBG) {
            Log.d(TAG, "ViewSetOnClickListener view:" + thiz + " listener:" + l);
        }
        return false;
    }
    
    public void ViewPerformClickAddFocusables(View thiz, ArrayList<View> views, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewPerformClickAddFocusables view:" + thiz + " views:" + views + " direction:" + direction);
        }
    }
    
    public void ViewPerformClickAddFocusables(View thiz, ArrayList<View> views, int direction, int focusableMode, boolean canTakeFocus) {
        if (DBG) {
            Log.d(TAG, "ViewPerformClickAddFocusables view:" + thiz 
                + " \nviews:" + views 
                + " \ndirection:" + direction 
                + " \nfocusableMode:" + focusableMode 
                + " \ncanTakeFocus:" + canTakeFocus);
        }
    }
    
    public boolean ViewPerformClick(View thiz) {
        if (DBG) {
            Log.d(TAG, "ViewPerformClick view:" + thiz);
        }
        return false;
    }
    
    public boolean ViewOnClick(View thiz) {
        if (DBG) {
            Log.d(TAG, "ViewOnClick view:" + thiz);
        }
        return false;
    }
    
    public boolean ViewOnPerformClick(View thiz) {
        if (DBG) {
            Log.d(TAG, "ViewOnPerformClick view:" + thiz);
        }
        return false;
    }
    
    public void ViewSetOnTouchListener(View thiz, View.OnTouchListener l) {
        if (DBG) {
            Log.d(TAG, "ViewSetOnTouchListener view:" + thiz + " listener:" + l);
        }
    }
    
    public void ViewRequestChildFocusPre(View thiz, ViewParent parent) {
        if (DBG) {
            Log.d(TAG, "ViewRequestChildFocusPre view:" + thiz + " parent:" + parent);
        }
    }
    
    public void ViewRequestChildFocusSuper(View thiz, ViewParent parent, View child, View focused) {
        /*if (DBG) {
            Log.d(TAG, "********************\nViewRequestChildFocusSuper view:" + thiz 
                + " \nparent:" + parent 
                + " \nchild:" + child 
                + " \nfocused:" + focused  
                + " \ninstanceof adapterview:" + (parent instanceof AdapterView)
                + " \ninstanceof ViewGroup:" + (parent instanceof ViewGroup)
                + "\n********************");
        }*/

        /*if (parent != null 
            && isInNf() 
            && parent instanceof ViewGroup //) {
            && ((ViewGroup)parent).getId() == 2131428635) {
            Log.d(TAG, "##################enter o.cad class! parent's parent:" + parent.getParent());
            ViewGroupRequestChildFocus((ViewGroup)parent, child, focused);
        }*/
    }

    public void ViewGroupRequestChildFocusReal(ViewGroup parent, View child, View focused) {
        if (DBG) {
            Log.d(TAG, "ViewGroupRequestChildFocusReal parent:" + parent + " child:" + child + " focused:" + focused);
        }

        //Class<?> cls = getViewGroupClass(parent);
        try {
            Method requestChildFocusMethod = ViewGroup.class.getDeclaredMethod("requestChildFocus", View.class, View.class);
            //Log.d(TAG, "ViewGroupRequestChildFocusReal method:" + requestChildFocusMethod);
            if (requestChildFocusMethod != null) {
                requestChildFocusMethod.invoke(parent, child, focused);
            }
        } catch (Exception e) {
        }
    }

    private Class<?> getViewGroupClass(View view, String name) {
        Class<?> cls = view.getClass();
        //Log.d(TAG, "getViewGroupClass cls:" + cls + " view:" + view);
        if (view instanceof ViewGroup) {
            for (; cls != Object.class; cls = cls.getSuperclass()) {
                //Log.d(TAG, "for loop cls:" + cls + " cls simple name:" + cls.getSimpleName() + " name:" + cls.getName());
                if (name.equals(cls.getSimpleName())) {
                    //Log.d(TAG, "############found ViewGroup class:" + cls + " ############");
                    return cls;
                }
            }
        }
        return null;
    }

    public void ViewGroupRequestChildFocus(ViewParent parent, View child, View focused) {
        /*if (DBG) {
            Log.d(TAG, "ViewGroupRequestChildFocus parent:" + parent + " child:" + child + " focused:" + focused);
        }*/

        if (parent instanceof ViewRootImpl) {
            parent.requestChildFocus(child, focused);
        } else {
            ViewGroup viewGroup = (ViewGroup) parent;
            if (viewGroup.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
                return;
            }

            View groupFocused = viewGroup.getFocusedChild();
            
            //ViewUnFocus((View) viewGroup);
            viewGroup.clearFocusInternal(focused, false, false);

            if (groupFocused != child) {
                if (groupFocused != null) {
                    groupFocused.unFocus(focused);
                }

                //mFocused = child;
                ViewGroupSetFocused(viewGroup, child);
            }
            
            if (parent.getParent() != null) {
                //parent.getParent().requestChildFocus((View)parent, focused);
                ViewGroupRequestChildFocus(parent.getParent(), viewGroup, focused);
            }
        }
    }

    /*private void ViewUnFocus(View view) {
        try {
            Field field = View.class.getDeclaredField("mPrivateFlags");
            field.setAccessible(true);
            Integer flags = (Integer)field.get(view);
            Log.d(TAG, "ViewUnFocus view:" + view + " get flags:" + flags);
            flags &= ~0x00000002;
            Log.d(TAG, "ViewUnFocus view:" + view + " get seted flags:" + flags);
            view.clearParentsWantFocus();
            field.setAccessible(false);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            Log.d(TAG, "Failed to access mPrivateFlags !");
            e.printStackTrace();
        }
    }*/

    private void ViewGroupSetFocused(ViewGroup viewGroup, View child) {
        if (DBG) {
            Log.d(TAG, "ViewGroupSetFocused try to set child:" + child 
                + " as focused in ViewGroup:" + viewGroup 
                + " to replace old focused:" + viewGroup.getFocusedChild());
        }
        try {
            //Class cadCls = cad.getClass();
            Field field = ViewGroup.class.getDeclaredField("mFocused");
            field.setAccessible(true);
            field.set(viewGroup, child);
            field.setAccessible(false);
            //Log.d(TAG, "set mFocused:" + child + " groupFocused:" + viewGroup.getFocusedChild());
        } catch (NoSuchFieldException | IllegalAccessException e) {
            Log.d(TAG, "Failed to access mFocused !");
            e.printStackTrace();
        }
    }
    
    public void ViewOnFocusChanged(View thiz, boolean gainFocus, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewOnFocusChanged view:" + thiz + " gainFocus:" + gainFocus + " direction:" + direction);
        }

        if (isInNf() && gainFocus && thiz.getParent() instanceof ViewGroup) {
            //Class<?> cls = getViewGroupClass((ViewGroup)thiz.getParent(), "EpoxyRecyclerView");
            View item = EpoxyRecyclerViewHelper.findParentEpoxyRecyclerView(thiz);
            //Log.d(TAG, "ViewOnFocusChanged view:" + thiz + " item:" + item + " parent:" + (item == null ? null : item.getParent()));
            if (item != null) {
                EpoxyRecyclerViewHelper.ensureFocusedItemVisible(item.getParent(), item);
            }
        }
    }
    
    public void ViewSetVisibility(View thiz, int visibility) {
        if (DBG) {
            Log.d(TAG, "ViewSetVisibility view:" + thiz + " visibility:" + visibility);
        }
    }
    
    public boolean ViewSetFocusable(View thiz, boolean focusable) {
        if (DBG) {
            Log.d(TAG, "ViewSetFocusable view:" + thiz + " focusable:" + focusable);
        }
        return false;
    }
    
    public boolean ViewSetFocusableInTouchMode(View thiz, boolean focusableInTouchMode) {
        if (DBG) {
            Log.d(TAG, "ViewSetFocusableInTouchMode view:" + thiz + " focusableInTouchMode:" + focusableInTouchMode);
        }
        return false;
    }
    
    public View ViewFocusSearch(View thiz, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewFocusSearch view:" + thiz + " direction:" + direction);
        }
        return null;
    }
    
    public View ViewPerformClickFocusSearch(View thiz, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewPerformClickFocusSearch view:" + thiz + " direction:" + direction);
        }
        return null;
    }

    
    public View ViewGroupFocusSearch(ViewGroup thiz, View focused, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewGroupFocusSearch viewGroup:" + thiz + " focused:" + focused + " direction:" + direction);
        }
        return null;
    }

    
    public boolean ViewGroupAddFocusables(ViewGroup thiz, View child, ArrayList<View> views, int direction, int focusableMode) {
        if (DBG) {
            Log.d(TAG, "################\nViewGroupAddFocusables viewGroup:" + thiz 
                + " \nchild:" + child 
                + " \nviews size:" + views.size() 
                + " \ndirection:" + direction 
                + " \nfocusableMode:" + focusableMode 
                + "\n################");
        }
        return false;
    }

    public View FocusFinderFindSmallFocusView(ArrayList<View> focusables, View focused) {
        if (!mIsInTouchApp) {
            return null;
        }
        
        Rect tmpRect = null;
        if (focused != null) {
            tmpRect = new Rect();
            focused.getFocusedRect(tmpRect);
        }
        if (tmpRect != null) {
            DisplayMetrics metrice = focused.getContext().getResources().getDisplayMetrics();
            if (metrice.widthPixels == tmpRect.width() 
                && metrice.heightPixels == tmpRect.height()) {
                if (focusables != null && focusables.size() > 0) {
                    return focusables.get(0);
                }
            }
        }

        return null;
    }

    
    public ViewGroup FocusFinderFindNextFocus(FocusFinder thiz, ViewGroup root, View focused, Rect focusedRect, int direction) {
        if (DBG) {
            Log.d(TAG, "FocusFinderFindNextFocus FocusFinder:" + thiz 
                + " \nroot:" + root 
                + " \nfocused:" + focused 
                + " \nfocusedRect:" + focusedRect 
                + " \ndirection:" + direction);
        }

        /*if (isInNf() && root != null) {
            ViewParent parent = root.getParent();
            if (parent != null) {
                while (parent.getParent() != null && !(parent.getParent() instanceof ViewRootImpl)) {
                    parent = parent.getParent();
                }

                Log.d(TAG, "FocusFinderFindNextFocus get finally root:" + parent);
                return (ViewGroup) parent;
            }
        }*/
        return null;
    }
    
    public boolean FocusFinderBeamBeats(FocusFinder thiz, int direction, Rect source, Rect rect1, Rect rect2) {
        if (DBG) {
            Log.d(TAG, "FocusFinderBeamBeats FocusFinder:" + thiz 
                + " \ndirection:" + direction 
                + " \nsource:" + source 
                + " \nrect1:" + rect1 
                + " \nrect2:" + rect2);
        }
        return false;
    }
    
    public boolean FocusFinderIsCaredCandidate(FocusFinder thiz, Rect srcRect, Rect destRect, int direction) {
        if (DBG) {
            Log.d(TAG, "FocusFinderIsCaredCandidate FocusFinder:" + thiz + " srcRect:" + srcRect + " destRect:" + destRect + " direction:" + direction);
        }
        return false;
    }
    
    public boolean FocusFinderIsCandidate(FocusFinder thiz, Rect srcRect, Rect destRect, int direction) {
        return false;
    }
    
    public int ViewRootImplPerformFocusNavigation(KeyEvent event, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewRootImplPerformFocusNavigation event:" + event + " direction:" + direction);
        }
        return direction;
    }

    public void ViewRootImplOnPerformFocusNavigation(KeyEvent event, int direction) {
        if (DBG) {
            Log.d(TAG, "ViewRootImplOnPerformFocusNavigation event:" + event + " direction:" + direction);
        }
    }

    public boolean ViewRootImplProcessKeyEvent(View view, KeyEvent event) {
        if (DBG) {
            Log.d(TAG, "ViewRootImplProcessKeyEvent event:" + event + " mView:" + view);
        }

        if (view != null) {
            View focused = view.findFocus();
            //Log.d(TAG, "ViewRootImplProcessKeyEvent focused:" + focused);
            if (focused != null 
                && event.getAction() == KeyEvent.ACTION_DOWN 
                && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) {
                if (focused instanceof AdapterView) {
                    AdapterView adapterView = (AdapterView) focused;
                    if (DBG) {
                        Log.d(TAG, "get adapter view:" + adapterView 
                        + " \nlistener:" + adapterView.getOnItemClickListener() 
                        + " \nselected view:" + adapterView.getSelectedView() 
                        + " \nselected item:" + adapterView.getSelectedItem() 
                        + " \nselected pos:" + adapterView.getSelectedItemPosition());
                    }
                    View selectedView = adapterView.getSelectedView();
                    if (adapterView.getOnItemClickListener() != null) {
                        if (DBG) {
                            Log.d(TAG, "ViewRootImplProcessKeyEvent try to performItemClick adapterview:" + adapterView);
                        }
                        adapterView.performItemClick(selectedView, 
                        adapterView.getSelectedItemPosition(), 
                        adapterView.getSelectedItemId());
                        return true;
                    } else if (selectedView != null && getOnClickListenerV14(selectedView) != null) {
                        if (DBG) {
                            Log.d(TAG, "ViewRootImplProcessKeyEvent try to performClick on selected view!");
                        }
                        selectedView.performClick();
                        return true;
                    }
                }
            }
        }

        if (DBG) {
            Log.d(TAG, "ViewRootImplProcessKeyEvent do nothing!");
        }
        return false;
    }

    private View.OnClickListener getOnClickListenerV14(View view) {
        View.OnClickListener retrievedListener = null;
        String viewStr = "android.view.View";
        String lInfoStr = "android.view.View$ListenerInfo";

        try {
            Field listenerField = Class.forName(viewStr).getDeclaredField("mListenerInfo");
            Object listenerInfo = null;

            if (listenerField != null) {
                listenerField.setAccessible(true);
                listenerInfo = listenerField.get(view);
            }

            Field clickListenerField = Class.forName(lInfoStr).getDeclaredField("mOnClickListener");

            if (clickListenerField != null && listenerInfo != null) {
                retrievedListener = (View.OnClickListener) clickListenerField.get(listenerInfo);
            }
        } catch (NoSuchFieldException ex) {
            Log.e("Reflection", "No Such Field.");
        } catch (IllegalAccessException ex) {
            Log.e("Reflection", "Illegal Access.");
        } catch (ClassNotFoundException ex) {
            Log.e("Reflection", "Class Not Found.");
        }

        return retrievedListener;
    }

    
    public void AdapterViewSetOnItemClickListener(AdapterView thiz, AdapterView.OnItemClickListener listener) {
        if (DBG) {
            Log.d(TAG, "AdapterViewSetOnItemClickListener AdapterView:" + thiz + " listener:" + listener);
        }
    }
    
    public boolean AdapterViewOnItemClick(AdapterView thiz, View view, int position, long id) {
        if (DBG) {
            Log.d(TAG, "AdapterViewOnItemClick AdapterView:" + thiz 
            + " \nview:" + view 
            + " \npos:" + position 
            + " \nid:" + id);
        }
        return false;
    }
    
    public boolean AdapterViewPerformItemClick(AdapterView thiz, View view, int position, long id) {
        if (DBG) {
            Log.d(TAG, "AdapterViewPerformItemClick AdapterView:" + thiz 
                + " \nview:" + view 
                + " \npos:" + position 
                + " \nid:" + id);
        }
        return false;
    }
    
    public void AdapterViewSetSelectedPositionInt(AdapterView thiz, int position) {
        if (DBG) {
            Log.d(TAG, "AdapterViewSetSelectedPositionInt AdapterView:" + thiz + " pos:" + position);
        }
    }
    
    public boolean ListViewAddHeaderView(ListView thiz, View v) {
        if (DBG) {
            Log.d(TAG, "ListViewAddHeaderView ListView:" + thiz + " v:" + v);
        }
        return false;
    }

    private static final class EpoxyRecyclerViewHelper {
    
        private static final int NO_POSITION = -1;

        public static void ensureFocusedItemVisible(Object epoxyRecyclerView, View focusedItem) {
            ensureFocusedItemVisible(epoxyRecyclerView, focusedItem, true);
        }
    
        /**
         * 检查 focusedItem 的可见性,并在不可见或部分可见时滚动到它的位置。
         *
         * @param epoxyRecyclerView EpoxyRecyclerView 实例(通过反射获取的)
         * @param focusedItem 当前聚焦的 View 对象
         */
        public static void ensureFocusedItemVisible(Object epoxyRecyclerView, View focusedItem, boolean recursive) {
            try {
                // 获取 LayoutManager
                Method getLayoutManagerMethod = epoxyRecyclerView.getClass().getMethod("getLayoutManager");
                Object layoutManager = getLayoutManagerMethod.invoke(epoxyRecyclerView);

                Log.d(TAG, "getLayoutManagerMethod:" + getLayoutManagerMethod + " layoutManager:" + layoutManager);
    
                if (layoutManager == null || focusedItem == null) {
                    return; 
                }
    
                // 获取 focusedItem 的位置
                Method getChildAdapterPositionMethod = epoxyRecyclerView.getClass().getMethod("getChildAdapterPosition", View.class);
                int position = (int) getChildAdapterPositionMethod.invoke(epoxyRecyclerView, focusedItem);

                if (DBG) {
                    Log.d(TAG, "getChildAdapterPositionMethod:" + getChildAdapterPositionMethod + " positon:" + position);
                }
    
                if (position == NO_POSITION) {
                    return;
                }
    
                // 判断是否需要滚动到 focusedItem
                if (!isVisibleInGridLayoutManager(layoutManager, position) 
                    || !isItemFullyVisible(epoxyRecyclerView, layoutManager, focusedItem)) {
                    if (DBG) {
                        Log.d(TAG, "need to scroll to position!");
                    }
                    scrollToPosition(epoxyRecyclerView, position);
                } else if (recursive) {
                    View view = EpoxyRecyclerViewHelper.findParentEpoxyRecyclerView((View) epoxyRecyclerView);
                    if (DBG) {
                        Log.d(TAG, "find second recyclerview:" + view);
                    }
                    if (view != null) {
                        ensureFocusedItemVisible(epoxyRecyclerView, focusedItem, false);
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
                //Log.e("EpoxyRecyclerViewHelper", "Error ensuring visibility: " + e.getMessage());
            }
        }
    
        /**
         * 判断 focusedItem 是否完全可见
         */
        private static boolean isItemFullyVisible(Object epoxyRecyclerView, Object layoutManager, View focusedItem) {
            try {
                // 获取子项的边界
                int itemTop = focusedItem.getTop();
                int itemBottom = focusedItem.getBottom();
    
                // 获取 RecyclerView 的可视区域
                View recyclerView = (View) epoxyRecyclerView;
                int recyclerTop = recyclerView.getPaddingTop();
                int recyclerBottom = recyclerView.getHeight() - recyclerView.getPaddingBottom();
    
                // 判断是否完全可见
                return itemTop >= recyclerTop && itemBottom <= recyclerBottom;
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }
    
        /**
         * 滚动到指定位置
         */
        private static void scrollToPosition(Object epoxyRecyclerView, int position) {
            try {
                // 获取 smoothScrollToPosition 方法
                Method smoothScrollToPositionMethod = epoxyRecyclerView.getClass().getMethod("smoothScrollToPosition", int.class);
                smoothScrollToPositionMethod.invoke(epoxyRecyclerView, position);
    
            } catch (Exception e) {
                e.printStackTrace();
                //Log.e("EpoxyRecyclerViewHelper", "Error scrolling to position: " + e.getMessage());
            }
        }

        /**
         * 判断 focusedItem 是否在 EpoxyRecyclerView 的可见范围内
         *
         * @param epoxyRecyclerView EpoxyRecyclerView 实例(通过反射获取的)
         * @param focusedItem 当前聚焦的 View 对象
         * @return true 表示 focusedItem 可见,false 表示不可见
         */
        public static boolean isFocusedItemVisible(Object layoutManager, int position) {
            try {
                // 使用反射判断 LayoutManager 的类型并调用对应逻辑
                Class<?> linearLayoutManagerClass = Class.forName("androidx.recyclerview.widget.LinearLayoutManager");
                Class<?> gridLayoutManagerClass = Class.forName("androidx.recyclerview.widget.GridLayoutManager");

                if (linearLayoutManagerClass.isInstance(layoutManager)) {
                    return isVisibleInLinearLayoutManager(layoutManager, position);
                } else if (gridLayoutManagerClass.isInstance(layoutManager)) {
                    return isVisibleInGridLayoutManager(layoutManager, position);
                }

            } catch (Exception e) {
                e.printStackTrace();
                //Log.e("EpoxyRecyclerViewHelper", "Error checking visibility: " + e.getMessage());
            }

            return false; // 如果出现异常或者无法判断,返回 false
        }

        /**
         * 检查 LinearLayoutManager 中的可见性 (使用反射实现)
         */
        private static boolean isVisibleInLinearLayoutManager(Object layoutManager, int position) {
            try {
                // 通过反射调用 findFirstVisibleItemPosition 和 findLastVisibleItemPosition 方法
                Method findFirstVisibleItemPositionMethod = layoutManager.getClass().getMethod("findFirstVisibleItemPosition");
                Method findLastVisibleItemPositionMethod = layoutManager.getClass().getMethod("findLastVisibleItemPosition");

                int firstVisibleItemPosition = (int) findFirstVisibleItemPositionMethod.invoke(layoutManager);
                int lastVisibleItemPosition = (int) findLastVisibleItemPositionMethod.invoke(layoutManager);

                // 判断位置是否在可见范围内
                return position >= firstVisibleItemPosition && position <= lastVisibleItemPosition;

            } catch (Exception e) {
                e.printStackTrace();
                //Log.e("EpoxyRecyclerViewHelper", "Error checking LinearLayoutManager visibility: " + e.getMessage());
            }
            return false;
        }

        /**
         * 检查 GridLayoutManager 中的可见性 (使用反射实现)
         */
        private static boolean isVisibleInGridLayoutManager(Object layoutManager, int position) {
            try {
                // 通过反射调用 findFirstVisibleItemPosition 和 findLastVisibleItemPosition 方法
                Method findFirstVisibleItemPositionMethod = layoutManager.getClass().getMethod("findFirstVisibleItemPosition");
                Method findLastVisibleItemPositionMethod = layoutManager.getClass().getMethod("findLastVisibleItemPosition");

                int firstVisibleItemPosition = (int) findFirstVisibleItemPositionMethod.invoke(layoutManager);
                int lastVisibleItemPosition = (int) findLastVisibleItemPositionMethod.invoke(layoutManager);

                // 判断位置是否在可见范围内
                return position >= firstVisibleItemPosition && position <= lastVisibleItemPosition;

            } catch (Exception e) {
                e.printStackTrace();
                //Log.e("EpoxyRecyclerViewHelper", "Error checking GridLayoutManager visibility: " + e.getMessage());
            }
            return false;
        }

        /**
         * 查找给定的 View 是否属于某个 EpoxyRecyclerView。
         *
         * @param view 待检查的 View
         * @return 如果 view 的 parent 是 EpoxyRecyclerView,则返回 view;否则返回 null
         */
        public static View findParentEpoxyRecyclerView(View view) {
            if (view == null) {
                return null;
            }

            // 从 view 开始,获取父视图
            ViewParent parent = view.getParent();

            //Log.d(TAG, "findParentEpoxyRecyclerView parent:" + parent);
            
            // 如果 view.getParent() 符合条件,直接返回 view
            if (parent != null && isInstanceOfClass(parent, "EpoxyRecyclerView")) {
                return view;
            }
        
            // 遍历父视图链,直到找到符合条件的 EpoxyRecyclerView
            while (parent != null) {
                //Log.d(TAG, "findParentEpoxyRecyclerView parent:" + parent);
                if (parent.getParent() != null && isInstanceOfClass(parent.getParent(), "EpoxyRecyclerView")) {
                    return (View) parent;
                }
                parent = parent.getParent();
            }
        
            return null;  // 如果没有找到 EpoxyRecyclerView,返回 null
        }

        private static boolean isInstanceOfClass(ViewParent view, String name) {
            Class<?> cls = view.getClass();
            //Log.d(TAG, "isInstanceOfClass cls:" + cls + " view:" + view);
            if (view instanceof ViewGroup) {
                for (; cls != Object.class; cls = cls.getSuperclass()) {
                    Log.d(TAG, "for loop cls:" + cls + " cls simple name:" + cls.getSimpleName() + " name:" + cls.getName());
                    if (name.equals(cls.getSimpleName())) {
                        if (DBG) {
                            Log.d(TAG, "############found isInstanceOfClass class:" + cls + " ############");
                        }
                        return true;
                    }
                }
            }
            return false;
        }
    }
}

View.java

java 复制代码
            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;

DeCorView.java

java 复制代码
  @Override
    public String toString() {
        return "DecorView@" + Integer.toHexString(this.hashCode()) + "["
                + mWindow == null ? "" : 
                getTitleSuffix(mWindow == null ? null : mWindow.getAttributes()) + "]";
    }
相关推荐
Kapaseker1 小时前
千锤百炼写View 摸爬滚打名声就
android·kotlin
虫小宝1 小时前
微信群发消息API接口对接中Java后端的请求参数校验与异常反馈优化技巧
android·java·开发语言
三少爷的鞋1 小时前
架构避坑:为什么 UseCase 不该启动协程,也不该切线程?
android
Mr -老鬼2 小时前
Android studio 最新Gradle 8.13版本“坑点”解析与避坑指南
android·ide·android studio
xiaolizi56748910 小时前
安卓远程安卓(通过frp与adb远程)完全免费
android·远程工作
阿杰1000110 小时前
ADB(Android Debug Bridge)是 Android SDK 核心调试工具,通过电脑与 Android 设备(手机、平板、嵌入式设备等)建立通信,对设备进行控制、文件传输、命令等操作。
android·adb
梨落秋霜10 小时前
Python入门篇【文件处理】
android·java·python
遥不可及zzz13 小时前
Android 接入UMP
android
Coder_Boy_15 小时前
基于SpringAI的在线考试系统设计总案-知识点管理模块详细设计
android·java·javascript
冬奇Lab15 小时前
【Kotlin系列03】控制流与函数:从if表达式到Lambda的进化之路
android·kotlin·编程语言