| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android ListView -> 正文阅读 |
|
[移动开发]Android ListView |
1.Listview Listview是用来显示大量数据的控件,且不会因为展示大量数据而出现内存溢出的现象,原因是相关缓存机制保证了内存的合理使用。 首先看一下ListView的继承结构: ListView的继承结构还是很复杂的,它直接继承自AbsListView,而AbsListView有两个子实现类,一个是ListView,另一个就是GridView,从这一点就可以看出来,ListView和GridView在工作原理和实现上都是有很多共同点的。然后AbsListView继承自AdapterView,AdapterView继承自ViewGroup。 ? 2.RecycleBin机制 RecycleBin是ListView缓存的核心机制,它是ListView能够实现成百上千条数据都不会OOM最重要的原因。 RecycleBin是AbsListView的一个内部类,所有继承自AbsListView的子类,也就是ListView和GridView,都可以使用这个机制。 class RecycleBin { ? ? private RecyclerListener mRecyclerListener; ? ? private int mFirstActivePosition; ? ? //mActiveViews存放正在展示在屏幕上的view ,从显示在屏幕上的第一个view到最后一个view ? ? private View[] mActiveViews = new View[0]; ? ??//mScrapViews存放可以由适配器用作convert view的view,是一个数组,数组的每个元素类型为ArrayList<View> ? ??private ArrayList<View>[] mScrapViews; ? ? private int mViewTypeCount;? ? ? ? ? ??//mCurrentScrap是mScrapViews的第0个元素,当view种类数量为1时存放废弃view ? ??private ArrayList<View> mCurrentScrap;? ? ?? ? ??//Adapter中可以重写getViewTypeCount方法来表示ListView中有几种类型的数据项,而setViewTypeCount()方法就是为每种类型的数据项都单独启用一个RecycleBin缓存机制 ? ??public void setViewTypeCount(int viewTypeCount) { ? ? ? ??if (viewTypeCount < 1) { ? ? ? ? ? ? throw new IllegalArgumentException( "Can't have a viewTypeCount < 1"); ? ? ? ? } ? ? ? ? ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; ? ? ? ? for (int i = 0; i < viewTypeCount; i++) { ? ? ? ? ? ? scrapViews[i] = new ArrayList<View>(); ? ? ? ? } ? ? ? ??mViewTypeCount = viewTypeCount; ? ?? ? ?mCurrentScrap = scrapViews[0]; ? ? ? ??mScrapViews = scrapViews; ? ? }? ? ? ?? ?? ?// fillActiveViews()接收两个参数,一个参数表示mActiveViews数组最小要保存的View数量,第二个参数表示ListView中第一个可见元素的position值。?根据传入的参数将ListView中的指定元素存储到mActiveViews数组当中 ? ? void fillActiveViews(int childCount, int firstActivePosition) { ? ? ?? ?if (mActiveViews.length < childCount) { ? ? ? ? ? ? mActiveViews = new View[childCount]; ? ? ? ? } ? ? ? ? mFirstActivePosition = firstActivePosition; ? ? ? ? final View[] activeViews = mActiveViews; ? ? ?? ?for (int i = 0; i < childCount; i++) { ? ? ? ?? ? ?View child = getChildAt(i); ? ? ? ? ?? ?AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); ? ? ? ? ? ? if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { ? ? ? ? ? ? ? ? activeViews[i] = child; ? ? ? ? ? ? ? ? lp.scrappedFromPosition = firstActivePosition + i; ? ? ? ? ?? ?} ? ? ? ? } ?? ?}? ? ? ?? ? ? //getActiveView()方法用于从mActiveViews数组当中取出特定元素。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。如果在mActiveViews数组中没有找到,则返回null ? ??View getActiveView(int position) { ? ? ? ? int index = position - mFirstActivePosition; ? ? ? ? final View[] activeViews = mActiveViews; ? ? ? ??if (index >=0 && index < activeViews.length){ ? ? ? ? ? ? final View match = activeViews[index]; ? ? ? ? ? ? activeViews[index] = null; ? ? ? ? ? ? return match; ? ? ?? ?} ? ? ? ??return null; ? ? }? ? ? ?? ? ? //getScrapView()用于从废弃缓存中取出一个View。这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回 ? ? View getScrapView(int position) { ? ? ? ??final int whichScrap = mAdapter.getItemViewType(position); ? ? ? ? if (whichScrap < 0) { ? ? ? ? ? ??return null; ? ? ? ? } ? ? ? ??if (mViewTypeCount == 1) { ? ? ? ? ?? ?return retrieveFromScrap(mCurrentScrap, position); ? ? ? ? } else if (whichScrap < mScrapViews.length){ ? ? ? ? ? ??return retrieveFromScrap( mScrapViews[whichScrap], position); ? ? ?? ?} ? ? ? ? return null; ? ? } ? ??//addScrapView()用于将一个废弃的View进行缓存。该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),应该调用这个方法来对View进行缓存。当view类型为1时则用mCurrentScrap存储废弃view,否则使用mScrapViews添加废弃view ? ??void addScrapView(View scrap, int position) { ? ? ? ? final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); ? ? ? ??if (lp == null) { ? ? ? ? ? ? return; ? ? ?? ?} ? ? ? ? lp.scrappedFromPosition = position; ? ? ? ??final int viewType = lp.viewType; ? ? ? ? if (!shouldRecycleViewType(viewType)) { ? ? ? ? ?? ?if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { ? ? ? ? ? ? ? ??getSkippedScrap().add(scrap); ? ? ? ? ? ? } ? ? ? ? ? ? return; ? ? ? ? } ? ? ?? ?scrap.dispatchStartTemporaryDetach(); ? ? ? ??notifyViewAccessibilityStateChangedIfNee ded(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); ? ? ? ? final boolean scrapHasTransientState = scrap.hasTransientState(); ? ? ? ??if (scrapHasTransientState) { ? ? ? ? ? ? if (mAdapter != null && mAdapterHasStableIds) { ? ? ? ? ? ? ?? ?if (mTransientStateViewsById == null) { ? ? ? ? ? ? ? ? ? ??mTransientStateViewsById = new LongSparseArray<>(); ? ? ? ? ? ? ? ? } ? ? ? ? ? ?? ? ?mTransientStateViewsById.put( lp.itemId, scrap); ? ? ? ? ? ? } else if (!mDataChanged) { ? ? ? ? ? ? ? ??if (mTransientStateViews == null) { ? ? ? ? ? ? ? ? ? ? mTransientStateViews = new SparseArray<>(); ? ? ? ? ? ? ? ??} ? ? ? ? ? ? ? ? mTransientStateViews.put(position, scrap); ? ? ? ? ?? ?} else { ? ? ? ? ? ? ? ??getSkippedScrap().add(scrap); ? ? ? ? ? ? } ? ? ? ??} else { ? ? ? ? ?? ?if (mViewTypeCount == 1) { ? ? ? ? ? ? ? ??mCurrentScrap.add(scrap); ? ? ? ? ?? ?} else { ? ? ? ? ? ? ? ? mScrapViews[viewType].add(scrap); ? ? ? ? ? ? } ? ? ? ? ? ? if (mRecyclerListener != null) { ? ? ? ? ? ? ? ??mRecyclerListener.onMovedToScra pHeap( scrap); ? ? ? ? ? ??} ? ? ? ??} ? ? } } ? 3.ListView的绘制流程 ListView本质上还是一个View,因此绘制过程还是分为三步:onMeasure、onLayout、onDraw。onMeasure测出其占用屏幕空间,最大为整个屏幕;onDraw用于将ListView内容绘制到屏幕上,在ListView中无实际意义,因为ListView本身只是提供了一种布局方式,真正的绘制是ListView中的子View完成的;而onLayout方法是最为关键的。 ①第一次onLayout ListView的OnLayout实现在AbsListView中,具体源码如下: @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { ? ? super.onLayout(changed, l, t, r, b); ? ? mInLayout = true; ? ? final int childCount = getChildCount(); ? ? if (changed) { ? ? ? ? for (int i = 0; i < childCount; i++) { ? ? ? ? ? ? getChildAt(i).forceLayout(); ? ? ? ? } ? ? ? ? mRecycler.markChildrenDirty(); ? ? } ? ? layoutChildren(); ? ? mInLayout = false; ? ? ... } 从代码可以看出,首先调用了父类的onLayout方法,再判断ListView是否发生了变化(大小、位置),如果ListView发生了变化,则changed变量为true,就会强制每个子布局都进行重新绘制,同时还进行了mRecycler.markChildrenDirty()操作,其中mRecycler就是一个RecycleBin对象,而markChildrenDirty()方法会为每一个scrap view调用forceLayout()。判断完changed变量后又调用了layoutChildren()方法,点进此方法发现它是一个空方法,因为每个子元素的布局实现应该由自己来实现,所以它的具体实现在ListView中。 @Override ? ? ... ? ? ... 它的方法过长,只贴出来一部分,此方法中首先会获取子元素的数量,由于是第一次onLayout,此时ListView中还没有任何子View,因为数据都是由Adapter管理的,还没有展示到界面上。接着又会判断dataChanged这个值,如果数据源发生变化则该值变为true,紧接着调用了RecycleBin的fillActiveViews()方法。可是这时ListView中还没有子View,因此fillActiveViews的缓存功能无法起作用。 接着往下分析,接下来又会判断mLayoutMode的值,默认情况下该值都是LAYOUT_NORMAL,此模式下会直接进入default语句中,其中有多次if条件判断。当前ListView中还没有任何子View,所以当前childCount数量为0,mStackFromBottom变量代表的是布局的顺序,默认的布局顺序是从上至下,因此会进入fillFromTop方法中。 private View fillFromTop(int nextTop) { ? ? mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); ? ? mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); ? ? if (mFirstPosition < 0) { ? ? ? ? mFirstPosition = 0; ? ? } ? ? return fillDown(mFirstPosition, nextTop); } private View fillDown(int pos, int nextTop) { ? ? View selectedView = null; ? ? int end = (mBottom - mTop); ? ? if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { ? ? ? ? end -= mListPadding.bottom; ? ? } ? ? while (nextTop < end && pos < mItemCount) { ? ? ? ? // is this the selected item? ? ? ? ? boolean selected = pos == mSelectedPosition; ? ? ? ? View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); ? ? ? ? nextTop = child.getBottom() + mDividerHeight; ? ? ? ? if (selected) { ? ? ? ? ? ? selectedView = child; ? ? ? ? } ? ? ? ? pos++; ? ? } ? ? setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); ? ? return selectedView; } fillFromTop首先计算出了mFirstPosition的值,并从mFirstPosition开始自顶至下调用fillDown填充。 fillDown中采用了while循环来填充,一开始时nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos是传入的mFirstPosition的值,end是ListView底部减去顶部所得的像素值,mItemCount是Adapter中的元素数量,因此nextTop是小于end的,pos也小于mItemCount,每次执行while循环时,pos加1,nextTop也会累加,当nextTop大于end时,也就是子元素超出屏幕了,或者pos大于mItemCount时,即Adapter中所有元素都被遍历了,出现以上两种情况中一种便会跳出while循环。 在此while循环中,调用了makeAndAddView这个方法: private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { ? ? if (!mDataChanged) { ? ? ? ? // Try to use an existing view for this position. ? ? ? ? final View activeView = mRecycler.getActiveView(position); ? ? ? ? if (activeView != null) { ? ? ? ? ? ? // Found it. We're reusing an existing child, so it just needs?to be positioned like a scrap view. ? ? ? ? ? ? setupChild(activeView, position, y, flow, childrenLeft, selected, true); ? ? ? ? ? ? return activeView; ? ? ? ? } ? ? } ? ? // Make a new view for this position, or convert an unused view if possible. ? ? final View child = obtainView(position, mIsScrap); ? ? // This needs to be positioned and measured. ? ? setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); ? ? return child; } 当Adapter的数据源未发生变化时,会从RecycleBin中获取一个activeView,但是目前RecycleBin中还没有缓存任何的View,因此这里得到的child为null,接着又调用了obtainView方法来获取一个View,obtainView方法在AbsListView里: View obtainView(int position, boolean[] outMetadata) { ? ? outMetadata[0] = false;? ? ? ? ...? ? ? ? final View scrapView = mRecycler.getScrapView(position); ? ? final View child = mAdapter.getView(position, scrapView, this); ? ? if (scrapView != null) { ? ? ? ? if (child != scrapView) { ? ? ? ? ? ? // Failed to re-bind the data, return scrap to the heap. ? ? ? ? ? ? mRecycler.addScrapView(scrapView, position); ? ? ? ? } else if (child.isTemporarilyDetached()) { ? ? ? ? ? ? outMetadata[0] = true; ? ? ? ? ? ? // Finish the temporary detach started in addScrapView(). ? ? ? ? ? ? child.dispatchFinishTemporaryDetach(); ? ? ? ? } ? ? } ? ? ...? ? ? ? return child; } 首先调用RecycleBin的getScrapView方法来尝试获取一个废弃缓存中的View,但是这里是获取不到的;接着又调用了getView方法,即自定义的Adapter中的getView方法,getView方法接收三个参数,第一个是当前子元素位置,第二个参数是convertView,在这里是null,说明没有covertView可以利用,因此在Adapter中判断convertView为null时可以调用LayoutInflater的inflate方法去加载一个布局,并将此view返回。同时可以看到,这个view最终也会作为obtainView方法的返回结果,并传入makeAndAddView方法中后续调用的setupChild()方法中。上面过程可以说明第一次layout过程中,所有子View都是调用LayoutInflater的inflate方法动态加载对应布局而产生的,解析布局的过程肯定是耗时的,但是在后续过程中,这种情况不会出现了。接下来,继续看下setupChild方法: private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,?boolean selected, boolean isAttachedToWindow) { ? ? ...? ? ? ? addViewInLayout(child, flowDown ? -1 : 0, p, true);? ? ? ? .... } 在setupChild方法中会调用addViewInLayout方法将它添加到ListView中,那么回到fillDown方法,其中的while循环就会让子元素View将整个ListView控件填满然后跳出,也就是说即使Adapter中有很多条数据,ListView也只会加载第一屏数据。下图是第一次onLayout的过程: ②第二次onLayout 即使是一个再简单的View,在展示到界面上之前都会经历至少两次onMeasure()和两次onLayout()过程,自然ListView的绘制过程也不例外。 首先还是从layoutChildren()方法看起: 再来看一遍该方法源码: ?@Override ?protected void layoutChildren() { ? ? ...?? ? ? final int childCount = getChildCount();? ?? ? ? ...? ? ? ? boolean dataChanged = mDataChanged;? ? ? ? ? ...? ?? ? ? if (dataChanged) { ? ? ? ? for (int i = 0; i < childCount; i++) { ? ? ? ? ? ? recycleBin.addScrapView(getChildAt(i), firstPosition+i); ? ? ? ? } ? ? } else { ? ? ? ? recycleBin.fillActiveViews(childCount, firstPosition); ? ? } ? ? ...? ?? ? ? // Clear out old views ? ? detachAllViewsFromParent();? ? ? ? switch (mLayoutMode) { ? ? ? ? ...? ? ? ? ? ? ? ? default: ? ? ? ? ? ? if (childCount == 0) { ? ? ? ? ? ? ? ? if (!mStackFromBottom) { ? ? ? ? ? ? ? ? ? ? final int position = lookForSelectablePosition(0, true); ? ? ? ? ? ? ? ? ? ? setSelectedPositionInt(position); ? ? ? ? ? ? ? ? ? ? sel = fillFromTop(childrenTop); ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? final int position = lookForSelectablePosition(mItemCount - 1,false); ? ? ? ? ? ? ? ? ? ? setSelectedPositionInt(position); ? ? ? ? ? ? ? ? ? ? sel = fillUp(mItemCount - 1, childrenBottom); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { ? ? ? ? ? ? ? ? ? ? sel = fillSpecific(mSelectedPosition, oldSel == null ? childrenTop : oldSel.getTop()); ? ? ? ? ? ? ? ? } else if (mFirstPosition < mItemCount) { ? ? ? ? ? ? ? ? ? ? sel = fillSpecific(mFirstPosition,? oldFirst == null ? childrenTop : oldFirst.getTop()); ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? sel = fillSpecific(0, childrenTop); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? ? ? break; ? ? } ? ? ...? ?? ?} 首先还是会获取子元素的数量,不同于第一次onLayout,此时获取到的子View数量不再为0,而是ListView中显示的子元素数量。下面又调用了RecycleBin的fillActiveViews()方法,目前ListView已经有子View了,这样所有的子View都会被缓存到RecycleBin中的mActiveViews数组中,后面会使用到它们。 接下来有一个重要的方法:detachAllViewsFromParent(),这个方法会将ListView中所有子View全部清除掉,从而保证第二次Layout过程不会产生一份重复数据,因为layoutChildren方法会向ListView中添加View,在第一次layout中已经添加了一次,如果第二次layout继续添加,那么必然会出现数据重复的问题,因此这里先调用detachAllViewsFromParent方法将第一次添加的View清除掉。 这样把已经加载好的View又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,刚刚调用了RecycleBin的fillActiveViews()方法来缓存子View,等会将直接使用这些缓存好的View来进行添加子View,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。 再进入判断childCount是否为0的逻辑中,此时会走和第一次layout相反的else逻辑分支,这其中又有三条逻辑分支,第一条一般不成立,因为开始时还没选中任何子View,第二条一般成立,mFirstPosition开始时为0,只要Adapter中数据量大于0即可,所以进入了fillSpecific方法: private View fillSpecific(int position, int top) { ? ? boolean tempIsSelected = position == mSelectedPosition; ? ? View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); ? ? // Possibly changed again in fillUp if we add rows above this one. ? ? mFirstPosition = position; ? ? View above; ? ? View below; ? ? final int dividerHeight = mDividerHeight; ? ? if (!mStackFromBottom) { ? ? ? ? above = fillUp(position - 1, temp.getTop() - dividerHeight); ? ? ? ? // This will correct for the top of the first view not touching the top of the list ? ? ? ? adjustViewsUpOrDown(); ? ? ? ? below = fillDown(position + 1, temp.getBottom() + dividerHeight); ? ? ? ? int childCount = getChildCount(); ? ? ? ? if (childCount > 0) { ? ? ? ? ? ? correctTooHigh(childCount); ? ? ? ? } ? ? } else { ? ? ? ? below = fillDown(position + 1, temp.getBottom() + dividerHeight); ? ? ? ? // This will correct for the bottom of the last view not touching the bottom of the list ? ? ? ? adjustViewsUpOrDown(); ? ? ? ? above = fillUp(position - 1, temp.getTop() - dividerHeight); ? ? ? ? int childCount = getChildCount(); ? ? ? ? if (childCount > 0) { ? ? ? ? ? ? ?correctTooLow(childCount); ? ? ? ? } ? ? } ? ? if (tempIsSelected) { ? ? ? ? return temp; ? ? } else if (above != null) { ? ? ? ? return above; ? ? } else { ? ? ? ? return below; ? ? } } fillSpecific()方法的功能和fillUp、fillDown差不多,但是fillSpecific()方法会优先加载指定位置的View,再加载该View上下的其它子View,由于这里传入的position就是第一个子元素的位置,因此此时其效果和上述的fillDown()基本一致。 可以看到,fillSpecific()方法中也调用了makeAndAddView()方法,因为我们之前调用detachAllViewsFromParent()方法把所有ListView当中的子View全部清除掉了,这里肯定要重新再加上,在makeAndAddView()方法中: 再来看一遍此方法源码: private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { ? ? if (!mDataChanged) { ? ? ? ? // Try to use an existing view for this position. ? ? ? ? final View activeView = mRecycler.getActiveView(position); ? ? ? ? if (activeView != null) { ? ? ? ? ? ? // Found it. We're reusing an existing child, so it just needs?to be positioned like a scrap view. ? ? ? ? ? ? setupChild(activeView, position, y, flow, childrenLeft, selected, true); ? ? ? ? ? ? return activeView; ? ? ? ? } ? ? } ? ? // Make a new view for this position, or convert an unused view if ? ? // possible. ? ? final View child = obtainView(position, mIsScrap); ? ? // This needs to be positioned and measured. ? ? setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); ? ? return child; } 首先还是会从RecycleBin中获取ActiveView,不同于第一次layout,这次能获取到了,那肯定就不会进入obtainView中了,而是直接调用setupChild()方法,此时setupChild()方法的最后一个参数是true,表明当前的view是被回收过的,再来看看setupChild()方法源码: private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,?boolean selected, boolean isAttachedToWindow) { ? ? ... ? ? if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter?&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) { ? ? ? ? attachViewToParent(child, flowDown ? -1 : 0, p); ? ? ? ? ... ? ? } else { ? ? ? ? ... ? ? } ? ? ... } 可以看到,setupChild()方法的最后一个参数是isAttachedToWindow,方法执行过程中会对这个变量进行判断,由于isAttachedToWindow现在是true,所以会执行attachViewToParent()方法,而第一次Layout过程则是执行的else语句中的addViewInLayout()方法。 这两个方法最大的区别在于,如果需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout()方法,而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent()方法。由于前面在layoutChildren()方法当中调用了detachAllViewsFromParent()方法,这样ListView中所有的子View都是处于detach状态的,所以这里attachViewToParent()方法是正确的选择。 经历了这样一个detach又attach的过程,ListView中所有的子View又都可以正常显示出来了,那么第二次Layout过程结束。 下图展示了第二次onLayout的过程: ? ? ? ? |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 1:52:04- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |