Floating Button rotation depending on View Pager scroll state - android

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.

Related

Android ViewPager - Dynamic width & multiple visible pages

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.

Confused about setTranslationX() when translating FloatingActionButton on ViewPager scroll

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.

Scrolling of View happens after the position of scroll by touch

I have a customised View in an android application which is put inside a HorizontalScrollView as shown.
<HorizontalScrollView
android:id="#+id/feedBackScroller"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none">
<com.my.views.OfflineFeedbackView
android:id="#+id/feedBackView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="#dimen/session_rec_page_title_bar_height"
android:layout_marginBottom="#dimen/session_rec_page_ctrl_bar_height"/>
</HorizontalScrollView>
The OfflineFeedbackView shows the pitch track of the audio track that I am playing and I scroll this view by calling scrollTo(newXPos, newYPos) based on the current time of the playback. The problem that I am facing is that if I scroll the screen by touching and then start the playback, the reference of scrolling seems to get changed on my view and the scrolling takes place from the position to which I scrolled to by touch. I went through the API docs and found that the scrollTo(newXPos, newYPos) internally calls onScrollChanged(int l, int t, int oldl, int oldt) where oldl and oldt are the old horizontal and vertical origins respectively. So, what I am interpreting from this is that when I scroll by touching on the screen these origin values get changed. So, I also tried calling onScrollChanged(newX, newY, 0, 0) but this just freezes the screen and there is no scroll. Am I doing something wrong? What can be other better ways to handle this?
Here problem looks like as you said HorizontalScrollView positions gets changed when you touch.
So one solution may be find the initial top-left position of your scroll view
HorizontalScrollView hsv = (HorizontalScrollView) findViewById(R.id.ScrollView);
int x, y;
x = hsv.getLeft();
y = hsv.getTop();
and always scroll your child view relative to these stored x,y positions i.e
childView.scrollTo(x+newXPos,y+newYPos);

Android - Carousel like widget which displays a portion of the left and right elements

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.

How to make a selection border that tracks the checked radio button?

I have a set of RadioButtons with a custom style. I want to display a border around the button that is currently checked. This should be easy enough using XML, but now I want the border to be animated. If a new radio button is checked, the border should "fly" to its new location with a fancy animation:
+------+
|* btn1| o btn2
+------+
+------+
o b|n1 * |tn2
+------+
+------+
o btn1 |* btn2|
+------+
Because of this, I decided to make the border into a separate View object, so I can animate it properly. The trouble is in tracking the location of the corresponding radio button on the screen.
I'm trying to get it to work without animations first. My current attempt looks something like this (only showing the relevant attributes):
<RelativeLayout
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true">
<RadioGroup
android:id="#+id/radio_group">
<RadioButton/>
<RadioButton/>
<RadioButton/>
</RadioGroup>
<View
android:id="#+id/selection_border"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>
In the OnCheckedChangeListener of the RadioGroup, I move the selection border around by setting its margin (I could set the position, but that's a little harder with a RelativeLayout):
View radioButton = findViewById(checkedId);
View selectionBorder = findViewById(R.id.selection_border);
ViewGroup radioGroup = (ViewGroup)findViewById(R.id.radio_group);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(selectionBorder.getLayoutParams());
params.leftMargin = radioGroup.getLeft() + radioButton.getLeft();
params.topMargin = radioGroup.getTop() + radioButton.getTop();
selection.setLayoutParams(params);
selection.requestLayout();
Trouble happens, however, on initialization. Because no layouting has been done yet, the position of the border is set incorrectly. It doesn't seem to be possible to force a relayout immediately, and it also doesn't seem to be possible to get an event after layouting has been done.
All this hassle leads me to believe that there must be a cleaner way to accomplish what I want. Any bright ideas?
best way I think is the treat check box as an image. Two types checked/not checked. Then you are basically moving an image across the screen. Handle everthing with TouchEvents and images if user mouse downs with the region radious of image you know they have selected. This is easy because now you are just dealing with images, a canvas and stardard x,y coordinates.
To address your trouble with initialization (I've had much trouble in the past with this myself): If I need to know when a view's layout is complete, I give it a runnable in Activity.onCreate() then call that runnable from View.onSizeChanged(). I've come to use this pattern quite a bit and I've found it very reliable. You just need to remember to only call the runnable from onSizeChanged() if the new width and height are greater than 0 and if they are not equal to the old width and height.
So in your activity:
public void onCreate() {
...
yourView.setLayoutRunnable(new Runnable() {
#Override
public void run() {
//do post-layout stuff...
}
});
...
}
and in your view:
public Runnable layoutRunner;
public void setLayoutRunnable(Runnable runner) {
layoutRunner = runner;
}
#Override
public void onSizeChanged(int w, int h, int oldw, int oldh) {
if (w > 0 && h > 0 && (w != oldw || h != oldh)) {
if (layoutRunner != null) layoutRunner.run();
}
}
If you have multiple views that all need to have their layouts finished before you do your stuff, you have to keep a count as you get the notifications so you know when they're done.
And regarding your topic in general, I implemented something similar to your radio button concept in a game I wrote, but I decided to override View from scratch to do it. I had all options for a set in a single view, so really I replaced the RadioGroup view rather than the RadioButton view. Each option had a value and an extent. I always used text, but images would work just as easily. Then I just tracked the taps and drags relative to the extents of the options and moved the "selector" on a timed handler (a la animations in javascript).

Categories

Resources