Android之ViewPager源码分析

    xiaoxiao2021-04-18  61

    20150210 ViewPager 焦点控制 在TV应用开发中ViewPager是很常用的控件,在ViewPager的页切换时焦点控制是很苦恼的事,有过相关开发经验的同学一定感同身受。废话不多说,我们分析一下ViewPager的相关源码。 对于ViewPager而已,一切按键的响应都是从dispatchKeyEvent开始的。 [java] view plain copy  public boolean dispatchKeyEvent(KeyEvent event) {       // Let the focused view and/or our descendants get the key first       return super.dispatchKeyEvent(event) || executeKeyEvent(event);   }   对于左右键KEYCODE_DPAD_RIGHT和KEYCODE_DPAD_LEFT,围绕Viewpager的view继承体系是不会拦截的,也就是说super.dispatchKeyEvent(event)返回false(注:这部分理解,可以学习相关知识),接着执行executeKeyEvent(event)方法。 [java] view plain copy  /**   * You can call this function yourself to have the scroll view perform   * scrolling from a key event, just as if the event had been dispatched to   * it by the view hierarchy.   *   * @param event The key event to execute.   * @return Return true if the event was handled, else false.   */   public boolean executeKeyEvent(KeyEvent event) {       boolean handled = false;       if (event.getAction() == KeyEvent.ACTION_DOWN) {           switch (event.getKeyCode()) {               case KeyEvent.KEYCODE_DPAD_LEFT:                   handled = arrowScroll(FOCUS_LEFT);                   break;               case KeyEvent.KEYCODE_DPAD_RIGHT:                   handled = arrowScroll(FOCUS_RIGHT);                   break;               case KeyEvent.KEYCODE_TAB:                   if (Build.VERSION.SDK_INT >= 11) {                       // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD                       // before Android 3.0. Ignore the tab key on those devices.                       if (KeyEventCompat.hasNoModifiers(event)) {                           handled = arrowScroll(FOCUS_FORWARD);                       } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {                           handled = arrowScroll(FOCUS_BACKWARD);                       }                   }                   break;           }       }       return handled;   }   左右键都会调用arrowScroll方法。 [java] view plain copy  public boolean arrowScroll(int direction) {       View currentFocused = findFocus();       if (currentFocused == this) {           currentFocused = null;       } else if (currentFocused != null) {           boolean isChild = false;           for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;                   parent = parent.getParent()) {               if (parent == this) {                   isChild = true;                   break;               }           }           if (!isChild) {               // This would cause the focus search down below to fail in fun ways.               final StringBuilder sb = new StringBuilder();               sb.append(currentFocused.getClass().getSimpleName());               for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup;                       parent = parent.getParent()) {                   sb.append(" => ").append(parent.getClass().getSimpleName());               }               Log.e(TAG, "arrowScroll tried to find focus based on non-child " +                       "current focused view " + sb.toString());               currentFocused = null;           }       }          boolean handled = false;          View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused,               direction);       if (nextFocused != null && nextFocused != currentFocused) {           if (direction == View.FOCUS_LEFT) {               // If there is nothing to the left, or this is causing us to               // jump to the right, then what we really want to do is page left.               final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;               final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;               if (currentFocused != null && nextLeft >= currLeft) {                   handled = pageLeft();               } else {                   handled = nextFocused.requestFocus();               }           } else if (direction == View.FOCUS_RIGHT) {               // If there is nothing to the right, or this is causing us to               // jump to the left, then what we really want to do is page right.               final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left;               final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left;               if (currentFocused != null && nextLeft <= currLeft) {                   handled = pageRight();               } else {                   handled = nextFocused.requestFocus();               }           }       } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) {           // Trying to move left and nothing there; try to page.           handled = pageLeft();       } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) {           // Trying to move right and nothing there; try to page.           handled = pageRight();       }       if (handled) {           playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));       }       return handled;   }   View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);会根据当前的获得焦点的currentFocused和方向direction来寻找下一个获得焦点的View。 Viewpager的每一页是一个ViewGroup,这个ViewGroup包含多个View,在同一页之间切换焦点没有任何问题,FocusFinder能找到下一个View,最后执行nextFocused.requstFocus()。Lovely,一切很完美。那么,问题来了。切换页时发生了什么? 切换页时,FocusFinder.getInstance().findNextFocus(this, currentFocused, direction)返回的是null,如果是KEYCODE_DPAD_RIGHT,执行pageRight()。 [java] view plain copy  boolean pageRight() {       if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) {           setCurrentItem(mCurItem+1, true);           return true;       }       return false;   }   我们知道setCurrentItem就是去执行翻页了。 [java] view plain copy  void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) {       setCurrentItemInternal(item, smoothScroll, always, 0);   }      void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {       if (mAdapter == null || mAdapter.getCount() <= 0) {           setScrollingCacheEnabled(false);           return;       }       if (!always && mCurItem == item && mItems.size() != 0) {           setScrollingCacheEnabled(false);           return;       }          if (item < 0) {           item = 0;       } else if (item >= mAdapter.getCount()) {           item = mAdapter.getCount() - 1;       }       final int pageLimit = mOffscreenPageLimit;       if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) {           // We are doing a jump by more than one page.  To avoid           // glitches, we want to keep all current pages in the view           // until the scroll ends.           for (int i=0; i<mItems.size(); i++) {               mItems.get(i).scrolling = true;           }       }       final boolean dispatchSelected = mCurItem != item;          if (mFirstLayout) {           // We don't have any idea how big we are yet and shouldn't have any pages either.           // Just set things up and let the pending layout handle things.           mCurItem = item;           if (dispatchSelected && mOnPageChangeListener != null) {               mOnPageChangeListener.onPageSelected(item);           }           if (dispatchSelected && mInternalPageChangeListener != null) {               mInternalPageChangeListener.onPageSelected(item);           }           requestLayout();       } else {           populate(item);           scrollToItem(item, smoothScroll, velocity, dispatchSelected);       }   }   populate和scrollToItem是两个很重要的方法,这两个方法很大,笔者目前没时间也不想去研究。大概是这样的: populate是构造数据,scrollToItem是滚动到要去的那一页。重点是“要去的那一页”控制焦点的代码是在populate中做的,如下代码: [java] view plain copy  void populate(int newCurrentItem) {   ...          if (hasFocus()) {           View currentFocused = findFocus();           ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;           if (ii == null || ii.position != mCurItem) {               for (int i=0; i<getChildCount(); i++) {                   View child = getChildAt(i);                   ii = infoForChild(child);                   if (ii != null && ii.position == mCurItem) {                       //我们修改如下                       Rect mRect = new Rect();                       currentFocused.getDrawingRect(mRect);                       offsetDescendantRectToMyCoords(currentFocused, mRect);                       offsetRectIntoDescendantCoords(child, mRect);                       if(child.requestFocus(focusDirection, mRect)){                           break;                       }                       //原生代码                       //if (child.requestFocus(focusDirection)) {                       //    break;                       //}                   }               }           }       }   }   child就是翻页后的控件,一般是个ViewGroup,可以是RelativeLayout也可以是LinearLayout等。 我们对其部分代码进行了修改,如上代码,将前一个焦点区域传给child。这样,我们在child中可操作空间就很大。 再说一下scrollToItem,我们程序员设置的OnPageChangeListener回调都是在scrollToItem执行的,所以在这些回调函数中控制焦点效果不是很到,而且难度很大。 假如child是RelativeLayout,requestFocus(focusDirection, mRect)方法是ViewGroup中的,代码如下 [java] view plain copy  @Override   public boolean requestFocus(int direction, Rect previouslyFocusedRect) {       if (DBG) {           System.out.println(this + " ViewGroup.requestFocus direction="                   + direction);       }       int descendantFocusability = getDescendantFocusability();          switch (descendantFocusability) {           case FOCUS_BLOCK_DESCENDANTS:               return super.requestFocus(direction, previouslyFocusedRect);           case FOCUS_BEFORE_DESCENDANTS: {               final boolean took = super.requestFocus(direction, previouslyFocusedRect);               return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);           }           case FOCUS_AFTER_DESCENDANTS: {               final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);               return took ? took : super.requestFocus(direction, previouslyFocusedRect);           }           default:               throw new IllegalStateException("descendant focusability must be "                       + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "                       + "but is " + descendantFocusability);       }   }   [java] view plain copy  /**   * Look for a descendant to call {@link View#requestFocus} on.   * Called by {@link ViewGroup#requestFocus(int, android.graphics.Rect)}   * when it wants to request focus within its children.  Override this to   * customize how your {@link ViewGroup} requests focus within its children.   * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT   * @param previouslyFocusedRect The rectangle (in this View's coordinate system)   *        to give a finer grained hint about where focus is coming from.  May be null   *        if there is no hint.   * @return Whether focus was taken.   */   @SuppressWarnings({"ConstantConditions"})   protected boolean onRequestFocusInDescendants(int direction,           Rect previouslyFocusedRect) {       int index;       int increment;       int end;       int count = mChildrenCount;       if ((direction & FOCUS_FORWARD) != 0) {           index = 0;           increment = 1;           end = count;       } else {           index = count - 1;           increment = -1;           end = -1;       }       final View[] children = mChildren;       for (int i = index; i != end; i += increment) {           View child = children[i];           if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {               if (child.requestFocus(direction, previouslyFocusedRect)) {                   return true;               }           }       }       return false;   }   onRequestFocusInDescendants是个很重要的方法,这个方法是计算让ViewGroup中哪个View获得焦点。所以,我们要想控制翻页后哪个View获得焦点就要复写这个方法,实现自己寻找VIew的算法。 读者会问,我们为什么要复写这个方法呢?在什么情况下需要复写这样方法呢? 我们在TV应用开发中,通常想实现一种效果:一个View获得焦点后要放大而且要在其他View上面。为了实现这样的效果,我们的ViewGroup会采用RelativeLayout(注:只有RelativeLayout才能实现此效果),同时让获得焦点的View调用bringToFront方法: [java] view plain copy  public void bringToFront() {       if (mParent != null) {           mParent.bringChildToFront(this);       }   }   [java] view plain copy  public void bringChildToFront(View child) {       int index = indexOfChild(child);       if (index >= 0) {           removeFromArray(index);           addInArray(child, mChildrenCount);           child.mParent = this;           requestLayout();           invalidate();       }   }   上面的代码大家自己研究一下,总的来说bringToFront会让ViewGroup中维护的children数组里面顺序发生变化,children数组放到就是所有的子View,当前获得焦点的那个View会移到children最后位置。大家发现没有这个children就是上面onRequestFocusInDescendants用到的,onRequestFocusInDescendants就是直接取第一个View requstFocus。我们想象一下,第一次翻页,取第一个View获得焦点,没有问题,一切显示正常,注意了bringToFront的作用,会把获得焦点的View移到children数组的末尾,我们第二次翻页的时候,还是去children中的第一个View获得焦点,你会发现页面中获得焦点的View不是你想象中的View,而是别的View。这就是我们为什么要复写onRequestFocusInDescendants了。 至于如何复写,我参考了ListView寻找最近的Item的算法,大家找找学习一下。 我直接贴上代码吧,既然找到了解决方案,就分享给大家。 [java] view plain copy  package com.sohuott.foxpad.launcher.moudle.usercenter.widget;      import android.content.Context;   import android.graphics.Rect;   import android.util.AttributeSet;   import android.view.View;   import android.widget.RelativeLayout;      /**   *    * @author zhongyili   *    */   public class CustomRelativeLayout extends RelativeLayout {          public CustomRelativeLayout(Context context, AttributeSet attrs,               int defStyle) {           super(context, attrs, defStyle);       }          public CustomRelativeLayout(Context context, AttributeSet attrs) {           super(context, attrs);       }          public CustomRelativeLayout(Context context) {           super(context);       }          /***       * 寻找最近的子View       */       @Override       protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {           int count = getChildCount();           Rect otherRect = new Rect();           int minDistance = Integer.MAX_VALUE;           int closetChildIndex = -1;           for (int i = 0; i < count; i++) {               View other = getChildAt(i);               other.getDrawingRect(otherRect);               offsetDescendantRectToMyCoords(other, otherRect);               int distance = getDistance(previouslyFocusedRect, otherRect, direction);               if (distance < minDistance) {                   minDistance = distance;                   closetChildIndex = i;               }           }           if (closetChildIndex >= 0) {               View child = getChildAt(closetChildIndex);               child.requestFocus();               return true;           }           return false;       }          public static int getDistance(Rect source, Rect dest, int direction) {              // TODO: implement this              int sX, sY; // source x, y           int dX, dY; // dest x, y           switch (direction) {           case View.FOCUS_RIGHT:               sX = source.right;               sY = source.top + source.height() / 2;               dX = dest.left;               dY = dest.top + dest.height() / 2;               break;           case View.FOCUS_DOWN:               sX = source.left + source.width() / 2;               sY = source.bottom;               dX = dest.left + dest.width() / 2;               dY = dest.top;               break;           case View.FOCUS_LEFT:               sX = source.left;               sY = source.top + source.height() / 2;               dX = dest.right;               dY = dest.top + dest.height() / 2;               break;           case View.FOCUS_UP:               sX = source.left + source.width() / 2;               sY = source.top;               dX = dest.left + dest.width() / 2;               dY = dest.bottom;               break;           case View.FOCUS_FORWARD:           case View.FOCUS_BACKWARD:               sX = source.right + source.width() / 2;               sY = source.top + source.height() / 2;               dX = dest.left + dest.width() / 2;               dY = dest.top + dest.height() / 2;               break;           default:               throw new IllegalArgumentException("direction must be one of "                       + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "                       + "FOCUS_FORWARD, FOCUS_BACKWARD}.");           }           int deltaX = dX - sX;           int deltaY = dY - sY;           return deltaY * deltaY + deltaX * deltaX;       }     

    }

    转载:http://blog.csdn.net/zhongyili_sohu/article/details/43707425

    转载请注明原文地址: https://ju.6miu.com/read-675528.html

    最新回复(0)