Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mimic Lollipop's phone app, with resizing of upper items before the scrollable content?

Background

On Lollipop's Phone app, when you reach the "recents" or "contacts" tab, and you try to scroll, first thing that happens is resizing of the upper area, which consist of a recent event and a search box, as such :

enter image description here

If there were a lot more than 3 items on the RecyclerView, the RecyclerView won't scroll down as long as the upper area can be shrunk to its minimal size.

Same goes for scrolling up: as long as the upper area isn't fully enlarged, the RecyclerView won't scroll up.

The problem

I can't find out the best way to implement this feature.

More specifically, I have an issue with the resizing of the upper area, so that it will be as smooth as it works on Lollipop.

To simplify, I only need to resize one part of the screen, and not 2 as shown on the phone's app (the "recent event" and the searchBox) .

What I've tried

There are multiple things I've tried to do:

  1. make the RecyclerView become as large as the whole screen, while having a fake, large,empty header . when I scroll, I also set "translationY" to the real content on the top. This has an issue of showing the scrollbar as part of the upper area (and I want to show the scrollbar in the right place). Plus, it makes it quite complex since you have to handle the other pages's RecyclerViews scrolling values too. This is a solution similar to some third party libraries, such as this one (yet here I've used a layout and not an ImageView, but the code is almost identical).

  2. the RecyclerView is below the upper area, and scrolling changes the layoutParams of the upper area, similar to what I did on this post.

  3. same as 2, yet instead of changing the LayoutParams, I change the scaleY of the views.

For both #2 and #3, the problem is that on some cases, as soon as I get an event of scrolling of +Y in some value, I also get a scrolling even of -Y of the same value (and vice versa), making the handling of the resizing effect "jumpy" (up and down, even if the user scrolled nicely ).

My guess as to why this occur is that when I move the RecyclerView , the next event made by the user is on a totally different position than the original one, because I've changed the way the RecyclerView is positioned or its size.

I have tried using both the setOnTouchListener (even together with GestureDetectorCompat) and setOnScrollListener. All caused the same weird issue.

The question

How do I solve this issue? Why does it happen, and how come it doesn't always occur?


EDIT: I've tried to change #2, so that instead of handling the touch event of the RecyclerView, I will handle the touch events of its container (some LinearLayout). For some reason, the container didn't respond to touch events, so I've replaced it with RelativeLayout, and put a view on top of everything inside this layout, which will respond to touch events (and change the upper area accordingly). This worked, but then I couldn't pass the events further to the child views when needed.

I think the best thing is to customize the RecyclerView (extend it) touch handling. Maybe add a listener for scrolling, that when the RecyclerView is about to scroll, it will ask if it's ok, and if not, it will not try to scroll till the touch-up event is fired and reset its state.

like image 764
android developer Avatar asked Jan 17 '15 09:01

android developer


1 Answers

OK, one solution (that I still don't like, but works)is to have a "TouchInterceptionFrameLayout" class being used (from the library I've found) that will allow you to decide when to allow to scroll and when not, and when it shouldn't scroll, you change the padding (or what you wish ) of the layout that has both the pager-tabs and the ViewPager.

The container of the viewPager&tabs cover the entire area, and have a padding that changes.

this is the layout:

        <LinearLayout
            android:id="@+id/header"
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:layout_height="wrap_content">
            <!-- put upper content here -->

        </LinearLayout>

        <LinearLayout
            android:id="@+id/pagerAndTabsContainer"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.astuetz.PagerSlidingTabStrip
                android:id="@+id/viewPagerSlidingTabsStrip"
                android:layout_width="match_parent"
                android:background="@color/default_background"
                android:layout_height="48dip"/>

            <android.support.v4.view.ViewPager
                android:background="@color/default_background"
                android:id="@+id/viewPager"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </LinearLayout>


    </com.github.ksoichiro.android.observablescrollview.TouchInterceptionFrameLayout>

Then, you need to have some code for initialization (when the layout phase is done and you can get the real sizes of views) :

// the upper area height, that can be shrunk to a size of 0 height when needed
mResizeableViewHeight = mHeaderView.getHeight(); 
// set initial padding if you wish to show the upper area by default
mPagerAndTabsContainer.setPadding(0, mResizeableViewHeight, 0, 0); 
prepareViewPager(mViewPager); // set the pages and fragments...
// init the "TouchInterceptionFrameLayout" to handle touch events
mContentView.setScrollInterceptionListener(mInterceptionListener);

on the "shouldInterceptTouchEvent" function of TouchInterceptionFrameLayout.TouchInterceptionListener , you decide when it should block touches, and on "onMoveMotionEvent" of this interface you should handle the blocked touches.

for each of the fragments that have a recyclerView, use ObservableRecyclerView instead, and call these lines:

    // get the fragment that shows the screen I've made (with the layout)
    Fragment fragment = getTargetFragment(); 
    // let the recyclerView handle filtered touches 
    mRecyclerView.setTouchInterceptionViewGroup(((IGetContainer) fragment).getContainer());
    // if you wish to put animations when you do a touch-up event, this may be handy (or use onUpOrCancelMotionEvent of the "TouchInterceptionListener" too/instead ) :
    if (fragment instanceof ObservableScrollViewCallbacks)                 
       mRecyclerView.setScrollViewCallbacks((ObservableScrollViewCallbacks) fragment);

I don't like this solution much because it covers the entire screen with a wrapper of touches, because it causes overdraw of the viewPager (and require you to set a background for it), and because it's a bit complex compared to what I had in mind. It's also much less smooth than what the Phone app of Lollipop works, no matter how much I play with the scrolling rules.

I could make the TouchInterceptionListener start handling touch event only if they originated on the RecyclerView, but that makes it even worse in complexity.

like image 63
android developer Avatar answered Oct 15 '22 08:10

android developer