Android之ViewPager源码分析

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值