Android View - 上拉刷新下拉加载ListView

11 篇文章 0 订阅
6 篇文章 0 订阅

虽然网上有很多上拉刷新库,效果也很好,只是有时候突然来的Bug不是很好处理,而且有时候还达不到效果,所以今天用ListView实现上拉刷新,下拉加载的效果。直接上码:

本着易扩展的理念,先写一个父类,实现上拉刷新,下拉加载的效果:

package com.johan.library.viewtoolkit.refreshlistview;

import android.animation.ValueAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ListView;

/**
 * Created by johan on 2017/7/27.
 */

public abstract class AbsRefreshListView extends ListView implements AbsListView.OnScrollListener {

    /** 动画每毫秒移动距离 */
    private static final int ANIMATION_STEP = 1;

    /** 移动速率 */
    private static final float MOVE_RATIO = 0.4f;

    /** 刷新View最大高度 */
    private static final int REFRESH_VIEW_MAX_HEIGHT = 500;

    /** 不没有刷新 */
    private static final int STATE_NO_REFRESH = 1;
    /** 正在上拉或者下拉 */
    private static final int STATE_RELEASE_REFRESH = 2;
    /** 正在刷新 */
    private static final int STATE_REFRESHING = 3;

    /** 状态 */
    private int state = STATE_NO_REFRESH;

    /** 记录手指滑动的Y值 */
    private float currentY;

    /** headerView和footerView的原始值 */
    private int headerHeight, footerHeight;

    /** Header Footer View */
    private View headerView, footerView;

    /** 记录是否最顶和最底 */
    private boolean isTop = true, isBottom;

    /** 监听滑动的Listener,因为在AbsRefreshListView已经调用了setOnScrollListener,需要以另一种方式提供用户使用 */
    private OnScrollListener onScrollListener;

    /** 是否可以上拉和下拉 */
    private boolean canPullTop = true, canPullBottom = true;

    /** 判断是否在动画 */
    private boolean isAnimation;

    /** 记录动画时,上一次的值 */
    private int lastAnimationValue;

    public AbsRefreshListView(Context context) {
        super(context);
        init();
    }

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

    /**
     * 初始化
     */
    private void init() {
        headerView = initHeaderView();
        footerView = initFooterView();
        if (headerView != null) {
            // 获取headerView的高度
            headerView.measure(0, 0);
            headerHeight = headerView.getMeasuredHeight();
            post(new Runnable() {
                @Override
                public void run() {
                    // 设置headerView的高度
                    setViewHeight(headerView, 0);
                }
            });
            addHeaderView(headerView);
        } else {
            canPullTop = false;
        }
        if (footerView != null) {
            // 获取footerView的高度
            footerView.measure(0, 0);
            footerHeight = footerView.getMeasuredHeight();
            post(new Runnable() {
                @Override
                public void run() {
                    // 设置footerView的高度
                    setViewHeight(footerView, 0);
                }
            });
            addFooterView(footerView);
        } else {
            canPullBottom = false;
        }
        setOnScrollListener(this);
    }

    /**
     * 设置View的高度
     * Bug1 : 当height=0时,view会恢复原来的高度
     * Bug1解决办法 : 如果height=0时,隐藏view,并设为1
     * Bug2 : 当height=0时,分割线还会显示
     * Bug2解决办法 : 如果height=0时,隐藏分割线,还好ListView有设置是否显示分割线的方法
     * @param view
     * @param height
     */
    private void setViewHeight(View view, int height) {
        int visibility = height == 0 ? View.GONE : View.VISIBLE;
        view.setVisibility(visibility);
        boolean isShowDivider = height != 0;
        if (view == headerView) {
            setHeaderDividersEnabled(isShowDivider);
        } else {
            setFooterDividersEnabled(isShowDivider);
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (params == null) {
            params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        }
        height = height == 0 ? 1 : height;
        params.height = height;
        view.setLayoutParams(params);
    }

    /**
     * 初始化Header,子类实现
     * @return
     */
    protected abstract View initHeaderView();

    /**
     * 初始化Footer,子类实现
     * @return
     */
    protected abstract View initFooterView();

    /**
     * 重写onTouchEvent实现上拉和下拉功能
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isAnimation) {
            return super.onTouchEvent(event);
        }
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN :
                currentY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE :
                // 单次移动的距离:手指移动的距离乘以移动速率,为了不让刷新的View显示太快
                float moveY = (event.getY() - currentY) * MOVE_RATIO;
                currentY = event.getY();
                // 上拉
                if (isTop && canPullTop) {
                    // 在最顶,如果上滑,不拦截
                    if (state == STATE_NO_REFRESH && moveY < 0) return super.onTouchEvent(event);
                    // 设置headerView高度
                    updateHeader((int) moveY);
                    // 如果不处理的话,下拉至刷新状态之后,不放手,然后上滑,发现设置不了headerView的高度
                    // 因为此时的ListView也会处理ACTION_MOVE事件下滑,所以我们要返回true,表示我们要处理事件
                    return true;
                }
                // 下拉
                if (isBottom && canPullBottom) {
                    // 在最底,如果下滑,不拦截
                    if (state == STATE_NO_REFRESH && moveY > 0) return super.onTouchEvent(event);
                    // 设置footerView高度
                    updateFooter((int) -moveY);
                    // 因为我们处理事件,所以ListView的滑动就停止了
                    // 所以,虽然我们设置了footerView的高度,但是还是显示不出来
                    // 因此,需要我们手动设置滑动
                    scrollBy(0, (int) -moveY);
                    // 表示处理事件
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP :
                currentY = 0;
                if (state == STATE_RELEASE_REFRESH) {
                    if (headerView.getHeight() >= headerHeight) {
                        // 上拉刷新
                        releaseRefresh();
                        state = STATE_REFRESHING;
                        onRefreshing(true);
                    } else if (footerView.getHeight() >= footerHeight) {
                        // 下拉加载
                        releaseRefresh();
                        state = STATE_REFRESHING;
                        onRefreshing(false);
                    } else {
                        // 没有刷新的话,回到原始状态
                        completeRefresh();
                    }
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 更新headerView
     * @param moveY
     */
    private void updateHeader(int moveY) {
        state = STATE_RELEASE_REFRESH;
        int height = headerView.getHeight() + moveY;
        height = Math.max(0, Math.min(REFRESH_VIEW_MAX_HEIGHT, height));
        setViewHeight(headerView, height);
        onProgress(true, height, headerHeight);
    }

    /**
     * 更新footerView
     * @param moveY
     */
    private void updateFooter(int moveY) {
        state = STATE_RELEASE_REFRESH;
        int height = footerView.getHeight() + moveY;
        height = Math.max(0, Math.min(REFRESH_VIEW_MAX_HEIGHT, height));
        setViewHeight(footerView, height);
        onProgress(false, footerView.getHeight(), footerHeight);
    }

    /**
     * 松手刷新
     */
    private void releaseRefresh() {
        final View refreshView = headerView.getHeight() > 1 ? headerView : footerView.getHeight() > 1 ? footerView : null;
        int refreshViewHeight = headerView.getHeight() > 1 ? headerHeight : footerView.getHeight() > 1 ? footerHeight : 0;
        if (refreshView == null) {
            return;
        }
        if (refreshViewHeight == 0) {
            return;
        }
        if (refreshView.getHeight() < 2) {
            return;
        }
        if (refreshView.getHeight() > refreshViewHeight) {
            lastAnimationValue = refreshView.getHeight();
            ValueAnimator animator = ValueAnimator.ofInt(refreshView.getHeight(), refreshViewHeight);
            animator.setDuration((refreshView.getHeight() - refreshViewHeight) / ANIMATION_STEP + 1);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int height = (int) animation.getAnimatedValue();
                    setViewHeight(refreshView, height);
                    // 因为上拉加载时,我们手动滚动了ListView,现在要恢复滚动的位置
                    if (isBottom) {
                        scrollBy(0, height - lastAnimationValue);
                        lastAnimationValue = height;
                    }
                }
            });
            animator.start();
        }
    }

    /**
     * 完成刷新
     * Bug1 : 手指正在滑动,打算取消刷新,而此时,外部调用了completeRefresh完成刷新,把refreshView重置了,
     *        造成手指还在滑动,refreshView突然不见
     * Bug1解决办法 : 根据 currentY != 0 判断手指是否在滑动,如果手指还在滑动,则不做任何操作,否则重置refreshView
     * 注意1 : 不能用 headerView.getHeight() != 0 这种方式判断是否显示有headerView,具体原因请看setViewHeight方法
     *         要用 headerView.getHeight() > 1 方式判断
     */
    public void completeRefresh() {
        if (currentY != 0) return;
        isAnimation = true;
        final View refreshView = headerView.getHeight() > 1 ? headerView : footerView.getHeight() > 1 ? footerView : null;
        if (refreshView == null) {
            isAnimation = false;
            return;
        }
        if (refreshView.getHeight() < 2) {
            isAnimation = false;
            return;
        }
        lastAnimationValue = refreshView.getHeight();
        ValueAnimator animator = ValueAnimator.ofInt(refreshView.getHeight(), 0);
        animator.setDuration(refreshView.getHeight() / ANIMATION_STEP + 1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int height = (int) animation.getAnimatedValue();
                setViewHeight(refreshView, height);
                // 因为上拉加载时,我们手动滚动了ListView,现在要恢复滚动的位置
                if (isBottom) {
                    scrollBy(0, height - lastAnimationValue);
                    lastAnimationValue = height;
                }
                if (height == 0) {
                    state = STATE_NO_REFRESH;
                    isAnimation = false;
                    onComplete(refreshView == headerView);
                }
            }
        });
        animator.start();
    }

    /**
     * 判断是否最顶或者最低或者都不是
     * @param view
     * @param firstVisibleItem
     * @param visibleItemCount
     * @param totalItemCount
     */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (firstVisibleItem == 0) {
            isTop = true;
            isBottom = false;
        } else if ((firstVisibleItem + visibleItemCount) == totalItemCount) {
            isBottom = true;
            isTop = false;
        } else {
            isTop = false;
            isBottom = false;
        }
        if (onScrollListener != null) {
            onScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (onScrollListener != null) {
            onScrollListener.onScrollStateChanged(view, scrollState);
        }
    }

    /**
     * 设置是否可以上拉
     * @param canPullTop
     */
    public void setCanPullTop(boolean canPullTop) {
        this.canPullTop = canPullTop;
    }

    /**
     * 设置是否可以下拉
     * @param canPullBottom
     */
    public void setCanPullBottom(boolean canPullBottom) {
        this.canPullBottom = canPullBottom;
    }

    /**
     * 是否在刷新
     * @return
     */
    public boolean isRefreshing() {
        return state == STATE_REFRESHING;
    }

    /**
     * 因为AbsRefreshListView已经调用了setOnScrollListener方法,不能在外面调用setOnScrollListener,否则会影响AbsRefreshListView
     * 请使用该方法,和setOnScrollListener的功能是一样的
     * @param onScrollListener
     */
    public void setScrollListener(OnScrollListener onScrollListener) {
        this.onScrollListener = onScrollListener;
    }

    /**
     * 下拉上拉时调用
     * @param isTop
     * @param currentHeight
     * @param viewHeight
     */
    protected void onProgress(boolean isTop, int currentHeight, int viewHeight) {

    }

    /**
     * 刷新时调用,子类实现
     * @param isTop
     */
    protected abstract void onRefreshing(boolean isTop);

    /**
     * 刷新完成时调用
     * @param isTop
     */
    protected void onComplete(boolean isTop) {

    }

}

代码的注释已经很清楚说明了上拉刷新和下拉加载的原理,还有遇到的一些坑和解决办法,相信你看得懂。
下面是我实现的子类:

package com.johan.library.viewtoolkit.refreshlistview;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.TextView;

import com.johan.library.R;

/**
 * Created by johan on 2017/7/27.
 */

public class RefreshListView extends AbsRefreshListView {

    private ImageView headerIconView, footerIconView;
    private TextView headerContentView, footerContentView;

    private RefreshMessage refreshMessage;

    private ObjectAnimator rotateAnimator;

    private OnRefreshListener onRefreshListener = new OnRefreshListener() {
        @Override
        public void onPullRefreshing() {}
        @Override
        public void onLoadRefreshing() {}
    };

    public RefreshListView(Context context) {
        super(context);
        refreshMessage = new RefreshMessage();
        initView();
    }

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 支持attr自定义属性
        refreshMessage = new RefreshMessage();
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RefreshListView);
        if (array.hasValue(R.styleable.RefreshListView_refresh_pull_icon)) {
            refreshMessage.pullIcon = array.getDrawable(R.styleable.RefreshListView_refresh_pull_icon);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_pull_tip)) {
            refreshMessage.pullTip = array.getString(R.styleable.RefreshListView_refresh_pull_tip);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_pull_release_refresh_tip)) {
            refreshMessage.pullReleaseRefreshTip = array.getString(R.styleable.RefreshListView_refresh_pull_release_refresh_tip);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_pull_refreshing_tip)) {
            refreshMessage.pullRefreshingTip = array.getString(R.styleable.RefreshListView_refresh_pull_refreshing_tip);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_load_icon)) {
            refreshMessage.loadIcon = array.getDrawable(R.styleable.RefreshListView_refresh_load_icon);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_load_tip)) {
            refreshMessage.loadTip = array.getString(R.styleable.RefreshListView_refresh_load_tip);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_load_release_refresh_tip)) {
            refreshMessage.loadReleaseRefreshTip = array.getString(R.styleable.RefreshListView_refresh_load_release_refresh_tip);
        }
        if (array.hasValue(R.styleable.RefreshListView_refresh_load_refreshing_tip)) {
            refreshMessage.loadRefreshingTip = array.getString(R.styleable.RefreshListView_refresh_load_refreshing_tip);
        }
        array.recycle();
        initView();
    }

    @Override
    protected View initHeaderView() {
        View headerView = LayoutInflater.from(getContext()).inflate(R.layout.header_default, null);
        headerIconView = (ImageView) headerView.findViewById(R.id.header_default_icon);
        headerContentView = (TextView) headerView.findViewById(R.id.header_default_content);
        return headerView;
    }

    @Override
    protected View initFooterView() {
        View footerView = LayoutInflater.from(getContext()).inflate(R.layout.footer_default, null);
        footerIconView = (ImageView) footerView.findViewById(R.id.footer_default_icon);
        footerContentView = (TextView) footerView.findViewById(R.id.footer_default_content);
        return footerView;
    }

    private void initView() {
        if (refreshMessage.pullIcon != null) {
            headerIconView.setImageDrawable(refreshMessage.pullIcon);
        }
        headerContentView.setText(refreshMessage.pullTip);
        if (refreshMessage.loadIcon != null) {
            footerIconView.setImageDrawable(refreshMessage.loadIcon);
        }
        footerContentView.setText(refreshMessage.loadTip);
    }

    @Override
    protected void onRefreshing(boolean isTop) {
        View refreshView = isTop ? headerIconView : footerIconView;
        startRotateAnimation(refreshView);
        if (isTop) {
            onRefreshListener.onPullRefreshing();
            headerContentView.setText(refreshMessage.pullRefreshingTip);
        } else {
            onRefreshListener.onLoadRefreshing();
            footerContentView.setText(refreshMessage.loadRefreshingTip);
        }
    }

    @Override
    protected void onProgress(boolean isTop, int currentHeight, int viewHeight) {
        if (isTop) {
            String headerContent = currentHeight >= viewHeight ? refreshMessage.pullReleaseRefreshTip : refreshMessage.pullTip;
            headerContentView.setText(headerContent);
            rotateIcon(headerIconView, 360 * (currentHeight % viewHeight) / viewHeight);
        } else {
            String footerContent = currentHeight >= viewHeight ? refreshMessage.loadReleaseRefreshTip : refreshMessage.loadTip;
            footerContentView.setText(footerContent);
            rotateIcon(footerIconView, 360 * (currentHeight % viewHeight) / viewHeight);
        }
        endRotateAnimation();
    }

    @Override
    protected void onComplete(boolean isTop) {
        endRotateAnimation();
    }

    private void startRotateAnimation(View view) {
        rotateAnimator = ObjectAnimator.ofFloat(view, "rotation", 0, 359.0f);
        rotateAnimator.setDuration(500);
        rotateAnimator.setRepeatCount(-1);
        rotateAnimator.setInterpolator(new LinearInterpolator());
        rotateAnimator.start();
    }

    private void endRotateAnimation() {
        if (rotateAnimator != null) {
            rotateAnimator.end();
            rotateAnimator = null;
        }
    }

    public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
        this.onRefreshListener = onRefreshListener;
    }

    public interface OnRefreshListener {
        void onPullRefreshing();
        void onLoadRefreshing();
    }

    private void rotateIcon(ImageView imageView, int angle) {
        imageView.setPivotX(imageView.getWidth() / 2);
        imageView.setPivotY(imageView.getHeight() / 2);
        imageView.setRotation(angle);
    }

    public void setPullTip(String pullTip) {
        refreshMessage.pullTip = pullTip;
    }

    public void setPullReleaseRefreshTip(String pullReleaseRefreshTip) {
        refreshMessage.pullReleaseRefreshTip = pullReleaseRefreshTip;
    }

    public void setPullRefreshingTip(String pullRefreshingTip) {
        refreshMessage.pullRefreshingTip = pullRefreshingTip;
    }

    public void setPullIcon(int pullIcon) {
        headerIconView.setImageResource(pullIcon);
    }

    public void setLoadTip(String loadTip) {
        refreshMessage.loadTip = loadTip;
    }

    public void setLoadReleaseRefreshTip(String loadReleaseRefreshTip) {
        refreshMessage.loadReleaseRefreshTip = loadReleaseRefreshTip;
    }

    public void setLoadRefreshingTip(String loadRefreshingTip) {
        refreshMessage.loadRefreshingTip = loadRefreshingTip;
    }

    public void setLoadIcon(int loadIcon) {
        footerIconView.setImageResource(loadIcon);
    }

    class RefreshMessage {
        public String pullTip = "下拉刷新";
        public String pullReleaseRefreshTip = "松手刷新";
        public String pullRefreshingTip = "正在刷新";
        public Drawable pullIcon = null;
        public String loadTip = "上拉加载";
        public String loadReleaseRefreshTip = "松手加载";
        public String loadRefreshingTip = "正在加载";
        public Drawable loadIcon = null;
    }

}

只要模仿RefreshListView,继承AbsRefreshListView,就很轻松实现自己的上拉刷新下拉加载的ListView。

代码已经上传到我的github:https://github.com/JohanMan/viewtoolkit

因为RefreshListView是已经实现了上拉刷新下拉加载了,可以直接使用。

使用实例:

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:refresh_view="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.johan.viewtoolkit.MainActivity">

    <com.johan.library.viewtoolkit.refreshlistview.RefreshListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:dividerHeight="1px"
        android:divider="#cccccc"
        refresh_view:refresh_pull_icon="@drawable/my_refresh_icon"
        refresh_view:refresh_pull_tip="下拉刷新哦"
        refresh_view:refresh_pull_release_refresh_tip="松手刷新哦"
        refresh_view:refresh_pull_refreshing_tip="正在刷新哈"
        />

</LinearLayout>

activity

final RefreshListView listView = (RefreshListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
listView.setLoadIcon(R.drawable.my_refresh_icon);
listView.setLoadTip("上拉加载哦");
listView.setOnRefreshListener(new RefreshListView.OnRefreshListener() {
    @Override
    public void onPullRefreshing() {
        // 模拟延时3秒
        listView.postDelayed(new Runnable() {
            @Override
            public void run() {
                listView.completeRefresh();
            }
        }, 3000);
    }
    @Override
    public void onLoadRefreshing() {
        // 模拟延时3秒
        listView.postDelayed(new Runnable() {
            @Override
            public void run() {
                listView.completeRefresh();
            }
        }, 3000);
    }
});
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值