标签:
以下是 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