Android ListView源码分析 点滴记录

ListView是android开发中常用的控件,ListView是按行显示的。

我们可能有不同的应用需求,一行只显示一个“内容”,一行显示多个“内容”。

如果一行只显示一个内容的话,我们可以监听ListView提供的监听接口setOnItemClickListener,不过需要注意的是,里面的“内容”不能获取焦点,如果获取焦点的话,setOnItemClickListener是监听不到的,至于焦点我们可以使用ListView行的焦点。

如果一行显示多个内容的话,一般这种情况,每个内容都是可以获取焦点、都可以点击的,这个时候ListView的监听是没有多大用的,我们只要每个内容设置自己的焦点,设置自己的监听就好。同时调用ListView的setItemsCanFocus(true)。


如何禁止listview的item项获得焦点,而让item的子控件获得焦点


以下内容转载至:http://www.2cto.com/kf/201405/299601.html

android中Baseadapter的 getItem 和 getItemId 的作用和重写

重写Baseadapter时,我们知道需要重写以下四个方法:getCount,getItem(int position),getItemId(int position),getView方法,
getCount决定了listview一共有多少个item,而getView返回了每个item项所显示的view。
可是getItem(int position),getItemId(int position)有什么作用呢?该怎么重写呢?

首先看 getItem:

@Override
public Object getItem(int position) {

. ...
}

官方解释是Get the data item associated with the specified position in the data set.即获得相应数据集合中特定位置的数据项。那么该方法是在哪里被调用呢?什么时候被调用呢?

通过查看源代码发现,getItem方法不是在Baseadapter类中被调用的,而是在Adapterview中被调用的。

adapterView类中,我们找到了如下方法:

    /**
     * Gets the data associated with the specified position in the list.
     *
     * @param position Which data to get
     * @return The data associated with the specified position in the list
     */
    public Object getItemAtPosition(int position) {
        T adapter = getAdapter();
        return (adapter == null || position < 0) ? null : adapter.getItem(position);
    }

那么getItemAtPosition(position) 又是什么时候被调用?答案:它也不会被自动调用,它是用来在我们设置setOnItemClickListener、setOnItemLongClickListener、setOnItemSelectedListener的点击选择处理事件中方便地调用来获取当前行数据的。官方解释Impelmenters can call getItemAtPosition(position) if they need to access the data associated with the selected item.所以一般情况下,我们可以这样写:

        @Override
        public Object getItem(int position) {
            return (dataList == null) ? null : dataList.get(position);
        }

当然如果你喜欢,也可以在里面直接返回null。

至于getItemId(int position),它返回的是该postion对应item的id,adapterview也有类似方法:

    public long getItemIdAtPosition(int position) {
        T adapter = getAdapter();
        return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
    }

不同getItem的是,某些方法(如onclicklistener的onclick方法)有id这个参数,而这个id参数就是取决于getItemId()这个返回值的。

我们一般可以这样实现:

        @Override
        public long getItemId(int position) {
            return position;
        }


20141206 ListView获取Focus源码分析

在使用横向ListView(横向ListView,GitHub上有别人基于ListView改写的源码),处理focus时,遇到了一些问题,为了解决问题,简单分析了一下ListView在获取focus处理的相关源码。

在讲解之前说明一下,android应用在触屏模式下,我们很少需要考虑foucs的问题,可是,在适配遥控器时,我们就需要考虑了。在适配遥控器时,一般会比触控模式下用户多看到一个状态,比如,在触控模式下,用户点击Button,但是,在遥控器模式下,用户得首先选中Button(视觉上会有变化),然后再按OK键点击Button。问题来了,我们在什么时机让选中视觉(出现焦点框或者变大)发生变化呢?我们首先会想到在onFocusChanged中处理,这其实是采用了view的focus状态发生变化时进行处理的。但是,有时候由于某些因数,view的focus状态不好用或者不能使用,这时,我们可以考虑使用selected状态。

在ListView的中每一项的选中状态高亮就是采用selected状态做到的。我们可以从View的setSelected函数的解释中验证这一点。


void android.view.View.setSelected(boolean selected)

Changes the selection state of this view. A view can be selected or not. Note that selection is not the same as focus. Views are typically selected in the context of an AdapterView like ListView or GridView; the selected view is the view that is highlighted.

Parameters:
selected true if the view must be selected, false otherwise
下面我们正式开始讲:

在我们将focus从别的控件切换到ListView上时,会执行ListView的

protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)

onFocusChanged是override View的方法,ListView本身就是一个View。

看一下OnFocusChanged的源码

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);

        final ListAdapter adapter = mAdapter;
        int closetChildIndex = -1;
        int closestChildTop = 0;
        if (adapter != null && gainFocus && previouslyFocusedRect != null) {
	   /**
	    *下面这部分代码用来计算ListView的哪个item应该获取焦点
	    *最终closetChildIndex的值就是应该获取焦点的item位置
	    */
            previouslyFocusedRect.offset(mScrollX, mScrollY);

            // Don't cache the result of getChildCount or mFirstPosition here,
            // it could change in layoutChildren.
            if (adapter.getCount() < getChildCount() + mFirstPosition) {
                mLayoutMode = LAYOUT_NORMAL;
                layoutChildren();
            }

            // figure out which item should be selected based on previously
            // focused rect
            Rect otherRect = mTempRect;
            int minDistance = Integer.MAX_VALUE;
            final int childCount = getChildCount();
            final int firstPosition = mFirstPosition;

            for (int i = 0; i < childCount; i++) {
                // only consider selectable views
                if (!adapter.isEnabled(firstPosition + i)) {
                    continue;
                }

                View other = getChildAt(i);
                other.getDrawingRect(otherRect);
                offsetDescendantRectToMyCoords(other, otherRect);
                int distance = getDistance(previouslyFocusedRect, otherRect, direction);

                if (distance < minDistance) {
                    minDistance = distance;
                    closetChildIndex = i;
                    closestChildTop = other.getTop();
                }
            }
        }

        if (closetChildIndex >= 0) {
            setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop);
        } else {
            requestLayout();
        }
    }
需要强调的
setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop);

这句话就是让closetChildIndex位置的item被选中,同时要传入位置。如果传入位置是0,或者直接调用setSelection,被选中的item就会自动滚动到第一个的位置。这个问题就是笔者在使用HListView时(横向ListView)遇到的问题。

到这里,大家可能还有些疑问。怎么没有提到按键处理的事呢?

其实,会调用ListView  public boolean dispatchKeyEvent(KeyEvent event)  只是没做什么功能。由于笔者对按键这块理解的还不够系统,今天就先不说这块了。今后再补充。

20141224ListView的onFocuseChanged方法学习,下面是HListView中的代码:

	@Override
	protected void onFocusChanged( boolean gainFocus, int direction, Rect previouslyFocusedRect ) {
		super.onFocusChanged( gainFocus, direction, previouslyFocusedRect );
        Log.d("lizhongyi", "HListView onFocusChanged gainFocus = " + gainFocus + " direction = " + direction + " " + this.toString());
        Log.d("onFocusChanged", "HListView onFocusChanged gainFocus = " + gainFocus + " direction = " + direction + " " + this.toString());
		
        final ListAdapter adapter = mAdapter;
		int closetChildIndex = -1;
		int closestChildLeft = 0;
		if ( adapter != null && gainFocus && previouslyFocusedRect != null ) {
		    Log.d("onFocusChanged", "getScrollX() = " + getScrollX() + " getScrollY() = " + getScrollY());
			previouslyFocusedRect.offset( getScrollX(), getScrollY() );

			// Don't cache the result of getChildCount or mFirstPosition here,
			// it could change in layoutChildren.
			if ( adapter.getCount() < getChildCount() + mFirstPosition ) {
				mLayoutMode = LAYOUT_NORMAL;
				layoutChildren();
			}

			// figure out which item should be selected based on previously
			// focused rect
			Rect otherRect = mTempRect;
			int minDistance = Integer.MAX_VALUE;
			final int childCount = getChildCount();
			final int firstPosition = mFirstPosition;

			for ( int i = 0; i < childCount; i++ ) {
				// only consider selectable views
				if ( !adapter.isEnabled( firstPosition + i ) ) {
					continue;
				}

				View other = getChildAt( i );
				other.getDrawingRect( otherRect );
				Log.d("onFocusChanged", "otherRect : " + otherRect.left + " " + otherRect.top + " " + otherRect.right + " " + otherRect.bottom);
				offsetDescendantRectToMyCoords( other, otherRect );
                Log.e("onFocusChanged", "otherRect : " + otherRect.left + " " + otherRect.top + " " + otherRect.right + " " + otherRect.bottom);
                Log.e("onFocusChanged", "previouslyFocusedRect : " + previouslyFocusedRect.left + " " + previouslyFocusedRect.top + " " + previouslyFocusedRect.right + " " + previouslyFocusedRect.bottom);
                int distance = getDistance( previouslyFocusedRect, otherRect, direction );
				Log.e("onFocusChanged", "distance = " + distance + " minDistance = " + minDistance);
				if ( distance < minDistance ) {
					minDistance = distance;
					closetChildIndex = i;
					closestChildLeft = other.getLeft();
				}
			}
		}

		if ( closetChildIndex >= 0 ) {
			setSelectionFromLeft( closetChildIndex + mFirstPosition, closestChildLeft );
		} else {
			requestLayout();
		}
	}
先看一下方法定义:

protected void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect)

Added in  API level 1

Called by the view system when the focus state of this view changes. When the focus change event is caused by directional navigation, direction and previouslyFocusedRect provide insight into where the focus is coming from. When overriding, be sure to call up through to the super class so that the standard focus handling will occur.

Parameters
gainFocus True if the View has focus; false otherwise.
direction The direction focus has moved when requestFocus() is called to give this view focus. Values are FOCUS_UPFOCUS_DOWNFOCUS_LEFT,FOCUS_RIGHTFOCUS_FORWARD, or FOCUS_BACKWARD. It may not always apply, in which case use the default.
previouslyFocusedRect The rectangle, in this view's coordinate system, of the previously focused view. If applicable, this will be passed in as finer grained information about where the focus is coming from (in addition to direction). Will be null otherwise.

other.getDrawingRect(otherRect)就是把other的坐标(我认为这个坐标是相对于自己的坐标)赋给otherRect,这个方法和getFocusedRect()其实是一样的。

    public void getDrawingRect(Rect outRect) {
        outRect.left = mScrollX;
        outRect.top = mScrollY;
        outRect.right = mScrollX + (mRight - mLeft);
        outRect.bottom = mScrollY + (mBottom - mTop);
    }
    public void getFocusedRect(Rect r) {
        getDrawingRect(r);
    }
offsetDescendantRectToMyCoords(other, otherRect)这个函数是把other的坐标转换到ListView的坐标系下

从上面的源码我们可以看到,focuse切换到ListView的时候,其实是根据前一个焦点的位置,遍历能看到的ListView项,找到最近的一个。

20150126 ListView滚动源码分析

在实际项目开发过程中,想自己控制移动距离,所以研究了一下相关代码,在此记录一下。

在ListView中实现滚动的方式是:

    boolean arrowScroll(int direction) {
        try {
            mInLayout = true;
            final boolean handled = arrowScrollImpl(direction);
            if (handled) {
                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
            }
            return handled;
        } finally {
            mInLayout = false;
        }
    }

    private boolean arrowScrollImpl(int direction) {
        if (getChildCount() <= 0) {
            return false;
        }

        View selectedView = getSelectedView();
        int selectedPos = mSelectedPosition;

        int nextSelectedPosition = nextSelectedPositionForDirection(selectedView, selectedPos, direction);
        int amountToScroll = amountToScroll(direction, nextSelectedPosition);

	......

        if (amountToScroll > 0) {
            scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll);
            needToRedraw = true;
        }

	......

        return false;
    }

arrowScrollImpl方法省略了部分代码。amountToScroll用来计算需要“滚动”距离,scrollListItemBy用来执行“滚动”。

    private int amountToScroll(int direction, int nextSelectedPosition) {
        final int listBottom = getHeight() - mListPadding.bottom;
        final int listTop = mListPadding.top;
	
	//numChidren是屏幕上能看到的item数量
        int numChildren = getChildCount();

        if (direction == View.FOCUS_DOWN) {
            int indexToMakeVisible = numChildren - 1;
            if (nextSelectedPosition != INVALID_POSITION) {
                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
            }
            while (numChildren <= indexToMakeVisible) {
                // Child to view is not attached yet.
                addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1);
                numChildren++;
            }
            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
            final View viewToMakeVisible = getChildAt(indexToMakeVisible);

            int goalBottom = listBottom;
	    //这个地方很重要,在下面讲解
            if (positionToMakeVisible < mItemCount - 1) {
                goalBottom -= getArrowScrollPreviewLength();
            }

            if (viewToMakeVisible.getBottom() <= goalBottom) {
                // item is fully visible.
                return 0;
            }

            if (nextSelectedPosition != INVALID_POSITION
                    && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) {
                // item already has enough of it visible, changing selection is good enough
                return 0;
            }

            int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);

            if ((mFirstPosition + numChildren) == mItemCount) {
                // last is last in list -> make sure we don't scroll past it
                final int max = getChildAt(numChildren - 1).getBottom() - listBottom;
                amountToScroll = Math.min(amountToScroll, max);
            }

            return Math.min(amountToScroll, getMaxScrollAmount());
        } else {
            int indexToMakeVisible = 0;
            if (nextSelectedPosition != INVALID_POSITION) {
                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
            }
            while (indexToMakeVisible < 0) {
                // Child to view is not attached yet.
                addViewAbove(getChildAt(0), mFirstPosition);
                mFirstPosition--;
                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
            }
            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
            int goalTop = listTop;
            if (positionToMakeVisible > 0) {
                goalTop += getArrowScrollPreviewLength();
            }
            if (viewToMakeVisible.getTop() >= goalTop) {
                // item is fully visible.
                return 0;
            }

            if (nextSelectedPosition != INVALID_POSITION &&
                    (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) {
                // item already has enough of it visible, changing selection is good enough
                return 0;
            }

            int amountToScroll = (goalTop - viewToMakeVisible.getTop());
            if (mFirstPosition == 0) {
                // first is first in list -> make sure we don't scroll past it
                final int max = listTop - getChildAt(0).getTop();
                amountToScroll = Math.min(amountToScroll,  max);
            }
            return Math.min(amountToScroll, getMaxScrollAmount());
        }
    }

goalBottom -= getArrowScrollPreViewLength()这句话是很重要的,getArrowScrollPreViewLength从单词组合我们可以理解,这是获得下一个item的预览长度,在ListView中,如果没有预览是不能滚动的。

    /**
     * @return The amount to preview next items when arrow srolling.
     */
    private int getArrowScrollPreviewLength() {
        return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength());
    }
在getVerticalFadingEdgeLength()得到为0时,会取MIN_SCROLL_PREVIEW_PIXELS,这个值为2。

getVerticalFadingEdgeLength()是指渐变的长度,要求获得值可以通过如下设置:

setVerticalFadingEdgeEnabled(true);
setFadingEdgeLength(需要渐变的长度);

int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);
anountToScroll是算出实际要“滚动”的距离,通过上面初略的讲解和大家的分析,应该知道这个距离包括未显示全item的部分+渐变的长度。

至此,需要滚动的距离算完了,下面就是“滚动”了。

    /**
     * Scroll the children by amount, adding a view at the end and removing
     * views that fall off as necessary.
     *
     * @param amount The amount (positive or negative) to scroll.
     */
    private void scrollListItemsBy(int amount) {
        offsetChildrenTopAndBottom(amount);

        final int listBottom = getHeight() - mListPadding.bottom;
        final int listTop = mListPadding.top;
        final AbsListView.RecycleBin recycleBin = mRecycler;

        if (amount < 0) {
            // shifted items up

            // may need to pan views into the bottom space
            int numChildren = getChildCount();
            View last = getChildAt(numChildren - 1);
            while (last.getBottom() < listBottom) {
                final int lastVisiblePosition = mFirstPosition + numChildren - 1;
                if (lastVisiblePosition < mItemCount - 1) {
                    last = addViewBelow(last, lastVisiblePosition);
                    numChildren++;
                } else {
                    break;
                }
            }

            // may have brought in the last child of the list that is skinnier
            // than the fading edge, thereby leaving space at the end.  need
            // to shift back
            if (last.getBottom() < listBottom) {
                offsetChildrenTopAndBottom(listBottom - last.getBottom());
            }

            // top views may be panned off screen
            View first = getChildAt(0);
            while (first.getBottom() < listTop) {
                AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
                    recycleBin.addScrapView(first, mFirstPosition);
                }
                detachViewFromParent(first);
                first = getChildAt(0);
                mFirstPosition++;
            }
        } else {
            // shifted items down
            View first = getChildAt(0);

            // may need to pan views into top
            while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
                first = addViewAbove(first, mFirstPosition);
                mFirstPosition--;
            }

            // may have brought the very first child of the list in too far and
            // need to shift it back
            if (first.getTop() > listTop) {
                offsetChildrenTopAndBottom(listTop - first.getTop());
            }

            int lastIndex = getChildCount() - 1;
            View last = getChildAt(lastIndex);

            // bottom view may be panned off screen
            while (last.getTop() > listBottom) {
                AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
                    recycleBin.addScrapView(last, mFirstPosition+lastIndex);
                }
                detachViewFromParent(last);
                last = getChildAt(--lastIndex);
            }
        }
    }
我们在上面提到scrollListItemBy用来执行“滚动”,在ScrollListItemBy中最重要的是

offsetChildrenTopAndBottom(amount);
offsetChildrenTopAndBottom是ViewGroup中的方法。

    /**
     * Offset the vertical location of all children of this view by the specified number of pixels.
     *
     * @param offset the number of pixels to offset
     *
     * @hide
     */
    public void offsetChildrenTopAndBottom(int offset) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        boolean invalidate = false;

        for (int i = 0; i < count; i++) {
            final View v = children[i];
            v.mTop += offset;
            v.mBottom += offset;
            if (v.mDisplayList != null) {
                invalidate = true;
                v.mDisplayList.offsetTopAndBottom(offset);
            }
        }

        if (invalidate) {
            invalidateViewProperty(false, false);
        }
    }
这个方法是public的,但是被打了hide标签。怪不得让给改写ListView为横向List的外国哥们(alessandro crugnola)很气愤,这个哥们是这样说的:“Why the hell Google guys set public methods hidden??? fuck you ” 呵呵,脾气很大的牛人。

offsetChildrenTopAndBottom其实就是变量了一遍屏幕上能看到的Item,让每个item改变位置重新画一遍,所以,我前面讲解中在滚动一词上加了引号,因为实际操作不是滚动。

至此,ListView实现“滚动”的总体流程讲完了,大家有什么不懂或者有什么见解,欢迎留言,大家共同学习共同进步。


你可能感兴趣的:(ListView,源码分析)