5

I am working with ViewPager2 where i have 2 fragments in the viewpager. It works perfectly but there is a problem that i want it to just be swiped from FragmentTwo to FragmentOne fragment as it is working now. But when i swipe it from FragmentOne to FragmentTwo then it will be able to swipe only from the FirstFragment child fragment of FragmentOne fragment.

Image of my project, the image

In this image MainActivity is base activity where the viewpager2 is placed. It contains 2 fragment, FragmentOne and FragmentTwo. FragmentOne has a customized viewpager2 for BottomNavigationView. It has 3 fragments the first one is HomeFragment and this fragment has a button two transact to FragmentTwo. So i just want that the swipe transaction can be happen only from the HomeFragment, Not from its child or other two fragments. Like instagram where we can just swipe only from HomeFragment to ChatFragment.

Edited

Code for ViewPager in MainActivity

  override fun onCreate(savedInstanceState: Bundle?) {
    val mViewPager = findViewById<ViewPager2>(R.id.view_pager_main_activity)
        mViewPager?.offscreenPageLimit = 2
        mViewPager?.apply { // to disable overScrollAnimation
           (getChildAt(0) as? RecyclerView)?.overScrollMode = RecyclerView.OVER_SCROLL_NEVER
        }
  }


internal class MyViewPagerAdapter(fragmentActivity: FragmentActivity)
    :FragmentStateAdapter(fragmentActivity)
{

    private val fragments: ArrayList<Fragment> = ArrayList()

    fun addFragment(fragment: Fragment)
    {
        fragments.add(fragment)
    }

    override fun getItemCount(): Int {
        return 2
    }

    override fun createFragment(position: Int): Fragment {
        when (position) {
            0 -> {
                return MainFragment()
            }
            1 ->{
                return MessengerFragment()
            }
        }
        return fragments[position]
    }

}

I've tried this way by getting the current fragment & last fragment & .isVisible or .add fragment in the MainActivity but nothing works.

Example:

if (!HomeFragment().isVisible){
            mViewPager?.isUserInputEnabled = false
        }
mr. groot
  • 334
  • 4
  • 18

2 Answers2

1

This is an awful problem. On ios it is much easier. For me this worked:

If you attach the background, so the pager, the swipe is completely normal. But if you attach a child of the current fragment, the swipe will be registered in the onTouch method of the child. Thats pretty basic right.

You cannot return false from the onTouch interface of the child, because you have to touch it vertically...

Solution: You create a SwipeListener for every child view that can be attached. The SwipeListener can detect if you are swiping.

But that's not the plan. You have to check whether the user swiped vertically. If so you can get the offsetX. It is normally a very small amount, which depends on the scroll speed. You connect the offsetX from the SwipeListener with the fakeDragBy(float) of the viewPager.

The scroll from the child will be put on the viewPager scroll. You connect onSwipe(...) with fakeDragBy(...).

Of course you have to calculate the amount of fakeDragBy but that is simple. Look up the documentary. docs

Tip: Call beginFakeDrag before dragging.

Example:

package com.exsent.app.act;

import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;

import com.exsent.app.R;

/**
* Created by Jim-Linus Valentin Ahrend on 3/26/21.
* 
**/
public class EXAMPLE extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ViewPager2 vp2 = findViewById(R.id.viewPager);
    RecyclerView.Adapter anyAdapter = /* your adapter */ null;
    vp2.setAdapter(anyAdapter);

    //so this is the basic background
    //now goto the Adapter


}

void drag(int dx){
    ViewPager2 vp2 = findViewById(R.id.viewPager);
    vp2.beginFakeDrag();
    vp2.fakeDragBy(dx);
    
}

static class MyExampleAdapter extends 
     RecyclerView.Adapter<MyExampleAdapter.ViewHolder>{

    private EXAMPLE x;
    MyExampleAdapter(EXAMPLE x){this.x = x;}
    private GestureDetector detector;

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int 
         position) {

        if(position == 0){

            //gesture detector

            this.detector = new GestureDetector(x, new 
       MyGestureListener());

            RecyclerView recyclerView = 
        holder.itemView.findViewWithTag("recycler");
            recyclerView.setOnTouchListener(new 
         View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    return detector.onTouchEvent(event);
                }
            });


        }

    }

    @Override
    public int getItemCount() {
        return 2;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

        FrameLayout fragemntView = new FrameLayout(x);
        fragemntView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

        RecyclerView recyclerView = new RecyclerView(x);
        recyclerView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        recyclerView.setTag("recycler");

        fragemntView.addView(recyclerView);

        return new ViewHolder(fragemntView);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
        }


    }

    static class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
        
        
        EXAMPLE e;
        public MyGestureListener(EXAMPLE e){
            this.e = e;
        }

        @Override
        public boolean onDown(MotionEvent event) {
            Log.d("TAG","onDown: ");

            // don't return false here or else none of the other
            // gestures will work
            return true;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            Log.i("TAG", "onSingleTapConfirmed: ");
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.i("TAG", "onLongPress: ");
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            Log.i("TAG", "onDoubleTap: ");
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                                float distanceX, float distanceY) {
            Log.i("TAG", "onScroll: ");
            
            
            //drag
            if(distanceX>distanceY){ //this condition should check if it is vertical or horizontal. you can define it better if you want.
                
                e.drag(distanceX);
                
            }
            
            return true;
        }

        @Override
        public boolean onFling(MotionEvent event1, MotionEvent 
                               event2,
                               float velocityX, float velocityY) {
            Log.d("TAG", "onFling: ");
            return true;
        }
    }
   }
}
  • Hey can you please give me some codebase example, I have tried with touch listeners in child fragments but it does not work. and as you wrote about fakeDragBy with swipeListener... i really did not get it that how & where to use them. – mr. groot Mar 26 '21 at 08:27
  • i tried it, and after a run i got blank white screen. i think it is not getting the viewPager. please check the code i added in the question. – mr. groot Mar 28 '21 at 05:56
1

Well, as ViewPager2 is a final class, we cannot simply extend it and override the onInterceptTouchEvent to achive this one-way swap feature.

But I think we can use a custom view class extended from FrameLayout which accepts a ViewPager2 as its only child, then override the dispatchTouchEvent() to manipulate the touch event dispatching.

Register a listener to ViewPager2's page change listener, and store the current page value when the callback is invoked.

In the dispatchTouchEvent(), when the page is 1, call super.dispatchTouchEvent() for normal dispatching, when the page is 0, dispatch the motion event object directly to ViewPager2's first child.

I didn't write actual code to prove my thought. if you need, I could write a sample project to see if it actually work.

bbsimon
  • 237
  • 2
  • 2
  • a sample project I wrote https://github.com/ccl1115/one-way-view-pager-2-wrapper.git – bbsimon Mar 31 '21 at 13:11
  • Hey I'm sorry I didn't implement it yet, but will it work for a child fragment of the viewpager's first page since you did not mention about any child fragments anywhere in answer or even in repo. So can you please add one more viewpager in it's first page and then try it to swipe from the first child of the first page's viewpager. Thank you. – mr. groot Mar 31 '21 at 14:08
  • The main idea to make this happens is to skip the dispatching of touch events to the ViewPager2 but to it's chrildren directly. I didn't use Fragment for this quick demo, but fragments are just container of views with lifecycles information, essentially what we deal with are still the views. what I implemented in the demo is just you can swipe from page 1 to page 2, and when you on page 2, the swipe is disabled by skipping dispatch events to the ViewPager2. And you can add other logic to decide when to skip it. I'll implement the scenario you described if I have free time. – bbsimon Mar 31 '21 at 15:27
  • And I'll add some comments in the demo to help you understand. – bbsimon Mar 31 '21 at 15:28