Background
Consider the following scenario:
There are 2 viewPagers, each of different width and height, but both have the exact same number of pages.
dragging on one should drag the other, so the effect is like a "parallax" effect.
same as #2, but for the other viewPager.
there is also an auto-switching mechanism between the viewpagers, so every 2 seconds it will auto-switch to the next page (and if on the last page, switch back to the first).
The problem
I think I got some functionality working, but it has some issues:
This is just simple XML. nothing special here.
I've used 'OnPageChangeListener' and there, in 'onPageScrolled' , I've used fake-dragging. Problem is, if the dragging of the user stops near the middle of the page (thus activates auto-scrolling the viewPager left/right), the fake dragging loses its sync, and weird things can occur: wrong page or even staying between pages...
For now, I don't handle the other viewPager with the dragging of the user, but it might be an issue too.
this can be an issue when I succeed solving #2 (or #3). For now, I'm using a handler that use a runnable and calls this command inside :
mViewPager.setCurrentItem((mViewPager.getCurrentItem() + 1) % mViewPager.getAdapter().getCount(), true);
What I've tried
I've tried this post, but it didn't work well, as it got the same issues when the user-dragging stops before finished swithching to another page.
Only similar solution that works, is with a single ViewPager (here), but I need to work with 2 ViewPagers, each affects the other.
So I've tried doing it myself: suppose one viewPager is "viewPager", and the other (that is supposed to follow "viewPager") is "viewPager2" , this is what I've done:
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
int lastPositionOffsetPixels=Integer.MIN_VALUE;
#Override
public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) {
if (!viewPager2.isFakeDragging())
viewPager2.beginFakeDrag();
if(lastPositionOffsetPixels==Integer.MIN_VALUE) {
lastPositionOffsetPixels=positionOffsetPixels;
return;
}
viewPager2.fakeDragBy((lastPositionOffsetPixels - positionOffsetPixels) * viewPager2.getWidth() / viewPager.getWidth());
lastPositionOffsetPixels = positionOffsetPixels;
}
#Override
public void onPageSelected(final int position) {
if (viewPager2.isFakeDragging())
viewPager2.endFakeDrag();
viewPager2.setCurrentItem(position,true);
}
#Override
public void onPageScrollStateChanged(final int state) {
}
});
I've chosen to use fake-dragging because even the docs say it could be useful for this exact same scenario :
A fake drag can be useful if you want to synchronize the motion of the
ViewPager with the touch scrolling of another view, while still
letting the ViewPager control the snapping motion and fling behavior.
(e.g. parallax-scrolling tabs.) Call fakeDragBy(float) to simulate the
actual drag motion. Call endFakeDrag() to complete the fake drag and
fling as necessary.
To make it easy to test, here's more code:
MainActivity.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
final ViewPager viewPager2 = (ViewPager) findViewById(R.id.viewPager2);
viewPager.setAdapter(new MyPagerAdapter());
viewPager2.setAdapter(new MyPagerAdapter());
//do here what's needed to make "viewPager2" to follow dragging on "viewPager"
}
private class MyPagerAdapter extends PagerAdapter {
int[] colors = new int[]{0xffff0000, 0xff00ff00, 0xff0000ff};
#Override
public int getCount() {
return 3;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
((ViewPager) container).removeView((View) object);
}
#Override
public boolean isViewFromObject(final View view, final Object object) {
return (view == object);
}
#Override
public Object instantiateItem(final ViewGroup container, final int position) {
TextView textView = new TextView(MainActivity.this);
textView.setText("item" + position);
textView.setBackgroundColor(colors[position]);
textView.setGravity(Gravity.CENTER);
final LayoutParams params = new LayoutParams();
params.height = LayoutParams.MATCH_PARENT;
params.width = LayoutParams.MATCH_PARENT;
params.gravity = Gravity.CENTER;
textView.setLayoutParams(params);
textView.setTextColor(0xff000000);
container.addView(textView);
return textView;
}
}
activity_main
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="viewPager:"/>
<android.support.v4.view.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="viewPager2:"/>
<android.support.v4.view.ViewPager
android:id="#+id/viewPager2"
android:layout_width="match_parent"
android:layout_height="100dp"/>
</LinearLayout>
The question
The code almost works well. I just need to know, what is missing to avoid the issues of stopping the dragging (by the user)?
The other issues might have easier solutions.
A possible solution
After a lot of work, we've found a solution that almost has no bugs. There is a rare bug that while scrolling, the other viewPager flashes. Weird thing is that it happens only when the user scrolls the second viewPager.
All other tries had other issues, like empty page or "jumpy"/"rubber" effect when finishing the scrolling to a new page.
Here's the code:
private static class ParallaxOnPageChangeListener implements ViewPager.OnPageChangeListener {
private final AtomicReference<ViewPager> masterRef;
/**
* the viewpager that is being scrolled
*/
private ViewPager viewPager;
/**
* the viewpager that should be synced
*/
private ViewPager viewPager2;
private float lastRemainder;
private int mLastPos = -1;
public ParallaxOnPageChangeListener(ViewPager viewPager, ViewPager viewPager2, final AtomicReference<ViewPager> masterRef) {
this.viewPager = viewPager;
this.viewPager2 = viewPager2;
this.masterRef = masterRef;
}
#Override
public void onPageScrollStateChanged(int state) {
final ViewPager currentMaster = masterRef.get();
if (currentMaster == viewPager2)
return;
switch (state) {
case ViewPager.SCROLL_STATE_DRAGGING:
if (currentMaster == null)
masterRef.set(viewPager);
break;
case ViewPager.SCROLL_STATE_SETTLING:
if (mLastPos != viewPager2.getCurrentItem())
viewPager2.setCurrentItem(viewPager.getCurrentItem(), false);
break;
case ViewPager.SCROLL_STATE_IDLE:
masterRef.set(null);
viewPager2.setCurrentItem(viewPager.getCurrentItem(), false);
mLastPos = -1;
break;
}
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (masterRef.get() == viewPager2)
return;
if (mLastPos == -1)
mLastPos = position;
float diffFactor = (float) viewPager2.getWidth() / this.viewPager.getWidth();
float scrollTo = this.viewPager.getScrollX() * diffFactor + lastRemainder;
int scrollToInt = scrollTo < 0 ? (int) Math.ceil(scrollTo) : (int) Math.floor(scrollTo);
lastRemainder = scrollToInt - scrollTo;
if (mLastPos != viewPager.getCurrentItem())
viewPager2.setCurrentItem(viewPager.getCurrentItem(), false);
viewPager2.scrollTo(scrollToInt, 0);
}
#Override
public void onPageSelected(int position) {
}
}
usage:
/**the current master viewPager*/
AtomicReference<ViewPager> masterRef = new AtomicReference<>();
viewPager.addOnPageChangeListener(new ParallaxOnPageChangeListener(viewPager, viewPager2, masterRef));
viewPager2.addOnPageChangeListener(new ParallaxOnPageChangeListener(viewPager2, viewPager, masterRef));
For the auto-switching, the original code works fine.
Github Project
Since it's quite hard to test it, and I want to make it easier for you guys to try it out, here's a Github repo:
[https://github.com/AndroidDeveloperLB/ParallaxViewPagers][4]
Do note that as I've mentioned, it still has issues. Mainly the "flashing" effect from time to time, but for some reason, only when scrolling on the second ViewPager.
I don't think 2 ViewPagers are apt for this. Both encapsulate their own gesture and animation logic, and were not built to be synchronized externally.
You should have a look at the CoordinatorLayout. It is designed specifically to co-ordinate transitions and animations of its child views. Each child view can have a Behaviour and can monitor changes to its dependent sibling views and update its own state.
Report the first ViewPager's scroll changes to the second ViewPager:
viewPager.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
#Override
public void onScrollChanged() {
viewPager2.scrollTo(viewPager.getScrollX(), viewPager2.getScrollY());
}
});
With complex pages, this method might make the second pager lag behind.
Edit:
Instead of waiting for the scroll to change, you can report to the method calls directly with a custom class:
public class SyncedViewPager extends ViewPager {
...
private ViewPager mPager;
public void setSecondViewPager(ViewPager viewPager) {
mPager = viewPager;
}
#Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
if (mPager != null) {
mPager.scrollBy(x, mPager.getScrollY());
}
}
#Override
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
if (mPager != null) {
mPager.scrollTo(x, mPager.getScrollY());
}
}
}
And set the pager.
viewPager.setSecondViewPager(viewPager2);
Note: neither of these methods will invoke the page change listeners on the second pager.You can just add a listener to the first one and account for both pagers there.
You can use Paralloid. I have an application with parallex effect. Not a pager but a horizontalschrollbar which is quite the same in your context. This library runs well. Alternatively use parallaxviewpager which I didn't use but looks more towards your direction.
Related
I'm working on a Circular ViewPager, and i've implemented this exactly solution (https://stackoverflow.com/a/12965787/1083564).
The only thing is missing, is the fact that i need to smoothScroll when i'm using the setCurrentItem(int i, bol b) method, that instantly goes to the pixel limit, without using the smoothScroll.
I already have the access to use this method, using the following code:
package android.support.v4.view;
import android.content.Context;
import android.util.AttributeSet;
public class MyViewPager extends ViewPager {
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attr) {
super(context, attr);
}
public void smoothScrollTo(int x, int y, int velocity) {
super.smoothScrollTo(x, y, velocity);
}
}
But i couldn't figure it out where and how to use it. I have the number of pixels that i need to run smoothly by using this code inside the setOnPageChangeListener on my ViewPager:
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
Log.d( "viewpager", positionOffsetPixels+"");
}
Before it goes to 0, instantly, because of the setCurrentItem, i have the value of pixels left to reach 0 (to the left) or x (to the right, depending of screen). I dont know how can i get this x number too.
PS: I think this solution is the exatcly one used by IMDB app. You can see this scrolling from the first to the last but one, without remove your finger (use 2 fingers to do it). You will see that the "white limit" will show from the left side of the ViewPager. The only difference is that they know how to smooth scroll after using the setCurrentItem.
If you need some more information, please, ask! Thanks!
Issue: When you detect circular scrolling has to be perfomed, calling setCurrentItem immediately will cause the ViewPager to scroll to the real fragment immediately without smooth scrolling as it is set to false.
Solution: Instead allow the ViewPager to scroll to the fake fragment smoothly as it does for other fragments and then scroll to the real fragment after some delay with smooth scrolling set to false. User will not notice the change.
When we are performing circular scrolling, call setCurrentItem in a runnable with some delay. Use onPageSelected to know the index of the page selected.
public void onPageSelected(int position) {
// Consider eg. : C' A B C A'
boolean circularScroll = false;
if(position == 0) {
// Position 0 is C', we need to scroll to real C which is at index 3.
position = mPager.getAdapter().getCount() - 2;
circularScroll = true;
}
int lastIndex = mPager.getAdapter().getCount() - 1;
if(position == lastIndex) {
// Last index is A', we need to scroll to real A, which is at index 1.
position = 1;
circularScroll = true;
}
if(circularScroll) {
final int realPosition = position;
mPager.postDelayed(new Runnable() {
#Override
public void run() {
mPager.setCurrentItem(realPosition, false);
}
}, 500L);
}
}
When you set the second parameter of the setCurrentItem to true it should smooth scroll
#Override
public void onPageScrollStateChanged (int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
int curr = viewPager.getCurrentItem();
int lastReal = viewPager.getAdapter().getCount() - 2;
if (curr == 0) {
viewPager.setCurrentItem(lastReal, true);
} else if (curr > lastReal) {
viewPager.setCurrentItem(1, true);
}
}
}
I'd like to change the background color as the user changes pages, for that I need to use ViewPager's setOnPageChangeListener. But it seems that this brakes ViewPagerIndicator, as the indicator is stuck in the first page. Here's the code
viewPager.setOnPageChangeListener(new OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
ColorDrawable[] colors = {new ColorDrawable(backgroundColors[previousPosition]), new ColorDrawable(backgroundColors[position])};
TransitionDrawable trans = new TransitionDrawable(colors);
viewPager.setBackgroundDrawable(trans);
trans.startTransition(200);
previousPosition = position;
}
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {}
#Override
public void onPageScrollStateChanged(int arg0) {}
});
I ended up using ViewPagerIndicator's setOnPageChangeListener instead of ViewPager's method
mIndicator = (IconPageIndicator)findViewById(R.id.indicator);
mIndicator.setViewPager(viewPager);
The code becomes :
mIndicator.setOnPageChangeListener(new OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
ColorDrawable[] colors = {new ColorDrawable(backgroundColors[previousPosition]), new ColorDrawable(backgroundColors[position])};
TransitionDrawable trans = new TransitionDrawable(colors);
viewPager.setBackgroundDrawable(trans);
trans.startTransition(200);
previousPosition = position;
}
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {}
#Override
public void onPageScrollStateChanged(int arg0) {}
});
This way of changing colors between tabs is a first approach but I think that is not that difficult to come up with a better one.
In your approach:
I think that in your approach what you are doing is as soon as a new page is selected you trigger a transition in the background color. In that case the transitions are fixed in time (200 milliseconds in your case) but it doesn't feel natural, is even worse when you change pages swiping between them. The problem is that the onPageSelected is a on/off trigger. You are on page 0 or 1 or 2, but there is no such state as "between 0 and 1".
Wouldn't be great if the color would be exactly proportional to the amount of swipe, this is proportional to the real state in the transition between tabs??
How to do it?
In the activity hosting the viewpager, set a layout with as many background layers as tabs you want in the view pager. If you have 3 tabs then set 3 background layers overlapping one to each other.
For example:
<LinearLayout
android:id="#+id/background_main_fragment_activity_bottom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/background_gradient_radial_blue_vivid"
android:orientation="vertical"
tools:visibility="visible">
</LinearLayout>
<LinearLayout
android:id="#+id/background_main_fragment_activity_middle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/background_gradient_radial_blue_samknows"
android:orientation="vertical"
tools:visibility="visible">
</LinearLayout>
<LinearLayout
android:id="#+id/background_main_fragment_activity_top"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/background_gradient_radial_blue_light"
android:orientation="vertical"
tools:visibility="visible">
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="#+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>
Each LinearLayout is a background for a particular tab.
The idea is modify the alpha value of the linear layouts making them visible, partially visible or invisible.
Instead of using the onPageSelected method we are gonna use the onPageScrolled, take a look:
// Set a listener that will be invoked whenever the page changes or is incrementally scrolled.
// This listener is used for changing smoothly the colour of the background, this is modifying the visibility of the different layers
viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener()
{
// Called when the scroll state changes
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels)
{
super.onPageScrolled(position, positionOffset, positionOffsetPixels);
switch (position)
{
case 0:
layout_ll_background_middle.setAlpha(positionOffset);
break;
case 1:
layout_ll_background_top.setAlpha(positionOffset);
break;
default:
break;
}
}
// This method will be invoked when a new page becomes selected
#Override
public void onPageSelected(int position)
{ // When swiping between pages, select the corresponding tab
getActionBar().setSelectedNavigationItem(position);
// Set the title
actionBar.setTitle(adapter_ViewPager.getPageTitle(position));
}
});
Hope it helps to make it better.
Good luck
Since timed transitions don't quite fit the usecase and alpha shouldn't be used (in that way) for transitions/animations on android here is what I came up with:
First of all be sure that no child view has a background set where it shouldn't. In your base layout set the background of the layout to your start color.
In your Activity.onCreate() add this:
final ViewPager mPager = (ViewPager) findViewById(R.id.ofYourPager);
// if you have a page indicator
final YourPageIndicator pageIndicator = (YourPageIndicator) findViewById(R.id.ofYourIndicator);
// change background on swipe
final int[] colors = {
getResources().getColor(R.color.yourFirstColor),
getResources().getColor(R.color.yourSecondColor),
getResources().getColor(R.color.yourThirdColor),
// --- add as many colours as you have pages ---
};
final ArgbEvaluator argbEvaluator = new ArgbEvaluator();
mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (position < (mPagerAdapter.getCount() - 1) && position < (colors.length - 1)) {
Integer color = (Integer) argbEvaluator.evaluate(positionOffset, colors[position], colors[position + 1]);
mPager.setBackgroundColor(color);
pageIndicator.setBackgroundColor(color);
} else {
mPager.setBackgroundColor(colors[colors.length - 1]);
pageIndicator.setBackgroundColor(colors[colors.length - 1]);
}
}
#Override
public void onPageSelected(int position) {}
#Override
public void onPageScrollStateChanged(int state) {}
});
I took the idea from this post I found: http://kubaspatny.github.io/2014/09/18/viewpager-background-transition/
I have a concept for a view. Please guide me as to how I can achieve it. Please check the wireframe.
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.
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);
}
});
}
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
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
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)
I need to set the child view as center of the ViewPager and also I would like to show some part of the next and previous views to the current view sides(like current screen below 1). But currently the current view is starting at left side of the ViewPager(like expected screen below 2). How can I achieve that?
Here is my code..
MyViewPagerAdapter
public class MyViewPagerAdapter extends PagerAdapter {
private Activity mActivity;
private int mPageCount;
public MyViewPagerAdapter(Activity activity,int pageCount) {
mActivity = activity;
mPageCount = pageCount;
}
#Override
public int getCount() {
return mPageCount;
}
#Override
public boolean isViewFromObject(View view, Object obj) {
return (view ==(View)obj);
}
#Override
public Object instantiateItem(ViewGroup container,final int position) {
ViewGroup viewGroup = (ViewGroup)mActivity.getLayoutInflater().inflate(
R.layout.item_view, null);
viewGroup.setBackgroundColor(randomColor());
TextView textView = (TextView)viewGroup.findViewById(R.id.textView1);
textView.setText("Page: "+(position+1));
Button button = (Button) viewGroup.findViewById(R.id.button1);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(mActivity, "Hey, Its clicked!!! at page "+(position+1), Toast.LENGTH_LONG).show();
}
});
container.addView(viewGroup);
return viewGroup;
}
Random rnd = new Random();
private int randomColor(){
return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
}
#Override
public void destroyItem(ViewGroup collection, int position, Object view) {
//must be overridden else throws exception as not overridden.
Log.d("Tag", collection.getChildCount()+"");
collection.removeView((View) view);
}
#Override
public float getPageWidth(int position) {
return 0.8f;
}
}
MainActivity
public class MainActivity extends Activity {
private ViewPager viewPager;
LinearLayout linearLayout;
private int ID = 100;
private final int count = 8;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager = (ViewPager) findViewById(R.id.viewPager);
linearLayout = (LinearLayout) findViewById(R.id.indicator_layout);
generateIndicators(count);
MyViewPagerAdapter adapter = new MyViewPagerAdapter(this, count);
viewPager.setAdapter(adapter);
viewPager.setOnPageChangeListener(new OnPageChangeListener() {
int oldPosition = 0;
#Override
public void onPageSelected(int position) {
//this changes the old position's view state image
((TextView)linearLayout.getChildAt(oldPosition)).setText("");
oldPosition = position;
//this changes the current position's view state image
((TextView)linearLayout.getChildAt(position)).setText((position+1)+"");
}
//this method will be called repeatedly upto another item comes as front one(active one)
#Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
//this will be called as per scroll state
#Override
public void onPageScrollStateChanged(int arg0) {
}
});
viewPager.setOffscreenPageLimit(4);
}
private void generateIndicators(int count) {
/// Converts 14 dip into its equivalent px
int padd = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
for(int i=0;i<count;i++){
TextView textView = new TextView(this);
textView.setId(ID+i);
final int currentItem = i;
textView.setBackgroundResource(R.drawable.white_cell);
textView.setPadding(padd,padd,padd,padd);
/// Converts 14 dip into its equivalent px
int size = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics());
textView.setTextSize(size);
textView.setGravity(Gravity.CENTER);
/// Converts 14 dip into its equivalent px
int px = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, getResources().getDisplayMetrics());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(px, px);
linearLayout.addView(textView,params);
}
((TextView)linearLayout.getChildAt(0)).setText("1");
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<android.support.v4.view.ViewPager
android:id="#+id/viewPager"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" >
</android.support.v4.view.ViewPager>
<LinearLayout
android:id="#+id/indicator_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="19dp" >
</LinearLayout>
</RelativeLayout>
item_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="match_parent"
android:id="#+id/root_view"
android:orientation="vertical" >
<TextView
android:id="#+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="Text"
android:textAppearance="?android:attr/textAppearanceLarge" />
<Button
android:id="#+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="click me" />
</LinearLayout>
Current screen
expected screen
For one app I implemented similar the following way, with standard ViewPager:
Make pages full-screen with the actual content in an inner layout. For example, make the full-screen layout a RelativeLayout with transparent background and the actual content another RelativeLayout centered in the parent. If I remember right, the reason for this was that with just the inner layout as a page, the ViewPager would not have taken all the screen width on some devices such as Galaxy Nexus.
Use ViewPager.setPageMargin() to set up a negative page margin i.e. how much of the next/previous page you want to show. Make sure it only overlaps the transparent region of the parent full-screen layout.
Call ViewPager.setOffscreenPageLimit() to adjust the off-screen page count to at least 2 from the default 1 to ensure smooth paging by really creating the pages off-screen. Otherwise you will see next/previous pages being drawn while already partially showing on screen.
For anyone upset that the OP didn't update his question with the solution here is a link that explains, with minimal effort, how to pull this off in XML: http://blog.neteril.org/blog/2013/10/14/android-tip-viewpager-with-protruding-children/
Basically when you declare your viewpager in XML, give it the same left and right padding and set android:clipToPadding="false". (The clipToPadding is missing in his xml sample and necessary to achieve this effect)
Finally, I have added my solution for this question in GitHub. I have done some pretty tricks to get the workaround solution. You can get the project from the below link(Actually I have planned to create a blog with the explanation , but I dint have that much time to do).
Here is the link(https://github.com/noundla/Sunny_Projects/tree/master/CenterLockViewPager)
You have to copy the files from com.noundla.centerviewpagersample.comps package to your project. And you can see the usage of that Viewpager in MainActivity class.
Please let me know if anyone has problems with this.
I found solution in this post, below the code i used:
// Offset between sibling pages in dp
int pageOffset = 20;
// Visible part of sibling pages at the edges in dp
int sidePageVisibleWidth = 10;
// Horizontal padding will be
int horPadding = pageOffset + sidePageVisibleWidth;
// Apply parameters
viewPager.setClipToPadding(false);
viewPager.setPageMargin(UIUtil.dpToPx(pageOffset, getContext()));
viewPager.setPadding(UIUtil.dpToPx(horPadding, getContext()), 0, UIUtil.dpToPx(horPadding, getContext()), 0);
dpToPx code:
public static int dpToPx(int dp, Context context) {
float density = context.getResources().getDisplayMetrics().density;
return Math.round((float) dp * density);
}
This is all you need
You can use padding for viewPager and set clipToPadding false
Java
viewPager.setClipToPadding(false);
viewPager.setPadding(50, 0, 50, 0);
Kotlin
viewPager.clipToPadding = false
viewPager.setPadding(50, 0, 50, 0)
I had to center current page in view pager with different page widths, so solution with paddings was not suitable. Also user scrolling was disabled (it was tab bar view pager, scrolled by another view pager). Here is a very simple solution to do that - just override ViewPager.ScrollTo method just like this (C# code, Xamarin):
public override void ScrollTo(int x, int y)
{
x -= (int) (MeasuredWidth * (1 - Adapter.GetPageWidth(CurrentItem)) / 2);
base.ScrollTo(x, y);
}
And if you calculate page width for each fragment don't forget to cache them in array.
Extend HorizontalScrollView class as the parent for the scrolling view. In the onMeasure() method you can specify the width and height of each child. Little cumbersome way but the effect will be good and you can have a good hold on your child view.
Goal
Build a Circular ViewPager.
The first element lets you peak to the last element and swipe to it, and vice versa. You should be able to swipe in either direction forever.
Now this has been accomplished before, but these questions do not work for my implementation. Here are a few for reference:
how to create circular viewpager?
ViewPager as a circular queue / wrapping
https://github.com/antonyt/InfiniteViewPager
How I Tried to Solve the Problem
We will use an array of size 7 as an example. The elements are as follows:
[0][1][2][3][4][5][6]
When you are at element 0, ViewPagers do not let you swipe left! How terrible :(. To get around this, I added 1 element to the front and end.
[0][1][2][3][4][5][6] // Original
[0][1][2][3][4][5][6][7][8] // New mapping
When the ViewPageAdapter asks for (instantiateItem()) element 0, we return element 7. When the ViewPageAdapter asks for element 8 we return element 1.
Likewise in the OnPageChangeListener in the ViewPager, when the onPageSelected is called with 0, we setCurrentItem(7), and when it's called with 8 we setCurrentItem(1).
This works.
The Problem
When you swipe to the left from 1 to 0, and we setCurrentItem(7), it will animate all the way to right by 6 full screens. This doesn't give the appearance of a circular ViewPager, it gives the appearence rushing to the last element in the opposite direction the user requested with their swipe motion!
This is very very jarring.
How I Tried to Solve This
My first inclination was to turn off smooth (ie, all) animations. It's a bit better, but it's now choppy when you move from the last element to the first and vice versa.
I then made my own Scroller.
http://developer.android.com/reference/android/widget/Scroller.html
What I found was that there is always 1 call to startScroll() when moving between elements, except when I move from 1 to 7 and 7 to 1.
The first call is the correct animation in direction and amount.
The second call is the animation that moves everything to the right by multiple pages.
This is where things got really tricky.
I thought the solution was to just skip the second animation. So I did. What happens is a smooth animation from 1 to 7 with 0 hiccups. Perfect! However, if you swipe, or even tap the screen, you are suddenly (with no animation) at element 6! If you had swiped from 7 to 1, you'll actually be at element 2. There is no call to setCurrentItem(2) or even a call to the OnPageChangeListener indicating that you arrived at 2 at any point in time.
But you're not actually at element 2, which is kind of good. You are still at element 1, but the view for element 2 will be shown. And then when you swipe to the left, you go to element 1. Even though you were really at element 1 already.. How about some code to help clear things up:
Animation is broken, but no weird side effects
#Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, duration);
}
Animation works! But everything is strange and scary...
#Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
if (dx > 480 || dx < -480) {
} else {
super.startScroll(startX, startY, dx, dy, duration);
}
}
The ONLY difference is that when the second animation (bigger than the width of the 480 pixel screen) is called, we ignore it.
After reading through the Android Source code for Scroller, I found that startScroll does not start scrolling anything. It sets up all the data to be scrolled, but doesn't initiate anything.
My Hunch
When you do the circular action (1 to 7 or 7 to 1), there are two calls to startScroll(). I think something in between the two calls is causing an issue.
User scrolls from element 1 to element 7 causing a jump from 0 to 7. This should animate to the left.
startScroll() is called indicating a short animation to the left.
STUFF HAPPENS THAT MAKES ME CRY PROBABLY I THINK
startScroll() is called indicating a long animation to the right.
Long animation to the right occurs.
If I comment out 4, then 5 becomes "Short correct animation to the left, things go crazy"
Summary
My implementation of a Circular ViewPager works, but the animation is broken. Upon trying to fix the animation, it breaks the functionality of the ViewPager. I am currently spinning my wheels trying to figure out how to make it work. Help me! :)
If anything is unclear please comment below and I will clarify. I realize I was not very precise with how things are broken. It's difficult to describe because it's not even clear what I'm seeing on the screen. If my explanation is an issue I can work on it, let me know!
Cheers,
Coltin
Code
This code is slightly modified to make it more readable on its own, though the functionality is identical to my current iteration of the code.
OnPageChangeListener.onPageSelected
#Override
public void onPageSelected(int _position) {
boolean animate = true;
if (_position < 1) {
// Swiping left past the first element, go to element (9 - 2)=7
setCurrentItem(getAdapter().getCount() - 2, animate);
} else if (_position >= getAdapter().getCount() - 1) {
// Swiping right past the last element
setCurrentItem(1, animate);
}
}
CircularScroller.startScroll
#Override
public void startScroll(int _startX, int _startY, int _dx, int _dy, int _duration) {
// 480 is the width of the screen
if (dx > 480 || dx < -480) {
// Doing nothing in this block shows the correct animation,
// but it causes the issues mentioned above
// Uncomment to do the big scroll!
// super.startScroll(_startX, _startY, _dx, _dy, _duration);
// lastDX was to attempt to reset the scroll to be the previous
// correct scroll distance; it had no effect
// super.startScroll(_startX, _startY, lastDx, _dy, _duration);
} else {
lastDx = _dx;
super.startScroll(_startX, _startY, _dx, _dy, _duration);
}
}
CircularViewPageAdapter.CircularViewPageAdapter
private static final int m_Length = 7; // For our example only
private static Context m_Context;
private boolean[] created = null; // Not the best practice..
public CircularViewPageAdapter(Context _context) {
m_Context = _context;
created = new boolean[m_Length];
for (int i = 0; i < m_Length; i++) {
// So that we do not create things multiple times
// I thought this was causing my issues, but it was not
created[i] = false;
}
}
CircularViewPageAdapter.getCount
#Override
public int getCount() {
return m_Length + 2;
}
CircularViewPageAdapter.instantiateItem
#Override
public Object instantiateItem(View _collection, int _position) {
int virtualPosition = getVirtualPosition(_position);
if (created[virtualPosition - 1]) {
return null;
}
TextView tv = new TextView(m_Context);
// The first view is element 1 with label 0! :)
tv.setText("Bonjour, merci! " + (virtualPosition - 1));
tv.setTextColor(Color.WHITE);
tv.setTextSize(30);
((ViewPager) _collection).addView(tv, 0);
return tv;
}
CircularViewPageAdapter.destroyItem
#Override
public void destroyItem(ViewGroup container, int position, Object view) {
ViewPager viewPager = (ViewPager) container;
// If the virtual distance is distance 2 away, it should be destroyed.
// If it's not intuitive why this is the case, please comment below
// and I will clarify
int virtualDistance = getVirtualDistance(viewPager.getCurrentItem(), getVirtualPosition(position));
if ((virtualDistance == 2) || ((m_Length - virtualDistance) == 2)) {
((ViewPager) container).removeView((View) view);
created[getVirtualPosition(position) - 1] = false;
}
}
I think the best doable approach would be instead of using a normal list to have a wrapper to the List that when the get(pos) method is executed to obtain the object to create the view, you make something like this get(pos % numberOfViews) and when it ask for the size of the List you put that the List is Integer.MAX_VALUE and you start your List in the middle of it so you can say that is mostly impossible to have an error, unless they actually swipe to the same side until the reach the end of the List. I will try to post a proof of concept later this weak if the time allows me to do so.
EDIT:
I have tried this piece of code, i know is a simple textbox shown on each view, but the fact is that it works perfectly, it might be slower depending on the total amount of views but the proof of concept is here. What i have done is that the MAX_NUMBER_VIEWS represents what is the maximum numbers of times a user can completely give before he is stopped. and as you can see i started the viewpager at the length of my array so that would be the second time it appears so you have one turn extra to the left and right but you can change it as you need it. I hope i do not get more negative points for a solution that in fact does work.
ACTIVITY:
pager = (ViewPager)findViewById(R.id.viewpager);
String[] articles = {"ARTICLE 1","ARTICLE 2","ARTICLE 3","ARTICLE 4"};
pager.setAdapter(new ViewPagerAdapter(this, articles));
pager.setCurrentItem(articles.length);
ADAPTER:
public class ViewPagerAdapter extends PagerAdapter {
private Context ctx;
private String[] articles;
private final int MAX_NUMBER_VIEWS = 3;
public ViewPagerAdapter(Context ctx, String[] articles) {
this.ctx = ctx;
this.articles = articles.clone();
}
#Override
public int getCount() {
return articles.length * this.MAX_NUMBER_VIEWS;
}
#Override
public Object instantiateItem(ViewGroup container, int position) {
TextView view = new TextView(ctx);
view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
int realPosition = position % articles.length;
view.setText(this.articles[realPosition]);
((ViewPager) container).addView(view);
return view;
}
#Override
public void destroyItem(ViewGroup container, int position, Object object) {
((ViewPager) container).removeView((View) object);
}
#Override
public boolean isViewFromObject(View view, Object object) {
return view == ((View) object);
}
#Override
public Parcelable saveState() {
return null;
}
}