Android 关于Scrollview和listview嵌套以及下拉刷新问题(附源码)

本人Android菜鸟,在做东西时候发现有时候我们需要在项目中使用scrollview和listview的结合才能使项目看起来更加完善,但是谷歌官网是不推荐scrollview和listview一起嵌套的,因为这俩个东西嵌套后会出现很多事件冲突。但是有时候就是需要这样的搭配怎么办?所以有些牛人自定义了view来满足这样的需求。好了,废话不多说,上代码


package com.example.pulldown;


import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.LinearLayout;
import android.widget.ScrollView;

/**
 * 
 * @author pengguichu
 *
 */
public class PullDownScrollView extends LinearLayout {

    private static final String TAG = "PullDownScrollView";

    private int refreshTargetTop = -60;
    private int headContentHeight;

    private RefreshListener refreshListener;

    private RotateAnimation animation;
    private RotateAnimation reverseAnimation;
    
    private final static int RATIO = 2;
    private int preY = 0;
    private boolean isElastic = false;
    private int startY;
    private int state;
    
    private String note_release_to_refresh = "松开更新";
    private String note_pull_to_refresh = "下拉刷新";
    private String note_refreshing = "正在更新...";
    
    private IPullDownElastic mElastic;
    

    public PullDownScrollView(Context context) {
        super(context);
        init();

    }

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

    private void init() {
        animation = new RotateAnimation(0, -180,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        animation.setInterpolator(new LinearInterpolator());
        animation.setDuration(250);
        animation.setFillAfter(true);

        reverseAnimation = new RotateAnimation(-180, 0,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        reverseAnimation.setInterpolator(new LinearInterpolator());
        reverseAnimation.setDuration(200);
        reverseAnimation.setFillAfter(true);
    }
    /**
     * 刷新监听
     * @param listener
     */
    public void setRefreshListener(RefreshListener listener) {
        this.refreshListener = listener;
    }
    /**
     * 下拉布局
     * @param elastic
     */
    public void setPullDownElastic(IPullDownElastic elastic) {
        mElastic = elastic;
        
        headContentHeight = mElastic.getElasticHeight();
        refreshTargetTop = - headContentHeight;
        LayoutParams lp = new LinearLayout.LayoutParams(
                LayoutParams.FILL_PARENT, headContentHeight);
        lp.topMargin = refreshTargetTop;
        addView(mElastic.getElasticLayout(), 0, lp);
    }
    
    /**
     * 设置更新提示语
     * @param pullToRefresh 下拉刷新提示语
     * @param releaseToRefresh 松开刷新提示语
     * @param refreshing 正在刷新提示语
     */
    public void setRefreshTips(String pullToRefresh, String releaseToRefresh, String refreshing) {
        note_pull_to_refresh = pullToRefresh;
        note_release_to_refresh = releaseToRefresh;
        note_refreshing = refreshing;
    }
    /*
     * 该方法一般和ontouchEvent 一起用 (non-Javadoc)
     * 
     * @see
     * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Logger.d(TAG, "onInterceptTouchEvent");
        printMotionEvent(ev);
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            preY = (int) ev.getY();
        }
        if (ev.getAction() == MotionEvent.ACTION_MOVE) {

            Logger.d(TAG, "isElastic:" + isElastic + " canScroll:"+ canScroll() + " ev.getY() - preY:"+(ev.getY() - preY));
            if (!isElastic && canScroll()
                    && (int) ev.getY() - preY >= headContentHeight / (3*RATIO)
                    && refreshListener != null && mElastic != null) {

                isElastic = true;
                startY = (int) ev.getY();
                Logger.i(TAG, "在move时候记录下位置startY:" + startY);
                return true;
            }

        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Logger.d(TAG, "onTouchEvent");
        printMotionEvent(event);
        handleHeadElastic(event);
        return super.onTouchEvent(event);
    }

    private void handleHeadElastic(MotionEvent event) {
        if (refreshListener != null && mElastic != null) {
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Logger.i(TAG, "down");
                break;
            case MotionEvent.ACTION_UP:

                Logger.i(TAG, "up");
                if (state != IPullDownElastic.REFRESHING && isElastic) {
                    
                    if (state == IPullDownElastic.DONE) {
                        // 什么都不做
                        setMargin(refreshTargetTop);
                    }
                    if (state == IPullDownElastic.PULL_To_REFRESH) {
                        state = IPullDownElastic.DONE;
                        setMargin(refreshTargetTop);
                        changeHeaderViewByState(state, false);
                        Logger.i(TAG, "由下拉刷新状态,到done状态");
                    }
                    if (state == IPullDownElastic.RELEASE_To_REFRESH) {
                        state = IPullDownElastic.REFRESHING;
                        setMargin(0);
                        changeHeaderViewByState(state, false);
                        onRefresh();
                        Logger.i(TAG, "由松开刷新状态,到done状态");
                    }

                }
                isElastic = false;
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.i(TAG, "move");
                int tempY = (int) event.getY();
                
                if (state != IPullDownElastic.REFRESHING && isElastic) {
                    // 可以松手去刷新了
                    if (state == IPullDownElastic.RELEASE_To_REFRESH) {
                        if (((tempY - startY) / RATIO < headContentHeight)
                                && (tempY - startY) > 0) {
                            state = IPullDownElastic.PULL_To_REFRESH;
                            changeHeaderViewByState(state, true);
                            Logger.i(TAG, "由松开刷新状态转变到下拉刷新状态");
                        } else if (tempY - startY <= 0) {
                            state = IPullDownElastic.DONE;
                            changeHeaderViewByState(state, false);
                            Logger.i(TAG, "由松开刷新状态转变到done状态");
                        }
                    }
                    if (state == IPullDownElastic.DONE) {
                        if (tempY - startY > 0) {
                            state = IPullDownElastic.PULL_To_REFRESH;
                            changeHeaderViewByState(state, false);
                        }
                    }
                    if (state == IPullDownElastic.PULL_To_REFRESH) {
                        // 下拉到可以进入RELEASE_TO_REFRESH的状态
                        if ((tempY - startY) / RATIO >= headContentHeight) {
                            state = IPullDownElastic.RELEASE_To_REFRESH;
                            changeHeaderViewByState(state, false);
                            Logger.i(TAG, "由done或者下拉刷新状态转变到松开刷新");
                        } else if (tempY - startY <= 0) {
                            state = IPullDownElastic.DONE;
                            changeHeaderViewByState(state, false);
                            Logger.i(TAG, "由DOne或者下拉刷新状态转变到done状态");
                        }
                    }
                    if (tempY - startY > 0) {
                        setMargin((tempY - startY)/2 + refreshTargetTop);
                    }
                }
                break;
            }
        }
    }
    
    /**
     * 
     */
    private void setMargin(int top) {
        LinearLayout.LayoutParams lp = (LayoutParams) mElastic.getElasticLayout()
                .getLayoutParams();
        lp.topMargin = top;
        // 修改后刷新
        mElastic.getElasticLayout().setLayoutParams(lp);
        mElastic.getElasticLayout().invalidate();
    }

    private void changeHeaderViewByState(int state, boolean isBack) {

        mElastic.changeElasticState(state, isBack);
        switch (state) {
        case IPullDownElastic.RELEASE_To_REFRESH:
            mElastic.showArrow(View.VISIBLE);
            mElastic.showProgressBar(View.GONE);
            mElastic.showLastUpdate(View.VISIBLE);
            mElastic.setTips(note_release_to_refresh);

            mElastic.clearAnimation();
            mElastic.startAnimation(animation);
            Logger.i(TAG, "当前状态,松开刷新");
            break;
        case IPullDownElastic.PULL_To_REFRESH:
            mElastic.showArrow(View.VISIBLE);
            mElastic.showProgressBar(View.GONE);
            mElastic.showLastUpdate(View.VISIBLE);
            mElastic.setTips(note_pull_to_refresh);

            mElastic.clearAnimation();

            // 是由RELEASE_To_REFRESH状态转变来的
            if (isBack) {
                mElastic.startAnimation(reverseAnimation);
            }
            Logger.i(TAG, "当前状态,下拉刷新");
            break;
        case IPullDownElastic.REFRESHING:
            mElastic.showArrow(View.GONE);
            mElastic.showProgressBar(View.VISIBLE);
            mElastic.showLastUpdate(View.GONE);
            mElastic.setTips(note_refreshing);

            mElastic.clearAnimation();
            Logger.i(TAG, "当前状态,正在刷新...");
            break;
        case IPullDownElastic.DONE:
            mElastic.showProgressBar(View.GONE);
            mElastic.clearAnimation();
//            arrowImageView.setImageResource(R.drawable.goicon);
            // tipsTextview.setText("下拉刷新");
            // lastUpdatedTextView.setVisibility(View.VISIBLE);
            Logger.i(TAG, "当前状态,done");
            break;
        }
    }

    private void onRefresh() {
        // downTextView.setVisibility(View.GONE);
//        scroller.startScroll(0, i, 0, 0 - i);
//        invalidate();
        if (refreshListener != null) {
            refreshListener.onRefresh(this);
        }
    }

    /**
     * 
     */
    @Override
    public void computeScroll() {
//        if (scroller.computeScrollOffset()) {
//            int i = this.scroller.getCurrY();
//            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView
//                    .getLayoutParams();
//            int k = Math.max(i, refreshTargetTop);
//            lp.topMargin = k;
//            this.refreshView.setLayoutParams(lp);
//            this.refreshView.invalidate();
//            invalidate();
//        }
    }

    /**
     * 结束刷新事件,UI刷新完成后必须回调此方法
     * @param text 一般传入:“上次更新时间:12:23”
     */
    public void finishRefresh(String text) {
        if (mElastic == null) {
            Logger.e(TAG, "finishRefresh mElastic:" + mElastic);
            return;
        }
        if (state == IPullDownElastic.DONE) {
            Logger.e(TAG, "==> finishRefresh state has already done");
        }
        state = IPullDownElastic.DONE;
        if (text != null) {
            mElastic.setLastUpdateText(text);
        }
        changeHeaderViewByState(state,false);
        Logger.i(TAG, "==>执行了=====finishRefresh "+ text);

        mElastic.showArrow(View.VISIBLE);
        mElastic.showLastUpdate(View.VISIBLE);
        setMargin(refreshTargetTop);
//        scroller.startScroll(0, i, 0, refreshTargetTop);
//        invalidate();
    }

    private boolean canScroll() {
        View childView;
        if (getChildCount() > 1) {
            childView = this.getChildAt(1);
            if (childView instanceof AbsListView) {
                int top = ((AbsListView) childView).getChildAt(0).getTop();
                int pad = ((AbsListView) childView).getListPaddingTop();
                if ((Math.abs(top - pad)) < 3
                        && ((AbsListView) childView).getFirstVisiblePosition() == 0) {
                    return true;
                } else {
                    return false;
                }
            } else if (childView instanceof ScrollView) {
                if (((ScrollView) childView).getScrollY() == 0) {
                    return true;
                } else {
                    return false;
                }
            }

        }
        return canScroll(this);
    }
    
    /**
     * 子类重写此方法可以兼容其它的子控件,目前只兼容AbsListView和ScrollView
     * @param view
     * @return
     */
    public boolean canScroll(PullDownScrollView view) {
        return false;
    }

    private void printMotionEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Logger.d(TAG, "down");
            break;
        case MotionEvent.ACTION_MOVE:
            Logger.d(TAG, "move");
            break;
        case MotionEvent.ACTION_UP:
            Logger.d(TAG, "up");
        default:
            break;
        }
    }
    /**
     * 刷新监听接口
     */
    public interface RefreshListener {
        public void onRefresh(PullDownScrollView view);
    }

}
既然官网不支持scrollview嵌套listview,那么我们就使用其他的View来代替scrollview,所以我参照了网上的一些代码,改了一些东西,重写的LinearLayout,将他的触摸事件改成了类似于scrollview,方法给了注释,想要研究的可以看看。

项目只需将PullDownScrollView.java这个类包裹scrollview就行,然后个给scrollview添加一个属性  

android:scrollbars="none"
就可以了

然后你就可以布置自己的布局,但是当你加入listview时,你会发现你的listview只会显示几行(本博主显示俩行),然后我查了一些资料,说需要重新定义listview的高度,但是博主觉得重新定义高度有点麻烦,就想着既然都自定义视图了,干脆在定义listview不就得了,代码如下


import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View.MeasureSpec;
import android.widget.ListView;

/**
 * 
 * 不能在一个拥有Scrollbar的组件中嵌入另一个拥有Scrollbar的组件,因为这不科学,会混淆滑动事件,导致只显示一到两行数据。那么就换一种思路,
 * 首先让子控件的内容全部显示出来,禁用了它的滚动。如果超过了父控件的范围则显示父控件的scrollbar滚动显示内容,思路是这样,一下是代码。
 * 重载onMeasure方法:
 * 
 * @author pengguichu
 * 
 */
public class MyListView extends ListView {

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

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

	public MyListView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	/***
	 * 
	 * 改变高度 其中onMeasure函数决定了组件显示的高度与宽度;
	 * makeMeasureSpec函数中第一个函数决定布局空间的大小,第二个参数是布局模式
	 * MeasureSpec.AT_MOST的意思就是子控件需要多大的控件就扩展到多大的空间
	 * 之后在ScrollView中添加这个组件就OK了,同样的道理,ListView也适用。
	 */
	@Override
	public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
				MeasureSpec.AT_MOST);
		super.onMeasure(widthMeasureSpec, expandSpec);
	}
	
	
}
代码很简单,只是继承了listview,重写了listview的onMeasure方法,让其先计算高度,再显示内容,将他替换掉你的listview,这样你的listview的内容就能全部显示出来了。

随后就是下拉刷新了,在MainActivity中获取到PullDownScrollView,实现其方法,代码如下

mPullDownScrollView = (PullDownScrollView) findViewById(R.id.refresh_root);
设置监听
mPullDownScrollView.setRefreshListener(this);  
mPullDownScrollView.setPullDownElastic(new PullDownElasticImp(this));
然后实现为实现的方法
@Override  
    public void onRefresh(PullDownScrollView view) {  
        new Handler().postDelayed(new Runnable() {  
              
            @Override  
            public void run() {  
                // TODO Auto-generated method stub  
                mPullDownScrollView.finishRefresh("上次刷新时间:12:23");  
            }  
        }, 2000);  
    }  
那么下拉刷新就搞定了。但是此时你会发现,每当你进入界面或者下拉的时候,他会自动定义到listview的顶端,这样当你scrollview和listview中间还有一些其他的布局时,这样用户体验很不好,于是我就想,进入的时候我手动把scrollview滚到顶部。这样是解决了进入时会自动滚到listview顶部,但是,当你下拉时发现,卧槽,它还是会滚动到listview的顶部,手动滚动的方法不管用了。这个问题困扰了博主很久,问了很多人,都不能给出很明确的回答,于是我查看了文档,发现listview和scrollview都会有焦点。
就是当你手触摸屏幕时会触发焦点事件,然后博主灵光一闪,禁用listview的焦点是不是listview就不会得到事件了。当博主抱着试一试的态度设置了
listView.setFocusable(false);
发现下拉刷新不会自己滚动到listview的顶部了,一切都归于平静。

如果有大神发现有什么不足之处希望指出,或者有什么更好的办法解决类似以下需求的方法,

<scrollview>

        <一些其他布局,需要动态刷新数据(如动态广告或者一些服务器更新了的内容)/>

        <listview></listview>

</scroolview>

附上源码下载地址点击打开链接http://download.csdn.net/detail/pengguichu/9316271

发布了16 篇原创文章 · 获赞 19 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/pengguichu/article/details/50129893