码迷,mamicode.com
首页 > 其他好文 > 详细

自定义实现ExpandableListView收缩的简单动画效果

时间:2015-04-26 06:57:28      阅读:524      评论:0      收藏:0      [点我收藏+]

标签:

    以下是 ExpandableListView 收缩的简单动画效果

  1 /*
  2  * Copyright (C) 2014 Gary Guo
  3  * 
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  * 
  8  *     http://www.apache.org/licenses/LICENSE-2.0
  9  * 
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  */
 16  
 17 package com.idunnololz.widgets;
 18 
 19 import java.util.ArrayList;
 20 import java.util.List;
 21 
 22 import android.annotation.SuppressLint;
 23 import android.content.Context;
 24 import android.graphics.Canvas;
 25 import android.graphics.drawable.Drawable;
 26 import android.os.Build;
 27 import android.util.AttributeSet;
 28 import android.util.SparseArray;
 29 import android.view.View;
 30 import android.view.ViewGroup;
 31 import android.view.animation.Animation;
 32 import android.view.animation.Animation.AnimationListener;
 33 import android.view.animation.Transformation;
 34 import android.widget.AbsListView;
 35 import android.widget.BaseExpandableListAdapter;
 36 import android.widget.ExpandableListAdapter;
 37 import android.widget.ExpandableListView;
 38 
 39 /**
 40  * This class defines an ExpandableListView which supports animations for
 41  * collapsing and expanding groups.
 42  */
 43 public class AnimatedExpandableListView extends ExpandableListView {
 44     /*
 45      * A detailed explanation for how this class works:
 46      *
 47      * Animating the ExpandableListView was no easy task. The way that this
 48      * class does it is by exploiting how an ExpandableListView works.
 49      *
 50      * Normally when {@link ExpandableListView#collapseGroup(int)} or
 51      * {@link ExpandableListView#expandGroup(int)} is called, the view toggles
 52      * the flag for a group and calls notifyDataSetChanged to cause the ListView
 53      * to refresh all of it‘s view. This time however, depending on whether a
 54      * group is expanded or collapsed, certain childViews will either be ignored
 55      * or added to the list.
 56      *
 57      * Knowing this, we can come up with a way to animate our views. For
 58      * instance for group expansion, we tell the adapter to animate the
 59      * children of a certain group. We then expand the group which causes the
 60      * ExpandableListView to refresh all views on screen. The way that
 61      * ExpandableListView does this is by calling getView() in the adapter.
 62      * However since the adapter knows that we are animating a certain group,
 63      * instead of returning the real views for the children of the group being
 64      * animated, it will return a fake dummy view. This dummy view will then
 65      * draw the real child views within it‘s dispatchDraw function. The reason
 66      * we do this is so that we can animate all of it‘s children by simply
 67      * animating the dummy view. After we complete the animation, we tell the
 68      * adapter to stop animating the group and call notifyDataSetChanged. Now
 69      * the ExpandableListView is forced to refresh it‘s views again, except this
 70      * time, it will get the real views for the expanded group.
 71      *
 72      * So, to list it all out, when {@link #expandGroupWithAnimation(int)} is
 73      * called the following happens:
 74      *
 75      * 1. The ExpandableListView tells the adapter to animate a certain group.
 76      * 2. The ExpandableListView calls expandGroup.
 77      * 3. ExpandGroup calls notifyDataSetChanged.
 78      * 4. As an result, getChildView is called for expanding group.
 79      * 5. Since the adapter is in "animating mode", it will return a dummy view.
 80      * 6. This dummy view draws the actual children of the expanding group.
 81      * 7. This dummy view‘s height is animated from 0 to it‘s expanded height.
 82      * 8. Once the animation completes, the adapter is notified to stop
 83      *    animating the group and notifyDataSetChanged is called again.
 84      * 9. This forces the ExpandableListView to refresh all of it‘s views again.
 85      * 10.This time when getChildView is called, it will return the actual
 86      *    child views.
 87      *
 88      * For animating the collapse of a group is a bit more difficult since we
 89      * can‘t call collapseGroup from the start as it would just ignore the
 90      * child items, giving up no chance to do any sort of animation. Instead
 91      * what we have to do is play the animation first and call collapseGroup
 92      * after the animation is done.
 93      *
 94      * So, to list it all out, when {@link #collapseGroupWithAnimation(int)} is
 95      * called the following happens:
 96      *
 97      * 1. The ExpandableListView tells the adapter to animate a certain group.
 98      * 2. The ExpandableListView calls notifyDataSetChanged.
 99      * 3. As an result, getChildView is called for expanding group.
100      * 4. Since the adapter is in "animating mode", it will return a dummy view.
101      * 5. This dummy view draws the actual children of the expanding group.
102      * 6. This dummy view‘s height is animated from it‘s current height to 0.
103      * 7. Once the animation completes, the adapter is notified to stop
104      *    animating the group and notifyDataSetChanged is called again.
105      * 8. collapseGroup is finally called.
106      * 9. This forces the ExpandableListView to refresh all of it‘s views again.
107      * 10.This time when the ListView will not get any of the child views for
108      *    the collapsed group.
109      */
110 
111     @SuppressWarnings("unused")
112     private static final String TAG = AnimatedExpandableListAdapter.class.getSimpleName();
113 
114     /**
115      * The duration of the expand/collapse animations
116      */
117     private static final int ANIMATION_DURATION = 100;
118 
119     private AnimatedExpandableListAdapter adapter;
120 
121     public AnimatedExpandableListView(Context context) {
122         super(context);
123     }
124 
125     public AnimatedExpandableListView(Context context, AttributeSet attrs) {
126         super(context, attrs);
127     }
128 
129     public AnimatedExpandableListView(Context context, AttributeSet attrs, int defStyle) {
130         super(context, attrs, defStyle);
131     }
132 
133     /**
134      * @see ExpandableListView#setAdapter(ExpandableListAdapter)
135      */
136     public void setAdapter(ExpandableListAdapter adapter) {
137         super.setAdapter(adapter);
138 
139         // Make sure that the adapter extends AnimatedExpandableListAdapter
140         if(adapter instanceof AnimatedExpandableListAdapter) {
141             this.adapter = (AnimatedExpandableListAdapter) adapter;
142             this.adapter.setParent(this);
143         } else {
144             throw new ClassCastException(adapter.toString() + " must implement AnimatedExpandableListAdapter");
145         }
146     }
147 
148     /**
149      * Expands the given group with an animation.
150      * @param groupPos The position of the group to expand
151      * @return  Returns true if the group was expanded. False if the group was
152      *          already expanded.
153      */
154     @SuppressLint("NewApi") 
155     public boolean expandGroupWithAnimation(int groupPos) {
156         boolean lastGroup = groupPos == adapter.getGroupCount() - 1;
157         if (lastGroup && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
158             return expandGroup(groupPos, true);
159         }
160         
161         int groupFlatPos = getFlatListPosition(getPackedPositionForGroup(groupPos));
162         if (groupFlatPos != -1) {
163             int childIndex = groupFlatPos - getFirstVisiblePosition();
164             if (childIndex < getChildCount()) {
165                 // Get the view for the group is it is on screen...
166                 View v = getChildAt(childIndex);
167                 if (v.getBottom() >= getBottom()) {
168                     // If the user is not going to be able to see the animation
169                     // we just expand the group without an animation.
170                     // This resolves the case where getChildView will not be
171                     // called if the children of the group is not on screen
172 
173                     // We need to notify the adapter that the group was expanded
174                     // without it‘s knowledge
175                     adapter.notifyGroupExpanded(groupPos);
176                     return expandGroup(groupPos);
177                 }
178             }
179         }
180 
181         // Let the adapter know that we are starting the animation...
182         adapter.startExpandAnimation(groupPos, 0);
183         // Finally call expandGroup (note that expandGroup will call
184         // notifyDataSetChanged so we don‘t need to)
185         return expandGroup(groupPos);
186     }
187 
188     /**
189      * Collapses the given group with an animation.
190      * @param groupPos The position of the group to collapse
191      * @return  Returns true if the group was collapsed. False if the group was
192      *          already collapsed.
193      */
194     public boolean collapseGroupWithAnimation(int groupPos) {
195         int groupFlatPos = getFlatListPosition(getPackedPositionForGroup(groupPos));
196         if (groupFlatPos != -1) {
197             int childIndex = groupFlatPos - getFirstVisiblePosition();
198             if (childIndex >= 0 && childIndex < getChildCount()) {
199                 // Get the view for the group is it is on screen...
200                 View v = getChildAt(childIndex);
201                 if (v.getBottom() >= getBottom()) {
202                     // If the user is not going to be able to see the animation
203                     // we just collapse the group without an animation.
204                     // This resolves the case where getChildView will not be
205                     // called if the children of the group is not on screen
206                     return collapseGroup(groupPos);
207                 }
208             } else {
209                 // If the group is offscreen, we can just collapse it without an
210                 // animation...
211                 return collapseGroup(groupPos);
212             }
213         }
214 
215         // Get the position of the firstChild visible from the top of the screen
216         long packedPos = getExpandableListPosition(getFirstVisiblePosition());
217         int firstChildPos = getPackedPositionChild(packedPos);
218         int firstGroupPos = getPackedPositionGroup(packedPos);
219 
220         // If the first visible view on the screen is a child view AND it‘s a
221         // child of the group we are trying to collapse, then set that
222         // as the first child position of the group... see
223         // {@link #startCollapseAnimation(int, int)} for why this is necessary
224         firstChildPos = firstChildPos == -1 || firstGroupPos != groupPos ? 0 : firstChildPos;
225 
226         // Let the adapter know that we are going to start animating the
227         // collapse animation.
228         adapter.startCollapseAnimation(groupPos, firstChildPos);
229 
230         // Force the listview to refresh it‘s views
231         adapter.notifyDataSetChanged();
232         return isGroupExpanded(groupPos);
233     }
234 
235     private int getAnimationDuration() {
236         return ANIMATION_DURATION;
237     }
238 
239     /**
240      * Used for holding information regarding the group.
241      */
242     private static class GroupInfo {
243         boolean animating = false;
244         boolean expanding = false;
245         int firstChildPosition;
246 
247         /**
248          * This variable contains the last known height value of the dummy view.
249          * We save this information so that if the user collapses a group
250          * before it fully expands, the collapse animation will start from the
251          * CURRENT height of the dummy view and not from the full expanded
252          * height.
253          */
254         int dummyHeight = -1;
255     }
256 
257     /**
258      * A specialized adapter for use with the AnimatedExpandableListView. All
259      * adapters used with AnimatedExpandableListView MUST extend this class.
260      */
261     public static abstract class AnimatedExpandableListAdapter extends BaseExpandableListAdapter {
262         private SparseArray<GroupInfo> groupInfo = new SparseArray<GroupInfo>();
263         private AnimatedExpandableListView parent;
264 
265         private static final int STATE_IDLE = 0;
266         private static final int STATE_EXPANDING = 1;
267         private static final int STATE_COLLAPSING = 2;
268 
269         private void setParent(AnimatedExpandableListView parent) {
270             this.parent = parent;
271         }
272 
273         public int getRealChildType(int groupPosition, int childPosition) {
274             return 0;
275         }
276 
277         public int getRealChildTypeCount() {
278             return 1;
279         }
280 
281         public abstract View getRealChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent);
282         public abstract int getRealChildrenCount(int groupPosition);
283 
284         private GroupInfo getGroupInfo(int groupPosition) {
285             GroupInfo info = groupInfo.get(groupPosition);
286             if (info == null) {
287                 info = new GroupInfo();
288                 groupInfo.put(groupPosition, info);
289             }
290             return info;
291         }
292 
293         public void notifyGroupExpanded(int groupPosition) {
294             GroupInfo info = getGroupInfo(groupPosition);
295             info.dummyHeight = -1;
296         }
297 
298         private void startExpandAnimation(int groupPosition, int firstChildPosition) {
299             GroupInfo info = getGroupInfo(groupPosition);
300             info.animating = true;
301             info.firstChildPosition = firstChildPosition;
302             info.expanding = true;
303         }
304 
305         private void startCollapseAnimation(int groupPosition, int firstChildPosition) {
306             GroupInfo info = getGroupInfo(groupPosition);
307             info.animating = true;
308             info.firstChildPosition = firstChildPosition;
309             info.expanding = false;
310         }
311 
312         private void stopAnimation(int groupPosition) {
313             GroupInfo info = getGroupInfo(groupPosition);
314             info.animating = false;
315         }
316 
317         /**
318          * Override {@link #getRealChildType(int, int)} instead.
319          */
320         @Override
321         public final int getChildType(int groupPosition, int childPosition) {
322             GroupInfo info = getGroupInfo(groupPosition);
323             if (info.animating) {
324                 // If we are animating this group, then all of it‘s children
325                 // are going to be dummy views which we will say is type 0.
326                 return 0;
327             } else {
328                 // If we are not animating this group, then we will add 1 to
329                 // the type it has so that no type id conflicts will occur
330                 // unless getRealChildType() returns MAX_INT
331                 return getRealChildType(groupPosition, childPosition) + 1;
332             }
333         }
334 
335         /**
336          * Override {@link #getRealChildTypeCount()} instead.
337          */
338         @Override
339         public final int getChildTypeCount() {
340             // Return 1 more than the childTypeCount to account for DummyView
341             return getRealChildTypeCount() + 1;
342         }
343         
344         protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
345             return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
346                                                 ViewGroup.LayoutParams.WRAP_CONTENT, 0);
347         }
348 
349         /**
350          * Override {@link #getChildView(int, int, boolean, View, ViewGroup)} instead.
351          */
352         @Override
353         public final View getChildView(final int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
354             final GroupInfo info = getGroupInfo(groupPosition);
355 
356             if (info.animating) {
357                 // If this group is animating, return the a DummyView...
358                 if (convertView instanceof DummyView == false) {
359                     convertView = new DummyView(parent.getContext());
360                     convertView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, 0));
361                 }
362 
363                 if (childPosition < info.firstChildPosition) {
364                     // The reason why we do this is to support the collapse
365                     // this group when the group view is not visible but the
366                     // children of this group are. When notifyDataSetChanged
367                     // is called, the ExpandableListView tries to keep the
368                     // list position the same by saving the first visible item
369                     // and jumping back to that item after the views have been
370                     // refreshed. Now the problem is, if a group has 2 items
371                     // and the first visible item is the 2nd child of the group
372                     // and this group is collapsed, then the dummy view will be
373                     // used for the group. But now the group only has 1 item
374                     // which is the dummy view, thus when the ListView is trying
375                     // to restore the scroll position, it will try to jump to
376                     // the second item of the group. But this group no longer
377                     // has a second item, so it is forced to jump to the next
378                     // group. This will cause a very ugly visual glitch. So
379                     // the way that we counteract this is by creating as many
380                     // dummy views as we need to maintain the scroll position
381                     // of the ListView after notifyDataSetChanged has been
382                     // called.
383                     convertView.getLayoutParams().height = 0;
384                     return convertView;
385                 }
386 
387                 final ExpandableListView listView = (ExpandableListView) parent;
388 
389                 final DummyView dummyView = (DummyView) convertView;
390 
391                 // Clear the views that the dummy view draws.
392                 dummyView.clearViews();
393 
394                 // Set the style of the divider
395                 dummyView.setDivider(listView.getDivider(), parent.getMeasuredWidth(), listView.getDividerHeight());
396 
397                 // Make measure specs to measure child views
398                 final int measureSpecW = MeasureSpec.makeMeasureSpec(parent.getWidth(), MeasureSpec.EXACTLY);
399                 final int measureSpecH = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
400 
401                 int totalHeight = 0;
402                 int clipHeight = parent.getHeight();
403 
404                 final int len = getRealChildrenCount(groupPosition);
405                 for (int i = info.firstChildPosition; i < len; i++) {
406                     View childView = getRealChildView(groupPosition, i, (i == len - 1), null, parent);
407                     
408                     LayoutParams p = (LayoutParams) childView.getLayoutParams();
409                     if (p == null) {
410                         p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
411                         childView.setLayoutParams(p);
412                     }
413                     
414                     int lpHeight = p.height;
415                     
416                     int childHeightSpec;
417                     if (lpHeight > 0) {
418                         childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
419                     } else {
420                         childHeightSpec = measureSpecH;
421                     }
422                     
423                     childView.measure(measureSpecW, childHeightSpec);
424                     totalHeight += childView.getMeasuredHeight();
425                     
426                     if (totalHeight < clipHeight) {
427                         // we only need to draw enough views to fool the user...
428                         dummyView.addFakeView(childView);
429                     } else {
430                         dummyView.addFakeView(childView);
431                         
432                         // if this group has too many views, we don‘t want to
433                         // calculate the height of everything... just do a light
434                         // approximation and break
435                         int averageHeight = totalHeight / (i + 1);
436                         totalHeight += (len - i - 1) * averageHeight;
437                         break;
438                     }
439                 }
440 
441                 Object o;
442                 int state = (o = dummyView.getTag()) == null ? STATE_IDLE : (Integer) o;
443 
444                 if (info.expanding && state != STATE_EXPANDING) {
445                     ExpandAnimation ani = new ExpandAnimation(dummyView, 0, totalHeight, info);
446                     ani.setDuration(this.parent.getAnimationDuration());
447                     ani.setAnimationListener(new AnimationListener() {
448 
449                         @Override
450                         public void onAnimationEnd(Animation animation) {
451                             stopAnimation(groupPosition);
452                             notifyDataSetChanged();
453                             dummyView.setTag(STATE_IDLE);
454                         }
455 
456                         @Override
457                         public void onAnimationRepeat(Animation animation) {}
458 
459                         @Override
460                         public void onAnimationStart(Animation animation) {}
461 
462                     });
463                     dummyView.startAnimation(ani);
464                     dummyView.setTag(STATE_EXPANDING);
465                 } else if (!info.expanding && state != STATE_COLLAPSING) {
466                     if (info.dummyHeight == -1) {
467                         info.dummyHeight = totalHeight;
468                     }
469 
470                     ExpandAnimation ani = new ExpandAnimation(dummyView, info.dummyHeight, 0, info);
471                     ani.setDuration(this.parent.getAnimationDuration());
472                     ani.setAnimationListener(new AnimationListener() {
473 
474                         @Override
475                         public void onAnimationEnd(Animation animation) {
476                             stopAnimation(groupPosition);
477                             listView.collapseGroup(groupPosition);
478                             notifyDataSetChanged();
479                             info.dummyHeight = -1;
480                             dummyView.setTag(STATE_IDLE);
481                         }
482 
483                         @Override
484                         public void onAnimationRepeat(Animation animation) {}
485 
486                         @Override
487                         public void onAnimationStart(Animation animation) {}
488 
489                     });
490                     dummyView.startAnimation(ani);
491                     dummyView.setTag(STATE_COLLAPSING);
492                 }
493 
494                 return convertView;
495             } else {
496                 return getRealChildView(groupPosition, childPosition, isLastChild, convertView, parent);
497             }
498         }
499 
500         @Override
501         public final int getChildrenCount(int groupPosition) {
502             GroupInfo info = getGroupInfo(groupPosition);
503             if (info.animating) {
504                 return info.firstChildPosition + 1;
505             } else {
506                 return getRealChildrenCount(groupPosition);
507             }
508         }
509 
510     }
511 
512     private static class DummyView extends View {
513         private List<View> views = new ArrayList<View>();
514         private Drawable divider;
515         private int dividerWidth;
516         private int dividerHeight;
517 
518         public DummyView(Context context) {
519             super(context);
520         }
521 
522         public void setDivider(Drawable divider, int dividerWidth, int dividerHeight) {
523             if(divider != null) {
524                 this.divider = divider;
525                 this.dividerWidth = dividerWidth;
526                 this.dividerHeight = dividerHeight;
527 
528                 divider.setBounds(0, 0, dividerWidth, dividerHeight);
529             }
530         }
531 
532         /**
533          * Add a view for the DummyView to draw.
534          * @param childView View to draw
535          */
536         public void addFakeView(View childView) {
537             childView.layout(0, 0, getWidth(), childView.getMeasuredHeight());
538             views.add(childView);
539         }
540         
541         @Override
542         protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
543             super.onLayout(changed, left, top, right, bottom);
544             final int len = views.size();
545             for(int i = 0; i < len; i++) {
546                 View v = views.get(i);
547                 v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
548             }
549         }
550 
551         public void clearViews() {
552             views.clear();
553         }
554 
555         @Override
556         public void dispatchDraw(Canvas canvas) {
557             canvas.save();
558             if(divider != null) {
559                 divider.setBounds(0, 0, dividerWidth, dividerHeight);
560             }
561             
562             final int len = views.size();
563             for(int i = 0; i < len; i++) {
564                 View v = views.get(i);
565                 
566                 canvas.save();
567                 canvas.clipRect(0, 0, getWidth(), v.getMeasuredHeight());
568                 v.draw(canvas);
569                 canvas.restore();
570                 
571                 if(divider != null) {
572                     divider.draw(canvas);
573                     canvas.translate(0, dividerHeight);
574                 }
575                 
576                 canvas.translate(0, v.getMeasuredHeight());
577             }
578             
579             canvas.restore();
580         }
581     }
582 
583     private static class ExpandAnimation extends Animation {
584         private int baseHeight;
585         private int delta;
586         private View view;
587         private GroupInfo groupInfo;
588 
589         private ExpandAnimation(View v, int startHeight, int endHeight, GroupInfo info) {
590             baseHeight = startHeight;
591             delta = endHeight - startHeight;
592             view = v;
593             groupInfo = info;
594 
595             view.getLayoutParams().height = startHeight;
596             view.requestLayout();
597         }
598 
599         @Override
600         protected void applyTransformation(float interpolatedTime, Transformation t) {
601             super.applyTransformation(interpolatedTime, t);
602             if (interpolatedTime < 1.0f) {
603                 int val = baseHeight + (int) (delta * interpolatedTime);
604                 view.getLayoutParams().height = val;
605                 groupInfo.dummyHeight = val;
606                 view.requestLayout();
607             } else {
608                 int val = baseHeight + delta;
609                 view.getLayoutParams().height = val;
610                 groupInfo.dummyHeight = val;
611                 view.requestLayout();
612             }
613         }
614     }
615 }

 

自定义实现ExpandableListView收缩的简单动画效果

标签:

原文地址:http://www.cnblogs.com/kingfly13/p/4457260.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!