您的位置:首页 > 移动开发 > Android开发

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

2015-10-28 22:40 711 查看
兑现我的承诺,开始了Android的学习,实现的demo目的很明确,就是对listview里面的内容点赞,然后赞的数目改变。

我先说一下将会解决的问题,没准你也遇到了



Listview在Scrollview中无法显示完全

子view中的button等劫持了listview中item的clickListener

OnClickListener接口的onClick方法中的View view参数究竟是什么

点赞后计数改变

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

先说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代码,需要的孩子可以好好研究。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息