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

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/Acmen99/article/details/70173419
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞