多重ScrollView、HorizontalScrollView、ListView嵌套的死磕!

写在前面的话:
本来计划第一篇自己的博客是计划从CustomView开始的,由于个人的原因呢。一直搁置了两个月下来,这段时间发生的事情,过程不算顺利,但是收获不可谓不大!本例呢是根据公司最近的需求做的demo,原创度不高,更多的是收集整合及问题的解决方法,但是这么复杂的嵌套真真是入坑出坑,那心酸!更心酸的是UI出效果图后跟需求没有一个是一样的!效果全变了,需求也相应的变更了,让自己的死磕变得毫无意义,毫无意义……!不过好的一点呢,这反而不是公司的东西了,反而没了后顾之忧!
大概的效果呢有点类似汽车之家汽车配置界面的表单,粗看其实没什么,细看后,一直没想出个解决办法来,本想着用纯CustomView的方式来完成。一开始就觉得采用组合形式的来实现觉得太low(然而然而被打脸了Crying),大至写了个demo越来越觉得不靠谱,实现的东西太多了(主要还是能力有限哈^^)。

汽车之家效果

然后无意中看到京东上列表联动的效果,可以从这方面入手,回来一搜还真有类似的实现(文末会贴出^^),解决办法呢其实就是两个ScrollView嵌套然后产生一个联动的效果。
这里主要是讲遇到的坑哈,前期需求并没有确定,那自己就只有全面的更全面的去考虑,预防备用着。本例一个是为记录下个人的过程,也是为了方便碰到类似问题的同学少走弯路吧,好了^^,闲言碎语就到这里吧,开始那百分之二十!

这个是本demo的效果,
这是本demo的效果

  • 这是整个xml嵌套的结构!疯了吧^^

xml结构

  • 核心代码块
    这里采用的是继承自LinearLayout 通过xml引用的方式作组合产生一个控件,中间包含时间轴的绘制及时间段常用置前,和用于时间标尺作用的TimeBar 当然这不是文章的重点请选择性忽略^^
public class AsyncMeetingView extends LinearLayout{

    private int timeModel=24;
    private int timeStart=7;

    /**item 宽高 最终于工具类转换*/
    private int itemWidth= (int) getResources().getDimension(R.dimen.activity_meeting_times_titelWidth);
    private int ItemHeight= (int) getResources().getDimension(R.dimen.activity_meeting_times_titelHeight);

    private AsyncHorizontalScrollView shs_titel;
    private AsyncHorizontalScrollView shs_rightcontent;

    private ListView lv_left_name;
    private ListView lv_rightcontent;

    private LinearLayout ll_rightitle;
    private Button btn_meetingType;
    private View view_timeBar;

    private BaseAdapter LeftNameAdapter,RightContentAdapter;
    private Context context;

    private View parentView=null;
    private Handler handler=new Handler();

    private String[] times;
    private ScrollView scrollView_parent;
    private AsyncScrollView innerScrollView_content;

    public AsyncMeetingView(Context context) {
        this(context,null);
    }

    public AsyncMeetingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context=context;
        parentView=LayoutInflater.from(context).inflate(R.layout.activity_main,this);
        initView();
    }

    private void initView() {
        initTitelTimesItem();

        btn_meetingType= (Button) findViewById(R.id.btn_meetingType);
        view_timeBar = (View) findViewById(R.id.view_timeBar);

        lv_rightcontent = (ListView)findViewById(R.id.lv_rightcontent);
        lv_rightcontent.setFocusable(false);

        shs_rightcontent = (AsyncHorizontalScrollView)findViewById(R.id.shs_rightcontent);
        shs_rightcontent.setFocusable(false);//关键关掉的

        lv_left_name = (ListView)findViewById(R.id.lv_left_name);
        lv_left_name.setFocusable(false);

        shs_titel = (AsyncHorizontalScrollView)findViewById(R.id.shs_titel);

//      嵌套问题
        scrollView_parent= (ScrollView) findViewById(R.id.scrollView_parent);
        innerScrollView_content=(AsyncScrollView) findViewById(R.id.innerScrollView_content);
        innerScrollView_content.setParentScrollView(scrollView_parent);
        innerScrollView_content.setBtn_meetingType(btn_meetingType);

        /**解决不置顶的问题*/
        scrollView_parent.setFocusable(true);
        scrollView_parent.setFocusableInTouchMode(true);
        scrollView_parent.requestFocus();

    }

    public void setTimeBarParams(int widht,int color){
        view_timeBar.setLayoutParams(new FrameLayout.LayoutParams(widht, LayoutParams.MATCH_PARENT));
        view_timeBar.setBackgroundColor(color);
    }

    public void setTimeBarTranslationX(final int location){

        view_timeBar.setTranslationX(location);

//              退一格,相对居中显示
        int scrollLocation=location-(int)(getResources().getDimension(R.dimen.activity_meeting_times_titelWidth));
                shs_rightcontent.smoothScrollTo(scrollLocation,0);

    }

    /**实例时间段*/
    private void initTitelTimesItem() {

        ll_rightitle= (LinearLayout)findViewById(R.id.ll_rightitle);
        times=transFormTime(timeModel,timeStart);
        for (int i=0;i<times.length;i++){
            AsyncMeetingTextView tv_time=new AsyncMeetingTextView(context);
            tv_time.setLayoutParams(
                    new LinearLayout.LayoutParams(itemWidth,ItemHeight));
            tv_time.setText(times[i]);
            tv_time.setGravity(Gravity.CENTER);
            tv_time.setTextSize(getResources().getDimension(R.dimen.activity_meeting_times_textSize));
            ll_rightitle.addView(tv_time);

            Log.v("times====",times[i]);
        }

    }

    /**
     * 根据列数生成相应的时间数组 以8:00/startPoint 起始点 开头
     * 以第二列开始,第一列为标注:时间/人员
     */
    public String[] transFormTime(int column,int startPoint) {//24,7

        String[] times = new String[column];
        for (int i = 0; i < times.length; i++) {
            if (i >= startPoint && i < times.length) {// =7 && <24
                if (i != 24) {
                    times[i-startPoint] = i + ":00";
                } else {
                    times[i-startPoint] = "00:00";//24:00转成 00:00
                }
            }else if(i>=0 && i<startPoint){//
                times[times.length - (startPoint- i)] = i + ":00";//(index 19-24)
            }
        }
        return times;
    }


    public void setTimeModel(int timeModel) {
        this.timeModel = timeModel;
    }

    public void setTimeStart(int timeStart) {
        this.timeStart = timeStart;
    }

    public int getItemWidth() {
        return itemWidth;
    }

    public void setItemWidth(int itemWidth) {
        this.itemWidth = itemWidth;
    }

    public int getItemHeight() {
        return ItemHeight;
    }

    public void setItemHeight(int itemHeight) {
        ItemHeight = itemHeight;
    }


    public void setView_timeBar(View view_timeBar) {
        this.view_timeBar = view_timeBar;
    }

    public Button getBtn_meetingType() {
        return btn_meetingType;
    }

    public String[] getTimes() {
        return times;
    }

    public void setLeftNameAdapter(BaseAdapter leftNameAdapter) {
        LeftNameAdapter = leftNameAdapter;
        if (LeftNameAdapter!=null)
            lv_left_name.setAdapter(LeftNameAdapter);
        shs_titel.setmView(shs_rightcontent);
        UtilTools.setListViewHeightBasedOnChildren(lv_left_name);
    }


    public void setRightContentAdapter(BaseAdapter rightContentAdapter) {
        RightContentAdapter = rightContentAdapter;
        if (RightContentAdapter!=null)
            lv_rightcontent.setAdapter(RightContentAdapter);

        shs_rightcontent.setmView(shs_titel);
        UtilTools.setListViewHeightBasedOnChildren(lv_rightcontent);
    }

}

这里大概分为五大问题进行说明

  • 问题一:关于联动分析
    这里主要是解决两个HorizontalScrollView横向滑动联动的效果(时间头跟内容体),自定义AsyncHorizontalScrollView继承自HorizontalScrollView。通过对其它HorizontalScrollView的引用,在onScrollChanged方法中进行scrollTo(x,y)设置,达到两个HorizontalScrollView绑定的效果,从面产生联动的效果
public class AsyncHorizontalScrollView extends HorizontalScrollView {

    private View mView;

    public AsyncHorizontalScrollView(Context context) {
        super(context);
    }

    public AsyncHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AsyncHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mView!=null)
        mView.scrollTo(l,t);
//      Log.v("滑动值:",l+"===="+t);
    }

    public void setmView(View mViwe) {
        this.mView = mViwe;
    }

}
  • 问题点二,三:嵌套在AsyncHorizontalScrollView中的ListView(也就是内容体中的ListView)重新测量的问题。
    由于ScrollView中嵌套ListView会产生只显示一条数据,这是因为adapter内容是后加载的,这就要手动的去根据item数量去重新测量整个ListView的高度,这里其实也带出了问题点三:也就是HorizontalScrollView宽度大于ListView宽度的问题,从面产生空白区域的问题,这里一并说了。这个工具类也是直接参考网上的,只是作了宽度的设置。
public class UtilTools {

    static int totalWidth=0;

    public static void setListViewHeightBasedOnChildren(ListView listView) {
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {
            return;
        }
        int totalHeight = 0;
        int len = listAdapter.getCount();

        for (int i = 0; i < len; i++) {
            View listItem = listAdapter.getView(i, null, listView);
            listItem.measure(0, 0);
            totalHeight += listItem.getMeasuredHeight();
            totalWidth=listItem.getMeasuredWidth();
        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
        params.width=totalWidth;//还是算出宽度,解决多出白边的问题
        listView.setLayoutParams(params);
    }

}
  • 问题点四:整个页面再次嵌套在一个主竖直ScrollView中
    由于竖直方向跟横向方向都要做相应的滑动,这里就会产生事件冲突的问题,这里主要还是采用能过内部拦截的方式去实现,那么子ScorllView就要从新自定义了,这里由于考虑了实际的需求多了两种情况的逻辑处理:
    • 一是已到达过底部,再滑动子控件,此时先转到父控件先显示出时间轴再滑动子控件
    • 二是子控件滑动到中间,这时切换到父控件滑动,此时时间轴不可见了,这时再滑动子控件时 判断时间轴是否可见
    • *
public class AsyncScrollView extends ScrollView {

    /**
     * 外层ScrollView
     */
    private ScrollView parentScrollView;
    /**
     * 外层的头部时间控件,用于判断时间轴是否正屏幕内可见
     * */
    private Button btn_meetingType;

    private int mTop = 10;
    private int lastScrollDelta = 0;
    private int currentY;

    /**判断时间轴是否在屏幕内显示*/
    private boolean isTimeItemShow=true;

    /**判断子控件是否滑动到底部*/
    private boolean isConvertDownGetParent=false;


    public AsyncScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void resume() {
        overScrollBy(0, -lastScrollDelta, 0, getScrollY(), 0, getScrollRange(), 0, 0, true);
        lastScrollDelta = 0;
    }
    /**
     * 将targetView滚到最顶端
     */
    public void scrollToTop(View targetView) {
        int oldScrollY = getScrollY();
        int top = targetView.getTop() - mTop;
        int delatY = top - oldScrollY;
        lastScrollDelta = delatY;
        overScrollBy(0, delatY, 0, getScrollY(), 0, getScrollRange(), 0, 0, true);
    }
    private int getScrollRange() {
        int scrollRange = 0;
        if (getChildCount() > 0) {
            View child = getChildAt(0);
            scrollRange = Math.max(0, child.getHeight() - (getHeight()));
        }
        return scrollRange;
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (parentScrollView == null) {
            return super.onInterceptTouchEvent(ev);
        } else {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                // 将父scrollview的滚动事件拦截
                currentY = (int)ev.getY();
                setParentScrollAble(false);

                return super.onInterceptTouchEvent(ev);
            } else if (ev.getAction() == MotionEvent.ACTION_UP) {
                // 把滚动事件恢复给父Scrollview
                setParentScrollAble(true);
                Log.v("onInterceptTouchEventUp","把滚动事件恢复给父Scrollview");

            } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {

                DisplayMetrics dm=getResources().getDisplayMetrics();

                int[] ints=new int[]{dm.widthPixels,dm.heightPixels};

//                  Log.v("onInterceptTouchEventMoveBtn",btn_meetingType.getLocationOnScreen(ints));
                isTimeItemShow=btn_meetingType.getLocalVisibleRect(new Rect(0,0,dm.widthPixels,dm.heightPixels));

                Log.v("onInterceptTouchEventMove","把滚动事件恢复给父Scrollview:"+isTimeItemShow);


            }
        }
        return super.onInterceptTouchEvent(ev);

    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        View child = getChildAt(0);
        if (parentScrollView != null) {
            if (ev.getAction() == MotionEvent.ACTION_MOVE) {
                int height = child.getMeasuredHeight();
                height = height - getMeasuredHeight();

                 Log.d("innr_onTouchEvent","height=" + height);
                int scrollY = getScrollY();
                 Log.d("innr_onTouchEvent","_scrollY=" + scrollY);
                int y = (int)ev.getY();

                // 手指向下滑动
                if (currentY < y) {
                    if (scrollY <= 0) {
                        // 如果向下滑动到头,就把滚动交给父Scrollview
                        setParentScrollAble(true);
                        return false;
                    } else {

                        /**两种情况向下滑动是,要由子控件先切到父控件显示出时间后再内部滑动,
                         * 一是已到达过底部,再滑动子控件,此时先转到父控件先显示出时间轴再滑动子控件
                         * 二是子控件滑动到中间,这时切换到父控件滑动,此时时间轴不可见了,这是再滑动子控件时 判断时间轴是否可见。。。*/
                        if ((isConvertDownGetParent==true |isTimeItemShow==false) && height-scrollY>40){
                            setParentScrollAble(true);
                            isConvertDownGetParent=false;
                        }else {
                            setParentScrollAble(false);
                        }
                    }
                } else if (currentY > y) {
                    if (scrollY >= height) {
//                      子控件已滑动到底部
                        isConvertDownGetParent=true;
                        // 如果向上滑动到头,就把滚动交给父Scrollview
                        setParentScrollAble(true);
                        return false;
                    } else {
                        setParentScrollAble(false);
                    }
                }
                currentY = y;
            }
        }

        return super.onTouchEvent(ev);
    }

    /**
     * 是否把滚动事件交给父scrollview
     *
     * @param flag
     */
    private void setParentScrollAble(boolean flag) {
        parentScrollView.requestDisallowInterceptTouchEvent(!flag);
    }

    public void setParentScrollView(ScrollView parentScrollView) {
        this.parentScrollView = parentScrollView;
    }

    public void setBtn_meetingType(Button btn_meetingType) {
        this.btn_meetingType = btn_meetingType;
    }
}
  • 问题点五:多重嵌套时产生的ScrollView中内容不置顶,反而ListView反而会置顶的问题
    这里有的文章中说到通过线程调用ScorllView中的smoothScrollTo(x,y)方法滑动到顶部的方法,单层嵌套这样处理是可以的,但是…本例嵌套过多,内部的子ListView还是会有这种问题,所以本例不适用。总结了下大至通过以下三个步骤实测可解决(不同的嵌套方式要自行调整哦)

一、在代码里去掉listview的焦点如

lv_rightcontent = (ListView)findViewById(R.id.lv_rightcontent);
lv_rightcontent.setFocusable(false);

lv_left_name = (ListView)findViewById(R.id.lv_left_name);
lv_left_name.setFocusable(false);

二、在Listview外套一层LinearLayout,如果是其它GroupView获取焦点

<com.demo.jiangyuehua.myhscorllrelatelistview.view.AsyncHorizontalScrollView
android:id="@+id/shs_rightcontent"
android:layout_width="match_parent"
android:layout_height="match_parent"              android:focusableInTouchMode="true"
android:focusable="true"
                        >
或
<LinearLayout
android:layout_width="match_parent"      android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true">

三、最后就是要在你的主ScrollView中通过代码添加获得焦点方法

scrollView_parent.setFocusable(true);
scrollView_parent.setFocusableInTouchMode(true);
scrollView_parent.requestFocus();

相关文章参考引用 Thanks ^^:
http://blog.csdn.net/elinavampire/article/details/42142551
http://blog.csdn.net/jiaoyaning1210/article/details/51084246

基本核心的就是这些了,前前后后,不停的入坑出坑,过程不可谓不波折,占用了你的百分之二十以及百分之三十甚至百分之四十,但是每一次问题的解决都是值得欣慰的,enjoy!

这里还是要说下小插曲,评审时当看到UI效果图跟需求原型没有一毛钱关系时,当时就懵逼了,两个星期的准备就这么白费了,当时那个心情呐。本身当天由于个人的事情一点状态也没有,不停的在挣扎、调整中,还来这么一出。哎……,可当看到iOS的哥们直接就暴走!然后balabala……,哈哈,当时心情瞬间就释然了!就是见不得别人比我惨,哈哈哈^^。

我是源码点我

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值