Android 开发总结分享(一)挖坑与填坑


做了快一年的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多些,而且加载的布局样式也是更加灵活。

说了半天似乎好像没有坑啊?那好现在我就来挖坑,大家都知道SwipeRefreshLayout控件,这是一个下拉刷新的控件,应用场景非常广。如果我要是把RecyclerView和SwipeRefreshLayout一起使用会出现什么状况?这里我就不上演示动画了,不过大家也能想象的到,向上滑动时,RecyclerView滑动正常,不过在向下滑动时问题就出现了,两者的滑动冲突了,此时包在里面的RecyclerView正常工作,而外面的SwipeRefreshLayout就彻底没法正常工作了,因为事件都被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,让其正常可以下拉操作。


好了,这篇文章就先分享这么多我挖的坑以及填坑的心得。其实这一年的开发过程中我给自己和团队挖了不少的坑,对此我深表惭愧。成长的过程中难免会遇到挫折,我很高兴自己坚持下来了,并且克服这些困难,每一次填坑我都会成长很多。文中若有错误还望大家指正,非常感谢。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值