Android Scrollview+Listview 实现不同条目点赞并计数功能及原理分析(1)

兑现我的承诺,开始了Android的学习,实现的demo目的很明确,就是对listview里面的内容点赞,然后赞的数目改变。
我先说一下将会解决的问题,没准你也遇到了
Android Scrollview+Listview 实现不同条目点赞并计数功能及原理分析(1)_第1张图片

  1. Listview在Scrollview中无法显示完全
  2. 子view中的button等劫持了listview中item的clickListener
  3. OnClickListener接口的onClick方法中的View view参数究竟是什么
  4. 点赞后计数改变

每个问题都是常见问题,但是我想好好说说,所以分了三篇博客,各说一个问题。

先说Listview在Scrollview中无法显示完全。
为什么listview显示不全,因为在Scrollview在measure时计算不出Listview的大小,所以只能显示一行。
View,也就是我们的具体每一个图案,View在绘图时首先measure之后layout最后draw,简单说就是先计算大小,之后计算布局,最后就是绘画了。我们这部分涉及的是measure。View 的measure是从root开始一级一级计算自己的child,listview在Scrollview中就是他的子view。
这个问题无论是从哪里出发,解决问题的关键点都是得到ListView的高度。然后Scrollview在调用子view的measure方法就可以得到具体高度
这个问题网上很多解释,最多的是openstack中的一位大神的解释,我看过很多demo都是这种解决方式。写个工具类,然后再listview.set(adapter)之后写上Utility.setListViewHeightBasedOnChildren(listview)

public class Utility {
    public static void setListViewHeightBasedOnChildren(ListView listView) {
    ListAdapter listAdapter = listView.getAdapter(); 
   if (listAdapter == null) {
        return;
    }

   int totalHeight = 0;
    for (int i = 0, len = listAdapter.getCount(); i < len; i++) {
        View listItem = listAdapter.getView(i, null, listView);
        listItem.measure(0, 0);
        totalHeight += listItem.getMeasuredHeight();
    }

   ViewGroup.LayoutParams params = listView.getLayoutParams();
    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
    listView.setLayoutParams(params);
}

这个方法确实可以解决一部分人的问题,但是有博客说,针对中文可能会有bug,于是上面的方法需要改写一下

public static void setListViewHeightBasedOnChildrenNew(ListView listView) {
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {
            return;
        }

        int totalHeight = 0;
        for (int i = 0, len = listAdapter.getCount(); i < len; i++) {
            View listItem = listAdapter.getView(i, null, listView);
            int desiredWidth = View.MeasureSpec.makeMeasureSpec(listView.getWidth(), View.MeasureSpec.AT_MOST);
            listItem.measure(desiredWidth, 0);
            totalHeight += listItem.getMeasuredHeight();
        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
        listView.setLayoutParams(params);
    }

可以发现主要改动的是measure方法的参数,能够显示完全了,但是还是有bug,就是感觉Scrollview滑到没有物体地方了。
所以继续查,第三种方法是写mylistview继承Listview,重写onMeasure方法。

public class MyListView extends ListView {
    public ScrollViewWithListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);

    }
}

这种方法可以完美的解决问题。好了先解决了问题,再来思考一下怎么回事。第一种方法就可以看出了他的思路就是计算这个listview到底有多高。
那我们看看源码

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
            widthMeasureSpec != mOldWidthMeasureSpec ||  
            heightMeasureSpec != mOldHeightMeasureSpec) {  
        mPrivateFlags &= ~MEASURED_DIMENSION_SET;  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);  
        }  
        onMeasure(widthMeasureSpec, heightMeasureSpec);  
        if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
            throw new IllegalStateException("onMeasure() did not set the"  
                    + " measured dimension by calling"  
                    + " setMeasuredDimension()");  
        }  
        mPrivateFlags |= LAYOUT_REQUIRED;  
    }  
    mOldWidthMeasureSpec = widthMeasureSpec;  
    mOldHeightMeasureSpec = heightMeasureSpec;  
}  

measure是final的,不能被子类修改,但是真正的计算view大小是onMeasure方法,这个方法是可以被子类继承并修改,也是我们经常做的。继续追下去

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
                || heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            // Lay out child directly against the parent measure spec so that
            // we can obtain exected minimum width and height.
            measureScrapChild(child, 0, widthMeasureSpec, heightSize);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState & MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);

        mWidthMeasureSpec = widthMeasureSpec;
    }

这里我们就要看以下为什么是listItem.measure(0, 0);
首先执行measure方法到onMeasure(0,0),这里会有几行代码

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

关于这个再看一下这个代码

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /** * Measure specification mode: The child can be as large as it wants up * to the specified size. */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /** * Creates a measure specification based on the supplied size and mode. * * The mode must always be one of the following: * <ul> * <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li> * <li>{@link android.view.View.MeasureSpec#EXACTLY}</li> * <li>{@link android.view.View.MeasureSpec#AT_MOST}</li> * </ul> * * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.</p> * * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

简单来说就是measure穿进去的值是一个int,这个32位的数字0-29位是他的size(大小)高2位是mode(模式)
UNSPECIFIED -0
EXACTLY-1
AT_MOST-2
这三个mode在View的计算中常用,但是有点走远了,有兴趣去看看源码。所以listview.measure(0,0)就是传进去了UNSPECIFIED size为0。之后就要计算高度

if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

measure之后才能调用listItem.getMeasuredHeight(),之前调用都会是0,现在就有了整个listview所有listItem的高度,最后加上item中间的缝隙就是最后的高度

/** * @return Returns the height of the divider that will be drawn between each item in the list. */
    public int getDividerHeight() {
        return mDividerHeight;
    }

至于第二种方法我弄不清楚是为什么。看样子应该是中文对listItem的宽度有一些影响。
接下来看最靠谱的解决方法,主要就是重写了onMeasure方法

int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >>    2,MeasureSpec.AT_MOST);

根据刚才的分析,这里是设置一个高度,模式是AT_MOST,size是最大的Integer左移2位的数字。
之后计算这个ListView的高度

if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

可以发现不同的MODE的计算方法是不一样的。继续追

    final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
            int maxHeight, int disallowPartialChildPosition) {
        final ListAdapter adapter = mAdapter;
        if (adapter == null) {
            return mListPadding.top + mListPadding.bottom;
        }

        // Include the padding of the list
        int returnedHeight = mListPadding.top + mListPadding.bottom;
        final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0;
        // The previous height value that was less than maxHeight and contained
        // no partial children
        int prevHeightWithoutPartialChild = 0;
        int i;
        View child;

        // mItemCount - 1 since endPosition parameter is inclusive
        endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
        final AbsListView.RecycleBin recycleBin = mRecycler;
        final boolean recyle = recycleOnMeasure();
        final boolean[] isScrap = mIsScrap;

        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);

            measureScrapChild(child, i, widthMeasureSpec, maxHeight);

            if (i > 0) {
                // Count the divider for all but one child
                returnedHeight += dividerHeight;
            }

            // Recycle the view before we possibly return from the method
            if (recyle && recycleBin.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                recycleBin.addScrapView(child, -1);
            }

            returnedHeight += child.getMeasuredHeight();

            if (returnedHeight >= maxHeight) {
                // We went over, figure out which height to return. If returnedHeight > maxHeight,
                // then the i'th position did not fit completely.
                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                            && (i > disallowPartialChildPosition) // We've past the min pos
                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
                            && (returnedHeight != maxHeight) // i'th child did not fit completely
                        ? prevHeightWithoutPartialChild
                        : maxHeight;
            }

            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
                prevHeightWithoutPartialChild = returnedHeight;
            }
        }

        // At this point, we went through the range of children, and they each
        // completely fit, so return the returnedHeight
        return returnedHeight;
    }

上面的方法就将会计算所有的ListView长度。最后只要super.onMeasure(widthMeasureSpec, expandSpec);就可以计算出ListVIew 的高度了。

第一个问题就解决了,下一篇博客继续解决问题,最后将会给出demo代码,需要的孩子可以好好研究。

你可能感兴趣的:(ListView,scrollview,详解,显示不全,无法滑动)