暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

从 Android 开发到读懂源码 第06期:焦点移动源码解析

悦专栏 2020-12-24
595

作者简介

罗铁锤,六年安卓踩坑经验,致力于底层平台、上层应用等多领域开发。文能静坐弹吉他,武能通宵写代码


这一节内容,我们来聊聊 dispatchKeyEvent。

1 按键机制

在市场上,手机硬件基本上占领 android 设备的绝大部分市场,而在 TV 上,由于人机交互的方式不同,并且当前主流的 TV 并不具备触摸屏(虽然目前的触屏电视已经面市,但是该类商显产品主要还是 2B。),传统TV还是通过遥控器的方向按键进行操控,在 android 系统中则是通过焦点的移动标识来展示给用户当前的控制点。下面就从接收到遥控器的按键事件开始,一步步分析下系统中的焦点机制是如何响应工作的。(本文基于 API 27 源码进行分析)
首先,从底层驱动接收到遥控器按键或者触摸屏触摸事件后,通过一步步的转换到 android framework 中的用户界面层,会回调给 ViewRootImpl 中的 ViewPostImeInputStage ,这个内部类的代码稍长,因为不论是触屏还是按键,都是在这里进行初始的分发处理,在此,我们只重点关注按键事件以及焦点的处理:
    <ViewRootImpl.java>
    /**
    * Delivers post-ime input events to the view hierarchy.
    */
    final class ViewPostImeInputStage extends InputStage {
    public ViewPostImeInputStage(InputStage next) {
    super(next);
    }


    @Override
    protected int onProcess(QueuedInputEvent q) {
    if (q.mEvent instanceof KeyEvent) {// 接收到的事件是按键事件
    return processKeyEvent(q);// 按键事件处理
    } else {
    final int source = q.mEvent.getSource();
    if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
    return processPointerEvent(q);
    } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
    return processTrackballEvent(q);
    } else {
    return processGenericMotionEvent(q);
    }
    }
    }


    @Override
    protected void onDeliverToNext(QueuedInputEvent q) {
    ...
    }


    private boolean performFocusNavigation(KeyEvent event) {
    ...
    }


    private boolean performKeyboardGroupNavigation(int direction) {
    ...
    }


    private int processKeyEvent(QueuedInputEvent q) {
    ...
    }


    private int processPointerEvent(QueuedInputEvent q) {
    ...
    }


    private void maybeUpdatePointerIcon(MotionEvent event) {
    ...
    }


    private int processTrackballEvent(QueuedInputEvent q) {
    ...
    }


    private int processGenericMotionEvent(QueuedInputEvent q) {
    ...
    }
    }


    2 QueuedInputEvent

      <ViewRootImpl.java>
      private static final class QueuedInputEvent {
      public static final int FLAG_DELIVER_POST_IME = 1 << 0;
      public static final int FLAG_DEFERRED = 1 << 1;
      public static final int FLAG_FINISHED = 1 << 2;
      public static final int FLAG_FINISHED_HANDLED = 1 << 3;
      public static final int FLAG_RESYNTHESIZED = 1 << 4;
      public static final int FLAG_UNHANDLED = 1 << 5;

      public QueuedInputEvent mNext;


      public InputEvent mEvent;
      public InputEventReceiver mReceiver;
      public int mFlags;
      ...
      }
      // InputEvent 的两个子类
      public class KeyEvent extends InputEvent implements Parcelable {}
      public final class MotionEvent extends InputEvent implements Parcelable {}
      触摸或者按键都是一系列的接收事件,QueuedInputEvent 实际上是类似Message的一个队列,mNext 变量指向的是下一个事件(单向链表的结构)。mEvent 变量标记了该事件的类型,我们可以看到 android中,InputEvent 只有两个子类,一个是 KeyEvent 按键事件,另一个是 MotionEvent 触摸事件。回到上面的 onProcess 方法,很明显我们TV端的是 KeyEvent 事件,进入 processKeyEvent 进行按键事件的处理。
        <ViewRootImpl.java>
        @Override
        protected int onProcess(QueuedInputEvent q) {
        if (q.mEvent instanceof KeyEvent) {// 接收到的事件是按键事件
        return processKeyEvent(q);// 进入这个分支,按键事件处理
        } else {
        final int source = q.mEvent.getSource();// 手指触摸的 touch 事件
        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
        return processPointerEvent(q);
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
        return processTrackballEvent(q);
        } else {
        return processGenericMotionEvent(q);
        }
        }
        }


        private int processKeyEvent(QueuedInputEvent q) {
        final KeyEvent event = (KeyEvent)q.mEvent;// 获取到该按键事件信息,我们常见的 KeyCode,Acton,RepeatCount 等信息都包含在里面

        // Deliver the key to the view hierarchy.
        if (mView.dispatchKeyEvent(event)) {// mView 实际上就是 DecorView,这里看到如果 dispatchKeyEvent 返回 true,会直接返回,这里的按键事件分发后面单独一篇讲解,对比 touch 事件分发要简单不少
        return FINISH_HANDLED;
        }

        if (shouldDropInputEvent(q)) {// 是否抛弃该事件,里面主要是判断 View 是否初始化或者还未 add 进来,window 失去焦点(window 失去焦点也就是说该 window 无法交互,所以接收事件也没用,直接返回)
        return FINISH_NOT_HANDLED;
        }

        int groupNavigationDirection = 0;
        // 根据 tab 和 shift 按键判断导航方向
        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;
        }


        3 方向键导航处理

        上面经过一系列判断,包括 Tab,Shift 和快捷键的处理,我们这里重点关注 performFocusNavigation(event):
          <ViewRootImpl.java>
          private boolean performFocusNavigation(KeyEvent event) {
          int direction = 0;
          switch (event.getKeyCode()) {// 将按键事件的键值转换为 View 的焦点导航方向值
          case KeyEvent.KEYCODE_DPAD_LEFT:
          if (event.hasNoModifiers()) {
          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 = View.FOCUS_UP;// 上
          }
          break;
          case KeyEvent.KEYCODE_DPAD_DOWN:
          if (event.hasNoModifiers()) {
          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;
          }
          if (direction != 0) {// 是上,下,左,右,前,后其中的一个
          View focused = mView.findFocus();// 从 decorview 中查找当前的焦点
          if (focused != null) {
          View v = focused.focusSearch(direction);// 根据方向查找下一个焦点,调用 parent 的 focusSearch 查找
          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)) {// 对查找到的焦点 view 调用 requestFocus,清除oldFocus 的焦点状态
          playSoundEffect(SoundEffectConstants// 播放焦点移动音效,处理结束
          .getContantForFocusDirection(direction));
          return true;
          }
          }

          // Give the focused view a last chance to handle the dpad key.
          if (mView.dispatchUnhandledMove(focused, direction)) {// 查找焦点失败,再提供一个机会去处理该次按键事件下 view 的移动
          return true;
          }
          } else {// 如果当前都没有焦点
          if (mView.restoreDefaultFocus()) {// 重新初始化默认焦点,处理完毕
          return true;
          }
          }
          }
          return false;
          }

          这里面首先将按键的键值转换为焦点导航方向,主要有6个:
          FOCUS_BACKWARD,
          FOCUS_FORWARD,
          FOCUS_LEFT,
          FOCUS_UP,
          FOCUS_RIGHT,
          FOCUS_DOWN ,接着通过 findFocus 查找到当前视图中的焦点。然后通过 focusSearch 方法(这个方法是查找焦点的关键方法,一些定制化逻辑可以通过修改此方法实现),根据当前焦点根据导航方向,去寻找下一个应该聚焦的 View:
            <View.java>
            public View focusSearch(@FocusRealDirection int direction) {
            if (mParent != null) {
            return mParent.focusSearch(this, direction);// 通过 parent 父 View 去查找下一个焦点
            } else {
            return null;
            }
            }


            <ViewGroup.java>
            @Override
            public View focusSearch(View focused, int direction) {
            if (isRootNamespace()) {// 当前 view == decorView,一般我们最终会走到这个分支
            // 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;
            }
            View.focusSearch 实际上是调用了 parent 的 focusSearch,一层一层往上,最终也就是走到根布局 DecorView 的 focusSearch,通过FocusFinder 来进行焦点查找:
              <FocusFinder.java>
              private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
              View next = null;// 下一个焦点
              ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
              if (focused != null) {
              next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);// 当前焦点不为 null,首先判断用户对当前焦点的 view 在该方向上是否指定 id,也就是我们通常 xml 中写的 nextFocusLeft 这种
              }
              if (next != null) {
              return next;// 如果用户指定了下个焦点 id,直接返回该 id 对应的 view
              }
              ArrayList<View> focusables = mTempList;// 这个集合是用来装所有可获得焦点的 View
              try {
              focusables.clear();
              effectiveRoot.addFocusables(focusables, direction);// 查找可获得焦点的 view,添加进集合
              if (!focusables.isEmpty()) {// 存在可获得焦点的 view
              next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 继续在所有可获得焦点的 view 集合中查找下一个焦点
              }
              } finally {
              focusables.clear();// 查找完毕后清理数据,释放内存
              }
              return next;// 返回下一个焦点
              }
              • 首先会去判断用户有没有手动在 xml 中指定该方向的下一个焦点 view 的 id,如果指定了直接返回该 view 作为下一个焦点,流程结束。对于 findNextUserSpecifiedFocus 方法逻辑还是比较好理解,在此不做展开分析。
              • 接着会查找所有可获得焦点的view,将它们添加到 focusables 集合中,缩小焦点查找范围。这里有个关键方法:addFocusables ,这个方法在平时定制化开发中可以用于焦点记忆,例如 leanback 视图中每一行 recyclerView 中的焦点记忆。
                <View.java>
                public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
                addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
                }


                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()) {// 触摸模式下,但是 focusInTouchMode 设置为 false,直接返回
                return;
                }
                views.add(this);// 将自己添加到集合中
                }


                <ViewGroup.java>
                @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);// 调用 View 的 addFocusables 将自己添加进集合
                }
                return;// 直接返回,不再添加自己 view 数结构下面的子 View
                }


                if (blockFocusForTouchscreen) {
                focusableMode |= FOCUSABLES_TOUCH_MODE;
                }


                if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
                super.addFocusables(views, direction, focusableMode);// 调用 View 的 addFocusables 将自己添加进集合
                }


                int count = 0;
                final View[] children = new View[mChildrenCount];
                for (int i = 0; i < mChildrenCount; ++i) {// 遍历当前 viewGroup下的所有子 View
                View child = mChildren[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {// view 处于可见状态
                children[count++] = child;// 赋值给 children 数组
                }
                }
                FocusFinder.sort(children, 0, count, this, isLayoutRtl());// 根据方向排序
                for (int i = 0; i < count; ++i) {
                children[i].addFocusables(views, direction, focusableMode);// 如果 children[i] 这个子 view 是viewGroup的话,递归调用继续查找该 child viewGroup下的子 View,直到查找所有最下层的子 view,最终调用 View.addFocusables 判断是否可聚焦,可聚焦则添加进集合
                }
                // 走到这里,views 中已经保存了所有可聚焦的子 View


                // 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()) {// 如果是 FOCUS_AFTER_DESCENDANTS,除了子 view 判断外,最后将自己也添加进去
                super.addFocusables(views, direction, focusableMode);
                }
                }

                经过上面的 addFocusables 已经将所有可见状态并且可以聚焦的 view 全部收集到了 focusables 这个集合中,接着在该集合中去查找下一个焦点:
                  <FocusFinder.java>
                  private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
                  ...
                  if (!focusables.isEmpty()) {// 存在可获得焦点的 view
                  next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 继续在所有可获得焦点的 view 集合中查找下一个焦点
                  }
                  ...
                  return next;// 返回下一个焦点
                  }




                  private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
                  int direction, ArrayList<View> focusables) {
                  if (focused != null) {// 当前焦点不为 null
                  if (focusedRect == null) {
                  focusedRect = mFocusedRect;
                  }
                  // fill in interesting rect from focused
                  focused.getFocusedRect(focusedRect);// 获取到当前焦点的 rect 区域
                  root.offsetDescendantRectToMyCoords(focused, focusedRect);// 考虑 scroll 滑动状态,即把视框拉伸至滑动到屏幕外的视图也可见状态,统一坐标系便于下面焦点查找计算
                  } 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);// 当前焦点为 null,将滑动后的左上角作为寻找起始点(scrollX,scrollY),走到这里的话这个 focusedRect 实际上是个点
                  break;
                  case View.FOCUS_FORWARD:
                  if (root.isLayoutRtl()) {// 会根据 rtl 区分(某些国家语言是从右往左书写习惯)
                  setFocusBottomRight(root, focusedRect);
                  } else {
                  setFocusTopLeft(root, focusedRect);
                  }
                  break;


                  case View.FOCUS_LEFT:
                  case View.FOCUS_UP:
                  setFocusBottomRight(root, focusedRect);// 当前焦点为 null,将滑动后的右下角作为寻找起始点(scrollX,scrollY),走到这里的话这个 focusedRect 实际上是个点
                  break;
                  case View.FOCUS_BACKWARD:
                  if (root.isLayoutRtl()) {// 会根据 rtl 区分是否将坐标反转
                  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);
                  }
                  }
                  • 如果当前焦点不为 null,先获取当前焦点的 rect 视图区域,考虑到 scroll 状态,将当前焦点的 rect 坐标系进行转换。
                  • 如果当前焦点为 null,根据导航方向,设置一个左上角或者右下角的 rect 为默认的起始参考点,根据这个点再结合方向去计算下一个焦点。这里我们重点看下上下左右移动的这个方法 findNextFocusInAbsoluteDirection ,大致看下它内部查找焦点算法:
                    <FocusFinder.java>
                    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);// 将当前焦点的 rect 赋值给 mBestCandidateRect
                    switch(direction) {// 在反方向上偏移一个 width 或者 height + 1 个像素点,虚构出来的下一个候补焦点(优先级应该是最低的,因为是反方向平移)
                    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
                    View focusable = focusables.get(i);


                    // only interested in other non-root views
                    if (focusable == focused || focusable == root) continue;// 如果集合中的 view 是当前的焦点或者 viewGroup,直接跳过继续查找下一个


                    // get focus bounds of other view in same coordinate system
                    focusable.getFocusedRect(mOtherRect);
                    root.offsetDescendantRectToMyCoords(focusable, mOtherRect);// 将该 view 也进行坐标系转换,和当前焦点在同一个坐标系进行计算


                    if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                    mBestCandidateRect.set(mOtherRect);// 如果找到一个符合的,则将其的区域赋值给虚构的候补焦点,参照物变了之后,继续遍历看有没有更优的
                    closest = focusable;// 这个 closest 会不断刷新,因为每次进入该分支,最新的 focusable 符合条件都会优于上一个候补焦点
                    }
                    }
                    return closest;
                    }

                    先获取当前焦点的视图区域 rect,然后将该区域按照导航方向的反方向偏移 1 个像素 + 当前焦点的 width 或者 height,得到一个虚构的焦点区域 mBestCandidateRect 。接着就开始遍历之前收集到的所有可见可聚焦的 view 集合,如果当前遍历的 view 就是当前焦点或者rootView,直接忽略跳过继续往下遍历查找。遍历的时候,会将遍历的 view 坐标转换,只有转换坐标后和当前焦点在同一个坐标系,这样才能为下面算法提供准确参数:
                      <FocusFinder.java>
                      // 几个参数含义:direction 方向,source 当前焦点,rect1 当前对比的 view,rect2 虚构的候补焦点(如果有符合的,rect2 会刷新为当前符合条件的 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)) {// 先将当前遍历的 view 与当前焦点比较
                      return false;
                      }
                      // 走到这里说明上面的 isCandidate 返回 true,也就是当前遍历的 rect1 符合条件。例如 direction 为左,说明 rect1 在当前焦点的左侧,符合条件,加入候选,进行下一步判断
                      // we know that rect1 is a candidate.. if rect2 is not a candidate,
                      // rect1 is better
                      if (!isCandidate(source, rect2, direction)) {// 第一次走到这的话这个 isCandidate 肯定返回 false,因为 rect2 第一次是我们之前虚构的候补焦点,是在导航的反方向,肯定为 false,直接返回 true。再后面的话,相当于上一个候补和当前焦点进行比较,肯定返回 true,继续下一步判断
                      return true;
                      }


                      // if rect1 is better by beam, it wins
                      if (beamBeats(direction, source, rect1, rect2)) {// 当前遍历的 view 也符合条件,将它和上一个候补进行比较
                      return true;// 当前遍历的 view 优于上一个候补,将当前遍历的赋值给最新的 closest,也就是目前遍历过程中最优焦点
                      }


                      // 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(// 计算 rect1 和 rect2 相对于当前焦点的距离
                      majorAxisDistance(direction, source, rect1),
                      minorAxisDistance(direction, source, rect1))
                      < getWeightedDistanceFor(
                      majorAxisDistance(direction, source, rect2),
                      minorAxisDistance(direction, source, rect2)));
                      }


                      4 重点算法 1 isCandidate

                      重点算法 1,计算是否在导航的那侧,在导航方向上允许有重叠。这个算法都是比较 xy 方向的边界大小,相对于下面的算法 2 真的是容易理解很多,稍微画几个图就能理解了。
                        <FocusFinder.java>
                        // srcRect 当前焦点,destRect 比较的 view,direction 方向
                        boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
                        switch (direction) {
                        case View.FOCUS_LEFT:// 向左:只比较 left 和 right,就是 dest 是否整体在 src 的左侧,这里说的是整体,dest 可以与 src 有交集,但是 dest 的左右边界都不能超过 src 的右边界
                        return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
                        && srcRect.left > destRect.left;
                        case View.FOCUS_RIGHT:// 向右:只比较 left 和 right,就是 dest 是否整体在 src 的右侧,这里说的是整体, dest 可以与 src 有交集,但是 src 的左右边界都不能超过 dest 的右边界
                        return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
                        && srcRect.right < destRect.right;
                        case View.FOCUS_UP:// 向上:只比较 top 和 bottom,就是 dest 是否整体在 src 的上面,这里说的是整体,dest 可以与 src 有交集,但是 dest 的上下边界都不能超过 src 的下边界
                        return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
                        && srcRect.top > destRect.top;
                        case View.FOCUS_DOWN:// 向下:只比较 top 和 bottom,就是 dest 是否整体在 src 的下面,这里说的是整体,dest 可以与 src 有交集,但是 src 的上下边界都不能超过 dest 的下边界
                        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}.");
                        }


                        5 重点算法 2 beamBeats

                        这算法看着真的很乱,官方的注释也不好理解,不好描述,还是自己画几张图按流程跑一下去理解吧。
                          <FocusFinder.java>
                          // direction 方向,source 当前焦点,rect1 比较的 view1,rect2 比较的 view2(rect1 和rect2 具体看上面算法调用的顺序)
                          // 第一次调用:rect1 当前遍历的 view,rect2 上一次符合条件的候补焦点
                          // 第二次调用:rect1 上一次符合条件的候补焦点,rect2 当前遍历的 view
                          boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
                          // rect1 和当前焦点在相对于导航方向的垂直方向是否有重叠,导航方向为左右 x 轴时比较 y 轴重叠
                          final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
                          // rect2 和当前焦点在相对于导航方向的垂直方向是否有重叠,导航方向为上下 y 轴时比较 x 轴重叠
                          final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);


                          // if rect1 isn't exclusively in the src beam, it doesn't win
                          if (rect2InSrcBeam || !rect1InSrcBeam) {// rect2 有重叠,或者 rect1 没有重叠
                          // 第一次调用:上一次符合条件的候补焦点与当前焦点有重叠,或者当前遍历的 view 与当前焦点没有重叠
                          return false;// 如果第一次进入此 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));
                          }
                          • 计算相对于导航方向的垂直方向上是否有重叠
                            <FocusFinder.java>
                            // direction 方向,rect1 当前焦点,rect2 待比较的 view
                            boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
                            switch (direction) {
                            case View.FOCUS_LEFT:
                            case View.FOCUS_RIGHT:
                            return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom);// 左右按键时比较 y 方向是否重叠
                            case View.FOCUS_UP:
                            case View.FOCUS_DOWN:
                            return (rect2.right > rect1.left) && (rect2.left < rect1.right);// 上下按键时比较 x 方向是否重叠
                            }
                            throw new IllegalArgumentException("direction must be one of "
                            + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
                            }
                            • 计算是否完全在当前 src 的一侧
                              <FocusFinder.java>
                              // src 当前焦点,dest 待比较的 view
                              boolean isToDirectionOf(int direction, Rect src, Rect dest) {// 比较 dest 是否完全在当前焦点的左/右/上/下
                              switch (direction) {
                              case View.FOCUS_LEFT:
                              return src.left >= dest.right;
                              case View.FOCUS_RIGHT:
                              return src.right <= dest.left;
                              case View.FOCUS_UP:
                              return src.top >= dest.bottom;
                              case View.FOCUS_DOWN:
                              return src.bottom <= dest.top;
                              }
                              throw new IllegalArgumentException("direction must be one of "
                              + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
                              }
                              • 计算主轴方向距离
                                <FocusFinder.java>
                                // 计算主轴方向距离
                                static int majorAxisDistance(int direction, Rect source, Rect dest) {
                                return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
                                }


                                static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
                                switch (direction) {
                                case View.FOCUS_LEFT:
                                return source.left - dest.right;
                                case View.FOCUS_RIGHT:
                                return dest.left - source.right;
                                case View.FOCUS_UP:
                                return source.top - dest.bottom;
                                case View.FOCUS_DOWN:
                                return dest.top - source.bottom;
                                }
                                throw new IllegalArgumentException("direction must be one of "
                                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
                                }
                                • 计算相对于主轴方向的垂直方向距离
                                  <FocusFinder.java>
                                  // 计算次轴方向距离
                                  static int minorAxisDistance(int direction, Rect source, Rect dest) {
                                  switch (direction) {
                                  case View.FOCUS_LEFT:
                                  case View.FOCUS_RIGHT:
                                  // the distance between the center verticals
                                  return Math.abs(
                                  ((source.top + source.height() 2) -
                                  ((dest.top + dest.height() 2))));
                                  case View.FOCUS_UP:
                                  case View.FOCUS_DOWN:
                                  // the distance between the center horizontals
                                  return Math.abs(
                                  ((source.left + source.width() 2) -
                                  ((dest.left + dest.width() 2))));
                                  }
                                  throw new IllegalArgumentException("direction must be one of "
                                  + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
                                  }
                                  • 计算相对距离,以 FOCUS_LEFT 为例,majorAxisDistance 相当于当前焦点左侧与比较view的右侧的x轴距离,minorAxisDistance 相当于在y轴方向上,当前焦点中心点与比较 view 的中心点的距离。计算 13 * x² * y²,这个 13 的权重系数不知道 google 是如何制定的,这里就理解为主轴的权重优先级更高吧。(如果是我设计的话,应该会直接计算x和y的距离平方根进行比较了。)
                                    <FocusFinder.java>
                                    int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
                                    return 13 * majorAxisDistance * majorAxisDistance
                                    + minorAxisDistance * minorAxisDistance;
                                    }
                                    • 该方法又得和上面的 majorAxisDistance 进行区分,以 FOCUS_LEFT 为例,同样是计算 x 轴方向,但是 majorAxisDistance 计算的是 souce 的左侧和待比较 view 的右侧距离,这个方法计算的是 source 的左侧和待比较 view 的左侧的距离:
                                      <FocusFinder.java>
                                      static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
                                      return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
                                      }


                                      // 也是计算主轴方向,但是和 majorAxisDistance 有区别
                                      static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
                                      switch (direction) {
                                      case View.FOCUS_LEFT:
                                      return source.left - dest.left;
                                      case View.FOCUS_RIGHT:
                                      return dest.right - source.right;
                                      case View.FOCUS_UP:
                                      return source.top - dest.top;
                                      case View.FOCUS_DOWN:
                                      return dest.bottom - source.bottom;
                                      }
                                      throw new IllegalArgumentException("direction must be one of "
                                      + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
                                      }
                                      遍历过程中,每次进入 isBetterCandidate 成立后,closest 都会更新为下一个焦点的最优解,遍历结束后,这个 closest 就是计算出来的下一个焦点,直接返回给上面的 ViewRootImpl.performFocusNavigation ,至此寻焦结束,接着用该查找出来的焦点 view 调用requestFocus,requestFocus 之前已经分析过,主要就是清除上一个焦点的状态,刷新当前焦点,流程结束。

                                      专栏《从 Android 开发到读懂源码》系列文章推荐
                                      第01期:requestFocus 源码分析
                                      第02期:NestScroll 机制源码解析
                                      第03期:View.post 源码解析
                                      第04期:LiveData 源码解析
                                      第05期:Leanback 结构源码简析


                                      欢迎加入 Android 交流社群
                                      群内不定期邀请一些身边的 Android 大牛
                                      交流分享,解答工作中遇到的的问题
                                      分享工作经验、面试技巧等!
                                      入群请添加下方群秘微信,备注Android




                                      LIKECOLUMN

                                      悦专栏


                                      在这里,学好编程

                                      做更优秀的 IT人!



                                      文章转载自悦专栏,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                      评论