Merge "MediaRouter: Implement insertion/deletion animation in MRCD" into mnc-ub-dev
diff --git a/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml b/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
index 091f5f8..94ad82b 100644
--- a/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
+++ b/v7/mediarouter/res/layout/mr_controller_material_dialog_b.xml
@@ -82,13 +82,14 @@
<include android:id="@+id/mr_volume_control"
layout="@layout/mr_volume_control" />
</LinearLayout>
- <ListView android:id="@+id/mr_volume_group_list"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
- android:scrollbarStyle="outsideOverlay"
- android:clipToPadding="false"
- android:visibility="gone" />
+ <android.support.v7.app.OverlayListView
+ android:id="@+id/mr_volume_group_list"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/mr_controller_volume_group_list_padding_top"
+ android:scrollbarStyle="outsideOverlay"
+ android:clipToPadding="false"
+ android:visibility="gone" />
</LinearLayout>
</FrameLayout>
<include layout="@layout/abc_alert_dialog_button_bar_material" />
diff --git a/v7/mediarouter/res/layout/mr_controller_volume_item.xml b/v7/mediarouter/res/layout/mr_controller_volume_item.xml
index 4d693e2..3e7b39b 100644
--- a/v7/mediarouter/res/layout/mr_controller_volume_item.xml
+++ b/v7/mediarouter/res/layout/mr_controller_volume_item.xml
@@ -16,31 +16,36 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
- android:layout_height="@dimen/mr_controller_volume_group_list_item_height"
- android:paddingLeft="24dp"
- android:paddingRight="60dp"
- android:paddingBottom="8dp"
- android:orientation="vertical" >
- <TextView android:id="@+id/mr_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="?attr/mediaRouteControllerSecondaryTextStyle"
- android:singleLine="true" />
- <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout android:id="@+id/volume_item_container"
+ android:layout_width="fill_parent"
+ android:layout_height="@dimen/mr_controller_volume_group_list_item_height"
+ android:paddingLeft="24dp"
+ android:paddingRight="60dp"
+ android:paddingBottom="8dp"
+ android:orientation="vertical" >
+ <TextView android:id="@+id/mr_name"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:gravity="center_vertical">
- <ImageView android:id="@+id/mr_volume_item_icon"
- android:layout_width="@dimen/mr_controller_volume_group_list_item_icon_size"
- android:layout_height="@dimen/mr_controller_volume_group_list_item_icon_size"
- android:layout_marginTop="8dp"
- android:layout_marginBottom="8dp"
- android:scaleType="fitCenter"
- android:src="?attr/mediaRouteAudioTrackDrawable" />
- <android.support.v7.app.MediaRouteVolumeSlider android:id="@+id/mr_volume_slider"
- android:layout_width="fill_parent"
- android:layout_height="40dp"
- android:minHeight="40dp"
- android:maxHeight="40dp" />
+ android:textAppearance="?attr/mediaRouteControllerSecondaryTextStyle"
+ android:singleLine="true" />
+ <LinearLayout android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+ <ImageView android:id="@+id/mr_volume_item_icon"
+ android:layout_width="@dimen/mr_controller_volume_group_list_item_icon_size"
+ android:layout_height="@dimen/mr_controller_volume_group_list_item_icon_size"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:scaleType="fitCenter"
+ android:src="?attr/mediaRouteAudioTrackDrawable" />
+ <android.support.v7.app.MediaRouteVolumeSlider
+ android:id="@+id/mr_volume_slider"
+ android:layout_width="fill_parent"
+ android:layout_height="40dp"
+ android:minHeight="40dp"
+ android:maxHeight="40dp" />
+ </LinearLayout>
</LinearLayout>
</LinearLayout>
diff --git a/v7/mediarouter/res/values/dimens.xml b/v7/mediarouter/res/values/dimens.xml
index 10e5751..d41b0b7 100644
--- a/v7/mediarouter/res/values/dimens.xml
+++ b/v7/mediarouter/res/values/dimens.xml
@@ -35,5 +35,9 @@
<dimen name="mr_controller_volume_group_list_padding_top">16dp</dimen>
<!-- Group list expand/collapse animation duration. -->
- <integer name="mr_controller_volume_group_list_animation_duration_ms">200</integer>
+ <integer name="mr_controller_volume_group_list_animation_duration_ms">400</integer>
+ <!-- Group list fade in animation duration. -->
+ <integer name="mr_controller_volume_group_list_fade_in_duration_ms">400</integer>
+ <!-- Group list fade out animation duration. -->
+ <integer name="mr_controller_volume_group_list_fade_out_duration_ms">200</integer>
</resources>
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
index 21d78d8..1fde803 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java
@@ -34,6 +34,7 @@
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v7.app.OverlayListView.OverlayObject;
import android.support.v7.graphics.Palette;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
@@ -48,28 +49,35 @@
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import java.io.BufferedInputStream;
-import java.io.InputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
-import java.util.HashMap;
-import java.util.List;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@@ -128,9 +136,12 @@
private LinearLayout mVolumeControlLayout;
private View mDividerView;
- private ListView mVolumeGroupList;
+ private OverlayListView mVolumeGroupList;
private VolumeGroupAdapter mVolumeGroupAdapter;
private List<MediaRouter.RouteInfo> mGroupMemberRoutes;
+ private Set<MediaRouter.RouteInfo> mGroupMemberRoutesAdded;
+ private Set<MediaRouter.RouteInfo> mGroupMemberRoutesRemoved;
+ private Set<MediaRouter.RouteInfo> mGroupMemberRoutesAnimatingWithBitmap;
private SeekBar mVolumeSlider;
private VolumeChangeListener mVolumeChangeListener;
private MediaRouter.RouteInfo mRouteInVolumeSliderTouched;
@@ -149,11 +160,26 @@
private Bitmap mArtIconBitmap;
private Uri mArtIconUri;
private boolean mIsGroupExpanded;
- private boolean mIsGroupListAnimationNeeded;
+ private boolean mIsGroupListAnimating;
+ private boolean mIsGroupListAnimationPending;
private int mGroupListAnimationDurationMs;
+ private int mGroupListFadeInDurationMs;
+ private int mGroupListFadeOutDurationMs;
+
+ private Interpolator mInterpolator;
+ private Interpolator mLinearOutSlowInInterpolator;
+ private Interpolator mFastOutSlowInInterpolator;
+ private Interpolator mAccelerateDecelerateInterpolator;
private final AccessibilityManager mAccessibilityManager;
+ private Runnable mGroupListFadeInAnimation = new Runnable() {
+ @Override
+ public void run() {
+ startGroupListFadeInAnimation();
+ }
+ };
+
public MediaRouteControllerDialog(Context context) {
this(context, 0);
}
@@ -171,6 +197,13 @@
R.dimen.mr_controller_volume_group_list_padding_top);
mAccessibilityManager =
(AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+ R.interpolator.mr_linear_out_slow_in);
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+ R.interpolator.mr_fast_out_slow_in);
+ }
+ mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
}
/**
@@ -219,6 +252,7 @@
mVolumeControlEnabled = enable;
if (mCreated) {
updateVolumeControlLayout();
+ updateLayoutHeight(false);
}
}
}
@@ -261,7 +295,7 @@
: mMediaController.getMetadata();
mDescription = metadata == null ? null : metadata.getDescription();
mState = mMediaController == null ? null : mMediaController.getPlaybackState();
- update();
+ update(false);
}
/**
@@ -353,11 +387,11 @@
mVolumeChangeListener = new VolumeChangeListener();
mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
- mVolumeGroupList = (ListView) findViewById(R.id.mr_volume_group_list);
+ mVolumeGroupList = (OverlayListView) findViewById(R.id.mr_volume_group_list);
mGroupMemberRoutes = new ArrayList<MediaRouter.RouteInfo>();
mVolumeGroupAdapter = new VolumeGroupAdapter(mContext, mGroupMemberRoutes);
mVolumeGroupList.setAdapter(mVolumeGroupAdapter);
- updateVolumeGroupList();
+ mGroupMemberRoutesAnimatingWithBitmap = new HashSet<>();
MediaRouterThemeHelper.setMediaControlsBackgroundColor(mContext,
mMediaMainControlLayout, mVolumeGroupList, getGroup() != null);
@@ -375,14 +409,17 @@
if (mIsGroupExpanded) {
mVolumeGroupList.setVisibility(View.VISIBLE);
}
- // Request layout to update UI
- mDefaultControlLayout.requestLayout();
- mIsGroupListAnimationNeeded = true;
- updateLayoutHeight();
+ loadInterpolator();
+ updateLayoutHeight(true);
}
});
+ loadInterpolator();
mGroupListAnimationDurationMs = mContext.getResources().getInteger(
R.integer.mr_controller_volume_group_list_animation_duration_ms);
+ mGroupListFadeInDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_fade_in_duration_ms);
+ mGroupListFadeOutDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_fade_out_duration_ms);
mCustomControlView = onCreateMediaControlView(savedInstanceState);
if (mCustomControlView != null) {
@@ -414,7 +451,7 @@
// Ensure the mArtView is updated.
mArtIconBitmap = null;
mArtIconUri = null;
- update();
+ update(false);
}
@Override
@@ -454,7 +491,7 @@
return super.onKeyUp(keyCode, event);
}
- private void update() {
+ private void update(boolean animate) {
if (!mRoute.isSelected() || mRoute.isDefault()) {
dismiss();
return;
@@ -475,6 +512,7 @@
}
updateVolumeControlLayout();
updatePlaybackControlLayout();
+ updateLayoutHeight(animate);
}
private boolean canShowPlaybackControlLayout() {
@@ -512,16 +550,21 @@
&& !canShowPlaybackControlLayout) ? View.GONE : View.VISIBLE);
}
- private void updateLayoutHeight() {
+ private void updateLayoutHeight(final boolean animate) {
// We need to defer the update until the first layout has occurred, as we don't yet know the
// overall visible display size in which the window this view is attached to has been
// positioned in.
+ mDefaultControlLayout.requestLayout();
ViewTreeObserver observer = mDefaultControlLayout.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
- updateLayoutHeightInternal();
+ if (mIsGroupListAnimating) {
+ mIsGroupListAnimationPending = true;
+ } else {
+ updateLayoutHeightInternal(animate);
+ }
}
});
}
@@ -529,7 +572,7 @@
/**
* Updates the height of views and hide artwork or metadata if space is limited.
*/
- private void updateLayoutHeightInternal() {
+ private void updateLayoutHeightInternal(boolean animate) {
// Measure the size of widgets and get the height of main components.
int oldHeight = getLayoutHeight(mMediaMainControlLayout);
setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.FILL_PARENT);
@@ -551,10 +594,8 @@
int mainControllerHeight = getMainControllerHeight(canShowPlaybackControlLayout());
int volumeGroupListCount = mGroupMemberRoutes.size();
// Scale down volume group list items in landscape mode.
- for (int i = 0; i < mVolumeGroupList.getChildCount(); i++) {
- updateVolumeGroupItemHeight(mVolumeGroupList.getChildAt(i));
- }
- int expandedGroupListHeight = mVolumeGroupListItemHeight * volumeGroupListCount;
+ int expandedGroupListHeight = getGroup() == null ? 0 :
+ mVolumeGroupListItemHeight * getGroup().getRoutes().size();
if (volumeGroupListCount > 0) {
expandedGroupListHeight += mVolumeGroupListPaddingTop;
}
@@ -608,7 +649,7 @@
mMediaMainControlLayout.clearAnimation();
mVolumeGroupList.clearAnimation();
mDefaultControlLayout.clearAnimation();
- if (mIsGroupListAnimationNeeded) {
+ if (animate) {
animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
@@ -617,13 +658,14 @@
setLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
}
- mIsGroupListAnimationNeeded = false;
// Maximize the window size with a transparent layout in advance for smooth animation.
setLayoutHeight(mExpandableAreaLayout, visibleRect.height());
+ rebuildVolumeGroupList(animate);
}
private void updateVolumeGroupItemHeight(View item) {
- setLayoutHeight(item, mVolumeGroupListItemHeight);
+ LinearLayout container = (LinearLayout) item.findViewById(R.id.volume_item_container);
+ setLayoutHeight(container, mVolumeGroupListItemHeight);
View icon = item.findViewById(R.id.mr_volume_item_icon);
ViewGroup.LayoutParams lp = icon.getLayoutParams();
lp.width = mVolumeGroupListItemIconSize;
@@ -643,53 +685,261 @@
};
anim.setDuration(mGroupListAnimationDurationMs);
if (android.os.Build.VERSION.SDK_INT >= 21) {
- anim.setInterpolator(mContext, mIsGroupExpanded ? R.interpolator.mr_linear_out_slow_in
- : R.interpolator.mr_fast_out_slow_in);
- }
- if (view == mVolumeGroupList) {
- anim.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationStart(Animation animation) {
- mVolumeGroupList.setTranscriptMode(ListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
- }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- mVolumeGroupList.setTranscriptMode(ListView.TRANSCRIPT_MODE_DISABLED);
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) { }
- });
+ anim.setInterpolator(mInterpolator);
}
view.startAnimation(anim);
}
+ private void loadInterpolator() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mInterpolator = mIsGroupExpanded ? mLinearOutSlowInInterpolator
+ : mFastOutSlowInInterpolator;
+ } else {
+ mInterpolator = mAccelerateDecelerateInterpolator;
+ }
+ }
+
private void updateVolumeControlLayout() {
if (isVolumeControlAvailable(mRoute)) {
if (mVolumeControlLayout.getVisibility() == View.GONE) {
mVolumeControlLayout.setVisibility(View.VISIBLE);
mVolumeSlider.setMax(mRoute.getVolumeMax());
mVolumeSlider.setProgress(mRoute.getVolume());
- if (getGroup() == null) {
- mGroupExpandCollapseButton.setVisibility(View.GONE);
- } else {
- mGroupExpandCollapseButton.setVisibility(View.VISIBLE);
- }
+ mGroupExpandCollapseButton.setVisibility(getGroup() == null ? View.GONE
+ : View.VISIBLE);
}
- updateVolumeGroupList();
} else {
mVolumeControlLayout.setVisibility(View.GONE);
}
- updateLayoutHeight();
}
- private void updateVolumeGroupList() {
- mGroupMemberRoutes.clear();
- if (getGroup() != null) {
- mGroupMemberRoutes.addAll(getGroup().getRoutes());
+ private void rebuildVolumeGroupList(boolean animate) {
+ List<MediaRouter.RouteInfo> routes = getGroup() == null ? null : getGroup().getRoutes();
+ if (routes == null) {
+ mGroupMemberRoutes.clear();
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ } else if (MediaRouteDialogHelper.listUnorderedEquals(mGroupMemberRoutes, routes)) {
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ } else {
+ HashMap<MediaRouter.RouteInfo, Rect> previousRouteBoundMap = animate
+ ? MediaRouteDialogHelper.getItemBoundMap(mVolumeGroupList, mVolumeGroupAdapter)
+ : null;
+ HashMap<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap = animate
+ ? MediaRouteDialogHelper.getItemBitmapMap(mContext, mVolumeGroupList,
+ mVolumeGroupAdapter) : null;
+ mGroupMemberRoutesAdded =
+ MediaRouteDialogHelper.getItemsAdded(mGroupMemberRoutes, routes);
+ mGroupMemberRoutesRemoved = MediaRouteDialogHelper.getItemsRemoved(mGroupMemberRoutes,
+ routes);
+ mGroupMemberRoutes.addAll(0, mGroupMemberRoutesAdded);
+ mGroupMemberRoutes.removeAll(mGroupMemberRoutesRemoved);
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ if (animate && mIsGroupExpanded
+ && mGroupMemberRoutesAdded.size() + mGroupMemberRoutesRemoved.size() > 0) {
+ animateGroupListItems(previousRouteBoundMap, previousRouteBitmapMap);
+ } else {
+ mGroupMemberRoutesAdded = null;
+ mGroupMemberRoutesRemoved = null;
+ }
}
- mVolumeGroupAdapter.notifyDataSetChanged();
+ }
+
+ private void animateGroupListItems(final Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
+ final Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
+ mVolumeGroupList.setEnabled(false);
+ mVolumeGroupList.requestLayout();
+ mIsGroupListAnimating = true;
+ ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ animateGroupListItemsInternal(previousRouteBoundMap, previousRouteBitmapMap);
+ }
+ });
+ }
+
+ private void animateGroupListItemsInternal(
+ Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
+ Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
+ if (mGroupMemberRoutesAdded == null || mGroupMemberRoutesRemoved == null) {
+ return;
+ }
+ int groupSizeDelta = mGroupMemberRoutesAdded.size() - mGroupMemberRoutesRemoved.size();
+ boolean listenerRegistered = false;
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mVolumeGroupList.startAnimationAll();
+ mVolumeGroupList.postDelayed(mGroupListFadeInAnimation,
+ mGroupListAnimationDurationMs);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) { }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+
+ // Animate visible items from previous positions to current positions except routes added
+ // just before. Added routes will remain hidden until translate animation finishes.
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ Rect previousBounds = previousRouteBoundMap.get(route);
+ int currentTop = view.getTop();
+ int previousTop = previousBounds != null ? previousBounds.top
+ : (currentTop + mVolumeGroupListItemHeight * groupSizeDelta);
+ AnimationSet animSet = new AnimationSet(true);
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
+ previousTop = currentTop;
+ Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
+ alphaAnim.setDuration(mGroupListFadeInDurationMs);
+ animSet.addAnimation(alphaAnim);
+ }
+ Animation translationAnim = new TranslateAnimation(0, 0, previousTop - currentTop, 0);
+ translationAnim.setDuration(mGroupListAnimationDurationMs);
+ animSet.addAnimation(translationAnim);
+ animSet.setFillAfter(true);
+ animSet.setFillEnabled(true);
+ animSet.setInterpolator(mInterpolator);
+ if (!listenerRegistered) {
+ listenerRegistered = true;
+ animSet.setAnimationListener(listener);
+ }
+ view.clearAnimation();
+ view.startAnimation(animSet);
+ previousRouteBoundMap.remove(route);
+ previousRouteBitmapMap.remove(route);
+ }
+
+ // If a member route doesn't exist any longer, it can be either removed or moved out of the
+ // ListView layout boundary. In this case, use the previously captured bitmaps for
+ // animation.
+ for (Map.Entry<MediaRouter.RouteInfo, BitmapDrawable> item
+ : previousRouteBitmapMap.entrySet()) {
+ final MediaRouter.RouteInfo route = item.getKey();
+ final BitmapDrawable bitmap = item.getValue();
+ final Rect bounds = previousRouteBoundMap.get(route);
+ OverlayObject object = null;
+ if (mGroupMemberRoutesRemoved.contains(route)) {
+ object = new OverlayObject(bitmap, bounds).setAlphaAnimation(1.0f, 0.0f)
+ .setDuration(mGroupListFadeOutDurationMs)
+ .setInterpolator(mInterpolator);
+ } else {
+ int deltaY = groupSizeDelta * mVolumeGroupListItemHeight;
+ object = new OverlayObject(bitmap, bounds).setTranslateYAnimation(deltaY)
+ .setDuration(mGroupListAnimationDurationMs)
+ .setInterpolator(mInterpolator)
+ .setAnimationEndListener(new OverlayObject.OnAnimationEndListener() {
+ @Override
+ public void onAnimationEnd() {
+ mGroupMemberRoutesAnimatingWithBitmap.remove(route);
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ }
+ });
+ mGroupMemberRoutesAnimatingWithBitmap.add(route);
+ }
+ mVolumeGroupList.addOverlayObject(object);
+ }
+ }
+
+ private void startGroupListFadeInAnimation() {
+ clearGroupListAnimation(true);
+ mVolumeGroupList.requestLayout();
+ ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ startGroupListFadeInAnimationInternal();
+ }
+ });
+ }
+
+ private void startGroupListFadeInAnimationInternal() {
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.size() != 0) {
+ fadeInAddedRoutes();
+ } else {
+ finishAnimation(true);
+ }
+ }
+
+ private void finishAnimation(boolean animate) {
+ mGroupMemberRoutesAdded = null;
+ mGroupMemberRoutesRemoved = null;
+ mIsGroupListAnimating = false;
+ if (mIsGroupListAnimationPending) {
+ mIsGroupListAnimationPending = false;
+ updateLayoutHeight(animate);
+ }
+ mVolumeGroupList.setEnabled(true);
+ }
+
+ private void fadeInAddedRoutes() {
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ finishAnimation(true);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+ boolean listenerRegistered = false;
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ if (mGroupMemberRoutesAdded.contains(route)) {
+ Animation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
+ alphaAnim.setDuration(mGroupListFadeInDurationMs);
+ alphaAnim.setFillEnabled(true);
+ alphaAnim.setFillAfter(true);
+ if (!listenerRegistered) {
+ listenerRegistered = true;
+ alphaAnim.setAnimationListener(listener);
+ }
+ view.clearAnimation();
+ view.startAnimation(alphaAnim);
+ }
+ }
+ }
+
+ void clearGroupListAnimation(boolean exceptAddedRoutes) {
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ if (exceptAddedRoutes && mGroupMemberRoutesAdded != null
+ && mGroupMemberRoutesAdded.contains(route)) {
+ continue;
+ }
+ LinearLayout container = (LinearLayout) view.findViewById(R.id.volume_item_container);
+ container.setVisibility(View.VISIBLE);
+ AnimationSet animSet = new AnimationSet(true);
+ Animation alphaAnim = new AlphaAnimation(1.0f, 1.0f);
+ alphaAnim.setDuration(0);
+ animSet.addAnimation(alphaAnim);
+ Animation translationAnim = new TranslateAnimation(0, 0, 0, 0);
+ translationAnim.setDuration(0);
+ animSet.setFillAfter(true);
+ animSet.setFillEnabled(true);
+ view.clearAnimation();
+ view.startAnimation(animSet);
+ }
+ mVolumeGroupList.stopAnimationAll();
+ if (!exceptAddedRoutes) {
+ finishAnimation(false);
+ }
}
private void updatePlaybackControlLayout() {
@@ -754,7 +1004,6 @@
}
}
}
- updateLayoutHeight();
}
private boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) {
@@ -796,12 +1045,12 @@
private final class MediaRouterCallback extends MediaRouter.Callback {
@Override
public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
- update();
+ update(false);
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
- update();
+ update(true);
}
@Override
@@ -825,13 +1074,13 @@
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
mState = state;
- update();
+ update(false);
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
mDescription = metadata == null ? null : metadata.getDescription();
- update();
+ update(false);
}
}
@@ -960,6 +1209,22 @@
ImageView volumeItemIcon =
(ImageView) v.findViewById(R.id.mr_volume_item_icon);
volumeItemIcon.setAlpha(isEnabled ? 0xFF : (int) (0xFF * mDisabledAlpha));
+
+ // If overlay bitmap exists, real view should remain hidden until
+ // the animation ends.
+ LinearLayout container = (LinearLayout) v.findViewById(R.id.volume_item_container);
+ container.setVisibility(mGroupMemberRoutesAnimatingWithBitmap.contains(route)
+ ? View.INVISIBLE : View.VISIBLE);
+
+ // Routes which are being added will be invisible until animation ends.
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
+ Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
+ alphaAnim.setDuration(0);
+ alphaAnim.setFillEnabled(true);
+ alphaAnim.setFillAfter(true);
+ v.clearAnimation();
+ v.startAnimation(alphaAnim);
+ }
}
return v;
}
@@ -1056,7 +1321,7 @@
mArtView.setImageBitmap(art);
mArtView.setBackgroundColor(mBackgroundColor);
- updateLayoutHeight();
+ updateLayoutHeight(true);
}
}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
index 6cadb9a..a3d750c 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialogFragment.java
@@ -59,6 +59,14 @@
}
@Override
+ public void onStop() {
+ super.onStop();
+ if (mDialog != null) {
+ mDialog.clearGroupListAnimation(false);
+ }
+ }
+
+ @Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mDialog != null) {
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogHelper.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogHelper.java
index 99d414f..78f550b 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogHelper.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogHelper.java
@@ -17,10 +17,22 @@
package android.support.v7.app;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
import android.support.v7.mediarouter.R;
import android.util.DisplayMetrics;
import android.util.TypedValue;
+import android.view.View;
import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
final class MediaRouteDialogHelper {
/**
@@ -41,4 +53,99 @@
}
return ViewGroup.LayoutParams.WRAP_CONTENT;
}
+
+ /**
+ * Compares two lists regardless of order.
+ *
+ * @param list1 A list
+ * @param list2 A list to be compared with {@code list1}
+ * @return True if two lists have exactly same items regardless of order, false otherwise.
+ */
+ public static <E> boolean listUnorderedEquals(List<E> list1, List<E> list2) {
+ HashSet<E> set1 = new HashSet<>(list1);
+ HashSet<E> set2 = new HashSet<>(list2);
+ return set1.equals(set2);
+ }
+
+ /**
+ * Compares two lists and returns a set of items which exist
+ * after-list but before-list, which means newly added items.
+ *
+ * @param before A list
+ * @param after A list to be compared with {@code before}
+ * @return A set of items which contains newly added items while
+ * comparing {@code after} to {@code before}.
+ */
+ public static <E> Set<E> getItemsAdded(List<E> before, List<E> after) {
+ HashSet<E> set = new HashSet<>(after);
+ set.removeAll(before);
+ return set;
+ }
+
+ /**
+ * Compares two lists and returns a set of items which exist
+ * before-list but after-list, which means removed items.
+ *
+ * @param before A list
+ * @param after A list to be compared with {@code before}
+ * @return A set of items which contains removed items while
+ * comparing {@code after} to {@code before}.
+ */
+ public static <E> Set<E> getItemsRemoved(List<E> before, List<E> after) {
+ HashSet<E> set = new HashSet<>(before);
+ set.removeAll(after);
+ return set;
+ }
+
+ /**
+ * Generates an item-Rect map which indicates where member
+ * items are located in the given ListView.
+ *
+ * @param listView A list view
+ * @param adapter An array adapter which contains an array of items.
+ * @return A map of items and bounds of their views located in the given list view.
+ */
+ public static <E> HashMap<E, Rect> getItemBoundMap(ListView listView,
+ ArrayAdapter<E> adapter) {
+ HashMap<E, Rect> itemBoundMap = new HashMap<>();
+ int firstVisiblePosition = listView.getFirstVisiblePosition();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ int position = firstVisiblePosition + i;
+ E item = adapter.getItem(position);
+ View view = listView.getChildAt(i);
+ itemBoundMap.put(item,
+ new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
+ }
+ return itemBoundMap;
+ }
+
+ /**
+ * Generates an item-BitmapDrawable map which stores snapshots
+ * of member items in the given ListView.
+ *
+ * @param context A context
+ * @param listView A list view
+ * @param adapter An array adapter which contains an array of items.
+ * @return A map of items and snapshots of their views in the given list view.
+ */
+ public static <E> HashMap<E, BitmapDrawable> getItemBitmapMap(Context context,
+ ListView listView, ArrayAdapter<E> adapter) {
+ HashMap<E, BitmapDrawable> itemBitmapMap = new HashMap<>();
+ int firstVisiblePosition = listView.getFirstVisiblePosition();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ int position = firstVisiblePosition + i;
+ E item = adapter.getItem(position);
+ View view = listView.getChildAt(i);
+ itemBitmapMap.put(item, getViewBitmap(context, view));
+ }
+ return itemBitmapMap;
+ }
+
+ private static BitmapDrawable getViewBitmap(Context context, View view) {
+ Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ return new BitmapDrawable(context.getResources(), bitmap);
+ }
}
diff --git a/v7/mediarouter/src/android/support/v7/app/OverlayListView.java b/v7/mediarouter/src/android/support/v7/app/OverlayListView.java
new file mode 100644
index 0000000..ef322fd
--- /dev/null
+++ b/v7/mediarouter/src/android/support/v7/app/OverlayListView.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2016 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 android.support.v7.app;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.view.animation.Interpolator;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A ListView which has an additional overlay layer. {@link BitmapDrawable}
+ * can be added to the layer and can be animated.
+ */
+final class OverlayListView extends ListView {
+ private final List<OverlayObject> mOverlayObjects = new ArrayList<>();
+
+ public OverlayListView(Context context) {
+ super(context);
+ }
+
+ public OverlayListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OverlayListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Adds an object to the overlay layer.
+ *
+ * @param object An object to be added.
+ */
+ public void addOverlayObject(OverlayObject object) {
+ mOverlayObjects.add(object);
+ }
+
+ /**
+ * Starts all animations of objects in the overlay layer.
+ */
+ public void startAnimationAll() {
+ for (OverlayObject object : mOverlayObjects) {
+ if (!object.isAnimationStarted()) {
+ object.startAnimation(getDrawingTime());
+ }
+ }
+ }
+
+ /**
+ * Stops all animations of objects in the overlay layer.
+ */
+ public void stopAnimationAll() {
+ for (OverlayObject object : mOverlayObjects) {
+ object.stopAnimation();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mOverlayObjects.size() > 0) {
+ Iterator<OverlayObject> it = mOverlayObjects.iterator();
+ while (it.hasNext()) {
+ OverlayObject object = it.next();
+ BitmapDrawable bitmap = object.getBitmapDrawable();
+ if (bitmap != null) {
+ bitmap.draw(canvas);
+ }
+ if (!object.update(getDrawingTime())) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * A class that represents an object to be shown in the overlay layer.
+ */
+ public static class OverlayObject {
+ private BitmapDrawable mBitmap;
+ private float mCurrentAlpha = 1.0f;
+ private Rect mCurrentBounds;
+ private Interpolator mInterpolator;
+ private long mDuration;
+ private Rect mStartRect;
+ private int mDeltaY;
+ private float mStartAlpha = 1.0f;
+ private float mEndAlpha = 1.0f;
+ private long mStartTime;
+ private boolean mIsAnimationStarted;
+ private boolean mIsAnimationEnded;
+ private OnAnimationEndListener mListener;
+
+ public OverlayObject(BitmapDrawable bitmap, Rect startRect) {
+ mBitmap = bitmap;
+ mStartRect = startRect;
+ mCurrentBounds = new Rect(startRect);
+ if (mBitmap != null && mCurrentBounds != null) {
+ mBitmap.setAlpha((int) (mCurrentAlpha * 255));
+ mBitmap.setBounds(mCurrentBounds);
+ }
+ }
+
+ /**
+ * Returns the bitmap that this object represents.
+ *
+ * @return BitmapDrawable that this object has.
+ */
+ public BitmapDrawable getBitmapDrawable() {
+ return mBitmap;
+ }
+
+ /**
+ * Returns the started status of the animation.
+ *
+ * @return True if the animation has started, false otherwise.
+ */
+ public boolean isAnimationStarted() {
+ return mIsAnimationStarted;
+ }
+
+ /**
+ * Sets animation for varying alpha.
+ *
+ * @param startAlpha Starting alpha value for the animation, where 1.0 means
+ * fully opaque and 0.0 means fully transparent.
+ * @param endAlpha Ending alpha value for the animation.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setAlphaAnimation(float startAlpha, float endAlpha) {
+ mStartAlpha = startAlpha;
+ mEndAlpha = endAlpha;
+ return this;
+ }
+
+ /**
+ * Sets animation for moving objects vertically.
+ *
+ * @param deltaY Distance to move in pixels.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setTranslateYAnimation(int deltaY) {
+ mDeltaY = deltaY;
+ return this;
+ }
+
+ /**
+ * Sets how long the animation will last.
+ *
+ * @param duration Duration in milliseconds
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setDuration(long duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Sets the acceleration curve for this animation.
+ *
+ * @param interpolator The interpolator which defines the acceleration curve
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ /**
+ * Binds an animation end listener to the animation.
+ *
+ * @param listener the animation end listener to be notified.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setAnimationEndListener(OnAnimationEndListener listener) {
+ mListener = listener;
+ return this;
+ }
+
+ /**
+ * Starts the animation and sets the start time.
+ *
+ * @param startTime Start time to be set in Millis
+ */
+ public void startAnimation(long startTime) {
+ mStartTime = startTime;
+ mIsAnimationStarted = true;
+ }
+
+ /**
+ * Stops the animation.
+ */
+ public void stopAnimation() {
+ mIsAnimationStarted = true;
+ mIsAnimationEnded = true;
+ if (mListener != null) {
+ mListener.onAnimationEnd();
+ }
+ }
+
+ /**
+ * Calculates and updates current bounds and alpha value.
+ *
+ * @param currentTime Current time.in millis
+ */
+ public boolean update(long currentTime) {
+ if (mIsAnimationEnded) {
+ return false;
+ }
+ float normalizedTime = (currentTime - mStartTime) / (float) mDuration;
+ normalizedTime = Math.max(0.0f, Math.min(1.0f, normalizedTime));
+ if (!mIsAnimationStarted) {
+ normalizedTime = 0.0f;
+ }
+ float interpolatedTime = (mInterpolator == null) ? normalizedTime
+ : mInterpolator.getInterpolation(normalizedTime);
+ int deltaY = (int) (mDeltaY * interpolatedTime);
+ mCurrentBounds.top = mStartRect.top + deltaY;
+ mCurrentBounds.bottom = mStartRect.bottom + deltaY;
+ mCurrentAlpha = mStartAlpha + (mEndAlpha - mStartAlpha) * interpolatedTime;
+ if (mBitmap != null && mCurrentBounds != null) {
+ mBitmap.setAlpha((int) (mCurrentAlpha * 255));
+ mBitmap.setBounds(mCurrentBounds);
+ }
+ if (mIsAnimationStarted && normalizedTime >= 1.0f) {
+ mIsAnimationEnded = true;
+ if (mListener != null) {
+ mListener.onAnimationEnd();
+ }
+ }
+ return !mIsAnimationEnded;
+ }
+
+ /**
+ * An animation listener that receives notifications when the animation ends.
+ */
+ public interface OnAnimationEndListener {
+ /**
+ * Notifies the end of the animation.
+ */
+ public void onAnimationEnd();
+ }
+ }
+}