【0173】Android 面试-View相关
1.view树的绘制

【重要的参数-MeasureSpec】
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而specSize是指在某种测量模式下的规格大小。
MeasureSpec通过SpecMode和SpecSize打包成int值来避免过多对象内存分配,为了方便操作,其提供了打包和解包的方法。SpecModel和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包为一个MeasureSpec,
而一个MeasureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指MeasureSpec所代表的int值,而并非MeasureSpec本身。
SPecMode有三类,每一类都表示特殊的含义,如下所示
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。不使用
EXACTLY
父容器已经测量出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST
父容器指定了一个可用大小即SpecSize,View大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的Wrap_content,此时的父控件无法获取子控件的尺寸,只能子控件自己测量尺寸;
1.1 measure

【说明】




【说明】下面的3个方法都会被调用到;
【串联】measure是树遍历,从上到下,measure会为每个view和子节点的宽高进行赋值,该值可以通过get方法获取到,注意:这个宽高必须在父节点试图的宽高内;
父视图通过MeasureSpec进行约束,这样才能保证所有的父试图接收子视图传递的测量;
如果父视图觉得子视图传递的参数一致,会再次请求子视图进行测量;
父视图可以根据没有给定的dimension测量每个视图;
如果子视图尺寸不合适,父视图就会设置为一个确切的大小:at_most/exactly再次对子视图进行测量;
===========
【绘制的最重要的流程--measure】
measure方法的测量开始于父控件viewGroup,不断的遍历子控件的measure方法,然后会根据父控件的measurespc和子view的layoutparam来决定
子视图的measurespc,通过此measurespc然后获取子view的宽高,然后一层层的向下传递,不断的保存整个父控件的测量的宽高;
整个measure的测量流程就是一个树形递归过程;为整个view树计算实际的大小,每个view视图控件最终的宽和高,都是由父试图和这个view视图的layoutparam决定;

1.2 layout


【实例】线性布局的绘制

1 /** 2 * Position the children during a layout pass if the orientation of this 3 * LinearLayout is set to {@link #VERTICAL}. 4 * 5 * @see #getOrientation() 6 * @see #setOrientation(int) 7 * @see #onLayout(boolean, int, int, int, int) 8 * @param left 9 * @param top 10 * @param right 11 * @param bottom 12 */ 13 void layoutVertical(int left, int top, int right, int bottom) { 14 final int paddingLeft = mPaddingLeft; 15 16 int childTop; 17 int childLeft; 18+++++++++++++++++++++++++++++++++赋值计算+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 19 // Where right end of child should go 20 final int width = right - left; 21 int childRight = width - mPaddingRight; 22 23 // Space available for child 24 int childSpace = width - paddingLeft - mPaddingRight; 25 26 final int count = getVirtualChildCount(); 27 28 final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 29 final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; 30 31 switch (majorGravity) { 32 case Gravity.BOTTOM: 33 // mTotalLength contains the padding already 34 childTop = mPaddingTop + bottom - top - mTotalLength; 35 break; 36 37 // mTotalLength contains the padding already 38 case Gravity.CENTER_VERTICAL: 39 childTop = mPaddingTop + (bottom - top - mTotalLength) / 2; 40 break; 41 42 case Gravity.TOP: 43 default: 44 childTop = mPaddingTop; 45 break; 46 } 47 +++++++++++++++++++++++++++赋值计算结束++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 48 for (int i = 0; i < count; i++) { 49 final View child = getVirtualChildAt(i); 50 if (child == null) { 51 childTop += measureNullChild(i); 52 } else if (child.getVisibility() != GONE) { 53 final int childWidth = child.getMeasuredWidth(); //获取子控件的宽高 54 final int childHeight = child.getMeasuredHeight(); //获取子控件的宽高 55 56 final LinearLayout.LayoutParams lp = //父容器的测量规格 57 (LinearLayout.LayoutParams) child.getLayoutParams(); 58 59 int gravity = lp.gravity; 60 if (gravity < 0) { 61 gravity = minorGravity; 62 } 63 final int layoutDirection = getLayoutDirection(); 64 final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); 65 switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 66 case Gravity.CENTER_HORIZONTAL: 67 childLeft = paddingLeft + ((childSpace - childWidth) / 2) 68 + lp.leftMargin - lp.rightMargin; 69 break; 70 71 case Gravity.RIGHT: 72 childLeft = childRight - childWidth - lp.rightMargin; 73 break; 74 75 case Gravity.LEFT: 76 default: 77 childLeft = paddingLeft + lp.leftMargin; 78 break; 79 } 80 81 if (hasDividerBeforeChildAt(i)) { 82 childTop += mDividerHeight; 83 } 84 85 childTop += lp.topMargin; 86 setChildFrame(child, childLeft, childTop + getLocationOffset(child), //根据计算的长宽高绘制控件在屏幕的长宽高 87 childWidth, childHeight); 88 childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); 89 90 i += getChildrenSkipCount(child, i); 91 } 92 } 93 }
1.3 draw

【requestLayout】布局发生变化(方向变化、尺寸变化),就会调用requestLayout;
自定义视图会经常调用此方法,某些情况希望可以重新测量大小,手动调用该方法;
调用结束该方法就会触发measure和layout方法,但是不会调用draw方法;
invalidate()和postInvalidate()的使用与区别 Android提供了Invalidate方法实现界面刷新,但是Invalidate不能直接在线程中调用,因为他是违背了单线程模型: Android UI操作并不是线程安全的,并且这些操作必须在UI线程中调用。 invalidate()是用来刷新View的,必须是在UI线程中进行工作。比如在修改某个view的显示时,调用invalidate()才能 看到重新绘制的界面。invalidate()的调用是把之前的旧的view从主UI线程队列中pop掉。 invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通 知UI线程进行界面更新。而postInvalidate()在工作者线程中被调用。 一、利用invalidate()刷新界面 实例化一个Handler对象,并重写handleMessage方法调用invalidate()实现界面刷新;而在线程中通过sendMessage发送界面更新消息。 复制代码 1 // 在onCreate()中开启线程 2 3 new Thread(new GameThread()).start();、 4 5 // 实例化一个handler 6 7 Handler myHandler = new Handler() { 8 // 接收到消息后处理 9 public void handleMessage(Message msg) { 10 switch (msg.what) { 11 case Activity01.REFRESH: 12 mGameView.invalidate(); // 刷新界面 13 break; 14 } 15 16 super.handleMessage(msg); 17 } 18 }; 19 20 class GameThread implements Runnable { 21 public void run() { 22 while (!Thread.currentThread().isInterrupted()) { 23 Message message = new Message(); 24 message.what = Activity01.REFRESH; 25 // 发送消息 26 Activity01.this.myHandler.sendMessage(message); 27 try { 28 Thread.sleep(100); 29 } catch (InterruptedException e) { 30 Thread.currentThread().interrupt(); 31 } 32 } 33 } 34 } 二、使用postInvalidate()刷新界面 复制代码 1 使用postInvalidate则比较简单,不需要handler,直接在工作线程中调用postInvalidate即可。 2 3 class GameThread implements Runnable { 4 public void run() { 5 while (!Thread.currentThread().isInterrupted()) { 6 try { 7 Thread.sleep(100); 8 } catch (InterruptedException e) { 9 Thread.currentThread().interrupt(); 10 } 11 12 // 使用postInvalidate可以直接在工作线程中更新界面 13 mGameView.postInvalidate(); 14 } 15 } 16 }
1.4 总结:view树的绘制流程
![]()
当activity接收到用户的触摸焦点的时候,会被请求绘制布局,请求的处理是由framework层处理,处理方式是从根节点开始;
对布局进行测量和绘制,整个的绘制流程都是通过measure(是否需要重新计算测量)、layout(是否需要重新安置视图的位置)/draw(在测量和安置之后是否需要重新绘制)实现的;
2.事件分发机制

2.1 事件分发机制的认识



【activity】每一个 Activity 都持有一个 Window 对象
【PhotoWindow】Window的唯一实现类,是View的实际的管理容器;
【DecorView】一般PhoneWindow都是通过内部类decorView类进行消息的传递的;下面的view也是通过DecorView传递消息的;
标题栏,内容栏,顶级上看是加载在DecorView上的
Android 为 Window 提供了唯一的实现类 PhoneWindow。也就是说 Activity 中的 window 实例就是一个 PhoneWindow 对象。
但是 PhoneWindow 终究是 Window,它并不具备多少 View 相关的能力。
不过 PhoneWindow 中持有一个 Android 中非常重要的一个 View 对象 Decor(装饰)View,它在 PhoneWindow 中的定义如下:
public class PhoneWindow extends Window{
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
}
查看 DecorView 继承关系得知,DecorView 继承自 FrameLayout
public class DecorView extends FrameLayout {}
现在的关系就很明确了,每一个 Activity 持有一个 PhoneWindow 的对象,而一个 PhoneWindow 对象持有一个 DecorView 的实例,
所以 Activity 中 View 相关的操作其实大都是通过 DecorView 来完成
android DecorView深入理解
开发中,通常都是在onCreate()中调用setContentView(R.layout.custom_layout)来实现想要的页面布局。
页面都是依附在窗口之上的,而DecorView即是窗口最顶层的视图。
Android frameworks中,与窗口视图处理相关的类,主要是Window及其实现类PhoneWindow
public class PhoneWindow extends Window implements MenuBuilder.Callback { //... //窗口顶层View private DecorView mDecor; //所有自定义View的根View, id="@android:id/content" private ViewGroup mContentParent; //... } DecorView其实是PhoneWindow中的一个内部类,本质上也是一个View,其只是扩展了FrameLayout的实现 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker 添加至窗口流程 1 Activity中调用setContentView(R.layout.custom_layout), 具体实现为PhoneWindow中的同名方法 public void setContentView(int layoutResID) { //getWindow()获取的即是PhoneWindow对象 getWindow().setContentView(layoutResID); } 看一下window类 public abstract class Window { //... //指定Activity窗口的风格类型 public static final int FEATURE_NO_TITLE = 1; public static final int FEATURE_INDETERMINATE_PROGRESS = 5; //设置布局文件 public abstract void setContentView(int layoutResID); public abstract void setContentView(View view); //请求指定Activity窗口的风格类型 public boolean requestFeature(int featureId) { final int flag = 1<<featureId; mFeatures |= flag; mLocalFeatures |= mContainer != null ? (flag&~mContainer.mFeatures) : flag; return (mFeatures&flag) != 0; } //... } PhoneWindow执行setContentView(int layoutResource) PhoneWindow该类继承于Window类,是Window类的具体实现,即我们可以通过该类具体去绘制窗口。
并且,该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。
简而言之,PhoneWindow类是把一个FrameLayout类即DecorView对象进行一定的包装,将它作为应用窗口的根View,并提供一组通用的窗口操作接口。 public void setContentView(int layoutResID) { //初始,mContentParent为空 if (mContentParent == null) { installDecor(); } else { mContentParent.removeAllViews(); } //inflate自定义layout, 并将mContentParent作为其根视图 mLayoutInflater.inflate(layoutResID, mContentParent); //... } 该方法根据首先判断是否已经由setContentView()了获取mContentParent即View对象,
即是否是第一次调用该PhoneWindow对象setContentView()方法。
如果是第一次调用,则调用installDecor()方法,否则,移除该mContentParent内所有的所有子View。
最后将我们的资源文件通过LayoutInflater对象转换为View树,
并且添加至mContentParent视图中(在应用程序里,我们可以多次调用setContentView()来显示我们的界面)。 PhoneWindow.installDecor() private void installDecor() { if (mDecor == null) { //new一个DecorView mDecor = generateDecor(); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } if (mContentParent == null) { //这一步会设置窗口的修饰文件,并将id为ID_ANDROID_CONTENT的view find出来作为返回值赋值给mContentParent mContentParent = generateLayout(mDecor); //... } PhoneWindow.generateLayout(DecorView decor) protected ViewGroup generateLayout(DecorView decor) { //1,获取<Application android:theme=""/>, <Activity/>节点指定的themes或者代码requestWindowFeature()中指定的Features, 并设置 TypedArray a = getWindowStyle(); //... //2,获取窗口Features, 设置相应的修饰布局文件,这些xml文件位于frameworks/base/core/res/res/layout下 int layoutResource; int features = getLocalFeatures(); if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute(com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = com.android.internal.R.layout.screen_title_icons; } removeFeature(FEATURE_ACTION_BAR); } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) { layoutResource = com.android.internal.R.layout.screen_progress; //... mDecor.startChanging(); //3, 将上面选定的布局文件inflate为View树,添加到decorView中 View in = mLayoutInflater.inflate(layoutResource, null); decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //将窗口修饰布局文件中id="@android:id/content"的View赋值给mContentParent, 后续自定义的view/layout都将是其子View ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } //... } 该方法会做如下事情: 根据窗口的风格修饰类型为该窗口选择不同的窗口布局文件(根视图)。这些窗口修饰布局文件指定一个用来存放Activity自定义布局文件的ViewGroup视图,一般为FrameLayout 其id 为: android:id="@android:id/content"。 例如窗口修饰类型包括FullScreen(全屏)、NoTitleBar(不含标题栏)等。选定窗口修饰类型有两种: ①指定requestFeature()指定窗口修饰符,PhoneWindow对象调用getLocalFeature()方法获取值; ②为我们的Activity配置相应属性,即android:theme=“”,PhoneWindow对象调用getWindowStyle()方法获取值。 举例如下,隐藏标题栏有如下方法: requestWindowFeature(Window.FEATURE_NO_TITLE); 或者为Activity配置xml属性: android:theme="@android:style/Theme.NoTitleBar" 因此,在Activity中必须在setContentView之前调用requestFeature()方法。 确定好窗口风格之后,选定该风格对应的布局文件,这些布局文件位于 frameworks/base/core/res/layout/ , 典型的窗口布局文件有: R.layout.dialog_titile_icons R.layout.screen_title_icons R.layout.screen_progress R.layout.dialog_custom_title R.layout.dialog_title R.layout.screen_title // 最常用的Activity窗口修饰布局文件 R.layout.screen_simple //全屏的Activity窗口布局文件 最后页面中设置的自定义layout会被添加到mContentParent中 mLayoutInflater.inflate(layoutResID, mContentParent); 整个过程主要是如何把Activity的布局文件添加至窗口里,上面的过程可以概括为: 创建一个DecorView对象,该对象将作为整个应用窗口的根视图 创建不同的窗口修饰布局文件,并且获取Activity的布局文件该存放的地方,由该窗口修饰布局文件内id为content的FrameLayout指定 。 将Activity的布局文件添加至id为content的FrameLayout内。 最后,当AMS(ActivityManagerService)准备resume一个Activity时,
会回调该Activity的handleResumeActivity()方法,该方法会调用Activity的makeVisible方法 ,显示我们刚才创建的mDecor 视图族。 //系统resume一个Activity时,调用此方法 final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { ActivityRecord r = performResumeActivity(token, clearHide); //... if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } } void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); // 获取WindowManager对象 wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); //使其处于显示状况 } 布局层次结构 2
@Override protected void onCreate(Bundle savedInstanceState) { //设置窗口无标题栏 requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); setContentView(R.layout.activity_decor); } activity_decor.xml: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".DecorActivity"> <TextView android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true"/> </RelativeLayout> onCreate()中设置的Window.FEATURE_NO_TITLE对应的窗口修饰布局文件为screen_simple.xml, 源码如下 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical"> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout> 源码中id为"@android:id/content"的FrameLayout就是内容区域,
在整个流程中,其会赋值给PhoneWindow类中的属性mContentParent, 运行应用后,使用SDK提供的hierarchyviewer工具查看页面的ViewTree结构,可以看到结构如下:

2.2 事件分发的3个核心的方法

【说明】Activity和View之间没有事件的拦截;Activity是事件的原始分发者,view是事件传递的最末端,处理的方式:①消费掉事件;②将事件进行回传;

【onTouchEvent】功能:用来拦截事件,当父控件给子控件下发事件时,如果子控件需要对事件进行处理,子控件就会调用onTouchEvent对事件进行拦截;
然后就会到子控件中进行事件的拦截监听和对拦截事件的处理;
此方法是View当中的方法,传递的事件包括:按下屏幕、在屏幕上的移动、抬起屏幕、取消屏幕4个事件;
2.3 事件分发的流程
【说明】从上至下逐步分发,如果到最后一个view还没有处理,则将所有的事件返回回去到Activity,如果activity也没有处理则抛弃掉;

【说明】绿色的线是最底层的view返回的线;橙色的线是分发的判断线;此图是View将事件消费掉的示意图;

3.listView

3.1 listView
![]()
3.2 listView的适配器模式
【说明】
【1】listView只关心在指定的position上显示固定的数据;只关心将view准确无误的显示到固定的位置;listView和数据是分离的,不会直接接触;
listView只能通过adapter将数据加载到屏幕上;
【2】adapter是桥梁,负责将为每个数据制作view,然后交给listView显示;保证View和数据的分离;
【3】adapater是统一的接口,listView没有必要关心数据;但是子类必须继承adapter;

【说明】listView运作的机制:会首先调用getCount方式获取需要加载多少个item;然后调用getView方法依次获取不同的view,获取的次数是getCount返回的数值;

【文章扩展】https://blog.csdn.net/liuwan1992/article/details/52829952
3.3 RecycleBin
RecycleBin基本原理
下面先简要说一下RecycleBin中的工作原理,后面会结合源码详细说明。
在某一时刻,我们看到ListView中有许多View呈现在UI上,这些View对我们来说是可见的,这些可见的View可以称作OnScreen的View,即在屏幕中能看到的View,也可以叫做ActiveView,因为它们是在UI上可操作的。
当触摸ListView并向上滑动时,ListView上部的一些OnScreen的View位置上移,并移除了ListView的屏幕范围,此时这些OnScreen的View就变得不可见了,不可见的View叫做OffScreen的View,即这些View已经不在屏幕可见范围内了,
也可以叫做ScrapView,Scrap表示废弃的意思,ScrapView的意思是这些OffScreen的View不再处于可以交互的Active状态了。ListView会把那些ScrapView(即OffScreen的View)删除,这样就不用绘制这些本来就不可见的View了,
同时,ListView会把这些删除的ScrapView放入到RecycleBin中存起来,就像把暂时无用的资源放到回收站一样。
当ListView的底部需要显示新的View的时候,会从RecycleBin中取出一个ScrapView,将其作为convertView参数传递给Adapter的getView方法,从而达到View复用的目的,
这样就不必在Adapter的getView方法中执行LayoutInflater.inflate()方法了。
RecycleBin中有两个重要的View数组,分别是【mActiveViews】和【mScrapViews】。这两个数组中所存储的View都是用来复用的,只不过mActiveViews中存储的是OnScreen的View,
这些View很有可能被直接复用;而mScrapViews中存储的是OffScreen的View,这些View主要是用来间接复用的。








3.4 ListView优化

3.4.1 图片加载的优化
如果你的ListView中需要显示从网络上下载的图片的话,我们不要在ListView滑动的时候加载图片,
那样会使ListView变得卡顿,所以我们需要再监听器里面监听ListView的状态,
如果滑动的时候,停止加载图片,如果没有滑动,则开始加载图片
1 listView.setOnScrollListener(new OnScrollListener() { 2 3 @Override 4 public void onScrollStateChanged(AbsListView listView, int scrollState) { 5 //停止加载图片 6 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) { 7 imageLoader.stopProcessingQueue(); 8 } else { 9 //开始加载图片 10 imageLoader.startProcessingQueue(); 11 } 12 } 13 14 @Override 15 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 16 // TODO Auto-generated method stub 17 18 } 19 });
3.4.2 将ListView的scrollingCache和animateCache设置为false
scrollingCache本质上是drawing cache,你可以让一个View将他自己的drawing保存在cache中(保存为一个bitmap),
这样下次再显示View的时候就不用重画了,而是从cache中取出。默认情况下drawing cahce是禁用的,因为它太耗内存了,
但是它确实比重画来的更加平滑。
而在ListView中,scrollingCache是默认开启的,我们可以手动将它关闭。
animateCache: ListView默认开启了animateCache,这会消耗大量的内存,因此会频繁调用GC,我们可以手动将它关闭掉
1
优化前:
<ListView 2 android:id="@android:id/list" 3 android:layout_width="match_parent" 4 android:layout_height="wrap_content" 5 android:cacheColorHint="#00000000" 6 android:divider="@color/list_background_color" 7 android:dividerHeight="0dp" 8 android:listSelector="#00000000" 9 android:smoothScrollbar="true" 10 android:visibility="gone" /> 11 优化后: 12 <ListView 13 android:id="@android:id/list" 14 android:layout_width="match_parent" 15 android:layout_height="wrap_content" 16 android:divider="@color/list_background_color" 17 android:dividerHeight="0dp" 18 android:listSelector="#00000000" 19 android:scrollingCache="false" 20 android:animationCache="false" 21 android:smoothScrollbar="true" 22 android:visibility="gone" />
|
属性名称 |
描述 |
|
android:cacheColorHint |
指示该列表总是在固定的单色、不透明的背景下绘制。这允许列表优化其绘制过程 |
|
android:drawSelectorOnTop |
如果设为真,选择器将绘制在选中条目的上层。否则绘制在下层。默认为假 |
|
android:fastScrollEnabled |
允许使用快速滚动滑块,可以通过拖动该滑块在列表中快速滚动 |
|
android:listSelector |
用于在列表中指示当前选中条目的可绘制对象 |
|
android:scrollingCache |
当为真时,列表滚动使用绘图缓存。该选项使渲染更快,但占用更多的内存。 默认值为真 |
|
android:smoothScrollbar |
为真时,列表会使用更精确的基于条目在屏幕上的可见像素高度的计算方法。 默认该属性为真,如果你的适配器需要绘制可变高的条目,他应该设为假。 当该属性为真时,你在适配器在显示变高条目时,滚动条的把手会在滚动的 过程中改变大小。当设为假时,列表只使用适配器中的条目数和屏幕上的 可见条目来决定滚动条的属性 |
|
android:stackFromBottom |
用于 ListView 和 GridView,指示他们的内容栈从底部开始 |
|
android:textFilterEnabled |
设为真时,列表会过滤根据用户的要求,过滤结果集。列表的适配器必须实现了 Filterable 接口,才能使其可用 |
|
android:transcriptMode |
设置列表的跳转模式。在跳转模式下,当加入新条目时,列表会滚动到底部, 使新条目可见 |
3.4.3.减少item的布局的深度
我们应该尽量减少item布局深度,因为当滑动ListView的时候,这回直接导致测量与绘制,
因此会浪费大量的时间,所以我们应该将一些不必要的布局嵌套关系去掉。减少item布局深度
3.4.4.使用ViewHolder
这个大家应该非常熟悉了,但是不要小看这个ViewHolder,它可以大大提高我们ListView的性能
3.4.5 在adapter中的getView方法中尽量少使用逻辑
1 @Override 2 public View getView(int position, View convertView, ViewGroup paramViewGroup) { 3 Object current_event = mObjects.get(position); 4 ViewHolder holder = null; 5 if (convertView == null) { 6 holder = new ViewHolder(); 7 convertView = inflater.inflate(R.layout.row_event, null); 8 holder.ThreeDimension = (ImageView) convertView.findViewById(R.id.ThreeDim); 9 holder.EventPoster = (ImageView) convertView.findViewById(R.id.EventPoster); 10 convertView.setTag(holder); 11 12 } else { 13 holder = (ViewHolder) convertView.getTag(); 14 } 15 16 //在这里进行逻辑判断,这是有问题的 17 if (doesSomeComplexChecking()) { 18 holder.ThreeDimention.setVisibility(View.VISIBLE); 19 } else { 20 holder.ThreeDimention.setVisibility(View.GONE); 21 } 22 23 // 这是设置image的参数,每次getView方法执行时都会执行这段代码,这显然是有问题的 24 RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(measuredwidth, rowHeight); 25 holder.EventPoster.setLayoutParams(imageParams); 26 27 return convertView;
【优化后】
1 @Override 2 public View getView(int position, View convertView, ViewGroup paramViewGroup) { 3 Object object = mObjects.get(position); 4 ViewHolder holder = null; 5 6 if (convertView == null) { 7 holder = new ViewHolder(); 8 convertView = inflater.inflate(R.layout.row_event, null); 9 holder.ThreeDimension = (ImageView) convertView.findViewById(R.id.ThreeDim); 10 holder.EventPoster = (ImageView) convertView.findViewById(R.id.EventPoster); 11 //设置参数提到这里,只有第一次的时候会执行,之后会复用 12 RelativeLayout.LayoutParams imageParams = new RelativeLayout.LayoutParams(measuredwidth, rowHeight); 13 holder.EventPoster.setLayoutParams(imageParams); 14 convertView.setTag(holder); 15 } else { 16 holder = (ViewHolder) convertView.getTag(); 17 } 18 19 // 我们直接通过对象的getter方法代替刚才那些逻辑判断,那些逻辑判断放到别的地方去执行了 20 holder.ThreeDimension.setVisibility(object.getVisibility()); 21 22 return convertView;
【转载文章】链接:https://www.jianshu.com/p/b7741023bc6f
listview怎么优化?
关于Listview的优化,只要面试过的人,我相信都对这个题很熟悉,不管有没有人问过你这个题,我想你自己也一定准备过,否则,嘿嘿!!!!!而且网上也一搜一大把这里就简单提几个主要的:
1、复用convertView,对convetView进行判空,当convertView不为空时重复使用,为空则初始化,从而减少了很多不必要的View的创建、减少findViewById的次数,
2、避免在getView方法中做耗时操作
3、采用ViewHolder模式缓存item条目的引用
4、给listView设置滚动监听器 根据不同状态 不同处理数据 分批分页加载 根据listView的状态去操作,比如当列表快速滑动时不去开启大量的异步任务去请求图片
5、listview每个item层级结构不要太复杂
6、listview每个item中异步加载图片,并对图片加载做优化,(关于Listview分页加载和图片异步加载思路请看接下来的文章内容)
7、listview每个item中不要创建线程
8、尽量能保证 Adapter 的 hasStableIds() 返回 true 这样在 notifyDataSetChanged() 的时候,如果item内容并没有变化,ListView 将不会重新绘制这个 View,达到优化的目的
9、在一些场景中,ScollView内会包含多个ListView,可以把listview的高度写死固定下来。 由于ScollView在快速滑动过程中需要大量计算每一个listview的高度,阻塞了UI线程导致卡顿现象出现,如果我们每一个item的高度都是均匀的,可以通过计算把listview的高度确定下来,避免卡顿现象出现
10、使用 RecycleView 代替listview: 每个item内容的变动,listview都需要去调用notifyDataSetChanged来更新全部的item,太浪费性能了。RecycleView可以实现当个item的局部刷新,并且引入了增加和删除的动态效果,在性能上和定制上都有很大的改善
11、ListView 中元素避免半透明: 半透明绘制需要大量乘法计算,在滑动时不停重绘会造成大量的计算,在比较差的机子上会比较卡。 在设计上能不半透明就不不半透明。实在要弄就把在滑动的时候把半透明设置成不透明,滑动完再重新设置成半透明。
下面就是关于Listview的一些相关拓展
1. 打开套有 ListVew的 ScrollView的页面布局 默认 起始位置不是最顶部?
解决办法有两种:
方法一:把套在里面的ListVew 不让获取焦点即可。listview.setFocusable(false);注意:在xml布局里面设置android:focusable=“false”不生效
方法二:myScrollView.smoothScrollTo(0,0);
2. 上拉加载和下拉刷新怎么实现?
实现OnScrollListener 接口重写onScrollStateChanged 和onScroll方法,
使用onscroll方法实现”滑动“后处理检查是否还有新的记录,如果有,调用 addFooterView,添加记录到adapter, adapter调notifyDataSetChanged 更新数据;
如果没有记录了,把自定义的mFooterView去掉。
使用onScrollStateChanged可以检测是否滚到最后一行且停止滚动然后执行加载
3. listview失去焦点怎么处理?
在listview子布局里面写,可以解决焦点失去的问题
android:descendantFocusability="blocksDescendants"
4. ListView图片异步加载实现思路?
1.先从内存缓存中获取图片显示(内存缓冲)
2.获取不到的话从SD卡里获取(SD卡缓冲,,从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅)
3.都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示(视情况看是否要显示)
5. 你知道Listview里有Button就点不动了你知道吗?
原因是button强制获取了item的焦点,只要设置button的focusable为false即可。
6. 如何自定义一个Adapter(一般)
继承自BaseAdapter实现里面的方法,listView在开始绘制的时候,系统首先调用getCount()函数,根据他的返回值得到listView的长度,然后根据这个长度,调用getView()逐一绘制每一行。如果你的getCount()返回值是0的话,列表将不显示同样return 1,就只显示一行。系统显示列表时,首先实例化一个适配器(这里将实例化自定义的适配器)。当手动完成适配时,必 须手动映射数据,这需要重写getView()方法。系统在绘制列表的每一行的时候将调用此方法。getView()有三个参数,position表示将显示的是第几行,covertView是从布局文件中inflate来的 布局。我们用LayoutInflater的方法将定义好的main.xml文件提取成View实例用来显示。
然后 将xml文件中的各个组件实例化(简单的findViewById()方法)。这样便可以将数据对应到各个组件上了。但是按钮为了响应点击事件,需要为它添加点击监听器,这样就能捕获点击事件。至此一个自定 义的listView就完成了,现在让我们回过头从新审视这个过程。系统要绘制ListView了,
他首先获得 要绘制的这个列表的长度,然后开始绘制第一行,怎么绘制呢?
调用getView()函数。在这个函数里面 首先获得一个View(实际上是一个ViewGroup),然后再实例并设置各个组件,显示之。好了,绘制完这一行了。那 再绘制下一行,直到绘完为止。在实际的运行过程中会发现listView的每一行没有焦点了,这是因为Button抢夺了listView的焦点,只要布局文件中将Button设置为没有焦点就OK了。
7. listview分页加载的步骤?
通常实现分页加载有两种方式,一种是在ListView底部设置一个按钮,用户点击即加载。另一种是当用户滑动到底部时自动加载。
在ListView底部设置一个按钮,用户点击即加载实现思路:
// 加上底部View,注意要放在setAdapter方法前
ListView.addFooterView(moreView);
bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
pg.setVisibility(View.VISIBLE);// 将进度条可见
bt.setVisibility(View.GONE);// 按钮不可见
handler.postDelayed(new Runnable() {
@Override
public void run() {
loadMoreDate();// 加载更多数据
bt.setVisibility(View.VISIBLE);
pg.setVisibility(View.GONE);
mSimpleAdapter.notifyDataSetChanged();// 通知listView刷新数据
}
}, 2000);
}
});
当用户滑动到底部时自动加载实现思路:
实现OnScrollListener 接口重写onScrollStateChanged 和onScroll方法,使用onscroll方法实现”滑动“后处理检查是否还有新的记录,
如果有,添加记录到adapter, adapter调用 notifyDataSetChanged 更新数据;如果没有记录了,则不再加载数据。使用onScrollStateChanged可以检测是否滚到最后一行且停止滚动然后执行加载.
8. ViewHolder内部类非得要声明成static的呢?
这不是Android的优化,而是Java提倡的优化,
如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。
因为非静态成员类的实例会包含一个额外的指向外围对象的引用,保存这份引用要消耗时间和空间,并且导致外围类实例符合垃圾回收时仍然被保留。
如果没有外围实例的情况下,也需要分配实例,就不能使用非静态成员类,因为非静态成员类的实例必须要有一个外围实例。
9. Listview每个item有特效进入视图
10. ScrollView、ListView剖析 - 上下拉伸回弹阻尼效果
11. 自定义控件-下拉刷新和上拉加载的listView
5.listVIew优化
【参考文章】https://www.jianshu.com/p/f0408a0f0610
示例是这样的:
由http://www.imooc.com/api/teacher?type=4&num=30 中加载json格式的数据,解析出来,然后用ListView显示出来。下面是完成后的图片(没有找到有效设置图片大小的方法,大家有知道的话,留言给我)
先看我的示例listView的item的布局:


1.对于这样一个需求,我们首先应该是解析得到的数据,然后将得到的数据提供给ListView的Adapter,有关于这部分的代码,写在了MainActivity.java,我放到文章后面,毕竟重点不在这。
2.当获得到数据之后,接下来就是编写Adapter了。
-
使用ViewHolder模式来提高效率
Viewholder模式充分了ListView的视图缓存机制,避免了每次在调用getView的时候都去通过findViewById实例化数据。《Android群英传》中说:据测试,使用ViewHolder将提高50%的效率。对应代码中就是这样,先在NewsAdapter(本示例中的Adapter)新建一个内部类:(详细代码见文章后的NewsAdapter.java)
//使用ViewHolder
private static class ViewHolder {
private TextView tvTitle, tvContent;
private ImageView ivIcon;
}
在重写的getView方法中这样写:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
//判断是否有缓存
if (convertView == null) {
//通过LayoutInflate实例化布局
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_layout, parent, false);
viewHolder.ivIcon = (ImageView) convertView.findViewById(R.id.iv_icon);
viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title);
viewHolder.tvContent = (TextView) convertView.findViewById(R.id.tv_content);
convertView.setTag(viewHolder);
} else {
//通过tag找到缓存的布局
viewHolder = (ViewHolder) convertView.getTag();
}
NewsBean newsBean = newsBeanList.get(position);
String urlString = newsBean.newsIconUrl;
viewHolder.ivIcon.setTag(urlString); // 将ImageView与url绑定
//普通异步加载
// mImageLoader.showImageByThread(viewHolder.ivIcon,urlString);
mImageLoader.showImageByAsyncTask(viewHolder.ivIcon,urlString);
viewHolder.tvTitle.setText(newsBean.newsTitle);
viewHolder.tvContent.setText(newsBean.newsContent);
return convertView;
}
-
异步加载:耗时的操作放在异步线程中
如果在adapter中的某些操作需要耗费大量的时间,这个时候就要用到异步线程来进行异步就在数据。比如:现在要加载图片,此时我们需要根据url访问网络得到数据,然后将数据解析为Bitmap设置给View。这些操作如果不进行异步处理而直接放入adapter,可想而知,我们的ListView会有多卡。
这里向大家提供两种异步加载线程的方式:(代码查看下面的ImageLoader.java)
向网络获得数据,异步中需要进行的操作:
public Bitmap getBitmapFormURL(String urlString) {
Bitmap bitmap;
InputStream inputStream = null;
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
inputStream = new BufferedInputStream(conn.getInputStream()); //得到图片的数据流
bitmap = BitmapFactory.decodeStream(inputStream); //根据数据流来解析出图片的bitmap
conn.disconnect();
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
一种是通过多线程方式通过Handler+Message进行异步加载
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//这个getTag是下面的问题要讲的优化,大家可以先看看
if (mImageView.getTag().equals(mUrl)) { //当url标记和原先设置的一样时,才设置ImageView
mImageView.setImageBitmap((Bitmap) msg.obj);
}
}
};
public void showImageByThread(ImageView imageView, final String url) {
this.mImageView = imageView;
this.mUrl = url;
new Thread() {
@Override
public void run() {
super.run();
Bitmap bitmap = getBitmapFormURL(url);
Message message = Message.obtain();
message.obj = bitmap;
handler.sendMessage(message);
}
}.start();
}
另一种是通过AsyncTask来进行异步操作
AsyncTask的使用方法,不熟悉的可以学习下,这里不讲了,毕竟不是重点。
public void showImageByAsyncTask(ImageView imageView,String url){
new NewsAsyncTask(imageView,url).execute(url);
}
构建的AsyncTask的内部类
class NewsAsyncTask extends AsyncTask<String,Void,Bitmap>{
private ImageView myImageView;
private String mUrl;
public NewsAsyncTask(ImageView imageView,String url){
myImageView = imageView;
mUrl = url;
}
//String...params是可变参数接受execute中传过来的参数
@Override
protected Bitmap doInBackground(String... params) {
String url=params[0];
//这里同样调用我们的getBitmapFromeUrl
Bitmap bitmap = getBitmapFromUrl(params[0]);
return bitmap;
}
//这里的bitmap是从doInBackgroud中方法中返回过来的
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
imageView.setImageBitmap(bitmap);
}
}
listView错位加载问题
当上面的代码写完时,如果先不用让大家先看看的mImageView.getTag().equals(mUrl)判断一下(上面的代码并不完整,下面会讲),你会发现如下图的情况,在Listview中的各个item加载的过程中,出现了item错位的情况。一张图片在多个位置显示,过了一段时间才正常。

对于上面的情况,我们先来看看产生这种情况的原因(下面内容主要引用自android listview 异步加载图片并防止错位):

网上找了一张图, listview 异步加载图片之所以错位的根本原因是重用了 convertView 且有异步操作.
如果不重用 convertView 不会出现错位现象, 重用 convertView 但没有异步操作也不会有问题。
我简单分析一下:
当重用 convertView 时,最初一屏显示 7 条记录, getView 被调用 7 次,创建了 7 个 convertView.
当 Item1 划出屏幕, Item8 进入屏幕时,这时没有为 Item8 创建新的 view 实例, Item8 复用的是
Item1 的 view 如果没有异步不会有任何问题,虽然 Item8 和 Item1 指向的是同一个 view,但滑到
Item8 时刷上了 Item8 的数据,这时 Item1 的数据和 Item8 是一样的,因为它们指向的是同一块内存,
但 Item1 已滚出了屏幕你看不见。
当 Item1 再次可见时这块 view 又涮上了 Item1 的数据。
但当有异步下载时就有问题了,假设 Item1 的图片下载的比较慢,Item8 的图片下载的比较快,你滚上去
使 Item8 可见,这时 Item8 先显示它自己下载的图片没错,但等到 Item1 的图片也下载完时你发现
Item8 的图片也变成了 Item1 的图片,因为它们复用的是同一个 view。
如果 Item1 的图片下载的比
Item8 的图片快, Item1 先刷上自己下载的图片,这时你滑下去,Item8 的图片还没下载完, Item8
会先显示 Item1 的图片,因为它们是同一快内存,当 Item8 自己的图片下载完后 Item8 的图片又刷成
了自己的,你再滑上去使 Item1 可见, Item1 的图片也会和 Item8 的图片是一样的,
因为它们指向的是同一块内存。
解决的办法由很多种,这里说一下最简单的一种:使用findViewWithTag
在NewsAdapter中为ImageView设置Tag标识位:
//将url设为imagView的标识位
String urlString = newsBean.newsIconUrl;
viewHolder.ivIcon.setTag(urlString); // 将ImageView与url绑定
然后再加载的过程中通过url来判断对应imagview位置是否一致来决定是否加载。
if (mImageView.getTag().equals(mUrl)) {
//当url标记和原先设置的一样时,才设置ImageView
mImageView.setImageBitmap((Bitmap) msg.obj);
}
为图片设置缓存
设置缓存的重要性不言而喻,在加载数据的过程中,你不可能每次都从网络上加载(除非数据量小)。如果即之一这样做就要面对两个问题:1.加载数据浪费的时间 2.加载数据消耗的流量 。信不信用户一言不合就把你这破app给卸载了。。。。。。
设置缓存的原理这里也不再说了,要说的话估计又成一片博文了。这里介绍用法,让你看看怎么写:(这部分代码,在文章末尾的ImageLoader.java中)
1.首先声明一个LrcCache
private LruCache<String,Bitmap> mMemoryCaches;
2.然后初始化
//下面是建立缓存
int maxMemory = (int) Runtime.getRuntime().maxMemory(); //运行时最大内存
int cacheSize = maxMemory/4; //缓存的大小
mCaches = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
3.添加方法,在适当的地方调用
//将bitmap添加到缓存
public void addBitmapToCache(String url,Bitmap bitmap){
if (getBitmapFormCache(url) == null){
mCaches.put(url, bitmap);
}
}
//从缓存中获取数据
public Bitmap getBitmapFormCache(String url){
return mCaches.get(url);
}
ListView的滑动时停止加载和分页加载
先说分页加载,我们不用每次把ListView所有的Item都一次性加载完毕,这样做没必要也很累。我们仅仅需要加载那部分显示在屏幕部分的Item即可,这样所要加载的数据就减少了很多。另一方面,我们需要考虑另一个问题,当用户滑动时,显示在屏幕的Item会不断的变化,如果只是加载显示在屏幕的Item,这也没有必要,因此我们应该在停止滑动时再加载数据。这样说,understand?
实现步骤:
1.让NewsAdapter实现接口AbsListView.OnScrollListener,并复写里面的方法
2.在复写的方法里面进行一些操作。
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case SCROLL_STATE_IDLE: //滑动停止时。
mImageLoader.loadImages(mStart, mEnd);
break;
case SCROLL_STATE_TOUCH_SCROLL: //正在滑动时
mImageLoader.cancelAllTasks();
break;
case SCROLL_STATE_FLING: //手指抛动时,即手指用力滑动在离开后ListView由于惯性而继续滑动
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mStart = firstVisibleItem;
mEnd = firstVisibleItem + visibleItemCount;
//第一次的时候预加载
if (mFirstIn && visibleItemCount > 0){
mImageLoader.loadImages(mStart, mEnd);
mFirstIn = false;
}
}
可以看到在滑动停止时我们调用了ImageLoader类的loadIamge方法,这是一个加载图片的方法,用于加载Item对应的第mSart项t到第mEnd项之间的数据(这两个数据怎么来的,你先把这个问题放在心里),看下loadImage法的代码实现:
public void loadImages(int start, int end){
for (int i = start; i < end; i++){
String url = NewsAdapter.URLS[i];
//由缓存中得到bitmap
Bitmap bitmap = getBitmapFormCache(url);
if (bitmap == null){
//当bitmap为空时,由AsyncTask进行加载,并在onPostExecute()方法中setImageBitmap
NewsAsyncTask task = new NewsAsyncTask(url);
task.execute(url);
mAsyncTask.add(task);
} else {
//当bitmap不为空时,直接进行setImageBitmap
ImageView imageView = (ImageView) mListView.findViewWithTag(url);
imageView.setImageBitmap(bitmap);
}
}
}
由代码中可以看到我们由NewsAdatper.URLS[]中取出了start到end的url,然后加载了这些数据,我们看下这个数组,这是在NewsAdapter的构造器中,可以明显看到里面存放了所有Item的图片的url:
//将图片的url存储在数组中
URLS = new String[data.size()];
for (int i = 0; i < data.size(); i++) {
URLS[i] = data.get(i).newsIconUrl;
}
此时再来看NewsAsyncTask的代码:
//参数1:启动任务输入的参数,参数2:后台任务执行的百分比,参数3,后台执行任务的返回方法
private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
private String mUrl;
public NewsAsyncTask(String stringUrl) {
mUrl = stringUrl;
}
//doInBackground方法的参数是上面输入的第一个参数,返回的对象会传递给onPostExecute方法
@Override
protected Bitmap doInBackground(String... params) {
String url = params[0];
Bitmap bitmap = getBitmapFormURL(url);
if (bitmap != null){
addBitmapToCache(url,bitmap); //将bitmap添加到缓存
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//根据url从listView中找到对应的ImageView
ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
if (imageView != null && bitmap != null){
imageView.setImageBitmap(bitmap); //为imageView设置图片
}
mAsyncTask.remove(this);
}
}
可以看到这就是根据url为所对应ImageView设置图片。看到这就只有一个问题了,那就是mStart和mEnd是怎么得到的,再看代码我们重写的onScroll方法:
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mStart = firstVisibleItem;
mEnd = firstVisibleItem + visibleItemCount;
//第一次的时候预加载
if (mFirstIn && visibleItemCount > 0){
mImageLoader.loadImages(mStart, mEnd);
mFirstIn = false;
}
}
这个方法有三个参数,对应的为
firstVisibleItem:ListView所有当前可见的Item第一个Item
visibleItemCount:可见Item的总数
totalItemCount :所有Item的总数
这样一来大家是不是都懂了。
code
MainActivity.java
public class MainActivity extends AppCompatActivity {
private ListView listView;
private static final String url = "http://www.imooc.com/api/teacher?type=4&num=30";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.lv);
new MainAsyncTask().execute(url);
}
public List<NewsBean> getJsonData(String url){
List<NewsBean> newsBeanList = new ArrayList<>();
try {
String jsonString = readStream(new URL(url).openStream());//利用readStream得到String数据
Log.e("JSON",jsonString); //打印出string数据
//下面解析得到的json数据
JSONObject jsonObject;
NewsBean newsBean;
try {
jsonObject = new JSONObject(jsonString);
JSONArray jsonArray = jsonObject.getJSONArray("data");
for (int i=0; i<jsonArray.length(); i++){
jsonObject = jsonArray.getJSONObject(i);
newsBean = new NewsBean();
newsBean.newsIconUrl = jsonObject.getString("picSmall");
newsBean.newsTitle = jsonObject.getString("name");
newsBean.newsContent = jsonObject.getString("description");
newsBeanList.add(newsBean);
}
} catch (JSONException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
return newsBeanList;
}
//由输入流中读取数据并将数据返回
public String readStream(InputStream in){
InputStreamReader reader;
String result = "";
String line = "";
try {
reader = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(reader);
while((line = br.readLine())!=null){
result += line;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
//异步线程类
class MainAsyncTask extends AsyncTask<String, Void, List<NewsBean>>{
//该方法运行在后台线程中
@Override
protected List<NewsBean> doInBackground(String... params) {
return getJsonData(params[0]);
}
@Override
protected void onPostExecute(List<NewsBean> newsBeanList) {
super.onPostExecute(newsBeanList);
NewsAdapter newsAdapter = new NewsAdapter(MainActivity.this, newsBeanList, listView);
listView.setAdapter(newsAdapter);
}
}
}
NewsAdapter.java
public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener {
private List<NewsBean> newsBeanList = new ArrayList<>();
private LayoutInflater mInflater;
private ImageLoader mImageLoader;
private int mStart, mEnd;
public static String[] URLS;
private boolean mFirstIn;
public NewsAdapter(Context context, List<NewsBean> data, ListView listView) {
newsBeanList = data;
mInflater = LayoutInflater.from(context);
mImageLoader = new ImageLoader(listView);
//将图片的url存储在数组中
URLS = new String[data.size()];
for (int i = 0; i < data.size(); i++) {
URLS[i] = data.get(i).newsIconUrl;
}
listView.setOnScrollListener(this);
mFirstIn = true;
}
@Override
public int getCount() {
return newsBeanList.size();
}
@Override
public Object getItem(int position) {
return newsBeanList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
//判断是否有缓存
if (convertView == null) {
//通过LayoutInflate实例化布局
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_layout, parent, false);
viewHolder.ivIcon = (ImageView) convertView.findViewById(R.id.iv_icon);
viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_title);
viewHolder.tvContent = (TextView) convertView.findViewById(R.id.tv_content);
convertView.setTag(viewHolder);
} else {
//通过tag找到缓存的布局
viewHolder = (ViewHolder) convertView.getTag();
}
NewsBean newsBean = newsBeanList.get(position);
String urlString = newsBean.newsIconUrl;
viewHolder.ivIcon.setTag(urlString); // 将ImageView与url绑定
//普通异步加载
// mImageLoader.showImageByThread(viewHolder.ivIcon,urlString);
mImageLoader.showImageByAsyncTask(viewHolder.ivIcon,urlString);
viewHolder.tvTitle.setText(newsBean.newsTitle);
viewHolder.tvContent.setText(newsBean.newsContent);
return convertView;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case SCROLL_STATE_IDLE: //滑动停止时。
mImageLoader.loadImages(mStart, mEnd);
break;
case SCROLL_STATE_TOUCH_SCROLL: //正在滑动时
mImageLoader.cancelAllTasks();
break;
case SCROLL_STATE_FLING: //手指抛动时,即手指用力滑动在离开后ListView由于惯性而继续滑动
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mStart = firstVisibleItem;
mEnd = firstVisibleItem + visibleItemCount;
//第一次的时候预加载
if (mFirstIn && visibleItemCount > 0){
mImageLoader.loadImages(mStart, mEnd);
mFirstIn = false;
}
}
//使用ViewHolder
private static class ViewHolder {
private TextView tvTitle, tvContent;
private ImageView ivIcon;
}
}
ImageLoder.java
public class ImageLoader {
private ImageView mImageView;
private String mUrl;
private LruCache<String, Bitmap> mCaches;
private ListView mListView;
private Set<NewsAsyncTask> mAsyncTask;
public ImageLoader(ListView listView){
mListView = listView;
mAsyncTask = new HashSet<>();
//下面是建立缓存
int maxMemory = (int) Runtime.getRuntime().maxMemory(); //运行时最大内存
int cacheSize = maxMemory/4;
mCaches = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
//将bitmap添加到缓存
public void addBitmapToCache(String url,Bitmap bitmap){
if (getBitmapFormCache(url) == null){
mCaches.put(url, bitmap);
}
}
//从缓存中获取数据
public Bitmap getBitmapFormCache(String url){
return mCaches.get(url);
}
//===================================下面为普通异步加载===========================================
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (mImageView.getTag().equals(mUrl)) { //当url标记和原先设置的一样时,才设置ImageView
mImageView.setImageBitmap((Bitmap) msg.obj);
}
}
};
public void showImageByThread(ImageView imageView, final String url) {
this.mImageView = imageView;
this.mUrl = url;
new Thread() {
@Override
public void run() {
super.run();
Bitmap bitmap = getBitmapFormURL(url);
Message message = Message.obtain();
message.obj = bitmap;
handler.sendMessage(message);
}
}.start();
}
//====================上面是使用普通的异步加载,下面是使用AsyncTask进行的异步加载==================
public Bitmap getBitmapFormURL(String urlString) {
Bitmap bitmap;
InputStream inputStream = null;
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
inputStream = new BufferedInputStream(conn.getInputStream()); //得到图片的数据流
bitmap = BitmapFactory.decodeStream(inputStream); //根据数据流来解析出图片的bitmap
conn.disconnect();
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
//加载图片
public void showImageByAsyncTask(ImageView ImageView, String url) {
Bitmap bitmap = getBitmapFormCache(url);
if (bitmap == null){
ImageView.setImageResource(R.mipmap.ic_launcher);
}else{
ImageView.setImageBitmap(bitmap);
}
}
public void cancelAllTasks(){
if (mAsyncTask != null){
for (NewsAsyncTask task : mAsyncTask){
task.cancel(false);
}
}
}
public void loadImages(int start, int end){
for (int i = start; i < end; i++){
String url = NewsAdapter.URLS[i];
//由缓存中得到bitmap
Bitmap bitmap = getBitmapFormCache(url);
if (bitmap == null){
//当bitmap为空时,由AsyncTask进行加载,并在onPostExecute()方法中setImageBitmap
NewsAsyncTask task = new NewsAsyncTask(url);
task.execute(url);
mAsyncTask.add(task);
} else {
//当bitmap不为空时,直接进行setImageBitmap
ImageView imageView = (ImageView) mListView.findViewWithTag(url);
imageView.setImageBitmap(bitmap);
}
}
}
//参数1:启动任务输入的参数,参数2:后台任务执行的百分比,参数3,后台执行任务的返回方法
private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
private String mUrl;
public NewsAsyncTask(String stringUrl) {
mUrl = stringUrl;
}
//doInBackground方法的参数是上面输入的第一个参数,返回的对象会传递给onPostExecute方法
@Override
protected Bitmap doInBackground(String... params) {
String url = params[0];
Bitmap bitmap = getBitmapFormURL(url);
if (bitmap != null){
addBitmapToCache(url,bitmap); //将bitmap添加到缓存
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//根据url从listView中找到对应的ImageView
ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
if (imageView != null && bitmap != null){
imageView.setImageBitmap(bitmap);
}
mAsyncTask.remove(this);
}
}
}

浙公网安备 33010602011771号