自定义ViewGroup实现ViewPager

1.scrollBy()和scrollTo()的区别:

scrollBy(x, y)源码:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

可以看到scrollBy()方法中就是调用了scrollTo()方法,但是传递给scrollTo()方法的参数是在当前View的偏移量的基础上增加了x,y。

这里的mScrollX,mScrollY 是偏移量,是相对自己初始位置的滑动偏移距离,只有当有scroll事件发生时,这两个方法才能有值。

所谓自己初始位置是指,控件在刚开始显示时、没有滑动前的位置。

可以看下这两个方法执行的区别

scrollBy(-30, -50)
scrollTo(-30, -50)

调用scrollBy(-30, -50) ,view就会会发生一段偏移,而scrollTo(-30, -50)view只会偏移一次。

还有一点需要注意:我们想对TextView进行移动,必须调用其父布局的scrollBy()或者scrollTo()方法,因为这两个方法是对View的内容进行偏移,并不是对View本身进行偏移。

2.getX / getY 和 getRawX / getRawY的区别

它们的区别其实很简单,getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

实现自定义ViewPager

public class MyViewPager extends ViewGroup {

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


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
        }
    }
}
public class MainActivity extends AppCompatActivity {

    private  MyViewPager myViewPager;

    private int[] ids = new int[]{R.mipmap.tu1,R.mipmap.tu2,R.mipmap.tu3};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        myViewPager = findViewById(R.id.my_view_pager);
        for (int i = 0; i < ids.length; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setBackgroundResource(ids[i]);
            myViewPager.addView(imageView);
        }

    }
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.customviewpager.MyViewPager
        android:id="@+id/my_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

重写onLayout()方法,获取所有的子View,各自调用layout()方法,确定它们各自的摆放位置。

  @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
        }
    }

接下来要让让ViewGroup中的元素,跟着手的滑动而滑动了。使用手势识别器Gesturedetector来处理滑动事件。

(1) 创建一个手势识别器:这里主要就是靠 scrollBy()方法,来实现View跟随手的滑动而滑动。这个方法会调用onScrollChanged方法,并刷新视图。

     mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //当有手指在屏幕上滑动的时候回调
                // distanceX为正时,向左移动,为负时,向右移动
                //dx表示x方向上移动的距离,dy表示y方向上移动的距离。往坐标轴正方向上移动的话,值就是正值;反之为负
               
                scrollBy((int) distanceX, 0);
                return super.onScroll(e1, e2, distanceX, distanceY);
            }
        });

(2)重写onTouchEvent(),将触摸事件传递给手势识别器处理,并返回true,让该控件消费该事件。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        return true;
    }

现在View已经可以跟随我们的手势滑动了,但离我们预期的效果,还差两个小问题待解决:边界情况的处理和平滑的回弹到指定位置。

(3)边界情况的处理。
我们期望的效果是:手指松开时,当滑动偏移的距离超出图片1/2时,自动切换到下个图片;小于1/2,回弹到初始位置。这里我们需要在onTouchEvent()中处理触摸事件,具体代码实现如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //滑到最后一张的时候,不能出边界
                if (position >= getChildCount()) {
                    position = getChildCount() - 1;
                }

                if (position < 0) {
                    position = 0;
                }
                break;
            case MotionEvent.ACTION_UP:
                //绝对滑动,直接滑到指定的x,y的位置,较迟钝
                scrollTo(position * getWidth(), 0);
                break;
        }

        return true;
    }

这里暂时我们使用的scrollTo(int x,int y)这个方法:让它到某个临界值时,滑动到指定位置,由于它是让view直接滚动到参数x和y所标定的坐标,可以看到下面的运行效果很迟钝。

(4) 如何实现平滑的回弹到指定位置呢?这里就要用到Scroller这个类了。
Android里Scroller类是为了实现View平滑滚动的一个Helper类。通常在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会导致View的滚动,通常是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。

Scroller mScroller = new Scroller(mContext);

在onTouchEvent()中的up事件中将scrollTo()方法替换为:mScroller.startScroll();

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //滑到最后一张的时候,不能出边界
                if (position >= getChildCount()) {
                    position = getChildCount() - 1;
                }

                if (position < 0) {
                    position = 0;
                }
                break;
            case MotionEvent.ACTION_UP:
                //绝对滑动,直接滑到指定的x,y的位置,较迟钝
                // scrollTo(position * getWidth(), 0);
                //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量
                mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
                //使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法
                invalidate();
                break;
        }

        return true;
    }

其实Scroller的原理就是用ScrollTo()来一段一段的进行,最后看上去跟自然的一样,必须使用postInvalidate(),这样才会一直回调computeScroll()这个方法,直到滑动结束。

@Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();
        }
    }

MyViewPager是左右滑动,子View(ScrollView)是上下滑动。事件传递的过程中,如果父View无拦截无消耗,那么当事件传递到子View时,默认会被子View(ScrollView)消费,那么事件在ScrollView中就传递结束了,所以父View(MyViewPager)的左右滑动就失效了。

解决冲突的办法:

就是重写父View的onInterceptTouchEvent()事件,在合适的时候,拦截该事件。

onInterceptTouchEvent()方法返回值的含义:

1、 如果return true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
2、 如果return false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;

根据我们的期望的效果:左右滑动时,让父View消费该事件;上下滑动时,直接放行,让子View(ScrollView)自己处理。代码如下:

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                originalX = (int) ev.getX();
                originalY = (int) ev.getY();
                //这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
                mGestureDetector.onTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                //上下滑动拦截,左右滑动不拦截
                int currentX = (int) ev.getX();
                int currentY = (int) ev.getY();

                int dx = currentX - originalX;
                int dy = currentY - originalY;

                if (Math.abs(dx) > Math.abs(dy)) {
                    // 左右滑动
                    return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理
                } else {
                    return false;
                }
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }

        return super.onInterceptTouchEvent(ev);
    }

完整代码:

package com.example.customviewpager;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;


public class MyViewPager extends ViewGroup {

    private GestureDetector mGestureDetector;
    private int scrollX;
    private int position;
    private Scroller mScroller;
    private int originalX = 0;
    private int originalY = 0;

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

    private void initView(Context context) {
        mScroller = new Scroller(context);
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //当有手指在屏幕上滑动的时候回调
                // distanceX为正时,向左移动,为负时,向右移动
                // 移动屏幕的方法scrollBy,很重要,这个方法会调用onScrollChanged方法,并刷新视图
                //dx表示x方向上移动的距离,dy表示y方向上移动的距离。往坐标轴正方向上移动的话,值就是正值;反之为负
                // scrollBy内部实际上是重写了scrollTo方法,scrollTo是将当前视图的基准点移动到某个坐标点
                scrollBy((int) distanceX, 0);
                return super.onScroll(e1, e2, distanceX, distanceY);
            }
        });

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                originalX = (int) ev.getX();
                originalY = (int) ev.getY();
                //这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
                mGestureDetector.onTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                //上下滑动拦截,左右滑动不拦截
                int currentX = (int) ev.getX();
                int currentY = (int) ev.getY();

                int dx = currentX - originalX;
                int dy = currentY - originalY;

                if (Math.abs(dx) > Math.abs(dy)) {
                    // 左右滑动
                    return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理
                } else {
                    return false;
                }
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }

        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                scrollX = getScrollX();//相对于初始位置滑动的距离
                //你滑动的距离加上屏幕的一半,除以屏幕宽度,就是当前图片显示的pos.如果你滑动距离超过了屏幕的一半,这个pos就加1
                position = (getScrollX() + getWidth() / 2) / getWidth();
                //滑到最后一张的时候,不能出边界
                if (position >= getChildCount()) {
                    position = getChildCount() - 1;
                }

                if (position < 0) {
                    position = 0;
                }
                break;
            case MotionEvent.ACTION_UP:
                //绝对滑动,直接滑到指定的x,y的位置,较迟钝
                // scrollTo(position * getWidth(), 0);
                //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量
                mScroller.startScroll(scrollX, 0, -(scrollX - position * getWidth()), 0);
                //使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法
                invalidate();
                break;
        }

        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
        }
    }
}
package com.example.customviewpager;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.ImageView;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private  MyViewPager myViewPager;

    private int[] ids = new int[]{R.mipmap.tu1,R.mipmap.tu2,R.mipmap.tu3};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        myViewPager = findViewById(R.id.my_view_pager);
        for (int i = 0; i < ids.length; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setBackgroundResource(ids[i]);
            myViewPager.addView(imageView);
        }

        View textView = View.inflate(this,R.layout.scroll_view,null);
        myViewPager.addView(textView);
    }
}

原创微博https://www.jianshu.com/p/af8e14ff5f0c

demo链接https://download.csdn.net/download/jingerlovexiaojie/12400548

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以使用ViewPager2来实现RecyclerView的左右滑动功能。以下是一些步骤可以帮助您完成这个实现: 1. 首先,在您的布局文件中,将ViewPager2添加为父容器,并设置其布局属性,以适应您的需求。例如: ```xml <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" /> ``` 2. 在您的活动或碎片中,找到对应的ViewPager2视图,并获取其实例: ```java ViewPager2 viewPager = findViewById(R.id.viewPager); ``` 3. 创建一个适配器类来管理RecyclerView的内容。这里我们使用RecyclerViewAdapter作为示例: ```java public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> { // 实现适配器的其他方法 // ... } ``` 4. 在您的活动或碎片中,初始化适配器和RecyclerView实例,并将RecyclerView设置给ViewPager2: ```java RecyclerViewAdapter adapter = new RecyclerViewAdapter(); RecyclerView recyclerView = new RecyclerView(this); recyclerView.setAdapter(adapter); viewPager.setAdapter(new RecyclerViewAdapterWrapper(recyclerView)); ``` 5. 创建一个RecyclerViewAdapterWrapper类,继承自RecyclerView.Adapter,用于将RecyclerView适配给ViewPager2: ```java public class RecyclerViewAdapterWrapper extends RecyclerView.Adapter<RecyclerViewAdapterWrapper.ViewHolder> { private RecyclerView recyclerView; public RecyclerViewAdapterWrapper(RecyclerView recyclerView) { this.recyclerView = recyclerView; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(recyclerView); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { // 不需要做任何操作 } @Override public int getItemCount() { return 1; } static class ViewHolder extends RecyclerView.ViewHolder { ViewHolder(@NonNull View itemView) { super(itemView); } } } ``` 6. 最后,您可以在RecyclerViewAdapter类中实现RecyclerView的内容和逻辑,根据您的需求进行自定义。 现在,您就可以在ViewPager2中左右滑动RecyclerView了。注意,ViewPager2还可以与其他类型的视图(如Fragment)结合使用,以实现更丰富的界面效果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值