[Android]Android焦点流程代码分析

简介:

以下内容为原创,欢迎转载,转载请注明
来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/7286503.html

通过View的View::focusSearch进行焦点搜索对应方向上的下一个可以获取焦点的View:

public View focusSearch(@FocusRealDirection int direction) {
   if (mParent != null) {
       return mParent.focusSearch(this, direction);
   } else {
       return null;
   }
}


不断地调用父控件来进行搜索,focusSearch有两个实现:ViewGroupRecyclerView,先看ViewGroup

@Override
public View focusSearch(View focused, int 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;
}


如果是最顶层,则直接调用FocusFinder::findNextFocus方法进行搜索;否则调用父控件的focusSearchFocusFinder::findNextFocus如下:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
   View next = null;
   if (focused != null) {
       next = findNextUserSpecifiedFocus(root, focused, direction);
   }
   if (next != null) {
       return next;
   }
   ArrayList<View> focusables = mTempList;
   try {
       focusables.clear();
       root.addFocusables(focusables, direction);
       if (!focusables.isEmpty()) {
           next = findNextFocus(root, focused, focusedRect, direction, focusables);
       }
   } finally {
       focusables.clear();
   }
   return next;
}

上面的root参数代表的是最顶层的view。

首先,通过尝试通过findNextUserSpecifiedFocus来查找下一个“指定的”可获得焦点的View,这个指定是开发者通过SDK自带的setNextFocusLeftId等方法进行手动设置的。如果查找到指定的下一个可获得焦点的View,则返回该View;否则,执行View::addFocusables方法,通过这个最顶层的View去拿到所有直接或间接的Focusable的子View,并添加到ArrayList<View> focusables中。

View::addFolcusables方法中有4种实现:

第一种是View中默认实现:

public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
            @FocusableMode int focusableMode) {
        if (views == null) {
            return;
        }
        if (!isFocusable()) {
            return;
        }
        if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                && !isFocusableInTouchMode()) {
            return;
        }
        views.add(this);
    }

如果自己是focusable的话,直接把自己添加进去。

第二种是ViewGroup实现:

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
   final int focusableCount = views.size();

   final int descendantFocusability = getDescendantFocusability();

   if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
       if (shouldBlockFocusForTouchscreen()) {
           focusableMode |= FOCUSABLES_TOUCH_MODE;
       }

       final int count = mChildrenCount;
       final View[] children = mChildren;

       for (int i = 0; i < count; i++) {
           final View child = children[i];
           if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
               child.addFocusables(views, direction, focusableMode);
           }
       }
   }

   // we add ourselves (if focusable) in all cases except for when we are
   // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  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
           // No focusable descendants
           || (focusableCount == views.size())) &&
           (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())) {
       super.addFocusables(views, direction, focusableMode);
   }
}

先会处理自身ViewGroup与它后代的关系(descendantFocusability),前面提到过,可能的几种情况:

  • FOCUS_BEFORE_DESCENDANTS: ViewGroup本身先对焦点进行处理,如果没有处理则分发给child View进行处理
  • FOCUS_AFTER_DESCENDANTS: 先分发给Child View进行处理,如果所有的Child View都没有处理,则自己再处理
  • FOCUS_BLOCK_DESCENDANTS: ViewGroup本身进行处理,不管是否处理成功,都不会分发给ChildView进行处理

所以,以上:

  1. 如果不是FOCUS_BLOCK_DESCENDANTS,则首先检查blockForTouchscreen并重置掉focusableMode,然后遍历所有的子View,调用child::addFocusables
  2. 如果不是FOCUS_AFTER_DESCENDANTS或者没有focusable的子View时自己处理,所以把自己加入到views中。

第三种是ViewPager实现:

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
   final int focusableCount = views.size();

   final int descendantFocusability = getDescendantFocusability();

   if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
       for (int i = 0; i < getChildCount(); i++) {
           final View child = getChildAt(i);
           if (child.getVisibility() == VISIBLE) {
               ItemInfo ii = infoForChild(child);
               if (ii != null && ii.position == mCurItem) {
                   child.addFocusables(views, direction, focusableMode);
               }
           }
       }
   }

   // we add ourselves (if focusable) in all cases except for when we are
   // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  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
           || (focusableCount == views.size())) { // No focusable descendants
       // Note that we can't call the superclass here, because it will
       // add all views in.  So we need to do the same thing View does.
       if (!isFocusable()) {
           return;
       }
       if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
               && isInTouchMode() && !isFocusableInTouchMode()) {
           return;
       }
       if (views != null) {
           views.add(this);
       }
   }
}

ViewGroup基本一致,在descendantFocusability不是FOCUS_BLOCK_DESCENDANTS时,遍历子View时判断view是否属于当前的page,如果是才加进去。如果不是FOCUS_AFTER_DESCENDANTS或者没有focusable的子View时自己处理,所以把自己加入到views中。

第四种是RecyclerView实现:

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
   if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) {
       super.addFocusables(views, direction, focusableMode);
   }
}

通过LayoutManager::onAddFocusables来进行管理,如果返回false,则直接调用父类ViewGroup的方法,看下LayoutManager::onAddFocusables的实现:

public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views,
      int direction, int focusableMode) {
  return false;
}

直接返回false,并且没有看到相关的LayoutManager对该方法的重写,所以,这里应该是直接调用了父类ViewGroup的方法。

addFocusables方法完毕,回到 FocusFinder::findNextFocus 方法中通过root.addFocusables(focusables, direction);加入所有可获取焦点的View之后,在非空的情况下调用如下代码:

next = findNextFocus(root, focused, focusedRect, direction, focusables);

所以重点是FocusFinder::findNextFocus方法的实现:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
       int direction, ArrayList<View> focusables) {
   if (focused != null) {
       if (focusedRect == null) {
           focusedRect = mFocusedRect;
       }
       // fill in interesting rect from focused
       focused.getFocusedRect(focusedRect);
       root.offsetDescendantRectToMyCoords(focused, focusedRect);
   } else {
       if (focusedRect == null) {
           focusedRect = mFocusedRect;
           // make up a rect at top left or bottom right of root
           switch (direction) {
               case View.FOCUS_RIGHT:
               case View.FOCUS_DOWN:
                   setFocusTopLeft(root, focusedRect);
                   break;
               case View.FOCUS_FORWARD:
                   if (root.isLayoutRtl()) {
                       setFocusBottomRight(root, focusedRect);
                   } else {
                       setFocusTopLeft(root, focusedRect);
                   }
                   break;

               case View.FOCUS_LEFT:
               case View.FOCUS_UP:
                   setFocusBottomRight(root, focusedRect);
                   break;
               case View.FOCUS_BACKWARD:
                   if (root.isLayoutRtl()) {
                       setFocusTopLeft(root, focusedRect);
                   } else {
                       setFocusBottomRight(root, focusedRect);
                   break;
               }
           }
       }
   }

   switch (direction) {
       case View.FOCUS_FORWARD:
       case View.FOCUS_BACKWARD:
           return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
                   direction);
       case View.FOCUS_UP:
       case View.FOCUS_DOWN:
       case View.FOCUS_LEFT:
       case View.FOCUS_RIGHT:
           return findNextFocusInAbsoluteDirection(focusables, root, focused,
                   focusedRect, direction);
       default:
           throw new IllegalArgumentException("Unknown direction: " + direction);
   }
}
  • 如果focused不是null,说明当前获取到焦点的View存在,则获得绘制焦点的Rect到focusedRect,然后根据rootView遍历所有ParentView从子View纠正坐标到根View坐标。
  • 如果focused是null,则说明当前没有View获取到焦点,则把focusedRect根据不同的direction重置为“一点”。

最后根据direction调用FocusFinder::findNextFocusInAbsoluteDirection方法进行对比查找“下一个”View。

View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
       Rect focusedRect, int direction) {
   // initialize the best candidate to something impossible
   // (so the first plausible view will become the best choice)
   mBestCandidateRect.set(focusedRect);
   switch(direction) {
       case View.FOCUS_LEFT:
           mBestCandidateRect.offset(focusedRect.width() + 1, 0);
           break;
       case View.FOCUS_RIGHT:
           mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
           break;
       case View.FOCUS_UP:
           mBestCandidateRect.offset(0, focusedRect.height() + 1);
           break;
       case View.FOCUS_DOWN:
           mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
   }

   View closest = null;

   int numFocusables = focusables.size();
   for (int i = 0; i < numFocusables; i++) {
       View focusable = focusables.get(i);

       // only interested in other non-root views
       if (focusable == focused || focusable == root) continue;

       // get focus bounds of other view in same coordinate system
       focusable.getFocusedRect(mOtherRect);
       root.offsetDescendantRectToMyCoords(focusable, mOtherRect);

       if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
           mBestCandidateRect.set(mOtherRect);
           closest = focusable;
       }
   }
   return closest;
}

首先把最优选择mBestCandidateRect设置为focusedRect,根据方向做1像素的偏移便于对比。遍历所有刚刚查询出来的focusables,拿到每一个的focusedRect区域并进行转换,然后通过FocusFinder::isBetterCandidate方法进行对比,然后拿到更好的,遍历完成后就是最优选择。接下来看下FocusFinder::isBetterCandidate方法来了解下是怎么做对比的:

下面代码意思是:以source这个rect来说,作为对应derection上下一个focus view,rect1是否比rect2更优?

boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

   // to be a better candidate, need to at least be a candidate in the first
   // place :)
   if (!isCandidate(source, rect1, direction)) {
       return false;
   }

   // we know that rect1 is a candidate.. if rect2 is not a candidate,
   // rect1 is better
   if (!isCandidate(source, rect2, direction)) {
       return true;
   }

   // if rect1 is better by beam, it wins
   if (beamBeats(direction, source, rect1, rect2)) {
       return true;
   }

   // if rect2 is better, then rect1 cant' be :)
   if (beamBeats(direction, source, rect2, rect1)) {
       return false;
   }

   // otherwise, do fudge-tastic comparison of the major and minor axis
   return (getWeightedDistanceFor(
                   majorAxisDistance(direction, source, rect1),
                   minorAxisDistance(direction, source, rect1))
           < getWeightedDistanceFor(
                   majorAxisDistance(direction, source, rect2),
                   minorAxisDistance(direction, source, rect2)));
}

首先确定rect1是否isCandidateisCandidate做的逻辑简单来说就是确定rect是否满足给定的derection作为下一个focus view这个条件,它的判断依据如下:

boolean isCandidate(Rect srcRect, Rect destRect, int 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:
           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}.");
}

代码比较简单。再回到 FocusFinder::isBetterCandidate 的代码逻辑:

  • 如果rect1不满足基本条件,则肯定返回false(基本的条件都不满足)
  • 如果rect2不满足基本条件,则返回true,认为rect1更优
  • 如果都满足基本条件的情况下,通过FocusFinder::beamBeats方法来判断哪种更优

接下来看下FocusFinder::beamBeats的实现:

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;
   }

   // 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;
   }

   // for horizontal directions, being exclusively in beam always wins
   if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
       return true;
   }        

   // for vertical directions, beams only beat up to a point:
   // now, as long as rect2 isn't completely closer, rect1 wins
   // e.g for direction down, completely closer means for rect2's top
   // edge to be closer to the source's top edge than rect1's bottom edge.
   return (majorAxisDistance(direction, source, rect1)
           < majorAxisDistanceToFarEdge(direction, source, rect2));
}

首先通过beamsOverlap方法来判断两个rect与source是否重叠等等。注意的是,在水平情况下,如果rect1重叠,则就是最优解(为什么?比较奇怪),最后如果是竖直情况,通过FocusFinder::majorAxisDistance方法来判断哪个离source最近。如果还是比较不出,则通过getWeightedDistanceFor方法来通过“主要距离”和“次要距离”做一个综合的比较。

RecyclerView

继续 focusSearch 代码的分析,刚刚只跟了ViewGroup,还有一个实现是RecyclerView的实现:

@Override
public View focusSearch(View focused, int direction) {
   View result = mLayout.onInterceptFocusSearch(focused, direction);
   if (result != null) {
       return result;
   }
   // ...
}

首先通过onInterceptFocusSearch进行拦截,如果返回具体的focus View,则直接返回;否则继续往下;onInterceptFocusSearch实现如下:

public View onInterceptFocusSearch(View focused, int direction) {
    return null;
}

默认为空实现,返回null,也没有其它的子类进行重写,所以暂时不管这个处理,继续看focusSearch

@Override
public View focusSearch(View focused, int direction) {
   View result = mLayout.onInterceptFocusSearch(focused, direction);
   if (result != null) {
       return result;
   }
   final boolean canRunFocusFailure = mAdapter != null && mLayout != null
           && !isComputingLayout() && !mLayoutFrozen;

   final FocusFinder ff = FocusFinder.getInstance();
   if (canRunFocusFailure
           && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) {
       // convert direction to absolute direction and see if we have a view there and if not
       // tell LayoutManager to add if it can.
       boolean needsFocusFailureLayout = false;
       if (mLayout.canScrollVertically()) {
           final int absDir =
                   direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
           final View found = ff.findNextFocus(this, focused, absDir);
           needsFocusFailureLayout = found == null;
           if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
               // Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
               direction = absDir;
           }
       }
       if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) {
           boolean rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
           final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
                   ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
           final View found = ff.findNextFocus(this, focused, absDir);
           needsFocusFailureLayout = found == null;
           if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) {
               // Workaround for broken FOCUS_BACKWARD in API 15 and older devices.
               direction = absDir;
           }
       }
       if (needsFocusFailureLayout) {
           consumePendingUpdateOperations();
           final View focusedItemView = findContainingItemView(focused);
           if (focusedItemView == null) {
               // panic, focused view is not a child anymore, cannot call super.
               return null;
           }
           eatRequestLayout();
           mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
           resumeRequestLayout(false);
       }
       result = ff.findNextFocus(this, focused, direction);
   } else {
       result = ff.findNextFocus(this, focused, direction);
       if (result == null && canRunFocusFailure) {
           consumePendingUpdateOperations();
           final View focusedItemView = findContainingItemView(focused);
           if (focusedItemView == null) {
               // panic, focused view is not a child anymore, cannot call super.
               return null;
           }
           eatRequestLayout();
           result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
           resumeRequestLayout(false);
       }
   }
   if (result != null && !result.hasFocusable()) {
       if (getFocusedChild() == null) {
           // Scrolling to this unfocusable view is not meaningful since there is no currently
           // focused view which RV needs to keep visible.
           return super.focusSearch(focused, direction);
       }
       // If the next view returned by onFocusSearchFailed in layout manager has no focusable
       // views, we still scroll to that view in order to make it visible on the screen.
       // If it's focusable, framework already calls RV's requestChildFocus which handles
       // bringing this newly focused item onto the screen.
       requestChildOnScreen(result, null);
       return focused;
   }
   return isPreferredNextFocus(focused, result, direction)
           ? result : super.focusSearch(focused, direction);
}

我们暂时只考虑direction为left,top,right,down的情况,则进入最外面if的else分支:

public View focusSearch(View focused, int direction) {
    // ...
    result = ff.findNextFocus(this, focused, direction);
    if (result == null && canRunFocusFailure) {
     consumePendingUpdateOperations();
     final View focusedItemView = findContainingItemView(focused);
     if (focusedItemView == null) {
         // panic, focused view is not a child anymore, cannot call super.
         return null;
     }
     eatRequestLayout();
     result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
     resumeRequestLayout(false);
    }
    // ...
}

首先通过FocusFinder::findNextFocus方法来获取下一个应该获得焦点的View,这里获取的结果与FocusFinder::findNextFocus 逻辑一致。


本文转自天天_byconan博客园博客,原文链接:http://www.cnblogs.com/tiantianbyconan/p/7286503.html,如需转载请自行联系原作者

相关文章
|
1天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
11 2
|
13天前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
27 3
|
1月前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
讲解Activity的启动流程了,Activity的启动流程相对复杂一下,涉及到了Activity中的生命周期方法,涉及到了Android体系的CS模式,涉及到了Android中进程通讯Binder机制等等, 首先介绍一下Activity,这里引用一下Android guide中对Activity的介绍:
41 4
|
1月前
|
Android开发 开发者
Android面试之Activity启动流程简述
每个Android开发者都熟悉的Activity,但你是否了解它的启动流程呢?本文将带你深入了解。启动流程涉及四个关键角色:Launcher进程、SystemServer的AMS、应用程序的ActivityThread及Zygote进程。核心在于AMS与ActivityThread间的通信。文章详细解析了从Launcher启动Activity的过程,包括通过AIDL获取AMS、Zygote进程启动以及ActivityThread与AMS的通信机制。接着介绍了如何创建Application及Activity的具体步骤。整体流程清晰明了,帮助你更深入理解Activity的工作原理。
41 0
|
2月前
|
Android开发
我的Android进阶修炼:安卓启动流程之init(1)
本文深入分析了Android系统中的init进程,包括其源码结构、主要功能以及启动流程的详细注解,旨在帮助读者理解init作为用户空间的1号进程在Android启动过程中的关键作用。
44 1
|
1月前
|
图形学 iOS开发 Android开发
从Unity开发到移动平台制胜攻略:全面解析iOS与Android应用发布流程,助你轻松掌握跨平台发布技巧,打造爆款手游不是梦——性能优化、广告集成与内购设置全包含
【8月更文挑战第31天】本书详细介绍了如何在Unity中设置项目以适应移动设备,涵盖性能优化、集成广告及内购功能等关键步骤。通过具体示例和代码片段,指导读者完成iOS和Android应用的打包与发布,确保应用顺利上线并获得成功。无论是性能调整还是平台特定的操作,本书均提供了全面的解决方案。
131 0
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
View的绘制和事件处理是两个重要的主题,上一篇《图解 Android事件分发机制》已经把事件的分发机制讲得比较详细了,这一篇是针对View的绘制,View的绘制如果你有所了解,基本分为measure、layout、draw 过程,其中比较难理解就是measure过程,所以本篇文章大幅笔地分析measure过程,相对讲得比较详细,文章也比较长,如果你对View的绘制还不是很懂,对measure过程掌握得不是很深刻,那么耐心点,看完这篇文章,相信你会有所收获的。
98 2
|
3月前
|
Java Android开发
android 设置系统时间的流程
android 设置系统时间的方法
300 2
|
4月前
|
ARouter IDE 开发工具
Android面试题之App的启动流程和启动速度优化
App启动流程概括: 当用户点击App图标,Launcher通过Binder IPC请求system_server启动Activity。system_server指示Zygote fork新进程,接着App进程向system_server申请启动Activity。经过Binder通信,Activity创建并回调生命周期方法。启动状态分为冷启动、温启动和热启动,其中冷启动耗时最长。优化技巧包括异步初始化、避免主线程I/O、类加载优化和简化布局。
75 3
Android面试题之App的启动流程和启动速度优化
|
4月前
|
安全 网络协议 算法
Android网络基础面试题之HTTPS的工作流程和原理
HTTPS简述 HTTPS基于TCP 443端口,通过CA证书确保服务器身份,使用DH算法协商对称密钥进行加密通信。流程包括TCP握手、证书验证(公钥解密,哈希对比)和数据加密传输(随机数加密,预主密钥,对称加密)。特点是安全但慢,易受特定攻击,且依赖可信的CA。每次请求可能复用Session ID以减少握手。
60 2