I am trying to achieve this in a ViewPager
The first fragment (blue) is displayed and the beginning of the next fragment must be displayed as well, so the user understands he can swipe to switch views.
The idea is scroll 20% of the screen programmatically to the left.
Any thoughts are welcome
EDIT: This is exactly what I need to to: the central view needs to overlap both side views
You can try adding this to your PageAdapter:
public float getPageWidth(int position) {
if (position == 0 || position == 2) {
return 0.8f;
}
return 1f;
}
Try using a negative value for ViewPager.setPageMargin.
This function worked better than the answer marked as correct (for me anyway).
#Override
public float getPageWidth(int position) {
return 0.9f;
}
Just place it in your custom PagerAdapter class.
viewpager.setClipToPadding(false);
viewpager.setPageMargin(-50);
#Override
public float getPageWidth(int position) {
return 0.9f;
}
Use this, this will work perfectly and make sure that your class extended to PagerAdapter
Related
I have a ViewPager Fragment with 3 fragments. Each of it has an image as background and a TextView. The TextView is always absolute on the left side of the screen (not the Fragment itself). When I move the fragments with my finger, it stays at the correct position. Also when I move it fast. But when I let it slide into the other Fragment, the TextView doesn't stays at the correct position. It looks like it lags around.
It should stay like in these 2 pictures:
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
SingleTabFragment singleTabFragment = (SingleTabFragment) mSectionsPagerAdapter.getRegisteredFragment(position);
TextView textView = singleTabFragment.getNonResizingTextView();
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams)textView.getLayoutParams();
marginLayoutParams.leftMargin = positionOffsetPixels;
textView.setLayoutParams(marginLayoutParams);
}
I really don't know what the problem is. I tried it multiple ways, i added Thread.sleep(50); to see if it also happens there. And yes, also there it doesn't work correct. How can I fix this?
EDIT:
Now I have put in the middle. But the problem is the same.
Here is how it should look like (I know, not that much frames. But I think you will get the point):
And if I let it slide it looks like this:
TextView textView = singleTabFragment.getNonResizingTextView();
textView.translationX = positionOffsetPixels;
You should not do marginLayoutParams.leftMargin = positionOffsetPixels; because changing the margin may change the position of the View, which will cause the View to be re-layout on every frame. That is why it is lagging.
Changing the translationX will only change how the view is drawn instead of it's position.
You can use following method to do the same instead of doing it in onPageScrolled:
mPager.setPageTransformer(false, new FadePageTransformer());
public class FadePageTransformer implements ViewPager.PageTransformer {
public void transformPage(View view, float position) {
view.setAlpha(1 - Math.abs(position));
if (position < 0) {
view.setScrollX(-(int) ((float) (view.getWidth()) * -position));
} else if (position > 0) {
view.setScrollX((int) ((float) (view.getWidth()) * position));
} else {
view.setScrollX(0);
}
}
}
use textView.setTranslationX(positionOffsetPixels); instead of margins
Have a look at Slowing speed of Viewpager controller in android
You can set custom duration for Viewpager scrolling.
Is it possible to setup different PageTransformers for different fragments in ViewPager?
I would like to use ZoomOutPageTransformer when the user swipes between (0, 1) and (n-1, n) fragments of ViewPager with n fragments and use the default transitions for all other cases.
What I tried:
Detect the current page with mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { ... }) and apply different transitions here. But, for example, if the user is on the first page - how to set ZoomOutPageTransformer only for the case when he will swipe to the left (0 page) and do it before animation is started?
I had a less similar requirement wherein I had applied Depth page transformer to all my pages by default and on tap of a button I changed it to Vertical page transformer and then back to Depth page. (A lil wierd but thats how it was)
I did the below:
Applied viewPager.setPageTransformer(true, new DepthPageTransformer()); for all the pages.
onClick I changed it to viewPager.setPageTransformer(true, new VerticalPageTransformer());. Now here's the catch in-order for this to function smoothly I had to use a handler in order to change the page a little later to ensure VerticalPager takes effect. I achieved this by calling viewPager.setCurrentPosition(viewPager.getCurrentItem() + 1)
After this inside onPageChangeListener's onPageSelected method I changed it back to viewPager.setPageTransformer(true, new DepthPageTransformer()); again inside a handler to ensure it's done after my vertical animation is completed.
Note - I had also accessed ViewPager's scroller to achieve the above to define my own speed for the page change.
Let me know if this makes sense, since you need it to be done based on page numbers. If it does I will share my code. :)
This is quite old, but wish it's not too late for somebody
Step 1:
Create custom ViewPager which extends from ViewPager.
Step 2:
Determine which Fragment positions (ViewPager page integers) you need to have a unique PageTransformer for them. Then add these indices into a list within the custom ViewPager.
Step 3:
Override onPageScrolled(int position, float offset, int offsetPixels):
Check if the position is in your list of indices, if so, then you have to create a Custom PageTransformer object; and then utilize the ViewPager setPageTransformer() to change the page transformation.
For positions other than that, then you've to set the default PageTransformer (or you can also you another custom PageTransformer object if you wish.
Code
public class CustomViewPager extends ViewPager {
public CustomViewPager(Context context) {
super(context);
}
public CustomViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
PageTransformer mDepthPageTransformer = new PageTransformer() {
private static final float MIN_SCALE = 0.75f;
public void transformPage(#NonNull View view, float position) {
int pageWidth = view.getWidth();
if (position < -1) { // [-Infinity,-1)
// This page is way off-screen to the left.
view.setAlpha(0f);
} else if (position <= 0) { // [-1,0]
// Use the default slide transition when moving to the left page
view.setAlpha(1f);
view.setTranslationX(0f);
view.setScaleX(1f);
view.setScaleY(1f);
} else if (position <= 1) { // (0,1]
// Fade the page out.
view.setAlpha(1 - position);
// Counteract the default slide transition
view.setTranslationX(pageWidth * -position);
// Scale the page down (between MIN_SCALE and 1)
float scaleFactor = MIN_SCALE
+ (1 - MIN_SCALE) * (1 - Math.abs(position));
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
} else { // (1,+Infinity]
// This page is way off-screen to the right.
view.setAlpha(0f);
}
}
};
PageTransformer mDefaultPageTransformer = new PageTransformer() {
#Override
public void transformPage(#NonNull View page, float position) {
}
};
#Override
protected void onPageScrolled(int position, float offset, int offsetPixels) {
List<Integer> pos = new ArrayList<>();
pos.add(0);
pos.add(n - 1); // n is the no. of pages as in the question
if (pos.contains(position)) {
this.setPageTransformer(true, mDepthPageTransformer);
} else {
this.setPageTransformer(true, mDefaultPageTransformer);
}
super.onPageScrolled(position, offset, offsetPixels);
}
}
Documentation
Slide between fragments using ViewPager
setPageTransformer() method
Note: mDepthPageTransformer used in my example is taken from the above mentioned documentation.
Inspite Of using mViewPager.setOnPageChangeListener , a single PageTransformer with different animations can help.
Have a look at first paragraph here ,
http://developer.android.com/training/animation/screen-slide.html#pagetransformer
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 have a ViewPager with a custom PagerAdapter that displays a set of fragments.
These fragments are (purposely) positioned on top of each other so that I can use a PageTransformer that makes it look as if the user is sliding the fragments from a stack (almost like a deck of cards).
The issue is that each fragment has their own Views/Widgets (e.g. a seekbar) which, due to the overlapping, are occupying the same coordinates and sometimes the touch event is caught by the fragment bellow the current one (e.g. the user adjusts a seekbar's position, but instead of updating the currently shown seekbar, it's the seekbar in the next fragment that's gets its progress updated).
I've come across this answer but it's not the same exact problem.
Has anyone ever found a similar issue? What's the smartest way (except for the lazy solution: change the PageTransformer to one that doesn't overlap the fragments) of dealing with this issue?
EDIT:
In my Fragment class I have:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment, container, false);
rootView.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
}
as suggested by Zsombor Erdődy-Nagy, but this doesn't help: it's still possible for the widget bellow the current fragment to receive the event instead of the current one's.
I've also looked at this open issue, with no success.
If you are still looking for the solution, than you should look at this:
Issue 58918.
Here you can find the answer to your problem. Quoting from the link:
If I remember right it was after 4.1 that the framework respects a
custom child drawing order as implied Z-ordering for dispatching touch
events. If your views overlap after this page transformation they may
not receive touch events in the expected order on older platform
versions. Check which view is receiving the touch events to be
certain.
If this is what you are seeing you have a few options:
Enforce the desired ordering as you add/remove child views in your PagerAdapter
Remove the X translation applied by the PageTransformer when a page is no longer fully visible - i.e. the "position" parameter reports a full -1 or 1.
Example:
this.viewPager.setPageTransformer(true, new PageTransformer() {
#Override
public void transformPage(View page, float position) {
float translationX;
float scale;
float alpha;
if (position >= 1 || position <= -1) {
// Fix for https://code.google.com/p/android/issues/detail?id=58918
translationX = 0;
scale = 1;
alpha = 1;
} else if (position >= 0) {
translationX = -page.getWidth() * position;
scale = -0.2f * position + 1;
alpha = Math.max(1 - position, 0);
} else {
translationX = 0.5f * page.getWidth() * position;
scale = 1.0f;
alpha = Math.max(0.1f * position + 1, 0);
}
ViewHelper.setTranslationX(page, translationX);
ViewHelper.setScaleX(page, scale);
ViewHelper.setScaleY(page, scale);
ViewHelper.setAlpha(page, alpha);
}
});
In this case all your fragments must have a background View that stops TochEvents propagating to fragments in the back.
I'm guessing that you already have opaque backgrounds for these fragments, or else the fragments in the back would show through the fragment currently on the top of the stack. So try setting a ClickListener for all your fragment's root ViewGroup instance that does nothing, it just catches the touch events.
In case anyone runs into this issue (which can happen not only with ViewPager), it is just a matter of adding this attribute to the layout at the top of your hierarchy:
android:clickable="true"
This way it will handle the clicks, not doing really anything with them, but at least avoiding them to reach the fragment in the background.
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;
}
}