Search code examples
androidandroid-fragmentskotlinandroid-viewpagerandroid-swipe

Android, Kotlin : how can I only allow my view pager swiping on the left when needed


well hello dear viewers.

My situation :

I have a viewpager that lets my user swiping on left and right. I have a screen (a fragment), that I want to be completed by the user before he can swipe.

EDIT 4 : SOLUTION

As the ViewPager2 works like a RecyclerView, I have added a MutableList to my adapter :

     var fragmentList = mutableListOf<Fragment>(
            BasicInfoFragment.newInstance(
                activity
            ),
            BondsFragment.newInstance(
                activity
            ),
    ...

At the beginning, there only are, in the list, the fragments displayed before the "locking". When my condition is fulfilled or satisfied, I add the next fragments to the adapter's list, so they are available for swiping

Below are old things that does not work :

What I have done :

I customized the viewpager to lock swiping when I want it to be locked (see my code), but it disables the both directions.

Ideally, I want to disable only left to right swiping : I want the user can swipe back (right to left). I just want to disable swiping forward (left to right).

I tried to implements a onTouchListener on my view pager to detect the swiping direction, but it only works when the ViewPager is unlocked.

Can you help me ?

Edit 3 : New problems

I have tried to implement the viewpager2, but now I got problems with the children Layout.

When I need to display a fragment into another fragment, I use an extension method that I wrote basing me on this : how-to-add-a-fragment-in-kotlin-way

It looks like that :

    inline fun FragmentManager.doTransaction(func: FragmentTransaction.() -> Unit) {
        Log.d(TAG, "doTransaction")
        val fragmentTransaction = beginTransaction()
        fragmentTransaction.func()
        fragmentTransaction.commit()
    }

    fun AppCompatActivity.replaceFragment(frameId: Int, fragment: Fragment) {
        Log.d(TAG, "replaceFragment")
            supportFragmentManager.doTransaction { replace(frameId, fragment) }
    }

So I have several fragment having things like this :

    companion object : CustomCompanion() {
            @JvmStatic
            override fun newInstance(activity: Activity): IdealsFragment {
                val fragment =
                    IdealsFragment(
                        activity
                    )
                   
                (activity as NewCharacterActivity).replaceFragment(
                    R.id.fragmentIdeals_container_ideals,
                    IdealsRecyclerViewFragment.newInstance(
                        activity
                    )
                )
    
                return fragment
            }
        }

The thing is that with the ViewPager2, I got a "no view found for id" exception.

I understood it was because I used the fragment manager instead of the ChildSupportFragmentManager, so I replaced the fragment manager by the good one, but now I have a crash for : "Fragment has not been attached yet.".

Once again, can you help me ?

Thanks so much !

Code :

 /**
 *   Class "LockableViewPager" :
 *   Custom viewpager that allows or not to swipe between fragments.
 **/
 class LockableViewPager : ViewPager {
    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)


    companion object {
        private const val TAG = "LockableViewPager"
    }

    /**
     * Is swiping enabled ?
     */
    private var swipeEnabled = true


    /**
     * Intercept all touch screen motion events.
     * Allows to watch events as they are dispatched, and
     * take ownership of the current gesture at any point.
     * 
     * @param event : The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {
        return when (swipeEnabled) {
            true -> super.onTouchEvent(event)
            false -> //  Do nothing.
                return false
        }
    }
    

    /**
     * Implement this method to intercept all touch screen motion events.  This
     * allows you to watch events as they are dispatched to your children, and
     * take ownership of the current gesture at any point.
     *
     * @param event : The motion event being dispatched down the hierarchy.
     * @return Return true to steal motion events from the children and have
     * them dispatched to this ViewGroup through onTouchEvent().
     * The current target will receive an ACTION_CANCEL event, and no further
     * messages will be delivered here.
     */
    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        return when (swipeEnabled) {
            true -> super.onInterceptTouchEvent(event)
            false -> swipeEnabled
        }
    }

    /**
     *  Sets the swipeEnabled value to lock or unlock swiping
     */
    fun setSwipeEnabled(swipeEnabled: Boolean) {
        this.swipeEnabled = swipeEnabled
    }    
}

Edit 2 : Maybe the solution ?

I put a raw View to the bottom of the activity layout that contains the view pager :

<View
    android:id="@+id/touch_layer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="true" />

I created an abstract class that implements the View.OnTouchListener :

abstract class OverlayTouchListener : View.OnTouchListener {
    companion object{
        private const val TAG = "OverlayTouchListener"
    }
}

Deserialized the overlay view in my activity :

var touchLayer = this.findViewById<View>(R.id.touch_layer)

Added the OverlayTouchListener to the touchLayer :

touchLayer?.setOnTouchListener(object : OverlayTouchListener() {
            /**
             * Called when a touch event is dispatched to a view. This allows listeners to
             * get a chance to respond before the target view.
             *
             * @param v The view the touch event has been dispatched to.
             * @param event The MotionEvent object containing full information about
             * the event.
             * @return True if the listener has consumed the event, false otherwise.
             */
            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
                Log.d(TAG, "onTouch")
                if(event != null){
                    //  Check the swiping direction and ViewPager.isSwipeEnabled 
                    viewPager?.setSwipeEnabled(true)
                    viewPager?.onTouchEvent(event)
                }

                return true
            }

        })

Now, i just have to check if swiping is enabled. If not, call the viewPager.onTouchEvent only if the swipe is from right to left.

It's a little bit tricky, so if you have any suggestion ...


Solution

  • first I recommend using ViewPager2 as it is hugely more optimized with a RecyclerView implementation, allow you to gracefully delete pages without frame lags.

    However, whatever your choice of implementation, I would just listen to whether the page has scrolled(they both have a onPageChange interface/callback you can add/register, and when the position > currPage and (positionOffset == 0 OR scroll state == IDLE) (the page has completely settled), I would delete the previous page.

    Let me know if you would like an example, however, everything above should be all you need to do.

    If you would like to keep your old implementation, I would capture the current x location of the user's finger (rightly with onTouch) and if it's less, set ViewPager2.isUserInputEnabled = false

    ViewPager2.isUserInputEnabled = newX >= originalX 
    

    However, this is tricky since the user can use multiple fingers and the ViewPager's onScroll implementation only trigger AFTER the user has scrolled, creating jank if you disable scrolls even after 1 pixel after the user has touched the screen, I recommend the page deletion.