滑动列表仿ios尼阻回弹效果实现

滑动列表实现仿ios的尼阻回弹,可作用于 listView , gridView ,RecyclerView等列表组件,别的组件没试过。有兴趣的可以试一试。

实现原理:定义一个外层容器,内层可以是recyclerview,Listview,ScrollView等可以滑动的组件,主要是在事件分发上动手脚,在事件分发dispatchTouchEvent中根据内层recyclerview中可见的item是否处于顶部或者底部来处理事件分发(自己消费掉或者将事件交由当前view的onlnterceptTouchEvent进行事件拦截,而此处事件不拦截,交给recycelrview进行消费)

下面的自定义控件是一个viewGroup,列表控件嵌套在其中,可实现尼阻回弹效果,

其中ANIM_TIME这个常量代表的是回弹的时间,值越大,回弹速度越快,这个根据自己设置就好。

不多说。直接复制代码干了。。。


import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ScrollView;

import java.util.ArrayList;
import java.util.List;

import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

/**
 * 阻尼回弹效果
 *
 * @author lixinxiao
 */
public class PullRecyclerViewGroup extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener {

    /**
     * 滚动时间
     */
    private static final long ANIM_TIME = 200;

    /**
     * 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
     */
    private View childView;

    /**
     * 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
     */
    private Rect originalRect = new Rect();

    //滚动时,移动的view和位置
    private List<View> mMoveViews = new ArrayList<>();
    private List<Rect> mMoveRects = new ArrayList<>();

    /**
     * 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
     */
    private boolean isMoved = false;

    /**
     * 如果按下时不能上拉和下拉,会在手指移动时更新为当前手指的Y值
     */
    private float startY;

    /**
     * 阻尼
     */
    private static final float OFFSET_RADIO = 0.5f;

    private boolean isRecyclerReuslt = false;


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

    public PullRecyclerViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init() {
        //关闭右侧滚动条
        this.setVerticalScrollBarEnabled(false);
    }

    /**
     * 加载布局后初始化,这个方法会在加载完布局后调用
     */
    @Override
    protected void onFinishInflate() {
        //此处为容器中的子view   必须有RecyclerView、ListView、ScrollView,当然这里忽略ListView和ScrollView
        if (getChildCount() > 0) {
            for (int i = 0; i < getChildCount(); i++) {
                if (getChildAt(i) instanceof RecyclerView || getChildAt(i) instanceof ListView || getChildAt(i) instanceof ScrollView) {
                    if (childView == null) {
                        childView = getChildAt(i);
                    } else {
                        throw new RuntimeException("PullRecyclerViewGroup 中只能存在一个RecyclerView、ListView或者ScrollView");
                    }
                }
            }
        }

        if (childView == null) {
            throw new RuntimeException("PullRecyclerViewGroup 子容器中必须有一个RecyclerView、ListView或者ScrollView");
        }
        //布局重绘监听,比如华为屏幕键盘可以弹出和隐藏,改变布局,加监听就可以虽键盘弹出关闭的变化而变化
        getViewTreeObserver().addOnGlobalLayoutListener(this);

        super.onFinishInflate();
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //ScrollView中唯一的子控件的位置信息,这个位置在整个控件的生命周期中保持不变
        originalRect.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
        for (int i = 0; i < mMoveViews.size(); i++) {
            final View v = mMoveViews.get(i);
            v.addOnLayoutChangeListener(new OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
                    Rect rect = new Rect();
                    rect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
                    mMoveRects.add(rect);
                    v.removeOnLayoutChangeListener(this);
                }
            });
        }
    }

    /**
     * 事件分发
     */

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (childView == null) {
            return super.dispatchTouchEvent(ev);
        }

        //如果当前view的Y上的位置
        boolean isTouchOutOfScrollView = ev.getY() >= originalRect.bottom || ev.getY() <= originalRect.top;
        //如果不在view的范围内
        if (isTouchOutOfScrollView) {
            //如果不在view的范围内
            if (isMoved) {
                recoverLayout();
            }
            return true;
        }

        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //记录按下时的Y
                startY = ev.getY();
            case MotionEvent.ACTION_MOVE:
                float nowY = ev.getY();
                int scrollY = (int) (nowY - startY);
                if ((isCanPullDown() && scrollY > 0) || (isCanPullUp() && scrollY < 0)
                        || (isCanPullDown() && isCanPullUp())) {
                    int offset = (int) (scrollY * OFFSET_RADIO);
                    childView.layout(originalRect.left, originalRect.top + offset, originalRect.right, originalRect.bottom + offset);
                    for (int i = 0; i < mMoveViews.size(); i++) {
                        if (mMoveViews.get(i) != null) {
                            mMoveViews.get(i).layout(mMoveRects.get(i).left, mMoveRects.get(i).top + offset, mMoveRects.get(i).right, mMoveRects.get(i).bottom + offset);
                        }
                    }
                    isMoved = true;
                    isRecyclerReuslt = false;
                    return true;
                } else {
                    startY = ev.getY();
                    isMoved = false;
                    isRecyclerReuslt = true;
                    recoverLayout();
                    return super.dispatchTouchEvent(ev);
                }
            case MotionEvent.ACTION_UP:

                if (isMoved) {
                    recoverLayout();
                }

                if (isRecyclerReuslt) {
                    return super.dispatchTouchEvent(ev);
                } else {
                    return true;
                }
            default:
                return true;
        }
    }

    /**
     * 位置还原
     */
    private void recoverLayout() {

        if (!isMoved) {
            //如果不在view的范围内
            return;
        }

        for (int i = 0; i < mMoveViews.size(); i++) {
            if (mMoveRects.get(i) != null) {
                TranslateAnimation anims = new TranslateAnimation(0, 0, mMoveViews.get(i).getTop(), mMoveRects.get(i).top);
                anims.setDuration(ANIM_TIME);
                mMoveViews.get(i).startAnimation(anims);
                mMoveViews.get(i).layout(mMoveRects.get(i).left, mMoveRects.get(i).top, mMoveRects.get(i).right, mMoveRects.get(i).bottom);
            }
        }

        TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop() - originalRect.top, 0);
        anim.setDuration(ANIM_TIME);
        childView.startAnimation(anim);

        childView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);

        isMoved = false;
    }

    /**
     * 容器的的事件都在事件分发中处理,这里处理的是事件分发传递过来的事件,
     * <p>
     * 传递过来的为RecyclerVIew的事件  不拦截,直接交给reyclerview处理
     *
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }


    /**
     * 判断是否可以下拉
     *
     * @return
     */
    private boolean isCanPullDown() {

        final RecyclerView.Adapter adapter = ((RecyclerView) childView).getAdapter();
        if (null == adapter) {
            return true;
        }
        final int firstVisiblePosition = ((LinearLayoutManager) ((RecyclerView) childView).getLayoutManager()).findFirstVisibleItemPosition();
        if (firstVisiblePosition != 0 && adapter.getItemCount() != 0) {
            return false;
        }
        int mostTop = (((RecyclerView) childView).getChildCount() > 0) ? ((RecyclerView) childView).getChildAt(0).getTop() : 0;
        return mostTop >= 0;
    }


    /**
     * 判断是否可以上拉
     *
     * @return
     */
    private boolean isCanPullUp() {
        final RecyclerView.Adapter adapter = ((RecyclerView) childView).getAdapter();

        if (null == adapter) {
            return true;
        }

        final int lastItemPosition = adapter.getItemCount() - 1;
        final int lastVisiblePosition = ((LinearLayoutManager) ((RecyclerView) childView).getLayoutManager()).findLastVisibleItemPosition();

        if (lastVisiblePosition >= lastItemPosition) {
            final int childIndex = lastVisiblePosition - ((LinearLayoutManager) ((RecyclerView) childView).getLayoutManager()).findFirstVisibleItemPosition();
            final int childCount = ((RecyclerView) childView).getChildCount();
            final int index = Math.min(childIndex, childCount - 1);
            final View lastVisibleChild = ((RecyclerView) childView).getChildAt(index);
            if (lastVisibleChild != null) {
                return lastVisibleChild.getBottom() <= childView.getBottom() - childView.getTop();
            }
        }
        return false;
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onGlobalLayout() {
        //华为手机屏幕下方的返回、home键显示隐藏改变布局
        requestLayout();
        getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }

    /**
     * 跟随弹性移动的view
     *
     * @param view
     */
    public void setMoveViews(View view) {
        this.mMoveViews.add(view);
        requestLayout();
    }
}

上面是java代码的实现。下面再贴上xml代码

<com.yikelive.view.PullRecyclerViewGroup
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_siteTitle">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="16dp"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            tools:listitem="@layout/item_live_detail_ticket"/>
</com.yikelive.view.PullRecyclerViewGroup>

然后就没有了。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现移动端touch事件的横向滑动列表效果可以使用原生的JavaScript和CSS3来实现。 首先,我们需要在HTML中创建一个容器元素,用来包含列表项。容器元素需要设置overflow-x属性为scroll,使得内容超出容器范围时可以滚动。 ```html <div class="container"> <ul class="list"> <li>项1</li> <li>项2</li> <li>项3</li> <li>项4</li> <li>项5</li> </ul> </div> ``` 然后,在CSS中,我们需要设置容器元素和列表项的样式,以及使用CSS3的transition属性来实现平滑的过渡效果。 ```css .container { width: 100%; overflow-x: scroll; -webkit-overflow-scrolling: touch; /* 添加iOS滚动效果 */ } .list { display: flex; flex-wrap: nowrap; /* 设置列表项不换行 */ transition: transform 0.3s ease; /* 添加平滑的过渡效果 */ } .list li { width: 100px; height: 100px; margin-right: 10px; background-color: #ccc; } ``` 最后,在JavaScript中,我们需要监听容器元素的touchstart、touchmove和touchend事件,计算滑动距离并通过改变列表项的transform属性来实现横向滑动效果。 ```javascript const container = document.querySelector('.container'); const list = document.querySelector('.list'); let isDragging = false; let startPosition = 0; let currentTranslate = 0; let prevTranslate = 0; let animationId = 0; container.addEventListener('touchstart', touchStart); container.addEventListener('touchmove', touchMove); container.addEventListener('touchend', touchEnd); container.addEventListener('touchcancel', touchEnd); function touchStart(event) { if (event.target.classList.contains('list')) { isDragging = true; startPosition = event.touches[0].clientX; animationId = requestAnimationFrame(updateAnimation); container.classList.add('grabbing'); } } function touchMove(event) { if (isDragging) { const currentPosition = event.touches[0].clientX; currentTranslate = prevTranslate + currentPosition - startPosition; } } function touchEnd() { isDragging = false; cancelAnimationFrame(animationId); prevTranslate = currentTranslate; container.classList.remove('grabbing'); } function updateAnimation() { list.style.transform = `translateX(${currentTranslate}px)`; animationId = requestAnimationFrame(updateAnimation); } ``` 通过以上代码,我们就成功地实现了移动端touch事件的横向滑动列表效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值