Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RecyclerView Q&A

I'm creating a Q&A where each question is a card. The answer starts showing the first line, but when its clicked it should expanded to show the full answer.

When an answer is expanded/collapsed the rest of the RecyclerView should animate to make room for the expansion or collapse to avoid showing a blank space.

I watched the talk on RecyclerView animations, and believe I want a custom ItemAnimator, where I override animateChange. At that point I should create an ObjectAnimator to animate the height of the View's LayoutParams. Unfortunately I'm having a hard time tying it all together. I also return true when overriding canReuseUpdatedViewHolder, so we reuse the same viewholder.

@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
    return true;
}


@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder,
                             @NonNull final RecyclerView.ViewHolder newHolder,
                             @NonNull ItemHolderInfo preInfo,
                             @NonNull ItemHolderInfo postInfo) {
    Log.d("test", "Run custom animation.");

    final ColorsAdapter.ColorViewHolder holder = (ColorsAdapter.ColorViewHolder) newHolder;

    FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) holder.tvColor.getLayoutParams();
    ObjectAnimator halfSize = ObjectAnimator.ofInt(holder.tvColor.getLayoutParams(), "height", params.height, 0);
    halfSize.start();
    return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
}

Right now I'm just trying to get something to animate, but nothing happens... Any ideas?

like image 511
eimmer Avatar asked Feb 04 '16 17:02

eimmer


People also ask

Is RecyclerView deprecated?

Today, suddenly Recyclerview. Viewholder became deprecated. Other, android project is no deprecated.

What is RecyclerView?

RecyclerView is the ViewGroup that contains the views corresponding to your data. It's a view itself, so you add RecyclerView into your layout the way you would add any other UI element. Each individual element in the list is defined by a view holder object.

What is the use of ViewHolder in RecyclerView Android?

A ViewHolder describes an item view and metadata about its place within the RecyclerView. Adapter implementations should subclass ViewHolder and add fields for caching potentially expensive findViewById results.

What is the use of onBindViewHolder in Android?

bindViewHolder. This method internally calls onBindViewHolder to update the ViewHolder contents with the item at the given position and also sets up some private fields to be used by RecyclerView.


1 Answers

I think your animation was not working because you cannot animate LayoutParams that way although it would be neat if you could. I tried the code you had and all it did was make my view jump to the new height. Only way I found to get this to work was to use a ValueAnimator as you can see in the example below.

I noticed some shortcomings when using the DefaultItemAnimator to show/hide a view by updating its visibility. Although it did make room for the new view and animated the rest of the items up and down based on the visibility of the expandable view, I noticed it did not animate the height of the expandable view. It simply faded into place and out of place using alpha value only.

Below is a custom ItemAnimator that has size and alpha animations based on hiding/showing a LinearLayout in the ViewHolder layout. It also allows the reuse of the same ViewHolder and attempts handling partial animations correctly if the user taps the header quickly:

public static class MyAnimator extends DefaultItemAnimator {
    @Override
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
        return true;
    }

    private HashMap<RecyclerView.ViewHolder, AnimatorState> animatorMap = new HashMap<>();

    @Override
    public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull final RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        final ValueAnimator heightAnim;
        final ObjectAnimator alphaAnim;

        final CustomAdapter.ViewHolder vh = (CustomAdapter.ViewHolder) newHolder;
        final View expandableView = vh.getExpandableView();
        final int toHeight; // save height for later in case reversing animation

        if(vh.isExpanded()) {
            expandableView.setVisibility(View.VISIBLE);

            // measure expandable view to get correct height
            expandableView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
            toHeight = expandableView.getMeasuredHeight();
            alphaAnim = ObjectAnimator.ofFloat(expandableView, "alpha", 1f);
        } else {
            toHeight = 0;
            alphaAnim = ObjectAnimator.ofFloat(expandableView, "alpha", 0f);
        }

        heightAnim = ValueAnimator.ofInt(expandableView.getHeight(), toHeight);
        heightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                expandableView.getLayoutParams().height = (Integer) heightAnim.getAnimatedValue();
                expandableView.requestLayout();
            }
        });

        AnimatorSet animSet = new AnimatorSet()
                .setDuration(getChangeDuration());
        animSet.playTogether(heightAnim, alphaAnim);
        animSet.addListener(new Animator.AnimatorListener() {
            private boolean isCanceled;

            @Override
            public void onAnimationStart(Animator animation) { }

            @Override
            public void onAnimationEnd(Animator animation) {
                if(!vh.isExpanded() && !isCanceled) {
                    expandableView.setVisibility(View.GONE);
                }

                dispatchChangeFinished(vh, false);
                animatorMap.remove(newHolder);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                isCanceled = true;
            }

            @Override
            public void onAnimationRepeat(Animator animation) { }
        });

        AnimatorState animatorState = animatorMap.get(newHolder);
        if(animatorState != null) {
            animatorState.animSet.cancel();

            // animation already running. Set start current play time of
            // new animations to keep them smooth for reverse animation
            alphaAnim.setCurrentPlayTime(animatorState.alphaAnim.getCurrentPlayTime());
            heightAnim.setCurrentPlayTime(animatorState.heightAnim.getCurrentPlayTime());

            animatorMap.remove(newHolder);
        }

        animatorMap.put(newHolder, new AnimatorState(alphaAnim, heightAnim, animSet));

        dispatchChangeStarting(newHolder, false);
        animSet.start();

        return false;
    }

    public static class AnimatorState {
        final ValueAnimator alphaAnim, heightAnim;
        final AnimatorSet animSet;

        public AnimatorState(ValueAnimator alphaAnim, ValueAnimator heightAnim, AnimatorSet animSet) {
            this.alphaAnim = alphaAnim;
            this.heightAnim = heightAnim;
            this.animSet = animSet;
        }
    }
}

This is the result using a slightly modified RecyclerView demo.

enter image description here

Update:

Just noticed your use case is actually a bit different after rereading the question. You have a text view and only want to show a single line of it and then later expand it to show all lines. Fortunately that simplifies the custom animator:

public static class MyAnimator extends DefaultItemAnimator {
    @Override
    public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
        return true;
    }

    private HashMap<RecyclerView.ViewHolder, ValueAnimator> animatorMap = new HashMap<>();

    @Override
    public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull final RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
        ValueAnimator prevAnim = animatorMap.get(newHolder);
        if(prevAnim != null) {
            prevAnim.reverse();
            return false;
        }

        final ValueAnimator heightAnim;
        final CustomAdapter.ViewHolder vh = (CustomAdapter.ViewHolder) newHolder;
        final TextView tv = vh.getExpandableTextView();

        if(vh.isExpanded()) {
            tv.measure(View.MeasureSpec.makeMeasureSpec(((View) tv.getParent()).getWidth(), View.MeasureSpec.AT_MOST), View.MeasureSpec.UNSPECIFIED);
            heightAnim = ValueAnimator.ofInt(tv.getHeight(), tv.getMeasuredHeight());
        } else {
            Paint.FontMetrics fm = tv.getPaint().getFontMetrics();
            heightAnim = ValueAnimator.ofInt(tv.getHeight(), (int)(Math.abs(fm.top) + Math.abs(fm.bottom)));
        }

        heightAnim.setDuration(getChangeDuration());
        heightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                tv.getLayoutParams().height = (Integer) heightAnim.getAnimatedValue();
                tv.requestLayout();
            }
        });

        heightAnim.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationEnd(Animator animation) {
                dispatchChangeFinished(vh, false);
                animatorMap.remove(newHolder);
            }

            @Override
            public void onAnimationCancel(Animator animation) { }

            @Override
            public void onAnimationStart(Animator animation) { }

            @Override
            public void onAnimationRepeat(Animator animation) { }
        });

        animatorMap.put(newHolder, heightAnim);

        dispatchChangeStarting(newHolder, false);
        heightAnim.start();

        return false;
    }
}

And the new demo:

enter image description here

like image 83
George Mulligan Avatar answered Oct 15 '22 21:10

George Mulligan