I have a concept for a view. Please guide me as to how I can achieve it. Please check the wireframe.
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 CoordinatedHeader
*
* @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
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
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);
}
});
}