Is there a way to add animation when opening an expandable list in Android?
I want it so that when the user clicks on the expandable list, it has an animation/effect like I'm opening a sliding drawer.
It moves slow until it is completely opened.
I've spent a lot of time searching with no luck. The existing solutions are just not smooth enough - if your layout is something more complex than just 2 buttons, it becomes laggy.
So, I've created my own ListAdapter that caches the whole view into a Bitmap and then performs the animation on the cached view instead of the view itself. It works much faster.
Here it is: https://github.com/dmitry-zaitsev/ExpandableAdapter
The good news is that you don't need to rewrite a bunch of code - just wrap my ExpandableAdapter around your adapter and provide the id of the view that will act like a toggle button and the id of the view that holds the content of the second level:
new ExpandableAdapter(context, yourAdapter, R.id.switch, R.id.holder);
And that is all.
I have tried to make this work as well. I have found one solution that works for the childViews. It does not animate the actual expanding of the group though, but animates the child cells as they fill the space the expansion leaves behind.
Edit: There is a bug in collapsing, which will make some cells that should not be hidden, become hidden. This is probably related to View-recycling in the listView. I will will update when I have a solution to this.
Animating with layoutAnimation in setOnGroupClickListener
mResultList.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
#Override
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
if(mResultList.isGroupExpanded(groupPosition)){
mProgAdap.prepareToCollapseGroup(groupPosition);
setupLayoutAnimationClose(groupPosition);
mResultList.requestLayout();
}else{
boolean autoScrollToExpandedGroup = false;
mResultList.expandGroup(groupPosition,autoScrollToExpandedGroup);
setupLayoutAnimation();
//*/
}
//telling the listView we have handled the group click, and don't want the default actions.
return true;
}
private void setupLayoutAnimation() {
AnimationSet set = new AnimationSet(true);
Animation animation = new AlphaAnimation(0.0f, 1.0f);
animation.setDuration(50);
set.addAnimation(animation);
animation = new ScaleAnimation(1.0f, 1.0f, 0.0f, 1.0f, 0.5f, 1.0f);
animation.setDuration(50);
set.addAnimation(animation);
LayoutAnimationController controller = new LayoutAnimationController(set, 0.75f);
mResultList.setLayoutAnimationListener(null);
mResultList.setLayoutAnimation(controller);
}
private void setupLayoutAnimationClose(final int groupPosition) {
AnimationSet set = new AnimationSet(true);
Animation animation = new AlphaAnimation(1.0f, 0.0f);
animation.setDuration(50);
animation.setFillAfter(true);
animation.setFillEnabled(true);
set.addAnimation(animation);
animation = new ScaleAnimation(1.0f, 1.0f, 1.0f, 0.0f, 0.5f, 0.0f);
animation.setDuration(50);
animation.setFillAfter(true);
animation.setFillEnabled(true);
set.addAnimation(animation);
set.setFillAfter(true);
set.setFillEnabled(true);
LayoutAnimationController controller = new LayoutAnimationController(set, 0.75f);
controller.setOrder(LayoutAnimationController.ORDER_REVERSE);
mResultList.setLayoutAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
mResultList.collapseGroup(groupPosition);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
mResultList.setLayoutAnimation(controller);
}
});
We need more tweaks to make the animation only apply to the actual children of the expanded/collapsed group. Because we can't overload the correct part in the LayoutAnimationController, we need to create a special ViewGroup class. This is the same technique as in , "Can LayoutAnimationController animate only specified Views".
In the ExpandableListViewAdapter, we now need some state handling to allow or ignore animation on items in the list.
#Override
public void onGroupExpanded(int groupPos){
super.onGroupExpanded(groupPos);
int childCount = getChildrenCount(groupPos);
Log.d("EXPLIST","setting children to be expanded:" + childCount);
for(int j=0; j < getGroupCount(); j++){
for(int k=0; k < getChildrenCount(j); k++){
GoalServiceCell cell = (GoalServiceCell)getChild(j,k);
cell.expandAnimState = GoalServiceCell.ExpandAnimState.SHOULD_NOT_ANIMATE;
}
}
for(int i=0; i < childCount; i++){
GoalServiceCell cell = (GoalServiceCell)getChild(groupPos,i);
cell.expandAnimState = GoalServiceCell.ExpandAnimState.SHOULD_START_EXPAND;
}
}
public void prepareToCollapseGroup(int groupPos){
int childCount = getChildrenCount(groupPos);
for(int j=0; j < getGroupCount(); j++){
for(int k=0; k < getChildrenCount(j); k++){
GoalServiceCell cell = (GoalServiceCell)getChild(j,k);
cell.expandAnimState = GoalServiceCell.ExpandAnimState.SHOULD_NOT_ANIMATE;
}
}
for(int i=0; i < childCount; i++){
GoalServiceCell cell = (GoalServiceCell)getChild(groupPos,i);
cell.expandAnimState = GoalServiceCell.ExpandAnimState.SHOULD_START_COLLAPSIN;
}
}
#Override
public void onGroupCollapsed(int groupPos){
super.onGroupCollapsed(groupPos);
int childCount = getChildrenCount(groupPos);
for(int i=0; i < childCount; i++){
GoalServiceCell cell = (GoalServiceCell)getChild(groupPos,i);
cell.expandAnimState = GoalServiceCell.ExpandAnimState.SHOULD_NOT_ANIMATE;
}
}
And in the ViewHolder of the children.
void expandOrCollapse(GoalServiceCell cell,int position){
AnimationAverseRelativeLayout hack = (AnimationAverseRelativeLayout)master;
boolean shouldAnim = cell.expandAnimState == GoalServiceCell.ExpandAnimState.SHOULD_START_EXPAND ||
cell.expandAnimState == GoalServiceCell.ExpandAnimState.SHOULD_START_COLLAPSIN;
hack.setIfShouldAnimate(shouldAnim);
}
The GroupViews are also contained in a AnimationAverseRelativeLayout. Since I have set "shouldAnimate" to default to false, I don't need to touch them.
I had this same exact problem. And I fixed it once and for all. I open-sourced it to github.
https://github.com/tjerkw/Android-SlideExpandableListView
Basically, you include this project dependency with your Android project. And then wrap your ListAdapter into a SlideExpandableListAdapter. The wrapper will then add the slide functionality with animation to your ListView.
Hope it helps you, I'm already using it in two projects.
So what I've done is use a regular ListView and then perform the animation in onListItemClick.
The animation is similar to what I do at this link: Android animate drop down/up view proper
But only for a portion of the row view. The row view is implemented in following way in xml:
<somelayout>
<normal>
</normal>
<expanded>
</expanded>
</somelayout>
The normal is used without expand. When expand is activated the expanded is set to visible instead of gone. You need to keep control of setting it to gone again when closing (remember this is set at your convertviews which are recycled).
I can clarify further if needed, it's just quite a lot of code to put in.
That's what i did in this situations.
ObjectAnimator objectAnimator =
ObjectAnimator.ofPropertyValuesHolder(list1,
PropertyValuesHolder.ofInt("bottom",
currentlistHeight,currentlistHeight*2 ));
What this would do is that the height of the listView will get doubled and it would be animated.
If you set the current list height ZERO. this would act like a drawer.
Related
I want to animate the change of my RecyclerViews GridLayoutManager. I defaulty show a list of items in a grid with 3 columns and the user can select to show more or less columns.
I would like the views in the RecyclerView to move/scale to their new positions, but I have no idea how this could be done.
What I want in the end
allow to scale the grid via an expand/contract touch gesture => I know how to do that
animate the change of the LayoutManager
Does anyone know how I can animate the change of the LayoutManager?
The source of inspiration here would be the Google Photos app,the stock Sony Gallery app
There are basically 2 approaches you can go with:
You modify the spancount of the GridLayoutManager using setSpanCount(int)
You set a very high span count(~100) use the SpanSizeLookUp to change the per item spanSize on the fly.
I have used the Gist provided by Musenkishi,for this answer to provide an animator to animate the changes in grid layout changes
I have used this approach in a sample GitHub project implementing the same.
Caveats:
I have currently used the click listener to keep modifying the the span size look up.This could be changed to a ItemGestureListener to capture pinch zoom events and change accordingly.
You need to determine a way to choose a span count so that all the items in a row occupy the entire screen width (and hence you do not see any empty space)
You call notifyItemRangeChanged using a runnable post delayed since you cannot call the notifyChanged methods from within bindView/createView etc.
After changing the span size,you need to notifyItemRangeChanged with an appropriate range so that all the items currently displayed on the screen are shifted accordingly.I have used (code at the bottom)
This is not a complete solution but a 2 hour solution for the same.You can obviously improve on all the points mentioned :).
I hope to keep updating the sample since this kind of views have always fascinated me.
Do not view this as the final solution but just a particular way of achieving this approach. If you were to use a StaggerredLayoutManager instead,you could easily avoid blank spaces between items.
public int calculateRange() {
int start = ((GridLayoutManager) grv.getLayoutManager()).findFirstVisibleItemPosition();
int end = ((GridLayoutManager) grv.getLayoutManager()).findLastVisibleItemPosition();
if (start < 0)
start = 0;
if (end < 0)
end = getItemCount();
return end - start;
}
I deal with the same problem as you, and so far I have not found a good solution.
Simple change of columns number in GridLayoutManager seems weird so for now I use animation to fade out/in entire layout. Something like this:
private void animateRecyclerLayoutChange(final int layoutSpanCount) {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new DecelerateInterpolator());
fadeOut.setDuration(400);
fadeOut.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationRepeat(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
productsRecyclerLayoutManager.setSpanCount(layoutSpanCount);
productsRecyclerLayoutManager.requestLayout();
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setInterpolator(new AccelerateInterpolator());
fadeIn.setDuration(400);
productsRecycler.startAnimation(fadeIn);
}
});
productsRecycler.startAnimation(fadeOut);
}
If you combine fade out/in animation with scaling each visible item, It will be a decent animation for GridLayoutManager changes.
You can do this with "gesture detector"
see the sample tutorial here http://wiki.workassis.com/pinch-zoom-in-recycler-view/ In this tutorial we will fetch images from gallery and show them in a grid layout in recycler view. You will be able to change layout on pinch gesture. Following are the screen shots of different layouts.
mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
#Override
public boolean onScale(ScaleGestureDetector detector) {
if (detector.getCurrentSpan() > 200 && detector.getTimeDelta() > 200) {
if (detector.getCurrentSpan() - detector.getPreviousSpan() < -1) {
if (mCurrentLayoutManager == mGridLayoutManager1) {
mCurrentLayoutManager = mGridLayoutManager2;
mRvPhotos.setLayoutManager(mGridLayoutManager2);
return true;
} else if (mCurrentLayoutManager == mGridLayoutManager2) {
mCurrentLayoutManager = mGridLayoutManager3;
mRvPhotos.setLayoutManager(mGridLayoutManager3);
return true;
}
} else if(detector.getCurrentSpan() - detector.getPreviousSpan() > 1) {
if (mCurrentLayoutManager == mGridLayoutManager3) {
mCurrentLayoutManager = mGridLayoutManager2;
mRvPhotos.setLayoutManager(mGridLayoutManager2);
return true;
} else if (mCurrentLayoutManager == mGridLayoutManager2) {
mCurrentLayoutManager = mGridLayoutManager1;
mRvPhotos.setLayoutManager(mGridLayoutManager1);
return true;
}
}
}
return false;
}
});
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;
}
So in my android app I have a HorizontalScrollView that will display images. All images are downloaded an added to the View before it is avalible to the user. However when it does apear I want each image to animate in seperatly. Ive tried a LayoutTransition object attached to my layout with this code to show the views:
transition = new LayoutTransition();
transition.setStagger(LayoutTransition.APPEARING, 500);
transition.setDuration(1000);
transition.setAnimateParentHierarchy(true);
for (int i = 0; i < mGallery.getChildCount(); i++) {
mGallery.getChildAt(i).setVisibility(View.VISIBLE);
transition.showChild(mGallery, mGallery.getChildAt(i));
}
I have also tried this method using the the AnimationEndListener, and a recursive animateView() method
private void animateView(final int index) {
if (mGallery.getChildAt(index) == null)
return;
final View child = mGallery.getChildAt(index);
final Animation an = AnimationUtils.loadAnimation(mRootView.getContext(), R.anim.slideup);
AnimationEndListener listener = new AnimationEndListener() {
#Override
public void onAnimationEnd(Animation animation) {
animateView(index + 1);
}
};
an.setAnimationListener(listener);
child.setAnimation(an);
child.setVisibility(View.VISIBLE);
an.start();
}
The first method is preferable however it always animates in all my images at the same time. The second method kind of works, however it will apply the animation to the first view and the all subsequent views will appear in sequence without the animation playing.
I'm trying to implement the same kind of animation that happens when clicking the "clear all" button in the notification bar:
This is what I have now (for a ListView) but it's not working correctly. Because of timing/pauses, I think.
Animation animation = AnimationUtils.loadAnimation(getActivity(), android.R.anim.slide_out_right);
animation.setDuration(300);
int count = mNotificationList.getCount();
for (int i = 0; i < count; i++) {
View view = mNotificationList.getChildAt(i);
if (view != null)
view.startAnimation(animation);
}
Anyone knows how to accomplish the animation?
In order to achieve such an effect you should use a different Animation for each item, so you can easily set different starting offset via Animation.setStartOffset(long) method.
Your code should look like this:
int count = mNotificationList.getCount();
for (int i = 0; i < count; i++) {
View view = mNotificationList.getChildAt(i);
if (view != null) {
// create an Animation for each item
Animation animation = AnimationUtils.loadAnimation(
getActivity(),
android.R.anim.slide_out_right);
animation.setDuration(300);
// ensure animation final state is "persistent"
animation.setFillAfter(true);
// calculate offset (bottom ones first, like in notification panel)
animation.setStartOffset(100 * (count - 1 - i) );
view.startAnimation(animation);
}
}
I have a FrameLayout that recognize swipe gestures (up and down).
For example: if a swipe up are performed, I should animate the current view (that is MATCH_PARENT x MATCH_PARENT) to goes up at the same time a new view come from bottom.
I can achieve this with animations?
I solved this way:
private void swipeUp() {
current.currentPage++;
final View hidingView = currentView;
TranslateAnimation hide = new TranslateAnimation(0, 0, 0, -getHeight());
hide.setAnimationListener(new AnimationListenerAdapter() {
#Override
public void onAnimationEnd(Animation animation) {
hidingView.setVisibility(View.GONE);
}
});
hide.setDuration(1000);
hidingView.startAnimation(hide);
TranslateAnimation show = new TranslateAnimation(0, 0, getHeight(), 0);
show.setFillAfter(true);
show.setDuration(1000);
View nextView = getView();
addView(nextView, createLP());
nextView.startAnimation(show);
currentView = nextView;
}
If you want to actually switch views, you need to implement an AnimationListener that takes care of the animation. If you want more complex behavior like a "finger following" scroller between views, you will likely have to use something a bit more complex, but if you're just saying
if(I flicked upwards)
move view up
then AnimationListener is perfect for you. Just make sure you set the listener to the Animation in code.
Hope this helps!