2016-02-04 7 views
19

Sto creando un Q & A in cui ogni domanda è una carta. La risposta inizia a mostrare la prima riga, ma quando viene cliccata dovrebbe espandersi per mostrare la risposta completa.RecyclerView Q & A

Quando una risposta viene espansa/accasciata, il resto di RecyclerView deve essere animato per fare spazio all'espansione o al collasso per evitare di mostrare uno spazio vuoto.

Ho visto il discorso su RecyclerView animations e credo di volere un oggetto ItemAnimator personalizzato, in cui sovrascrivo animateChange. A quel punto dovrei creare un ObjectAnimator per animare l'altezza del LayoutParams della vista. Purtroppo sto facendo fatica a legare tutto insieme. Ritorna anche true quando eseguo l'override di canReuseUpdatedViewHolder, quindi riutilizziamo lo stesso 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); 
} 

In questo momento sto solo cercando di ottenere qualcosa da animare, ma non succede niente ... Qualche idea?

+0

Ho aggiornato la mia risposta perché ho visto che non era proprio quello che stavi chiedendo. –

+0

@George Mulligan funziona benissimo –

+0

@eimmer hai risolto il problema. –

risposta

10

Credo che l'animazione non funzionava perché non si può animare LayoutParams in quel modo anche se sarebbe pulito se si potesse. Ho provato il codice che avevi e tutto ciò che ha fatto è stato far saltare la mia vista alla nuova altezza. L'unico modo che ho trovato per farlo funzionare era usare uno ValueAnimator come puoi vedere nell'esempio qui sotto.

Ho notato alcune carenze quando si utilizza DefaultItemAnimator per mostrare/nascondere una vista aggiornando la sua visibilità. Sebbene abbia fatto spazio per la nuova vista e animato il resto degli elementi su e giù in base alla visibilità della vista espandibile, ho notato che non animava l'altezza della vista espandibile. Semplicemente sbiadito nel posto e fuori luogo usando solo il valore alfa.

Di seguito è riportato un numero personalizzato ItemAnimator con animazioni di dimensione e alfa in base al fatto di nascondere/mostrare un LinearLayout nel layout ViewHolder. Inoltre permette il riutilizzo degli stessi ViewHolder e tentativi di manipolazione animazioni parziali correttamente se l'utente tocca l'intestazione rapidamente:

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; 
     } 
    } 
} 

Questo è il risultato usando un leggermente modificato RecyclerView demo.

enter image description here

Aggiornamento:

appena notato il tuo caso d'uso è in realtà un po 'diverso dopo aver riletto la domanda. Hai una visualizzazione di testo e vuoi solo mostrare una singola riga e poi espanderla per mostrare tutte le linee. Per fortuna che semplifica l'animatore personalizzato:

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; 
    } 
} 

E la nuova demo:

enter image description here

+1

Penso che questa sia la migliore risposta per ottenere le animazioni corrette. NOTA: tenere traccia delle animazioni nel viewholder potrebbe essere problematico quando ViewHolders viene riciclato. – eimmer

+0

@eimmer La tua nota è un buon punto, ma ho pensato che il 'ViewHolder' non verrà riciclato mentre è in corso un'animazione. Yigit menziona nel suo [blog] (http://www.birbit.com/recyclerview-animations-part-1-how-animations-work/) che le viste possono essere rimosse dal 'LayoutManager' ma rimangono nel' RecyclerView' così le animazioni si comportano correttamente. Vorrei poter trovare il codice sorgente per il video in modo da poter vedere cosa stanno facendo per invertire l'animazione. –

-1

Per espandere & crollo animazione Android c'è biblioteca GitHub per esso. ExpandableRecyclerView

1) dipendenze .add nel build.gradle file di

dependencies { 
    compile 'com.android.support:recyclerview-v7:22.2.0' 
    compile 'com.bignerdranch.android:expandablerecyclerview:1.0.3' 
} 

Image of Expand & Collapse Animation

2) Espandi Riduci & animazione per RecyclerView animazione

public static class ExampleViewHolder extends RecyclerView.ViewHolder 
    implements View.OnClickListener { 

    private int originalHeight = 0; 
    private boolean isViewExpanded = false; 
    private YourCustomView yourCustomView 

    public ExampleViewHolder(View v) { 
    super(v); 
    v.setOnClickListener(this); 

    // Initialize other views, like TextView, ImageView, etc. here 

    // If isViewExpanded == false then set the visibility 
    // of whatever will be in the expanded to GONE 

    if (isViewExpanded == false) { 
     // Set Views to View.GONE and .setEnabled(false) 
     yourCustomView.setVisibility(View.GONE); 
     yourCustomView.setEnabled(false); 
    } 

    } 

    @Override 
    public void onClick(final View view) { 
     // If the originalHeight is 0 then find the height of the View being used 
     // This would be the height of the cardview 
     if (originalHeight == 0) { 
       originalHeight = view.getHeight(); 
      } 

     // Declare a ValueAnimator object 
     ValueAnimator valueAnimator; 
      if (!mIsViewExpanded) { 
       yourCustomView.setVisibility(View.VISIBLE); 
       yourCustomView.setEnabled(true); 
       mIsViewExpanded = true; 
       valueAnimator = ValueAnimator.ofInt(originalHeight, originalHeight + (int) (originalHeight * 2.0)); // These values in this method can be changed to expand however much you like 
      } else { 
       mIsViewExpanded = false; 
       valueAnimator = ValueAnimator.ofInt(originalHeight + (int) (originalHeight * 2.0), originalHeight); 

       Animation a = new AlphaAnimation(1.00f, 0.00f); // Fade out 

       a.setDuration(200); 
       // Set a listener to the animation and configure onAnimationEnd 
       a.setAnimationListener(new Animation.AnimationListener() { 
        @Override 
        public void onAnimationStart(Animation animation) { 

        } 

        @Override 
        public void onAnimationEnd(Animation animation) { 
         yourCustomView.setVisibility(View.INVISIBLE); 
         yourCustomView.setEnabled(false); 
        } 

        @Override 
        public void onAnimationRepeat(Animation animation) { 

        } 
       }); 

       // Set the animation on the custom view 
       yourCustomView.startAnimation(a); 
      } 
      valueAnimator.setDuration(200); 
      valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 
      valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
       public void onAnimationUpdate(ValueAnimator animation) { 
        Integer value = (Integer) animation.getAnimatedValue(); 
        view.getLayoutParams().height = value.intValue(); 
        view.requestLayout(); 
       } 
      }); 
      valueAnimator.start(); 
    } 

} 

Spero che questo ti possa aiutare.

+4

Non hai bisogno di una libreria esterna quando Android lo fa in modo nativo. – Simon

+1

Dall'esempio fornito da NerdRanch, vengono utilizzati due proprietari di viste separati. Questi sembrano un'assoluta ultima risorsa se nient'altro funziona. – eimmer

9

Non è necessario implementare una personalizzata ItemAnimator il valore predefinito DefaultItemAnimator supporta già ciò che è necessario. Tuttavia è necessario comunicare a questo animatore quali visualizzazioni sono cambiate. Immagino tu stia chiamando notifyDataSetChanged() nel tuo adattatore. Ciò impedisce l'animazione per un singolo oggetto modificato in RecyclerView (nel tuo caso l'espansione/collasso dell'elemento).

È necessario utilizzare notifyItemChanged(int position) per gli articoli che sono stati modificati. Ecco un breve metodo itemClicked(int position) che espande/comprime le viste in RecyclerView. Il campo expandedPosition tiene traccia della voce attualmente ampliato:

private void itemClicked(int position) { 
    if (expandedPosition == -1) { 
     // selected first item 
     expandedPosition = position; 
     notifyItemChanged(position); 
    } else if (expandedPosition == position) { 
     // collapse currently expanded item 
     expandedPosition = -1; 
     notifyItemChanged(position); 
    } else { 
     // collapse previously expanded item and expand new item 
     int oldExpanded = expandedPosition; 
     expandedPosition = position; 
     notifyItemChanged(oldExpanded); 
     notifyItemChanged(position); 
    } 
} 

Questo è il risultato:

enter image description here

+1

In che modo crolli effettivamente la vista? Hai appena impostato la sua altezza a 0? Imposta la visibilità su GONE? Applicare un'animazione personalizzata? – eimmer

+0

@eimmer ogni volta che si chiama 'notifyItemChanged', viene chiamato il metodo' onBindViewHolder' nell'adattatore per le posizioni rilevanti. È sufficiente fornire la versione espansa dell'elemento per ottenere l'animazione. Le visualizzazioni degli elementi nella gif sono solo due 'TextViews' in un' LinearLayout'. La differenza tra la versione compressa/espansa è un testo più lungo nel secondo 'TextView' –

+0

Questo è un approccio semplice e accurato ma presenta alcune carenze. In questo caso, i ViewHolder vengono scambiati eseguendo una dissolvenza e la modifica effettiva della dimensione dell'elemento modificato non viene animata.In questo esempio non è evidente dal momento che si sta solo estendendo il testo, ma se si sostituisce il testo interamente con qualcosa di diverso, il testo si sovrapporrebbe al vecchio e al nuovo proprietario della vista. Inoltre, poiché l'altezza non è animata, è imbarazzante se l'utente si espande e collassa rapidamente. Forse ho fatto qualcosa di sbagliato durante i test, ma ho scritto un animatore personalizzato per provare a superare questi problemi di seguito. –

0

Prova questa classe:

import android.animation.Animator; 
import android.animation.ValueAnimator; 
import android.graphics.Paint; 
import android.support.v7.widget.RecyclerView; 
import android.view.View; 
import android.view.animation.AccelerateDecelerateInterpolator; 
import android.widget.TextView; 

/** 
* Created by ankitagrawal on 2/14/16. 
*/ 

public class AnimatedViewHolder extends RecyclerView.ViewHolder 
     implements View.OnClickListener { 

    private int originalHeight = 0; 
    private boolean mIsViewExpanded = false; 
    private TextView textView; 

    // ..... CODE ..... // 
    public AnimatedViewHolder(View v) { 
     super(v); 
     v.setOnClickListener(this); 

     // Initialize other views, like TextView, ImageView, etc. here 

     // If isViewExpanded == false then set the visibility 
     // of whatever will be in the expanded to GONE 

     if (!mIsViewExpanded) { 
      // Set Views to View.GONE and .setEnabled(false) 
      textView.setLines(1); 
     } 

    } 
    @Override 
    public void onClick(final View view) { 

     // Declare a ValueAnimator object 
     ValueAnimator valueAnimator; 
     if(mIsViewExpanded) { 
      view.measure(View.MeasureSpec.makeMeasureSpec(((View) view.getParent()).getWidth(), View.MeasureSpec.AT_MOST), View.MeasureSpec.UNSPECIFIED); 
      mIsViewExpanded = false; 
      valueAnimator = ValueAnimator.ofInt(view.getHeight(), view.getMeasuredHeight()); 
     } else { 
      Paint.FontMetrics fm = ((TextView)view).getPaint().getFontMetrics(); 
      valueAnimator = ValueAnimator.ofInt(view.getHeight(), (int) (Math.abs(fm.top) + Math.abs(fm.bottom))); 
      mIsViewExpanded = true; 
     } 
     valueAnimator.addListener(new Animator.AnimatorListener() { 
      @Override 
      public void onAnimationEnd(Animator animation) { 
      } 

      @Override 
      public void onAnimationCancel(Animator animation) { } 

      @Override 
      public void onAnimationStart(Animator animation) { } 

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

     valueAnimator.setDuration(200); 
     valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 
     valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
      public void onAnimationUpdate(ValueAnimator animation) { 
       view.getLayoutParams().height = (Integer) animation.getAnimatedValue(); 
       view.requestLayout(); 
      } 
     }); 


     valueAnimator.start(); 

    } 
} 

Il vantaggio di questo approccio è che solo aggiungere animazione evento onClick e quello più adatto alle tue esigenze.

l'aggiunta di animazione a viewholder sarà troppo onerosa per le vostre esigenze. e itemAnimator di cui doc sono animazioni per gli elementi di layout, quindi non sono più adatti alle tue esigenze.