Android Open Source - HydraListAndroid Expanding List View Delegate






From Project

Back to project page HydraListAndroid.

License

The source code is released under:

Apache License

If you think the Android project HydraListAndroid listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright (C) 2013 The Android Open Source Project
 * /*from  w w  w.j a va2s. c o m*/
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.fada21.android.hydralist.expandable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.graphics.Canvas;
import android.support.v4.view.ViewCompat;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.AbsListView;
import android.widget.AdapterView;

import com.fada21.android.hydralist.HydraListAdapter;
import com.fada21.android.hydralist.expandable.interfaces.ExpandableListItem;
import com.fada21.android.hydralist.util.PublicListView;

/**
 * A custom listview which supports the preview of extra content corresponding to each cell by clicking on the cell to hide and show the extra content.
 */
public class ExpandingListViewDelegate {

  private boolean mShouldRemoveObserver = false;

  private List<View> mViewsToDraw = new ArrayList<View>();

  private int[] mTranslate;

  private final PublicListView nlv;

  public ExpandingListViewDelegate(PublicListView nlv) {
    this.nlv = nlv;
    init();
  }

  @SuppressWarnings("unchecked")
  public HydraListAdapter<ExpandableListItem> getExpandingAdapter() {
    return (HydraListAdapter<ExpandableListItem>) nlv.getAdapter();
  }

  /**
   * Listens for item clicks and expands or collapses the selected view depending on its current state.
   */
  private AdapterView.OnItemClickListener mItemClickListener;

  protected void init() {
    mItemClickListener = new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        @SuppressWarnings("unchecked")
        ExpandableListItem item = (ExpandableListItem) nlv.getItemAtPosition(nlv.getPositionForView(view));
        if (item.isExpandable()) {
          if (!item.isExpanded()) {
            expandView(view, position, id);
          } else {
            collapseView(view, position, id);
          }
        }
      }
    };
    nlv.setOnItemClickListener(mItemClickListener);
  }

  /**
   * <p>
   * Calculates the top and bottom bound changes of the selected item. These values are also used to move the bounds of the items around the one that is
   * actually being expanded or collapsed.
   * </p>
   * 
   * <p>
   * This method can be modified to achieve different user experiences depending on how you want the cells to expand or collapse. In this specific demo, the
   * cells always try to expand downwards (leaving top bound untouched), and similarly, collapse upwards (leaving top bound untouched). If the change in
   * bounds results in the complete disappearance of a cell, its lower bound is moved is moved to the top of the screen so as not to hide any additional
   * content that the user has not interacted with yet. Furthermore, if the collapsed cell is partially off screen when it is first clicked, it is translated
   * such that its full contents are visible. Lastly, this behavior varies slightly near the bottom of the listview in order to account for the fact that the
   * bottom bounds of the actual listview cannot be modified.
   * </p>
   */
  private int[] getTopAndBottomTranslations(int top, int bottom, int yDelta, boolean isExpanding) {
    int yTranslateTop = 0;
    int yTranslateBottom = yDelta;

    int height = bottom - top;

    if (isExpanding) {
      boolean isOverTop = top < 0;
      boolean isBelowBottom = (top + height + yDelta) > nlv.getHeight();
      if (isOverTop) {
        yTranslateTop = top;
        yTranslateBottom = yDelta - yTranslateTop;
      } else if (isBelowBottom) {
        int deltaBelow = top + height + yDelta - nlv.getHeight();
        yTranslateTop = top - deltaBelow < 0 ? top : deltaBelow;
        yTranslateBottom = yDelta - yTranslateTop;
      }
    } else {
      int offset = nlv.computeVerticalScrollOffset();
      int range = nlv.computeVerticalScrollRange();
      int extent = nlv.computeVerticalScrollExtent();

      boolean isListFilled = range >= extent;

      float pixelsConvertion = (float) nlv.getHeight() / (float) extent;

      int allowedToShinkTop = (int) (offset * pixelsConvertion);
      int allowedToShinkBottom = (int) ((range - offset - extent) * pixelsConvertion);
      boolean isCollapsingBelowBottom = isListFilled && yTranslateBottom > allowedToShinkBottom;
      boolean isCellCompletelyDisappearing = bottom - yTranslateBottom < 0;

      if (isCollapsingBelowBottom) {
        yTranslateTop = Math.min(allowedToShinkTop, yTranslateBottom);
        yTranslateBottom = yDelta - yTranslateTop;
      } else if (isCellCompletelyDisappearing) {
        yTranslateBottom = bottom;
        yTranslateTop = yDelta - yTranslateBottom;
      }
    }

    return new int[] { yTranslateTop, yTranslateBottom };
  }

  /**
   * <p>
   * This method expands the view that was clicked and animates all the views around it to make room for the expanding view. There are several steps required
   * to do this which are outlined below.
   * </p>
   * 
   * <li>Store the current top and bottom bounds of each visible item in the listview.</li> <li>Update the layout parameters of the selected view. In the
   * context of this method, the view should be originally collapsed and set to some custom height. The layout parameters are updated so as to wrap the
   * content of the additional text that is to be displayed.</li>
   * 
   * <br/>
   * <br/>
   * <p>
   * After invoking a layout to take place, the listview will order all the items such that there is space for each view. This layout will be independent of
   * what the bounds of the items were prior to the layout so two pre-draw passes will be made. This is necessary because after the layout takes place, some
   * views that were visible before the layout may now be off bounds but a reference to these views is required so the animation completes as intended.
   * </p>
   * <br/>
   * 
   * <li>The first predraw pass will set the bounds of all the visible items to their original location before the layout took place and then force another
   * layout. Since the bounds of the cells cannot be set directly, the method setSelectionFromTop can be used to achieve a very similar effect.</li> <li>The
   * expanding view's bounds are animated to what the final values should be from the original bounds.</li> <li>The bounds above the expanding view are
   * animated upwards while the bounds below the expanding view are animated downwards.</li> <li>The extra text is faded in as its contents become visible
   * throughout the animation process.</li>
   * 
   * <br/>
   * <br/>
   * <p>
   * It is important to note that the listview is disabled during the animation because the scrolling behavior is unpredictable if the bounds of the items
   * within the listview are not constant during the scroll.
   * </p>
   * <br/>
   * 
   * @param view
   *            expanding view
   * @param position
   *            item position in list
   * @param id
   *            item id
   */
  private void expandView(final View view, int position, long id) {
    // final ExpandableListItem item = (ExpandableListItem) nlv.getItemAtPosition(nlv.getPositionForView(view));
    HydraListAdapter<ExpandableListItem> expandingAdapter = getExpandingAdapter();
    final ExpandableListItem item = expandingAdapter.getDataProvider().get(position);
    ExpandingLayout expandingLayout = expandingAdapter.getExpandingHelper().getExpandedView(view, item);

    /* Store the original top and bottom bounds of all the cells. */
    final int oldTop = view.getTop();
    final int oldBottom = view.getBottom();

    final HashMap<View, int[]> oldCoordinates = new HashMap<View, int[]>();

    int childCount = nlv.getChildCount();
    for (int i = 0; i < childCount; i++) {
      View v = nlv.getChildAt(i);
      ViewCompat.setHasTransientState(v, true);
      oldCoordinates.put(v, new int[] { v.getTop(), v.getBottom() });
    }

    /* Update the layout so the extra content becomes visible. */
    expandingLayout.setVisibility(View.VISIBLE);

    /*
     * Add an onPreDraw Listener to the listview. onPreDraw will get invoked after onLayout
     * and onMeasure have run but before anything has been drawn. This
     * means that the final post layout properties for all the items have already been
     * determined, but still have not been rendered onto the screen.
     */
    final ViewTreeObserver observer = nlv.getViewTreeObserver();
    observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {

      @Override
      public boolean onPreDraw() {
        /* Determine if this is the first or second pass. */
        if (!mShouldRemoveObserver) {
          mShouldRemoveObserver = true;

          /*
           * Calculate what the parameters should be for setSelectionFromTop.
           * The ListView must be offset in a way, such that after the animation
           * takes place, all the cells that remain visible are rendered completely
           * by the ListView.
           */
          int newTop = view.getTop();
          int newBottom = view.getBottom();

          int newHeight = newBottom - newTop;
          int oldHeight = oldBottom - oldTop;
          int delta = newHeight - oldHeight;

          mTranslate = getTopAndBottomTranslations(oldTop, oldBottom, delta, true);

          int currentTop = view.getTop();
          int futureTop = oldTop - mTranslate[0];

          int firstChildStartTop = nlv.getChildAt(0).getTop();
          int firstVisiblePosition = nlv.getFirstVisiblePosition();
          int deltaTop = currentTop - futureTop;

          int i;
          int childCount = nlv.getChildCount();
          for (i = 0; i < childCount; i++) {
            View v = nlv.getChildAt(i);
            int height = v.getBottom() - Math.max(0, v.getTop());
            if (deltaTop - height > 0) {
              firstVisiblePosition++;
              deltaTop -= height;
            } else {
              break;
            }
          }

          if (i > 0) {
            firstChildStartTop = 0;
          }

          nlv.setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop);

          /* Request another layout to update the layout parameters of the cells. */
          nlv.requestLayout();

          /*
           * Return false such that the ListView does not redraw its contents on
           * this layout but only updates all the parameters associated with its
           * children.
           */
          return false;
        }

        /* Remove the predraw listener so this method does not keep getting called. */
        mShouldRemoveObserver = false;
        observer.removeOnPreDrawListener(this);

        int yTranslateTop = mTranslate[0];
        int yTranslateBottom = mTranslate[1];

        ArrayList<Animator> animations = new ArrayList<Animator>();

        int index = nlv.indexOfChild(view);

        /*
         * Loop through all the views that were on the screen before the cell was
         * expanded. Some cells will still be children of the ListView while
         * others will not. The cells that remain children of the ListView
         * simply have their bounds animated appropriately. The cells that are no
         * longer children of the ListView also have their bounds animated, but
         * must also be added to a list of views which will be drawn in dispatchDraw.
         */
        for (View v : oldCoordinates.keySet()) {
          int[] old = oldCoordinates.get(v);
          v.setTop(old[0]);
          v.setBottom(old[1]);
          if (v.getParent() == null) {
            mViewsToDraw.add(v);
            int delta = old[0] < oldTop ? -yTranslateTop : yTranslateBottom;
            animations.add(getAnimation(v, delta, delta));
          } else {
            int i = nlv.indexOfChild(v);
            if (v != view) {
              int delta = i > index ? yTranslateBottom : -yTranslateTop;
              animations.add(getAnimation(v, delta, delta));
            }
            ViewCompat.setHasTransientState(v, false);
          }
        }

        /* Adds animation for expanding the cell that was clicked. */
        animations.add(getAnimation(view, -yTranslateTop, yTranslateBottom));

        /* Adds an animation for fading in the extra content. */
        animations.add(ObjectAnimator.ofFloat(view.findViewById(getExpandingAdapter().getExpandingHelper().getExpandingLayout()), View.ALPHA, 0, 1));

        /* Disabled the ListView for the duration of the animation. */
        nlv.setEnabled(false);
        nlv.setClickable(false);

        /* Play all the animations created above together at the same time. */
        AnimatorSet s = new AnimatorSet();
        s.playTogether(animations);
        s.addListener(new AnimatorListenerAdapter() {
          @Override
          public void onAnimationEnd(Animator animation) {
            item.setExpanded(true);
            nlv.setEnabled(true);
            nlv.setClickable(true);
            if (mViewsToDraw.size() > 0) {
              for (View v : mViewsToDraw) {
                ViewCompat.setHasTransientState(v, false);
              }
            }
            mViewsToDraw.clear();
          }
        });
        s.start();
        return true;
      }
    });
  }

  /**
   * By overriding dispatchDraw, we can draw the cells that disappear during the expansion process. When the cell expands, some items below or above the
   * expanding cell may be moved off screen and are thus no longer children of the ListView's layout. By storing a reference to these views prior to the
   * layout, and guaranteeing that these cells do not get recycled, the cells can be drawn directly onto the canvas during the animation process. After the
   * animation completes, the references to the extra views can then be discarded.
   */
  public void dispatchDraw(Canvas canvas) {
    if (mViewsToDraw.size() == 0) {
      return;
    }

    for (View v : mViewsToDraw) {
      canvas.translate(0, v.getTop());
      v.draw(canvas);
      canvas.translate(0, -v.getTop());
    }
  }

  /**
   * This method collapses the view that was clicked and animates all the views around it to close around the collapsing view. There are several steps
   * required to do this which are outlined below.
   * 
   * <li>Update the layout parameters of the view clicked so as to minimize its height to the original collapsed (default) state.</li> <li>After invoking a
   * layout, the listview will shift all the cells so as to display them most efficiently. Therefore, during the first predraw pass, the listview must be
   * offset by some amount such that given the custom bound change upon collapse, all the cells that need to be on the screen after the layout are rendered by
   * the listview.</li> <li>On the second predraw pass, all the items are first returned to their original location (before the first layout).</li> <li>The
   * collapsing view's bounds are animated to what the final values should be.</li> <li>The bounds above the collapsing view are animated downwards while the
   * bounds below the collapsing view are animated upwards.</li> <li>The extra text is faded out as its contents become visible throughout the animation
   * process.</li>
   * 
   * @param view
   *            collapsing view
   * @param position
   *            item position in list
   * @param id
   *            item id
   */
  private void collapseView(final View view, int position, long id) {
    final ExpandableListItem item = (ExpandableListItem) nlv.getItemAtPosition(nlv.getPositionForView(view));

    /* Store the original top and bottom bounds of all the cells. */
    final int oldTop = view.getTop();
    final int oldBottom = view.getBottom();

    final HashMap<View, int[]> oldCoordinates = new HashMap<View, int[]>();

    int childCount = nlv.getChildCount();
    for (int i = 0; i < childCount; i++) {
      View v = nlv.getChildAt(i);
      ViewCompat.setHasTransientState(v, true);
      oldCoordinates.put(v, new int[] { v.getTop(), v.getBottom() });
    }

    /* Update the layout so the extra content becomes invisible. */
    view.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, item.getCollapsedHeight()));

    /* Add an onPreDraw listener. */
    final ViewTreeObserver observer = nlv.getViewTreeObserver();
    observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {

      @Override
      public boolean onPreDraw() {

        if (!mShouldRemoveObserver) {
          /*
           * Same as for expandingView, the parameters for setSelectionFromTop must
           * be determined such that the necessary cells of the ListView are rendered
           * and added to it.
           */
          mShouldRemoveObserver = true;

          int newTop = view.getTop();
          int newBottom = view.getBottom();

          int newHeight = newBottom - newTop;
          int oldHeight = oldBottom - oldTop;
          int deltaHeight = oldHeight - newHeight;

          mTranslate = getTopAndBottomTranslations(oldTop, oldBottom, deltaHeight, false);

          int currentTop = view.getTop();
          int futureTop = oldTop + mTranslate[0];

          int firstChildStartTop = nlv.getChildAt(0).getTop();
          int firstVisiblePosition = nlv.getFirstVisiblePosition();
          int deltaTop = currentTop - futureTop;

          int i;
          int childCount = nlv.getChildCount();
          for (i = 0; i < childCount; i++) {
            View v = nlv.getChildAt(i);
            int height = v.getBottom() - Math.max(0, v.getTop());
            if (deltaTop - height > 0) {
              firstVisiblePosition++;
              deltaTop -= height;
            } else {
              break;
            }
          }

          if (i > 0) {
            firstChildStartTop = 0;
          }

          nlv.setSelectionFromTop(firstVisiblePosition, firstChildStartTop - deltaTop);

          nlv.requestLayout();

          return false;
        }

        mShouldRemoveObserver = false;
        observer.removeOnPreDrawListener(this);

        int yTranslateTop = mTranslate[0];
        int yTranslateBottom = mTranslate[1];

        int index = nlv.indexOfChild(view);
        int childCount = nlv.getChildCount();
        for (int i = 0; i < childCount; i++) {
          View v = nlv.getChildAt(i);
          int[] old = oldCoordinates.get(v);
          if (old != null) {
            /*
             * If the cell was present in the ListView before the collapse and
             * after the collapse then the bounds are reset to their old values.
             */
            v.setTop(old[0]);
            v.setBottom(old[1]);
            ViewCompat.setHasTransientState(v, false);
          } else {
            /*
             * If the cell is present in the ListView after the collapse but
             * not before the collapse then the bounds are calculated using
             * the bottom and top translation of the collapsing cell.
             */
            int delta = i > index ? yTranslateBottom : -yTranslateTop;
            v.setTop(v.getTop() + delta);
            v.setBottom(v.getBottom() + delta);
          }
        }

        final View expandingLayout = view.findViewById(getExpandingAdapter().getExpandingHelper().getExpandingLayout());

        /* Animates all the cells present on the screen after the collapse. */
        ArrayList<Animator> animations = new ArrayList<Animator>();
        for (int i = 0; i < childCount; i++) {
          View v = nlv.getChildAt(i);
          if (v != view) {
            float diff = i > index ? -yTranslateBottom : yTranslateTop;
            if (diff != 0f)
              animations.add(getAnimation(v, diff, diff));
          }
        }

        /* Adds animation for collapsing the cell that was clicked. */
        animations.add(getAnimation(view, yTranslateTop, -yTranslateBottom));

        /* Adds an animation for fading out the extra content. */
        animations.add(ObjectAnimator.ofFloat(expandingLayout, View.ALPHA, 1, 0));

        /* Disabled the ListView for the duration of the animation. */
        nlv.setEnabled(false);
        nlv.setClickable(false);

        /* Play all the animations created above together at the same time. */
        AnimatorSet s = new AnimatorSet();
        s.playTogether(animations);
        s.addListener(new AnimatorListenerAdapter() {
          @Override
          public void onAnimationEnd(Animator animation) {
            expandingLayout.setVisibility(View.GONE);
            view.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT));
            item.setExpanded(false);
            nlv.setEnabled(true);
            nlv.setClickable(true);
            /*
             * Note that alpha must be set back to 1 in case this view is reused
             * by a cell that was expanded, but not yet collapsed, so its state
             * should persist in an expanded state with the extra content visible.
             */
            expandingLayout.setAlpha(1);
          }
        });
        s.start();

        return true;
      }
    });
  }

  /**
   * This method takes some view and the values by which its top and bottom bounds should be changed by. Given these params, an animation which will animate
   * these bound changes is created and returned.
   */
  private Animator getAnimation(final View view, float translateTop, float translateBottom) {

    int top = view.getTop();
    int bottom = view.getBottom();

    int endTop = (int) (top + translateTop);
    int endBottom = (int) (bottom + translateBottom);

    PropertyValuesHolder translationTop = PropertyValuesHolder.ofInt("top", top, endTop);
    PropertyValuesHolder translationBottom = PropertyValuesHolder.ofInt("bottom", bottom, endBottom);

    return ObjectAnimator.ofPropertyValuesHolder(view, translationTop, translationBottom);
  }
}




Java Source Code List

com.fada21.android.hydralist.HydraListAdapter.java
com.fada21.android.hydralist.HydraListView.java
com.fada21.android.hydralist.data.HydraListDataProvider.java
com.fada21.android.hydralist.data.HydraListItem.java
com.fada21.android.hydralist.data.ListDataProvider.java
com.fada21.android.hydralist.dragable.DragableAdapterHelper.java
com.fada21.android.hydralist.dragable.DragableConsts.java
com.fada21.android.hydralist.dragable.DragableListViewDelegate.java
com.fada21.android.hydralist.dragable.interfaces.DragableListItem.java
com.fada21.android.hydralist.dragable.interfaces.OnItemMovedListener.java
com.fada21.android.hydralist.dragable.interfaces.Swappable.java
com.fada21.android.hydralist.expandable.BasicExpandableListItem.java
com.fada21.android.hydralist.expandable.ExpandableViewHolder.java
com.fada21.android.hydralist.expandable.ExpandingAdapterHelper.java
com.fada21.android.hydralist.expandable.ExpandingLayout.java
com.fada21.android.hydralist.expandable.ExpandingListViewDelegate.java
com.fada21.android.hydralist.expandable.interfaces.ExpandableListItem.java
com.fada21.android.hydralist.expandable.interfaces.OnSizeChangedListener.java
com.fada21.android.hydralist.helper.HydraListAdapterHelper.java
com.fada21.android.hydralist.helper.HydraListViewHolder.java
com.fada21.android.hydralist.helper.PlainAdapterHelper.java
com.fada21.android.hydralist.sample.CustomExpandingAdapterHelper.java
com.fada21.android.hydralist.sample.SampleConsts.java
com.fada21.android.hydralist.sample.SampleContents.java
com.fada21.android.hydralist.sample.SampleDataProvider.java
com.fada21.android.hydralist.sample.SampleHydraListActivity.java
com.fada21.android.hydralist.sample.SampleListItem.java
com.fada21.android.hydralist.sample.SamplePlainAdapterHelper.java
com.fada21.android.hydralist.sample.SampleViewHolder.java
com.fada21.android.hydralist.util.HydraListConsts.java
com.fada21.android.hydralist.util.HydraListUtils.java
com.fada21.android.hydralist.util.PublicListView.java