拆轮子系列之一步一步教你做仿美团下拉刷新效果

本文介绍了一种仿美团的下拉刷新效果实现方法,主要包括Scroller使用、onTouchEvent重写及自定义控件相关知识。通过实例展示了如何制作一个包含动画效果的下拉刷新组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章的开头奉送上代码,方便大家对照学习。

这二天写了一个仿美团的下拉刷新效果,效果图如下:

怎么样还不错吧,下面我就教大家做。这个做的还是太粗糙,学习还行,用在项目中还要再改改。

1.用到的知识点:

其实这个做起来出没那么难,主要用到的知识点如下:

1.Scroller的使用,这个主要有来松手时的回滚效果。
2.onTouchEvent重写
3.自定义控件的相关知识。

如果大家不会Scroller,请翻阅这一篇文章

2.思路流程

下面来说一下整体思路:

1、建一个类MyListView继承listView。
2.建一个类头部的View(HeaderView),并通过addHeaderView()和Mylistview相关联。
2、重写onTouchEvent,在滑动的时候不断计算滑动的距离,并分下列2种情况:
        a.如果滑动距离<60,当放开手指时,应该回滚到初始位置,回滚是利用Scrller做的。
        b.如果滑动距离>60,这个时候触发第一步动画(小孩有躺着到直立的动画),当松开手指时,应该回滚到60的位置上,并触发第三个动画(小孩不断的摇晃身子的动画)。
3.在滑动的过程中,头部HeaderView的高度要不断的随着滑动的距离增加而增加,而且要不断的控制小孩所在的imageview的大小。

整体的思路就是这么简单,大家是不是豁然开朗。来看一下小孩有躺着到直立的动画是怎么实现的:

这是一个帧动画,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true" >
    <item android:drawable="@drawable/pull_end_image_frame_01" android:duration="100"/>
    <item android:drawable="@drawable/pull_end_image_frame_02" android:duration="100"/>
    <item android:drawable="@drawable/pull_end_image_frame_03" android:duration="100"/>
    <item android:drawable="@drawable/pull_end_image_frame_04" android:duration="100"/>
    <item android:drawable="@drawable/pull_end_image_frame_05" android:duration="100"/>
</animation-list>

使用方式如下:

refreshImg.setBackgroundResource(R.drawable.pull_to_refresh_second_anim);
secondAnim = (AnimationDrawable) refreshImg.getBackground();// 启动
secondAnim.start();

其实第二步的动画(小孩不断摇晃)的动画也是一样的道理,无非图片不一样而已。

3.详细实现

我的代码编写是在eclipse上编写的,不用AS的原因是因为AS太卡了,不如eclipse来的方便,如果有不方便的地方,请大家见谅。

3.1HeadView的布局文件header_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="#F0F0F0"
android:padding="5dp"
android:layout_height="0dp"
 >
    <ImageView 
        android:id="@+id/refreshImg"
        android:background="@drawable/pull_image"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        />
</RelativeLayout>

为个布局文件中,之所以要用RelativeLayout,是要保证ImageView始终在RelativeLayout的底部。

3.2HeadView类代码如下

public class HeaderView extends RelativeLayout {

    private View view;
    private ImageView refreshImg;
    private AnimationDrawable secondAnim, threeAnim;
    private int LISTVIEW_STATE = MyListView.STATE_NORMAL;// 当前listview处于什么状态,刷新和平常2中状态

    public HeaderView(Context context) {
        super(context);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LayoutParams.MATCH_PARENT, 0);
        view = (View) LayoutInflater.from(context).inflate(
                R.layout.header_layout, null);
        refreshImg = (ImageView) view.findViewById(R.id.refreshImg);
        addView(view, lp);
    }

    public void setListViewState(int state) {
        LISTVIEW_STATE = state;
    }

    /**
     * 这个方法什么都不用管,只管设置高度。
     * 
     * @param height
     */
    public void setVisiableHeight(int height) {
        if (height < 0) {
            height = 0;
        }
        if (LISTVIEW_STATE == MyListView.STATE_NORMAL) {
            setImgCal((height<30)?height:30, 82, 60);
        } else if (LISTVIEW_STATE == MyListView.STATE_FRESH) {
            setImgCal(MyListView.RefreshDistance - getPaddingBottom() - getPaddingTop(), 82, 107);
        }
        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams();
        lp.height = height;
        view.setLayoutParams(lp);
    }

    public int getVisiableHeight() {
        return view.getHeight();
    }
//根据高度,按照比例设置图片的高和宽
    private void setImgCal(int heigth, int scaleW, int scaleH) {
        int width = (heigth * scaleW) / scaleH;
        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) refreshImg
                .getLayoutParams();
        lp.height = heigth;
        lp.width = width;
        refreshImg.setLayoutParams(lp); 
    }
    /**
     * 设置第二个阶段,当拉来一定的高度后,就设置一个小孩从躺着到直立的动画。
     */
    public void setSecondState() {
        refreshImg
                .setBackgroundResource(R.drawable.pull_to_refresh_second_anim);
        secondAnim = (AnimationDrawable) refreshImg.getBackground();// 启动
        secondAnim.start();
    }

    /**
     * 刷新阶段,一个小孩在左右晃的图片
     */
    public void setThirdState() {
        refreshImg.setBackgroundResource(R.drawable.pull_to_refresh_third_anim);
        threeAnim = (AnimationDrawable) refreshImg.getBackground();// 启动
        threeAnim.start();
    }
    /**
     * 设置成初始图片
     */
    public void setNormalState() {
        refreshImg.setBackgroundResource(R.drawable.pull_image);
        secondAnim.stop();
        threeAnim.stop();
    }
}
setSecondState()
setThirdState()
setNormalState()
这三个方法是设置HeaderView 要显示那种动画的。

setVisiableHeight()方法是根据滑动的距离来设置HeaderView的高度的。

setImgCal(int heigth, int scaleW, int scaleH)是用来根据高度,按比例计算图片宽度并设置。例如真实图片的宽和高是82和60,如果滑动的距离是30,那么图片的宽度是:(30 * 82) / 60。这样就会保证不被拉伸。

3.3核心类MyListView类代码如下 :

**
 * @author 作者 YYD
 * @version 创建时间:20161223日 上午11:20:17
 * @function 未添加
 */
@SuppressLint("NewApi")
public class MyListView extends ListView {
    /**
     * 本接口用来实现下拉刷新和滑动加载的数据加载
     */
    public interface MListViewListener {

        public void onRefresh();

        public void onLoadMore();
    }

    private MListViewListener mListViewListener;

    public void setmListViewListener(MListViewListener mListViewListener) {
        this.mListViewListener = mListViewListener;
    }

    /**
     * 调用接口刷新数据的方法
     * */
    private void startRefresh() {
        if (mListViewListener != null) {
            mListViewListener.onRefresh();
        }
    }

    /**
     * 调用接口加载数据的方法
     * */
    private void startLoadMore() {
        if (mListViewListener != null) {
            mListViewListener.onLoadMore();
        }
    }

    public static final int STATE_NORMAL = 1000;// 平常状态,包括普通和刷新结束
    public static final int STATE_FRESH = 1001;// 刷新的状态
    private int LISTVIEW_STATE = STATE_NORMAL;// 当前listview处于什么状态,刷新和平常2中状态

    public static final int RefreshDistance = 60;

    private float lastY = -1;// 记录最近一次的偏移量
    private final float OFFSET_RADIO = 1.8f;// 灵敏度
    private final int SCROLL_TIME = 300;// 动画返回的时间

    private HeaderView headerView;// 头部view
    private Scroller mScroller;// 下拉后返回的Scroller

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context, new OvershootInterpolator());
        headerView = new HeaderView(context);
        addHeaderView(headerView);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastY = ev.getRawY();
            break;
        // 用户滑动
        case MotionEvent.ACTION_MOVE:

                if (lastY == -1) {
                    lastY = ev.getRawY();
                }
                float distance = ev.getRawY() - lastY;
                lastY = ev.getRawY();
                moveState(distance);
                Log.d("refreshlistview", "" + distance);

            break;
        // 当用户手指抬起时
        case MotionEvent.ACTION_UP:
            upHeightBack();
            break;
        }

        return super.onTouchEvent(ev);
    }

    private void moveState(float distance) {
        if (getFirstVisiblePosition() == 0
                && (headerView.getVisiableHeight() > 0 || lastY > 0)) {
            // 如果当前是第一项并且头部视图可见高度大于0或者Y轴移动距离大于0(证明是从上往下拉的),更新头部视图
            distance /= OFFSET_RADIO;
            distance += headerView.getVisiableHeight();
            if (distance < RefreshDistance) {
                setListViewState(STATE_NORMAL);
            } else {
                if(LISTVIEW_STATE != STATE_FRESH){
                    headerView.setSecondState();
                    setListViewState(STATE_FRESH);
                    startRefresh();
                }

            }
            updateHeaderHeight(distance);
        }
    }

    public void setListViewState(int state) {
        headerView.setListViewState(state);
        this.LISTVIEW_STATE = state;
    }

    /**
     * MotionEvent.ACTION_UP 的时候,弹回去。
     */
    private void upHeightBack() {
        int height = headerView.getVisiableHeight();
        if (height == 0) {
            // 如果已经是原始位置,return
            return;
        }
        int scrollHeigth = 0;
        if (LISTVIEW_STATE == STATE_NORMAL) {
            scrollHeigth = 0 - height;
        } else if (LISTVIEW_STATE == STATE_FRESH) {
            scrollHeigth = MyListView.RefreshDistance - height;
            headerView.setThirdState();
        }
        // 松开刷新时,如果头部视图可见高度大于头部视图的实际高度,只显示头部视图实际的高度,
        mScroller.startScroll(0, height, 0, scrollHeigth, SCROLL_TIME);
        invalidate(); // 刷新视图(在UI线程中使用,在非UI线程中应该使用postInvalidate)
    }

    /**
     * 更新头部视图
     */
    private void updateHeaderHeight(float delta) {
        headerView.setVisiableHeight((int) delta);
        setSelection(0); // 恢复ListView原始的位置
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            // 滑动完成
            headerView.setVisiableHeight(mScroller.getCurrY());
            postInvalidate();// 刷新视图(在非UI线程中使用,在UI线程中应该使用invalidate)
        }
        super.computeScroll();
    }
    /**
     * 停止刷新
     */
    public void stopRefresh() {
        LISTVIEW_STATE = STATE_NORMAL;
        headerView.setNormalState();
        updateHeaderHeight(0);
    }
}

这里对这个类做一下解释如下:

MListViewListener 是回调接口,上下拉的时候会分别回调onRefresh()和onLoadMore()方法。

LISTVIEW_STATE是ListView的状态分2种:普通和正在刷新的状态。

mScroller是回滚时用的,如果不懂可以参考我前面写的博客。

onTouchEvent是核心方法,里面监听:手指按下,抬起,滑动的事件并做处理。

moveState(float distance)是处理滑动的逻辑,滑动期间要不断的通过updateHeaderHeight()改变HeaderView的高度。

upHeightBack()是手指抬起来的时候要回滚,注意区别普通状态和刷新状态。

文章的结尾奉送上代码,方便大家对照学习。

结尾

好了就讲到这里吧,整体的逻辑不是不难的。因为明天还要做别的事情,这个半成品只能暂且搁置,先做一个记录吧,等我有空再整理整理。希望对大家有所帮助。

在技术上我依旧是个小渣渣,加油勉励自己!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序编织梦想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值