I have some tabs in a ViewPager. Each tab except the last displays the FloatingActionButton at the bottom right corner. The last tab is supposed to display it in the bottom center, after translating it while this tab is being scrolled to. When scrolling away from this tab, the FloatingActionButton translates back into its bottom right position. I was implementing a solution, and was surprised to find that this, more or less, was all it took:
mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// One page away from last page
if (position == mAdapter.getCount() - 2) {
mFab.setTranslationX(positionOffsetPixels / -2f);
}
}
}
When I scroll to the last tab, we're translating the FAB to the left of its current position, because the argument passed to setTranslationX() is negative. Now, here's where I'm confused. When I scroll back one page, the FAB translates back to the right as desired--but how can this be? The argument to setTranslationX() is still negative. I expected the FAB to translate further left.
When you're scrolling forward from the position mAdapter.getCount() - 2, the values of positionOffsetPixels will be increasing positive numbers and positionOffsetPixels / -2f will be increasing negative numbers, so the FAB will translate to the left, as expected.
From the documentation of position:
Position index of the first page currently being displayed.
This means when scrolling back, the offset values will be relative to the page you're scrolling to, not to the page you're scrolling from.
When you're scrolling back from mAdapter.getCount() - 1, the value of position is mAdapter.getCount() - 2 (this is the position you're scrolling to) and the values of positionOffsetPixels will be decreasing positive numbers going down to 0.
0 / -2f == 0, so eventually mFab.setTranslationX(0) will be called, returning the FAB to its original position.
Related
My issue is quite complex and I have hard time finding a solution to achieve the following :
I need to have a sort of legend (a stepper) layout where a child is made of left bar, followed by a circle, followed by a right bar. Strickly speaking somethind like "-o-". So a serie of children looks like this : "-o-o-o-o-o". When the legend focuses on a child meaning that the current child is this one, this child has a bigger circle and longer bars: "--O--".
The final rendering looks therefore like this : "-o-o--O--o-o-".
Each child ("-o-") corresponds to a specific item in a database. When a child gets focuses ("--O--"), the relative item is displayed below. When the user swipes from right to left, the next item is showed, whereas a swipe from left to right will display the previous item.
When swiping, to show which child will display its corresponding item, say child will be animated to show it has focus. I use a Animator to :
increase the bars widths and the circle size of the child that is
getting the focus
to decrease these values of the previously focused child.
Displaying a legend requires to see what's before and what's next.
I believe using a ViewPager will help me achieve the following :
it has an "anchor effect", meaning a swipe will move from child to child, centering on the circle: the circle between the bars is the anchor, so to speak.
I can display multiple pages on the screen by overriding the getPageWidth() method of my custom adapter
The other solution I had in mind is the HorizontalScrollView.
If I were to use it, depending on the swipe properties (strengh, speed), the legend would indeterminately scroll without anchoring the circle.
But using the ViewPager has a big problem: each page has the same width. However, the anchored child of the legend needs to be bigger (so needs more width).
So how can I change the width dynamically ?
Unless I can customize a HorizontalScrollView to add the anchor effect by intercepting in swipe ?
There is also the CarouselLayoutManager which I think may include the effect you desire.
If you are using a ViewPager, I think the best way to do this would be with an OnPageChangeListener. You will get callbacks as the user is swiping left/right so that you can size your indicators based on the location of the user's swipe.
private int mCurrentPage = 0;
viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// positionOffset goes from 0.0 to just under 1.0 for the leftmost page
float otherOffset = 1.0 - positionOffset;
if (position == mCurrentPage) { // user is incrementing page, i.e. swiping left
// size of current page indicator based on otherOffset
// size of next page indicator based on positionOffset
} else { // user is decrementing page, i.e. swiping right
// size of current page indicator based on positionOffset
// size of previous page indicator based on otherOffset
}
}
#Override
public void onPageSelected(int position) {
mCurrentPage = position;
// ... set final sizes: page # position = 1.0 all others 0.0
}
});
Here's how to understand position and positionOffset: When a page has settled, it is centered. Let's say the current page position == 1 (i.e. page 2).
First case: User swipes left to get to the next page. Say the user swipes 10% of the distance to get to the next page. position will be 1 (leftmost displayed) and positionOffset will be 0.1 (viewport left edge is at 10% of leftmost page). So as user swipes, positionOffset will go from 0.0 to just below 1.0.
Second case: User swipes right to get to the previous page. When the user swipes 10% of the distance to the previous page, position will be 0 (leftmost displayed) and positionOffset will be 0.9 (viewport left edge is at 90% of the leftmost page). So as user swipes, positionOffset will go from just under 1.0 to 0.0.
As for the indicators themselves, I did a similar pager indicator where I used a LinearLayout with a horizontal orientation, adding a child indicator view for each page. I used pagerAdapter.getCount() to get the number of views to add.
Your indicator is more complex with the animation, but if I were to do this, I would use a plain View with ScaleDrawable backgrounds for graphics that could change size.
I'm developing an Android app and there is a feature I want to implement but because of lacking experience I don't know how to do it and unfortunately googling it didn't help.
So, there is a ViewPager with two tabs and a FloatingActionButton with an arrow image and what I want to do is to rotate the arrow depending on the scrolling state of pager (on first tab it should be a left arrow, then between the tabs it should smoothly rotate to face up or down (doesn't really matter), and finally on the second tab it should become a right arrow, and vice versa).
I've been trying RotateAnimation class, but I didn't manage to make it work.
Please notice that my app's minSdkVersion is 14 (Jelly Bean +), so Vector drawables and other just-Lollipop or even KitKat things wouldn't work for me.
Thanks in advance.
The library you're using includes the shadow in it's view, which means it's rotated along with the rest of the view when using setRotation. You can get around this by wrapping your Floating Action Button in a FrameLayout, remove the fab_icon attribute from your FAB, and add an ImageView on top of your FAB like so:
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="16dp"
android:layout_marginRight="16dp"
android:layout_marginEnd="16dp">
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_colorNormal="?attr/colorAccent"
app:fab_colorPressed="?attr/colorAccentHighlight"/>
<ImageView
android:id="#+id/fab_icon_overlay"
android:layout_width="#dimen/fab_icon_size"
android:layout_height="#dimen/fab_icon_size"
android:layout_gravity="center"
android:layout_marginTop="-3dp"
android:src="#drawable/ic_content_add"
android:tint="#android:color/white"/>
</FrameLayout>
Then, instead of rotating the FAB, you rotate the ImageView. The result is that the FAB appears to rotate, because it's a circle and the icon inside it is rotating. Please note that the value of android:layout_marginTop must be the negative value of fab_shadow_offset for the icon to be perfectly centered. The default value is 3dp.
What you want to do now is combine Ashtons answer with a call to setRotation(float rotation), please note that the rotation is in degrees (0.0f - 360.0f).
A solution might look like this:
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// positionOffset ranges from 0.0f to 1.0f, multiply it by 180.0f to rotate the
// icon clockwise, making the icon appear flipped when scrolled to the last page.
// Multiply by -180.0f to rotate counter-clockwise.
fabIconOverlay.setRotation(positionOffset * 180.0f);
}
You'll want to use PageChangedListener on a view pager. Then listening to onPageScrolled(int position, float positionOffset, int positionOffsetPixels) will tell you what you need to calculate the rotation angle.
Note that this is not an animation--you are going to explicitly set the rotation of the button based on the scroll of the viewpager. You need to keep track of the current page and compare that to the page you're getting in the position argument of onPageScrolled. To do this, you can use my code as an example:
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (currentPagePosition != position && (int) positionOffset == 0 && positionOffsetPixels == 0) {
// New page is set!
currentPagePosition = position;
onNewPage(currentPagePosition);
} else {
if (positionOffset == 0 && positionOffsetPixels == 0) {
// User scrolled a little, then let go
// and it went back without changing position
onNewPage(position);
return;
}
if (position == currentPagePosition) {
onPageScrolledToLeft(position, positionOffset);
} else if (position < currentPagePosition) {
onPageScrolledToRight(position, positionOffset);
}
}
}
With this, now you can implement those two methods to use positionOffset to calculate the rotation angle. Note that onPageScrolledToLeft means that the current page is moving left, or the user is scrolling to the right. When this happens, values of positionOffset will go from values of 1.0 to 0.0. When onPageScrolledToRight triggers, positionOffset will go from 0.0 - 1.0. Now after calculating the proper rotation (ie, 0.5 should probably be 0 degrees) you can use a view's setRotation(float rot) method.
from an answer to one of my other questions I found an Google Demo of a ListView subclass that allows item reorder.
The demo works great, but I am having some trouble to understand how the it works:
When an item is dragged above/below the bounds of the ListView, the ListView starts scrolling up/down to reveal new items. The necessary calculation uses different parameters of the underling ScrollView:
public boolean handleMobileCellScroll(Rect r) {
int offset = computeVerticalScrollOffset();
int height = getHeight();
int extent = computeVerticalScrollExtent();
int range = computeVerticalScrollRange();
int hoverViewTop = r.top;
int hoverHeight = r.height();
if (hoverViewTop <= 0 && offset > 0) {
smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
return true;
}
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
return true;
}
return false;
}
heightis the height of the ListView itself
offsetis the scroll position = how many units/pixels have been scrolled up/down
rangeis the height of the complete content.
extent - well, what is this?
ListView inherits computeVerticalScrollExtent() from View and the docu says:
Compute the vertical offset of the vertical scrollbar's thumb within
the horizontal range. This value is used to compute the position of
the thumb within the scrollbar's track.
If one looks at the code computeVerticalScrollExtent() is not implemented by one of the sublasses but only directly by View: It simply returns the height of the view.
This makes sense: If the ListView/ScrollView has a height of 500, the part of the scroll content that is visible at a time is also 500. Is this the meaning of the ScrollExtent? Why is ScrollExtent necessary? Why not simply use getHeight() directly?
I think I am missing something and I am happy about any hint!
compute*ScrollOffset - Defines the distance between the start of the scrollable area and the top of the current view window inside the scrollable area. So for example, if your list has 10 items and you've scrolled down so the 3rd item is at the top-most visible item, then the offset is 3 (or 3*itemHeight, see below).
compute*ScrollExtent - Defines the size of the current view window inside the scrollable area. So for example, if your list has 10 items and you can currently see 5 of those items, then the extent is 5 (or 5*itemHeight, see below).
compute*ScrollRange - Defines the size of the current scrollable area. So for example, if your list has 10 items then the range is 10 (or 10*itemHeight, see below).
Note that all these methods can return values in different units depending on their implementation, so for the examples above, I am using the indices, but in some cases these methods will return pixel values equivalent to the width or height of the items.
In particular, the LinearLayoutManager of the RecyclerView will return indices if the 'smooth scrollbar' feature is disabled, otherwise it will return pixel values. See ScrollbarHelper in the support library for more information.
Additional reading: https://groups.google.com/forum/#!topic/android-developers/Hxe-bjtQVvk
It's kinda late, but hope it's ok.
1) The method actually is implemented by the subclasses. For example, this is what it looks like for AbsListView (ListView's superclass).
2) View's height can be different from its vertical scroll's height - just imagine a View with weird top/bottom padding .
These two points pretty much make other questions irrelevant :)
This is a sample code which might help you to understand as to how to get scrollBar top and bottom using computeVerticalScrollExtent:
scrollbarTop = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*this.computeVerticalScrollOffset();
scrollbarBottom = (float)this.computeVerticalScrollExtent()/this.computeVerticalScrollRange()*(this.computeVerticalScrollExtent()+this.computeVerticalScrollOffset());
According to the article from here:
I found this explanation correct.
ListView with 30 items has scrollRange equals to 3000, that is due to scrollRange = numberOfItems * 100, thus scrollExtent = numberOfVisibleItems * 100 and scrollOffset = numberOfScrolledItems * 100. You can find evidance of these words in the source code of AbsListView
So I've searched high and low but couldn't find an answer to my question. What I basically need is the behavior provided by Android's ViewFlipper or ViewPager but I want to display portions of the left and right views, to indicate the user there are elements to scroll, instead of having the selected view occupying the whole screen.
I would also like to add some effects to the left and side views, like dimming and scaling then down a little. Is it possible to do it with the stock ViewFlipper or ViewPager or do I have to roll out my own view group, à la cover flow (http://www.inter-fuser.com/2010/01/android-coverflow-widget.html)?
(P.S. I don't want to use the Gallery widget, that component sucks).
This is what we need to display once a view is selected (left and right views are still displayed):
Flinging left or right would transition the main view out, dimming and reducing it a little and doing the opposite with the next or previous view.
I would like to give an update to anyone who might want the same feature. A lot of progress has been made to implement this feature so far and now the view is working exactly as we need it to.
The ViewPager has a method called setPageMargin(). This method can receive a negative value which will make the fragments/views to overlap each other. To arrive at the desired layout, we first dynamically calculated the left and right margins by a percentage of the screen. This can be done statically as well but since we will be targeting a range of different screen sizes, this seems to be the best approach.
Later we set the ViewPager's page margin to 2 times the size of the side margins. This makes the views snap back together. However, at this time, there will be more than one view being displayed by the ViewPager.
All you have left to do is to either apply a transform (scale) to the views to the left and right (Android 3.0+) or add some more margins around them to shrink them to the right size (pre 3.0).
The OnPageChangeListener.onPageScrolled() can be used to track the ViewPager's scrolling. A smooth
transformation can be achieved. The code looks like this:
private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// positionOffset varies from 0 to 1 and indicates how far the view is
// from the center of the ViewPager. When a view is selected (centered), this
// value is 0.
// Fades the text in an out while the ViewPager scrolls from one view to another.
// On our final design the text views are fixed to the center of the screen and
// do not scroll along with the views.
if (positionOffset < 0.5) {
setTextViewAlpha(1 - positionOffset * 2);
} else {
setTextViewAlpha((positionOffset - 0.5f) * 2);
}
// It's surprisingly complicated to get the current object being displayed by
// a ViewPager (there's no getCurrentObject method). ScaleableFragment is just
// a custom class to allow an adapter to cache it internally and also correctly
// manage a fragment's lifecycle and restore from a saved state.
ScaleableFragment sampleFragment = (ScaleableFragment) ((ShowHeroShotImageFragmentPagerAdapter) pager
.getAdapter()).getItem(position);
// Calculates by how much the current view will be scaled down. The RATIO_SCALE
// is 0.3 in our case, which makes the side views 70% of the size of the
// center view. When centered, the scale will be 1. When
// fully scrolled, the scale will be 0.7.
float scale = 1 - (positionOffset * RATIO_SCALE);
// Just a shortcut to findViewById(R.id.image).setScale(scale);
sampleFragment.scaleImage(scale);
// Now, if not in the last element, scale the next one up (in opposite direction).
if (position + 1 < pager.getAdapter().getCount()) {
sampleFragment = (ScaleableFragment) ((ShowHeroShotImageFragmentPagerAdapter) pager.getAdapter())
.getItem(position + 1);
scale = positionOffset * RATIO_SCALE + (1 - RATIO_SCALE);
sampleFragment.scaleImage(scale);
}
}
// Once scrolling is done. Make sure the views are in the right scale (1 for center,
// 0.7 for sides). Required as the onPageScrolled() method does not guarantee it
// will interpolate to 1.0 precisely.
#Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
setTextViewAlpha(1);
ScaleableFragment sampleFragment = (ScaleableFragment) ((ShowHeroShotImageFragmentPagerAdapter) pager
.getAdapter()).getItem(pager.getCurrentItem());
sampleFragment.scaleImage(1);
sampleFragment.enableClicks();
if (pager.getCurrentItem() > 0) {
sampleFragment = (ScaleableFragment) ((ShowHeroShotImageFragmentPagerAdapter) pager.getAdapter())
.getItem(pager.getCurrentItem() - 1);
sampleFragment.scaleImage(1 - RATIO_SCALE);
sampleFragment.disableClicks();
}
if (pager.getCurrentItem() + 1 < pager.getAdapter().getCount()) {
sampleFragment = (ScaleableFragment) ((ShowHeroShotImageFragmentPagerAdapter) pager.getAdapter())
.getItem(pager.getCurrentItem() + 1);
sampleFragment.scaleImage(1 - RATIO_SCALE);
sampleFragment.disableClicks();
}
}
}
};
This is it. I didn't post the full solution but I hope this is enough to get someone else started.
P.S. On 3.0+ enable hardware acceleration. Without it, the scrolling looked choppy on a samsung galaxy tab 10.1.
I played some with it, and my solution is pretty simple. I'm posting it here in case someone is interested.
First, the layout for each fragment should allow some gaps on either side. the simplest way I found of doing this is using weights:
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.1" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.8"
android:orientation="vertical" >
<!-- fragment contents go here -->
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.1" />
This will leave a nice 10% margin on either side. Be sure not to set a background on these, since it will cover the neighbor fragments.
Now for the pager itself: you need to use negative margins, as AngraX explained, but also be sure to pre-load the subsequent fragments, as they are visible before they would be regularly. I got good results with this:
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
mPager.setPageMargin(getResources().getDisplayMetrics().widthPixels / -7);
mPager.setOffscreenPageLimit(2);
the -7 value leaves the edge of the next fragment visible, play around with your specific composition. I also recommend framing each fragment.
Well, here's a potential answer that might be close to what you're looking for, but not exactly what you're looking for: there's a project called ViewFlow that I think provides the ability to notify the user that there are more views via indicators on the screen. It also provides the ability to buffer views to the left and right of your current view, so you might be able to poke at the code to render small parts of them by basically shrinking what it views as its available screen size.
Edit: Silly me; I should read the final question before I answer. :) I don't think you can do this with a ViewFlipper or Viewpager, unfortunately.
Just use a ViewPager and play with the width of the fragments and margins. I have done so and it works fine.
This question already has answers here:
Android ViewPager padding/margin between page fragments
(7 answers)
Closed 9 years ago.
I'm using the new ViewPager-view from the Android compatibility library, and I can't figure out how to get padding between the pages so that when you're swiping between them, there's a visible border.
My ViewPager width is set to fill_parent so it takes up the entire width of the display. I can get this border between the pages by setting android:padding on the ViewPager, but that has the added (unwanted) effect of reducing the visible size of the page when it is fully displayed.
I guess what I need is to introduce a border outside the width of the view somehow, if that even makes sense...
The new Android Market does this the way I'd like, but I just can't figure out how it's accomplished.
There's currently no way to do that in the XML layout, but you can use the following functions in java:
setPageMargin(int marginPixels)
setPageMarginDrawable(drawable d)
setPageMarginDrawable(int resId)
You'll need to grab the pager from your XML layout first:
ViewPager pager = (ViewPager) findViewById(R.id.pager);
It seems that more recent releases of the support package (since revision 5) have added support for margins between pages of ViewPager via setPageMargin() and setPageMarginDrawable().
This would be a bit of work, but you could achieve the effect you want by having a border UI element that you then continue to animate off the screen at the same rate as the swipe is occuring.
E.g. say you are swiping right to left:
1) The page that comes in to view has the extra border element on it's left-hand edge by default.
2) You monitor the swipe by implementing a ViewPager.OnPageChangeListener() with a custom onPageScrolled() method. Compute the velocity of the swipe in here as well.
3) When the incoming page's left hand edge hits the left hand edge of the screen, start animating the border element off the left at the velocity you calculated.
...Thinking more about this, I think the illusion would be more effective if you animated the border element being translated left by it's width throughout the duration of the swipe. It sounds weird, but once you picture it, it will make sense :)
The source for ViewPager is available with the android v4 compatibility libs.
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* #param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* #param positionOffset Value from [0, 1) indicating the offset from the page at position.
* #param positionOffsetPixels Value in pixels indicating the offset from position.
*/
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);