I haven't investigated this much yet, however I haven't seen a feature such as this done before or found much information regarding the subject, so I thought it would be best to reach out to SO to see if anyone has toyed with this idea before and could provide any advice. If not, I'll post the solution below when I find it.
Desired Outcome
Currently I have a GridView populated with content. I have a FloatingActionButton on top of the GridView and when the user taps on the FAB, I would like to display a new View. When tapped, I want each individual GridView item to rotate and move towards the edge of the screen resulting in the whole GridView parting to make room for the new View to slide in from the bottom. The RecyclerView will become unscrollable when the secondary View is present, and will stay that way until the user goes back, doing so would result in the opposite animation occurring bringing the ViewHolders back into the centre and closing the gap.
Excuse me for my appauling attempt at drawing my problem. :)
GridView pre-animation
GridView after animation
Each view item should animate independently from one another and the end animation will simulate a kind of exploding RecyclerView effect.
Possible Approach / Attempted so Far
I am currently reading up on the behaviour of the individual elements of the RecyclerView. I also got some pretty good information from this Android Summit video about RecyclerView animations.
Right now I think there are a couple of different approache:
Using two different LayoutManagers and attempting to animate between them.
Using an ItemDecorator to alter each ViewHolders margins and angle.
Any advice or recommendations will be greatly appreciated, I'll update the above list as I work through the problem.
Thanks!
Being able to animate the views within a RecyclerView turned out to be very simple! All you need is a custom RecyclerView.LayoutManager and you then have access to all the views which are visible for your animating pleasure.
The main method here is getChildCount() and getChildAt(int index), with both of these methods as seen below in getVisibleViews() you have access to all of the views which the RecyclerView is currently displaying. I then use a simple View.animate() call to animate each view to the desired X position and rotation. I'm sure this will work with all other types of animations too! I now have exactly what I wanted. :)
Here's the custom LayoutManager, remember if you don't extend a predefined LayoutManager such as GridLayoutManager or LinearLayoutManager you'll need to supply more methods such as generateDefaultLayoutParams().
public class AnimatingGridLayoutManager extends GridLayoutManager {
private static final int PARTED_ANIMATION_DURATION = 200;
private static final int PARTED_ANIMATION_VARIANCE = 20;
private static final int PARTED_ANIMATION_OFFSET = 25;
private static final int PARTED_ANIMATION_ANGLE = 15;
private boolean itemsParted;
...
public void setItemsParted(boolean itemsParted, Activity context) {
Display display = context.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenWidth = size.x;
if (this.itemsParted != itemsParted) {
this.itemsParted = itemsParted;
if (itemsParted) {
partItems(context, screenWidth);
} else {
closeItems(screenWidth);
}
}
}
private void partItems(Context context, int screenWidth) {
List<View> visibleViews = getVisibleViews();
for (int i = 0; i < visibleViews.size(); i++) {
int viewWidth = visibleViews.get(i).getWidth();
int offset = ViewUtils.getPixelsFromDp(getRandomNumberNearInput(PARTED_ANIMATION_OFFSET), context);
int xPos = (-viewWidth) + offset;
int rotation = getRandomNumberNearInput(-PARTED_ANIMATION_ANGLE);
if (viewPositionIsRight(i)) {
// invert values to send view to end of screen instead of start
xPos = screenWidth - offset;
rotation = -rotation;
}
visibleViews.get(i).animate()
.x(xPos)
.rotation(rotation)
.setDuration(PARTED_ANIMATION_DURATION)
.setInterpolator(new FastOutSlowInInterpolator());
}
}
private void closeItems(int screenWidth) {
List<View> visibleViews = getVisibleViews();
for (int i = 0; i < visibleViews.size(); i++) {
int xPos = 0;
if (viewPositionIsRight(i)) {
xPos = screenWidth / 2;
}
visibleViews.get(i).animate()
.x(xPos)
.rotation(0)
.setDuration(PARTED_ANIMATION_DURATION)
.setInterpolator(new FastOutSlowInInterpolator());
}
}
private boolean viewPositionIsRight(int position) {
// if isn't 2 row grid new logic is needed
return position % 2 != 0;
}
private int getRandomNumberNearInput(int input) {
Random random = new Random();
int max = input + PARTED_ANIMATION_VARIANCE;
int min = input - PARTED_ANIMATION_VARIANCE;
return random.nextInt(max - min) + min;
}
private List<View> getVisibleViews() {
List<View> visibleViews = new ArrayList<>();
for (int i = 0; i < getChildCount(); i++) {
visibleViews.add(getChildAt(i));
}
return visibleViews;
}
}
Related
I'm implementing ViewPager on Android TV to show 3 items like this (background green/purple are debug background to depict whole ViewPager's page):
I archieved that using:
<android.support.v4.view.ViewPager
...
android:paddingLeft="300px"
android:paddingRight="300px"
android:clipToPadding="false"
...
/>
When loosing focus on ViewPager I wanted central item to scale down and pages to come closer together. To achieve that plugged animation:
ValueAnimator animator = ValueAnimator.ofInt(300, 400);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator){
Integer paddingHorizontal = (Integer) valueAnimator.getAnimatedValue();
vpScreenshots.setPadding(paddingHorizontal, 0, paddingHorizontal, 0);
}
});
animator.setDuration(300);
animator.start();
The animation works perfect for 1st element:
However when moving to next pages (both left and right) the padding animation starts to behave strange. When moving e.g. to right padding animation doesn't take action on left item but rather central and right ones. The effects goes deeper the more I move to right. The results are:
So switching pages in ViewPager spoils items alignment when padding animation occurs.
Why does it happen? How ti fix it ?
I managed to solve the issue. The only way which seems to work is to manipulate left and right items' "x" parameter.
Here is the code for focus listener:
if (hasFocus) {
Map<Integer, View> neighbors = vpScreenshots.findNeighbors();
View leftNeighbor = neighbors.get(0);
View rightNeighbor = neighbors.get(1);
if (!firstFocus) {
leftNeighbor.animate().xBy(-100).setDuration(FOCUS_ANIMATION_DURATION).start();
rightNeighbor.animate().xBy(100).setDuration(FOCUS_ANIMATION_DURATION).start();
}
firstFocus = false;
} else {
Map<Integer, View> neighbors = vpScreenshots.findNeighbors();
View leftNeighbor = neighbors.get(0);
View rightNeighbor = neighbors.get(1);
leftNeighbor.animate().xBy(100).setDuration(FOCUS_ANIMATION_DURATION).start();
rightNeighbor.animate().xBy(-100).setDuration(FOCUS_ANIMATION_DURATION).start();
}
Looking for neighbor items is attached to MyViewPager class:
public Map<Integer, View> findNeighbors() {
neighbors = new HashMap<>();
int currentPosition = getCurrentItem();
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int childPosition = (int) childView.getTag();
if ((currentPosition - childPosition) == 1) {
neighbors.put(0, childView);
}
if ((currentPosition - childPosition) == -1) {
neighbors.put(1, childView);
}
}
return neighbors;
}
I am trying to get a parallax effect working. Such as in the sound cloud android app or something like this.
I have a RecyclerView where every list item has a FrameLayout and an ImageView in it and I need every background image in the RecyclerView to be parallaxed.
public class MyRecyclerView {
private static final Dictionary<Integer, Integer> listViewItemHeights = new Hashtable<Integer, Integer>();
...
}
... and in the onBindViewHolder method I have added this:
...
viewHolder.scrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
private int getScroll() {
View c = discountSectionLayoutManager.getChildAt(0);
int scrollY = -c.getTop();
listViewItemHeights.put(discountSectionLayoutManager.findFirstVisibleItemPosition(), c.getHeight());
for (int i = 0; i < discountSectionLayoutManager.findFirstVisibleItemPosition(); ++i) {
if (listViewItemHeights.get(i) != null)
scrollY += listViewItemHeights.get(i); //add all heights of the views that are gone
}
return scrollY;
}
#Override
public void onScrollChanged() {
int scrollY = - getScroll();
viewHolder.imageFrameLayout.setTranslationY(scrollY * 0.1f);
}
}
The problem with this is that EVERY item will scroll up, and not just the ones that are visible. My RecyclerView is large with many items, and the items at the bottom will be all scrolled up as well. (for example if I can see item 1-3 and scroll down, even items 4-15 will scroll right as now). Maybe the getScroll() method can be modified somehow to only only scroll the background of the visible items?
Actually I don't know how it properly called - overlay, parallax or slideUP, whatever, I have an Activity called "cafe details" which presents a Container(LinearLayout) with header information (name, min price, delivery time min e.t.c) and other container (ViewPager) which contains a ExpandableListView with something information (menus&dishes) and all I want to do is slide up my Viewpager when scrolls listview to scpecific Y position to cover(or overlay) header information.
A similar effect (but with parallax that I don't need to use) looks like this
I can detect when user scrolling listview down or up but how I can move container with ViewPager to overlay other container? Please give me ideas, regards.
UPD
I have tried a huge number of ways how to implement it and all of them unfortunately are not suitable. So now I have come to next variant - add scroll listener to ListView, calculate scrollY position of view and then based on that move the viewpager on y axis by calling setTranslationY();
Here is some code
1) ViewPager's fragment
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
#Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
if (getActivity() != null) {
((MainActivity) getActivity()).resizePagerContainer(absListView);
}
}
});
2) MainActivity
//object fields
int previousPos;
private float minTranslation;
private float minHeight;
<--------somewhere in onCreate
minTranslation = - (llVendorDescHeaders.getMeasuredHeight()+llVendorDescNote.getMeasuredHeight());
//llVendorDescHeaders is linearLayout with headers that should be hidden
//lVendorDescNote is a textView on green background;
minHeight = llVendorDescriptionPagerContainer.getMeasuredHeight();
//llVendorDescriptionPagerContainer is a container which contains ViewPager
--------->
public void resizePagerContainer(AbsListView absListView){
final int scrollY = getScrollY(absListView);
if (scrollY != previousPos) {
final float translationY = Math.max(-scrollY, minTranslation);
llVendorDescriptionPagerContainer.setTranslationY(translationY);
previousPos = scrollY;
}
}
private int getScrollY(AbsListView view) {
View child = view.getChildAt(0);
if (child == null) {
return 0;
}
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = child.getTop();
return -top + firstVisiblePosition * child.getHeight() ;
}
This simple solution unfortunately has a problem - it is blinking and twitching (I don't know how to call it right) when scrolls slowly. So instead setTranslationY() I've used an objectAnimator:
public void resizePagerContainer(AbsListView absListView){
.............
ObjectAnimator moveAnim = ObjectAnimator.ofFloat(llVendorDescriptionPagerContainer, "translationY", translationY);
moveAnim.start();
..............
}
I don't like this solution because 1) anyway it does resize viewpager with delay, not instantly 2) I don't think that is good idea to create many ObjectAnimator's objects every time when I scroll my listView.
Need your help and fresh ideas. Regards.
I'm assuming that you are scrolling the top header (the ImageView is a child of the header) based on the scrollY of the ListView/ScrollView, as shown below:
float translationY = Math.max(-scrollY, mMinHeaderTranslation);
mHeader.setTranslationY(translationY);
mTopImage.setTranslationY(-translationY / 3); // For parallax effect
If you want to stick the header/image to a certain dimension and continue the scrolling without moving it anymore, then you can change the value of mMinHeaderTranslation to achieve that effect.
//change this value to increase the dimension of header stuck on the top
int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height);
mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
mMinHeaderTranslation = -mHeaderHeight + tabHeight;
The code snippets above are in reference to my demo but I think it's still general enough for you.
If you're interested you can check out my demo
https://github.com/boxme/ParallaxHeaderViewPager
Have you tried CoordinatorLayout from this new android's design support library? It looks like it's what you need. Check this video from 3:40 or this blog post.
I have some basic item decoration which draws some stuff in ItemDecoration.onDrawOver method.
This RecyclerView also has DefaultItemAnimator set on it.
Animations are working, all is great. Except one thing.
When all existing items are swapped with a new item set in this adapter, the decorations are being shown while animation is running.
I need a way to hide them.
When animation finishes, they need to be shown, but while it is running, they must be hidden.
I tried the following:
public void onDrawOver(..., RecyclerView.State state) {
if(state.willRunPredictiveAnimations() || state.willRunSimpleAnimations()) {
return;
}
// else do drawing stuff here
}
but this isn't helping. Decoration is only removed for the short period of animation, but then appears again while it is still running.
Also setup includes a RecyclerView.Adapter which hasStableIds() (in case that bit matters).
It may depend somewhat on the type of animation you're using, but at least for DefaultItemAnimator you need to account for the X/Y translation being done during the animation. You can get these values with child.getTranslationX() and child.getTranslationY().
For example, for the vertical case of onDraw/onDrawOver:
private void drawVertical(Canvas c, RecyclerView parent) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
final int dividerHeight = mDivider.getIntrinsicHeight();
for (int i = 1; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int ty = (int)(child.getTranslationY() + 0.5f);
final int top = child.getTop() - params.topMargin + ty;
final int bottom = top + dividerHeight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
(You may prefer to use ViewCompat.getTranslationY(child) if you need to support < API 11.)
Note: for other types of animations, additional adjustments may need to be made. (For example, horizontal translation might also need to be accounted for.)
Found an answer myself:
To hide item decorations during an item animation one can simply use this check in onDraw/onDrawOver:
public void onDrawOver(..., RecyclerView parent, ...) {
if(parent.getItemAnimator() != null && parent.getItemAnimator().isRunning()) {
return;
}
// else do drawing stuff here
}
You can try to check child alpha (only for case default animation). If alpha 0 then do nothing
I want to scroll the a ListView in Android by number of pixels. For example I want to scroll the list 10 pixels down (so that the first item on the list has its top 10 pixel rows hidden).
I thought the obviously visible scrollBy or scrollTo methods on ListView would do the job, but they don't, instead they scroll the whole list wrongly (In fact, the getScrollY always return zero even though I have scrolled the list using my finger.)
What I'm doing is I'm capturing Trackball events and I want to scroll the listview smoothly according to the motion of the trackball.
The supported way to scroll a ListView widget is:
mListView.smoothScrollToPosition(position);
http://developer.android.com/reference/android/widget/AbsListView.html#smoothScrollToPosition(int)
However since you mentioned specifically that you would like to offset the view vertically, you must call:
mListView.setSelectionFromTop(position, yOffset);
http://developer.android.com/reference/android/widget/ListView.html#setSelectionFromTop(int,%20int)
Note that you can also use smoothScrollByOffset(yOffset). However it is only supported on API >= 11
http://developer.android.com/reference/android/widget/ListView.html#smoothScrollByOffset(int)
If you look at the source for the scrollListBy() method added in api 19 you will see that you can use the package scoped trackMotionScroll method.
public class FutureListView {
private final ListView mView;
public FutureListView(ListView view) {
mView = view;
}
/**
* Scrolls the list items within the view by a specified number of pixels.
*
* #param y the amount of pixels to scroll by vertically
*/
public void scrollListBy(int y) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mView.scrollListBy(y);
} else {
// scrollListBy just calls trackMotionScroll
trackMotionScroll(-y, -y);
}
}
private void trackMotionScroll(int deltaY, int incrementalDeltaY) {
try {
Method method = AbsListView.class.getDeclaredMethod("trackMotionScroll", int.class, int.class);
method.setAccessible(true);
method.invoke(mView, deltaY, incrementalDeltaY);
} catch (Exception ex) {
throw new RuntimeException(ex);
};
}
}
Here is some code from my ListView subclass. It can easily be adapted so it can be used in Activity code.
getListItemsHeight() returns the total pixel height of the list, and fills an array with vertical pixel offsets of each item. While this information is valid, getListScrollY() returns the current vertical pixel scroll position, and scrollListToY() scrolls the list to pixel position.
If the size or the content of the list changes, getListItemsHeight() has to be called again.
private int m_nItemCount;
private int[] m_nItemOffY;
private int getListItemsHeight()
{
ListAdapter adapter = getAdapter();
m_nItemCount = adapter.getCount();
int height = 0;
int i;
m_nItemOffY = new int[m_nItemCount];
for(i = 0; i< m_nItemCount; ++i){
View view = adapter.getView(i, null, this);
view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
m_nItemOffY[i] = height;
height += view.getMeasuredHeight();
}
return height;
}
private int getListScrollY()
{
int pos, nScrollY, nItemY;
View view;
pos = getFirstVisiblePosition();
view = getChildAt(0);
nItemY = view.getTop();
nScrollY = m_nItemOffY[pos] - nItemY;
return nScrollY;
}
private void scrollListToY(int nScrollY)
{
int i, off;
for(i = 0; i < m_nItemCount; ++i){
off = m_nItemOffY[i] - nScrollY;
if(off >= 0){
setSelectionFromTop(i, off);
break;
}
}
}
For now, ListViewCompat is probably a better solution.
android.support.v4.widget.ListViewCompat.scrollListBy(#NonNull ListView listView, int y)
if you want to move by pixels then u can use this
public void scrollBy(ListView l, int px){
l.setSelectionFromTop(l.getFirstVisiblePosition(),l.getChildAt(0).getTop() - px);
}
this works for even ones with massive headers