Created
November 13, 2013 23:36
-
-
Save sh0rtshift/7458523 to your computer and use it in GitHub Desktop.
Fixes for Daniel Olshansky's DevByte example "ListView Expanding Cells Animation" (lines 130-132), also adds compatibility back to API 11. http://www.youtube.com/watch?v=mwE61B56pVQ http://developer.android.com/shareables/devbytes/ListViewExpandingCells.zip Fixes include using ViewCompat for v.hasTransientState()
Hacks for getTopAndBottomTransla…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2013 The Android Open Source Project | |
* | |
* 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.example.ash.views; | |
import android.animation.*; | |
import android.content.Context; | |
import android.graphics.Canvas; | |
import android.support.v4.view.ViewCompat; | |
import android.util.AttributeSet; | |
import android.view.View; | |
import android.view.ViewTreeObserver; | |
import android.widget.AdapterView; | |
import android.widget.ImageView; | |
import android.widget.ListView; | |
import com.example.ash.R; | |
import com.example.ash.utils.L; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
/** | |
* 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 ExpandingListView extends ListView | |
{ | |
private boolean mShouldRemoveObserver = false; | |
private List<View> mViewsToDraw = new ArrayList<View>(); | |
private int[] mTranslate; | |
public ExpandingListView(Context context) | |
{ | |
super(context); | |
init(); | |
} | |
public ExpandingListView(Context context, AttributeSet attrs) | |
{ | |
super(context, attrs); | |
init(); | |
} | |
public ExpandingListView(Context context, AttributeSet attrs, int defStyle) | |
{ | |
super(context, attrs, defStyle); | |
init(); | |
} | |
private void init() | |
{ | |
setOnItemClickListener(mItemClickListener); | |
} | |
/** | |
* Listens for item clicks and expands or collapses the selected view | |
* depending on its current state. | |
*/ | |
private OnItemClickListener mItemClickListener = new OnItemClickListener() | |
{ | |
@Override | |
public void onItemClick(AdapterView<?> parent, View view, int position, | |
long id) | |
{ | |
CardListItem viewObject = (CardListItem) getItemAtPosition(getPositionForView(view)); | |
if (!viewObject.isExpanded()) | |
{ | |
expandView(view); | |
} | |
else | |
{ | |
collapseView(view); | |
} | |
} | |
}; | |
/** | |
* 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/> | |
* 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 behaviour 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. | |
*/ | |
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) > getHeight(); | |
if (isOverTop) | |
{ | |
yTranslateTop = top; | |
yTranslateBottom = yDelta - yTranslateTop; | |
} | |
else if (isBelowBottom) | |
{ | |
int deltaBelow = top + height + yDelta - getHeight(); | |
yTranslateTop = top - deltaBelow < 0 ? top : deltaBelow; | |
yTranslateBottom = yDelta - yTranslateTop; | |
} | |
} | |
else | |
{ | |
int offset = computeVerticalScrollOffset(); | |
int range = computeVerticalScrollRange(); | |
int extent = computeVerticalScrollExtent(); | |
int leftoverExtent = range - offset - extent; | |
// Hack to fix negative leftover caused by items not filling view | |
if (leftoverExtent < 0) | |
{ | |
leftoverExtent *= -1; | |
} | |
boolean isCollapsingBelowBottom = (yTranslateBottom > leftoverExtent); | |
boolean isCellCompletelyDisappearing = bottom - yTranslateBottom < 0; | |
// Hack to force the view to shrink correctly | |
isCollapsingBelowBottom = (yTranslateTop + yTranslateBottom > range) ? false | |
: isCollapsingBelowBottom; | |
if (isCollapsingBelowBottom) | |
{ | |
yTranslateTop = yTranslateBottom - leftoverExtent; | |
// Hack to force the view not shift the top bound when it is | |
// visible | |
yTranslateTop = yTranslateTop < range ? 0 : yTranslateTop; | |
yTranslateBottom = yDelta - yTranslateTop; | |
} | |
else if (isCellCompletelyDisappearing) | |
{ | |
yTranslateBottom = bottom; | |
yTranslateTop = yDelta - yTranslateBottom; | |
} | |
} | |
return new int[]{yTranslateTop, yTranslateBottom}; | |
} | |
/** | |
* 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/> | |
* 1. Store the current top and bottom bounds of each visible item in the | |
* listview. 2. 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. | |
* <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/> | |
* 3. 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. | |
* 4. The expanding view's bounds are animated to what the final values | |
* should be from the original bounds. 5. The bounds above the expanding | |
* view are animated upwards while the bounds below the expanding view are | |
* animated downwards. 6. The extra text is faded in as its contents become | |
* visible throughout the animation process. | |
* <p/> | |
* It is important to note that the listview is disabled during the | |
* animation because the scrolling behaviour is unpredictable if the bounds | |
* of the items within the listview are not constant during the scroll. | |
*/ | |
private void expandView(final View view) | |
{ | |
final CardListItem viewObject = (CardListItem) getItemAtPosition(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 = getChildCount(); | |
for (int i = 0; i < childCount; i++) | |
{ | |
View v = getChildAt(i); | |
ViewCompat.setHasTransientState(v, true); | |
oldCoordinates.put(v, new int[]{v.getTop(), v.getBottom()}); | |
} | |
/* Update the layout so the extra content becomes visible. */ | |
final View expandingLayout = view.findViewById(R.id.expanding_layout); | |
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 = 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 = getChildAt(0).getTop(); | |
int firstVisiblePosition = getFirstVisiblePosition(); | |
int deltaTop = currentTop - futureTop; | |
int i; | |
int childCount = getChildCount(); | |
for (i = 0; i < childCount; i++) | |
{ | |
View v = 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; | |
} | |
setSelectionFromTop(firstVisiblePosition, | |
firstChildStartTop - deltaTop); | |
/* | |
* Request another layout to update the layout parameters of | |
* the cells. | |
*/ | |
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 = 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 = 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(R.id.expanding_layout), View.ALPHA, | |
0, 1)); | |
/* Disabled the ListView for the duration of the animation. */ | |
setEnabled(false); | |
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) | |
{ | |
viewObject.setExpanded(true); | |
setEnabled(true); | |
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. | |
*/ | |
@Override | |
protected void dispatchDraw(Canvas canvas) | |
{ | |
super.dispatchDraw(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. | |
* <p/> | |
* 1. Update the layout parameters of the view clicked so as to minimize its | |
* height to the original collapsed (default) state. 2. 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. 3. On the second predraw pass, all the | |
* items are first returned to their original location (before the first | |
* layout). 4. The collapsing view's bounds are animated to what the final | |
* values should be. 5. The bounds above the collapsing view are animated | |
* downwards while the bounds below the collapsing view are animated | |
* upwards. 6. The extra text is faded out as its contents become visible | |
* throughout the animation process. | |
*/ | |
private void collapseView(final View view) | |
{ | |
final CardListItem viewObject = (CardListItem) getItemAtPosition(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 = getChildCount(); | |
for (int i = 0; i < childCount; i++) | |
{ | |
View v = 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 LayoutParams( | |
LayoutParams.MATCH_PARENT, viewObject | |
.getCollapsedHeight())); | |
/* Add an onPreDraw listener. */ | |
final ViewTreeObserver observer = 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 = getChildAt(0).getTop(); | |
int firstVisiblePosition = getFirstVisiblePosition(); | |
int deltaTop = currentTop - futureTop; | |
int i; | |
int childCount = getChildCount(); | |
for (i = 0; i < childCount; i++) | |
{ | |
View v = 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; | |
} | |
setSelectionFromTop(firstVisiblePosition, | |
firstChildStartTop - deltaTop); | |
requestLayout(); | |
return false; | |
} | |
mShouldRemoveObserver = false; | |
observer.removeOnPreDrawListener(this); | |
int yTranslateTop = mTranslate[0]; | |
int yTranslateBottom = mTranslate[1]; | |
int index = indexOfChild(view); | |
int childCount = getChildCount(); | |
for (int i = 0; i < childCount; i++) | |
{ | |
View v = 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(R.id.expanding_layout); | |
/* | |
* 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 = getChildAt(i); | |
if (v != view) | |
{ | |
float diff = i > index ? -yTranslateBottom | |
: yTranslateTop; | |
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. */ | |
setEnabled(false); | |
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 LayoutParams( | |
LayoutParams.MATCH_PARENT, | |
LayoutParams.WRAP_CONTENT)); | |
viewObject.setExpanded(false); | |
setEnabled(true); | |
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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just change from CardListItem to ExpandableListItem and it works