14

I have a concept for a view. Please guide me as to how I can achieve it. Please check the wireframe. enter image description here

I have already looked at FadingActionBar but it does not seem to help. The problem is I have multiple viewpagers in the screen and am going no where trying to achieve the desired result. It would be super-awesome if I am able to achieve a cool transition/parallax effect.

Any inputs will be much appreciated.

Edit1 :

The tabs are put on a PagerTabStrip and hooked up with the Viewpager below it. The attempt here is to scroll the view and dock the PagerTabStrip to the ActionBar and on Scroll down bring it down to reveal the ImageViewPager.

Neo
  • 1,181
  • 11
  • 22

3 Answers3

15

So, this can be achieved pretty easily, but it requires a little trick, more like an illusion actually. Also, I'm going to be using a ListView instead of a ScrollView for my "scrollable content", mostly because it's easier to work with in this situation and for my tabs I'll be using this open sourced library.

First, you need a View that can store y-coordinates for a given index. This custom View will be placed on top of your bottom ViewPager and appear as the "real" header for each ListView. You need to remember the header's y-coordinate for each page in the ViewPager so you can restore them later as the user swipes between them. I'll expand on this later, but for now here's what that View should look like:

CoordinatedHeader

public class CoordinatedHeader extends FrameLayout {

    /** The float array used to store each y-coordinate */
    private final float[] mCoordinates = new float[5];

    /** True if the header is currently animating, false otherwise */
    public boolean mAnimating;

    /**
     * Constructor for <code>CoordinatedHeader</code>
     * 
     * @param context The {@link Context} to use
     * @param attrs The attributes of the XML tag that is inflating the view
     */
    public CoordinatedHeader(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Animates the header to the stored y-coordinate at the given index
     * 
     * @param index The index used to retrieve the stored y-coordinate
     * @param duration Sets the duration for the underlying {@link Animator}
     */
    public void restoreCoordinate(int index, int duration) {
        // Find the stored value for the index
        final float y = mCoordinates[index];
        // Animate the header to the y-coordinate
        animate().y(y).setDuration(duration).setListener(mAnimatorListener).start();
    }

    /**
     * Saves the given y-coordinate at the specified index, the animates the
     * header to the requested value
     * 
     * @param index The index used to store the given y-coordinate
     * @param y The y-coordinate to save
     */
    public void storeCoordinate(int index, float y) {
        if (mAnimating) {
            // Don't store any coordinates while the header is animating
            return;
        }
        // Save the current y-coordinate
        mCoordinates[index] = y;
        // Animate the header to the y-coordinate
        restoreCoordinate(index, 0);
    }

    private final AnimatorListener mAnimatorListener = new AnimatorListener() {

        /**
         * {@inheritDoc}
         */
        @Override
        public void onAnimationCancel(Animator animation) {
            mAnimating = false;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onAnimationEnd(Animator animation) {
            mAnimating = false;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onAnimationRepeat(Animator animation) {
            mAnimating = true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onAnimationStart(Animator animation) {
            mAnimating = true;
        }
    };

}

Now you can create the main layout for your Activity or Fragment. The layout contains the bottom ViewPager and the CoordinatedHeader; which consists of, the bottom ViewPager and tabs.

Main layout

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <android.support.v4.view.ViewPager
        android:id="@+id/activity_home_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <org.seeingpixels.example.widget.CoordinatedHeader
        android:id="@+id/activity_home_header"
        android:layout_width="match_parent"
        android:layout_height="250dp" >

        <android.support.v4.view.ViewPager
            android:id="@+id/activity_home_header_pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <com.astuetz.viewpager.extensions.PagerSlidingTabStrip
            android:id="@+id/activity_home_tabstrip"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_gravity="bottom"
            android:background="@android:color/white" />
    </org.seeingpixels.example.widget.CoordinatedHeader>

</FrameLayout>

The only other layout you need is a "fake" header. This layout will be added to each ListView, giving the illusion the CoordinatedHeader in the main layout is the real one.

Note It's important that the height of this layout is the same as the CoordinatedHeader in the main layout, for this example I'm using 250dp.

Fake header

<View xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="250dp" />

Now you need to prepare each Fragment that will be displayed in the bottom ViewPager to control the CoordinatedHeader by attaching a AbsListView.OnScrollListener to your ListView. This Fragment should also pass a unique index upon creation using Fragment.setArguments. This index should represent its location in the ViewPager.

Note I'm using a ListFragment in this example.

Scrollable content Fragment

/**
 * {@inheritDoc}
 */
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    final Activity a = getActivity();

    final ListView list = getListView();
    // Add the fake header
    list.addHeaderView(LayoutInflater.from(a).inflate(R.layout.view_fake_header, list, false));

    // Retrieve the index used to save the y-coordinate for this Fragment
    final int index = getArguments().getInt("index");

    // Find the CoordinatedHeader and tab strip (or anchor point) from the main Activity layout
    final CoordinatedHeader header = (CoordinatedHeader) a.findViewById(R.id.activity_home_header);
    final View anchor = a.findViewById(R.id.activity_home_tabstrip);

    // Attach a custom OnScrollListener used to control the CoordinatedHeader 
    list.setOnScrollListener(new OnScrollListener() {

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                int totalItemCount) {

            // Determine the maximum allowed scroll height
            final int maxScrollHeight = header.getHeight() - anchor.getHeight();

            // If the first item has scrolled off screen, anchor the header
            if (firstVisibleItem != 0) {
                header.storeCoordinate(index, -maxScrollHeight);
                return;
            }

            final View firstChild = view.getChildAt(firstVisibleItem);
            if (firstChild == null) {
                return;
            }

            // Determine the offset to scroll the header
            final float offset = Math.min(-firstChild.getY(), maxScrollHeight);
            header.storeCoordinate(index, -offset);
        }

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            // Nothing to do
        }

    });
}

Finally, you'll need to setup the Coordinated header to restore its y-coordinates when the user swipes between pages using a ViewPager.OnPageChangeListener.

Note When attaching your PagerAdapter to your bottom ViewPager, it's important to call ViewPager.setOffscreenPageLimit and set that amount to the total amount of pages in your PagerAdapter. This is so the CoordinatedHeader can store the y-coordinate for each Fragment right away, otherwise you'll run into trouble with it being out of sync.

Main Activity

/**
 * {@inheritDoc}
 */
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);

    // Setup the top PagerAdapter
    final PagerAdapter topAdapter = new PagerAdapter(getFragmentManager());
    topAdapter.buildData(DummyColorFragment.newInstance(Color.RED));
    topAdapter.buildData(DummyColorFragment.newInstance(Color.WHITE));
    topAdapter.buildData(DummyColorFragment.newInstance(Color.BLUE));

    // Setup the top pager
    final ViewPager topPager = (ViewPager) findViewById(R.id.activity_home_header_pager);
    topPager.setAdapter(topAdapter);

    // Setup the bottom PagerAdapter
    final PagerAdapter bottomAdapter = new PagerAdapter(getFragmentManager());
    bottomAdapter.buildData(DummyListFragment.newInstance(0));
    bottomAdapter.buildData(DummyListFragment.newInstance(1));
    bottomAdapter.buildData(DummyListFragment.newInstance(2));
    bottomAdapter.buildData(DummyListFragment.newInstance(3));
    bottomAdapter.buildData(DummyListFragment.newInstance(4));

    // Setup the bottom pager
    final ViewPager bottomPager = (ViewPager) findViewById(R.id.activity_home_pager);
    bottomPager.setOffscreenPageLimit(bottomAdapter.getCount());
    bottomPager.setAdapter(bottomAdapter);

    // Setup the CoordinatedHeader and tab strip
    final CoordinatedHeader header = (CoordinatedHeader) findViewById(R.id.activity_home_header);
    final PagerSlidingTabStrip psts = (PagerSlidingTabStrip) findViewById(R.id.activity_home_tabstrip);
    psts.setViewPager(bottomPager);
    psts.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
        @Override
        public void onPageScrollStateChanged(int state) {
            if (state != ViewPager.SCROLL_STATE_IDLE) {
                // Wait until the pager is idle to animate the header
                return;
            }
            header.restoreCoordinate(bottomPager.getCurrentItem(), 250);
        }
    });
}
adneal
  • 30,484
  • 10
  • 122
  • 151
  • Woah! The video looks exactly like what I am after. Shall accept after I implement it. Thank you so much! – Neo Jan 30 '14 at 03:09
  • hi @adneal if you provide this as project,it will be more helpful. – Dinesh Raj Mar 25 '14 at 12:27
  • @DineshRaj All of the source is there, if there's a particular part you'd like help with, I'd be happy to answer that question. – adneal Mar 26 '14 at 06:57
  • ok then i send you the code i implemented now,plese help me to correct the problem . – Dinesh Raj Mar 26 '14 at 07:07
  • @adneal the pagestrip and listview are not in sync while scrolling.here is the link of my project.please help me to solve this issue alone. http://goo.gl/TZy9oH. Thank you – Dinesh Raj Mar 26 '14 at 07:16
  • You're doing two things wrong. 1) The size of the stored coordinates in `CoordinatedHeader` has to be greater than or equal to the number of `Fragment`s in the bottom pager. In your case, replace `5` with `8`. 2) You're passing in the wrong key into `ScrollableContent`. You're using `ARG_OBJECT`, but you should be using `ARG_SECTION_NUMBER`. If you need more help, you should post a question. – adneal Mar 26 '14 at 14:10
  • @adneal what do you mean by You're using ARG_OBJECT, but you should be using ARG_SECTION_NUMBER in the above comment – Ramz Jun 21 '14 at 19:50
  • @Ramz They're just constants he had defined in his app. – adneal Jun 21 '14 at 19:55
  • @adneal hai can i fix the tab like google plus profile page.i mean if i scroll one tab go to next it will showto scroll againfrom the beginning for the first time so my question is if i scroll tab one after scrolling little bit i select the tab 2.so the tab2list want to scroll up to the tab strip.This is just like google plus? how can be it done using this example – Ramz Jun 22 '14 at 12:25
  • 1
    @Ramz You'd need to calculate the header offset every time you swiped to a new page, then pass that number into `ListView.setSelectionFromTop`. But you don't need the `CoorindatedHeader` in that case. Something like: `ListView.setSelectionFromTop(1, headerHeight + headerY)` – adneal Jun 22 '14 at 12:47
  • i dont get that completely – Ramz Jun 22 '14 at 14:17
  • @adnel i not getting the answer i use an interface and pass the value but its not solving the problem – Ramz Jun 22 '14 at 15:57
  • @adneal is it possible to achieve this in scroll view, I tried the code snippet and it's pretty straight forward, so that i compiled it successfully for listview, but i'm wondering is there any way to achieve this with ScrollView. Your help is highly appreciated. – Nitin Misra Nov 13 '14 at 11:10
  • @NitinMisra As long as you calculate an offset to store. You could also just create an `Adapter` with one item, if you still wanted to use `ListView`. – adneal Nov 14 '14 at 02:20
  • Great solution but what if the header offset is not fixed? Say it changes based on user interaction. How should we handle the change? – SepehrM Aug 02 '16 at 18:30
3

You can also achieve this effect by using the Android-ParallaxHeaderViewPager a good example of scrolling tab header by kmshack Github page

The Sample code is give in this Here Git Hub link

Here is the Screen shot enter image description here

The solution by @adneal is also very useful one to achieve scrolling Tab header.

Hop this will help you

New update

Please check this answer Google+ profile like scrolling Effect

Community
  • 1
  • 1
Ramz
  • 7,116
  • 6
  • 63
  • 88
0

I use fragment container layout instead viewpager with FadingActionBar. Also i use MaterialTabs library.

Content layout

<xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/windowBackground"
    android:orientation="vertical">

    <it.neokree.materialtabs.MaterialTabHost
        android:id="@+id/materialTabHost"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        app:accentColor="@color/second"
        app:primaryColor="@color/actionbar_background"
        app:textColor="#FFFFFF" />

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="wrap_content"
        android:layout_height="match_parent">

    </FrameLayout>
</LinearLayout>

Activity

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

                FadingActionBarHelper helper = new FadingActionBarHelper()
                        .actionBarBackground(R.color.actionbar_background)
                        .headerLayout(R.layout.actionbar_header)
                        .contentLayout(R.layout.content_layout);
                final View root = helper.createView(this);
                setContentView(root);
                helper.initActionBar(this);
 mTabHost = (MaterialTabHost) view.findViewById(R.id.materialTabHost);
        //.......
        }
     @Override
        public void onTabSelected(MaterialTab materialTab) {
            int position = materialTab.getPosition();
            switch (position) {
                case 0:
                    switchFragment(new ProfileFragment(), position);
                    break;
                case 1:
                    switchFragment(new NotificationFragment(), position);
                    break;
            }
            mTabHost.setSelectedNavigationItem(position);
        }

Finally a have this result: link(gif)

user1381126
  • 73
  • 1
  • 5