做了快一年的Android开发,近期想总结一下这一年工作感受,分享一点我工作中遇到的BUG,然后分析并解决问题的思路吧,我尽量把过程写得详细些,这个系列共三篇文章。如有写的不对的地方,欢迎各位开发者指正,谢谢。
一“挖坑分页请求加载”
问题描述“手指滑动ListView,到Item最后一项请求第二页数据,注意,然后要快速滑动ListView,如果处理不善,就会出现数据重复填充显示的问题”。各位请注意是“快速”这两个字,为啥?因为快速的话就会造成网络请求时,滑动操作还在继续,此时本地的PageIndex还没有更新,请求的Page还是上一页的。如果不在滑动出添加限制,很容易会造成接口重复请求的问题的,进而导致数据重复。这里思路有两个,一个是直接限制请求次数,另一个是通过一个boolean值控制接口请求。这两个思路第一个是错误的,第二个是正确的。既然这里是分享经验,所以我就要把错误的思路和正确的思路都分析一下。
思路一:是限制接口请求次数,这里我先贴一段代码
private void setOnScrollListener() {
try {
listview.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
try {
if (view.getLastVisiblePosition() == (view.getCount() - 1)) {
if (pageSize > 1 && pageIndex < pageSize) {
if (countRequestmore < pageSize - 1) {
loadMore();
countRequestmore++;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
} catch (Exception e) {
这里我简单对上述代码描述一下,既然是滑动加载,那么肯定是要有对ListView的滑动监听,所以肯定是要调用setOnScrollListener()方法了,而view.getLastVisiblePosition() == (view.getCount() - 1)是要保证当前ListView滑动到底部且是Item的最后一个。红字的部分是限制接口请求次数的,假设当前数据有3页,那么loadmore()方法调用的次数必须小于等于总页数减1。这样看来就可以限制重复反问接口了?
恩,肯定可以限制住访问接口的次数。不过运行的现象倒是很有意思。如果要是当前网速不错,而且我们慢慢的滑动的话,结果运行正常,看日志出参数也都正常。但是!一旦我快速滑动,结果就不正常,会发现显示的内容有重复,就是第三页的内容和第二页的内容完全一样,为啥?为什么慢滑可以,快滑不可以?遇到问题打断点调试就能看到问题,经过一番排查,发现在滑动到第三页时,PageIndex的值居然还是2!这个肯定不对啊,为什么PageIndex的值没有更新,我们的PageIndex是在请求接口成功后重新复制更新,那从现在的运行结果来看,在滑动请求第三页时,第二页的网络请求还没有执行成功,进而导致值没有改变。在进一步理解,我们的网络访问是异步请求,滑动第二页时,开始请求网络,此时网络没有访问成功,但是view.getLastVisiblePosition() == (view.getCount() - 1)的值依然是true,可以继续调用loadmore()方法,此时PageIndex的值还是2,所以第三次请求还是第二页数据,永远也无法访问第三页数据。
填坑。明白原因之后 ,那问题就好解决了,其实就是请求没有同步。为此我们可以加一个阀门开关,当运行loadmore()时禁止滑动,当网络访问成功后将其滑动解禁。这里我将代码贴出来
private boolean isLoadMore;
public void onFailure(...){
if (isLoadMore) {
isLoadMore = false;
}
}
public void onSuccess(...){
isLoadMore = false;
}
if (!isLoadMore) {
loadMore();
isLoadMore = true;
}
通过isLoadMore来控制,这样的话无论网络如何都可以做到两者同步,完美填坑!稍微拓展一下,上面的滑动监听仅适用于ListView,对于功能强大的RecyclerView就不行了,为此我把RecyclerView的滑动代码也贴出来:
recycler_view.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
// 当不滚动时
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
//获取最后一个完全显示的ItemPosition
int lastVisibleItem = manager.findLastCompletelyVisibleItemPosition();
int totalItemCount = manager.getItemCount();
try {
// 判断是否滚动到底
if ( lastVisibleItem == (totalItemCount - 1)) {
。。。。。。。
二、“挖坑ListView嵌套使用与ListView高度动态计算 ”说到挖这个坑,填坑的过程那可真是一把辛酸泪啊,还好有大神助我填坑,现在回想起来,还真是受益匪浅。
先谈谈要显示的效果吧,界面里面有几处要显示多张图片,且图片数量不固定,因此可以使用ListView或RecyclerView来实现(这里我选择用ListView),而且这个界面里要求要放三个ListView去显示不同类型的图片。好了,既然一个界面里要放这么多ListView,那界面的高度肯定不固定了,最外层也要跟着滑动才可以,因此最外层可以选择使用ScrollView、ListView、RecyclerView。看似选择挺多,其实无论选择哪一个处理起来都挺麻烦,因为都要处理滑动冲突与高度计算的问题。网上有关于这三个讲解分析,我这就不想罗嗦这么多,只选择一个ListView作为外层进行讲解吧。先给大家看一下最后成功的效果图吧。
坑挖好了,现在开始填坑!,这里我就以部分代码来聊聊实现思路。
首先,界面最上面有几个标题,比如日期、姓名等,这些可以整体封装到head.xml中,然后直接插入到ListView中就可以了,例如
headerView = (LinearLayout) View.inflate(getApplication(),R.layout.head,null)
......
listview.addHeaderView(headerView);
注意!这里我做了一个处理, head.xml里最外层布局是一个LinearLayout,一方面界面的内容整体展现效果看起来就是一个垂直的,用LinearLayout操作非常方便,另一点就格外格外的重要,就是子ListView的每个Item必须是LinearLayout,不能是其他的,因为其他的Layout(如RelativeLayout)没有重写onMeasure(),所以会在onMeasure()时抛出异常。这里我在head里面就放放了两个ListView,然后插入父ListView的头部,head里面的两个ListView相对于最外层的父ListView就是Item,所以我在head.xml中最外层以LinearLayout作为主布局。
布局问题解决好了,接下来就要开始解决多个ListView嵌套冲突的问题了,先说说现象吧,如果不做处理,子ListView里的数据肯定显示不全,且滑动卡顿,子Item无法滑动。至于原因,因为默认情况下Android是禁止在ScrollView中放入另外的ScrollView的,它的高度是无法计算的。(So,原来是android系统挖的坑啊,然后作为开发者的我们就前赴后继的往里跳,跳多了,坑也就填平了,哈哈)既然如此,为避免冲突,我们可以将作为Item的两个ListView设置成不可滑动不就没有滑动事件冲突了吧。So,我们就自定义一个不能滑动的NoScrollListView。实现也不麻烦,网上也有源码。
public class NoScrollListView extends ListView{
public NoScrollListView(Context context) {
super(context);
}
public NoScrollListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NoScrollListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public NoScrollListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
用NoScrollListView去替换作为Item的两个子 ListView,这样就解决了滑动冲突的问题,方便吧。
接下来要解决另一个坑了,就是由于ListView高度计算有问题导致数据展示不全。既然是ListView自己计算有问题,那我们就帮他计算吧。
public static void measureNoScrollListViewWrongHeight(NoScrollListView listView, Activity context) {
// 获取ListView对应的ListAdapter
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
// 获取屏幕的宽度
WindowManager wm = context.getWindowManager();
int screenWidth = wm.getDefaultDisplay().getWidth();
// 获取ListView在布局时的宽度,listView的宽度 = screenWidth - 左右的padding - 左右的margin
int listViewWidth = screenWidth - dip2px(context, 10);
int widthSpec = View.MeasureSpec.makeMeasureSpec(listViewWidth, View.MeasureSpec.AT_MOST);
int totalHeight = 0;
// 遍历ListAdapter中的Item,获取每一个ItemView。调用ItemView.measure( )方法
for (int i = 0; i < listAdapter.getCount(); i++) {
View listItem = listAdapter.getView(i, null, listView);
// 计算ListView的宽度listViewWidth
listItem.measure(widthSpec, 0);
// 计算每一个子Item的高度
int itemHeight = listItem.getMeasuredHeight();
// 累加获取istView的总高度
totalHeight += itemHeight;
}
// 最后加上底部分割线的高度
int historyHeight = totalHeight + (listView.getDividerHeight() * listView.getCount() - 1);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) listView.getLayoutParams();
params.height = historyHeight;
listView.setLayoutParams(params);
listView.requestLayout();
}
这段代码的注释应该是写的非常详细易懂了,Listview高度的计算其实就是获取Listview里每一个Item的高度后累加求值并将最新高度重新设置到ListView上。使用的时候调用Utils.measureNoScrollListViewWrongHeight(noSrollListView, TestActivity.this)
好了,高度的坑也填好了,这样多个ListView相互嵌套的坑也就彻底填平了。当然,这里我是用的是ListView实现的,用RecyclerView实现的会更加灵活,以后有空我可以写写试试。
三、“挖坑RecyclerView的使用场景”
前面在ListView上挖了这么多坑,这期间也屡次提到了RecyclerView,So,我们的挖坑对象则么能少的了它呢。其实我开发前期基本没用过RecyclerView,只是稍作了解。直到后面有个需求场景我不得不使用它,那就是CoordinatorLayout和CollapsingToolbarLayout,其实就是一个折叠布局。向上滑动时,头部视图折叠,这里面如果包裹着的是ListView的话,那就无法正常滚动了,不过如果换成RecyclerView的话,就可以正常滚动,至于为什么,这是由于RecyclerView内部做的优化从而避免这样的问题,可以这么说目前RecyclerView使用的场景要比ListView多些,而且加载的布局样式也是更加灵活。
坑挖好了,现在开始填坑!先给各位看看布局代码吧
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/main_layout_bg"
android:clipToPadding="true"
android:fitsSystemWindows="true">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/rl_topbar">
<android.support.design.widget.CoordinatorLayout
android:id="@+id/course_detail_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/rl_topbar">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="@color/main_12b7f5"
app:layout_collapseParallaxMultiplier="0.6"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<LinearLayout
android:id="@+id/ll_head"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
app:layout_collapseMode="parallax">
<RelativeLayout
android:id="@+id/rl_1"
android:layout_width="match_parent"
android:layout_height="@dimen/y100"
android:background="@color/white"
android:visibility="visible">
.....
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/space_30"
android:background="@color/white"
android:visibility="visible">
...
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white">
....
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_detail"
android:layout_width="match_parent"
android:layout_height="@dimen/y66"
android:layout_marginBottom="@dimen/y16"
android:layout_marginTop="@dimen/y16"
android:background="@color/white">
...
</RelativeLayout>
</LinearLayout>
</android.support.design.widget.CollapsingToolbarLayout>
<com.yunke.xiaovo.widget.ViewPagerIndicator
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/y66"
android:background="@color/white" />
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/space_1"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>
看到这个布局,我们可以清楚的看到SwipeRefreshLayout的位置是外层包裹折叠控件、AppBarLayout等,这里必须要把它放在外面,否则执行起来会出异常。我们现在遇到的情况是一开始下拉时SwipeRefreshLayout执行正常,一旦向上滑动折叠,RecyclerView就把事件拦截了。为此我们可以利用AppBarLayout里的一个方法,在这个方法里面我们手动控制事件响应,onOffsetChanged(),这里首先要让Activity实现AppBarLayout.OnOffsetChangedListener 接口。这里我要说明下onOffsetChanged()的作用,官方给的解释说在AppBarLayout的布局偏移量发生改变时被调用。这个方法允许子view根据偏移量实现自定义的行为(比如在特定Y值的时候固定住一个View)。为了更加直观的理解,我对着代码讲
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
if (verticalOffset == 0) {
swipeRefresh.setEnabled(true);
} else {
swipeRefresh.setEnabled(false);
}
}
上面的代码就是填坑的核心!verticalOffset就是包裹在AppBarLayout里面要折叠的视图的位置偏移量,值为0说明当前是展开的,那么就让外层的SwipeRefreshLayout可以优先正常工作,而一旦控件发生折叠了即verticalOffset 值不为0,那就优先让RecyclerView工作。滑动回来后,再将事件交还给SwipeRefreshLayout,让其正常可以下拉操作。
好了,这篇文章就先分享这么多我挖的坑以及填坑的心得。其实这一年的开发过程中我给自己和团队挖了不少的坑,对此我深表惭愧。成长的过程中难免会遇到挫折,我很高兴自己坚持下来了,并且克服这些困难,每一次填坑我都会成长很多。文中若有错误还望大家指正,非常感谢。