Add animation when switching output device

Bug: 155822415
Test: manual test
Merged-In: Ia3370222427b77099d987d59d5d5fd08c11557d7
Change-Id: Ia3370222427b77099d987d59d5d5fd08c11557d7
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
index 92d0858..ac8b7b5 100644
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -75,18 +75,6 @@
             android:textSize="12sp"
             android:fontFamily="roboto-regular"
             android:visibility="gone"/>
-        <ProgressBar
-            android:id="@+id/volume_indeterminate_progress"
-            style="@*android:style/Widget.Material.ProgressBar.Horizontal"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="16dp"
-            android:layout_marginEnd="15dp"
-            android:layout_marginBottom="1dp"
-            android:layout_alignParentBottom="true"
-            android:indeterminate="true"
-            android:indeterminateOnly="true"
-            android:visibility="gone"/>
         <SeekBar
             android:id="@+id/volume_seekbar"
             android:layout_width="match_parent"
@@ -94,6 +82,17 @@
             android:layout_alignParentBottom="true"/>
     </RelativeLayout>
 
+    <ProgressBar
+        android:id="@+id/volume_indeterminate_progress"
+        style="@*android:style/Widget.Material.ProgressBar.Horizontal"
+        android:layout_width="258dp"
+        android:layout_height="18dp"
+        android:layout_marginStart="68dp"
+        android:layout_marginTop="40dp"
+        android:indeterminate="true"
+        android:indeterminateOnly="true"
+        android:visibility="gone"/>
+
     <View
         android:layout_width="1dp"
         android:layout_height="36dp"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index a4f87f0..f002a27 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1410,4 +1410,5 @@
     <dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
     <dimen name="media_output_dialog_header_icon_padding">16dp</dimen>
     <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
+    <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 9b6a9ea..d1630eb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -44,6 +44,8 @@
     private static final String TAG = "MediaOutputAdapter";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
+    private ViewGroup mConnectedItem;
+
     public MediaOutputAdapter(MediaOutputController controller) {
         super(controller);
     }
@@ -79,18 +81,6 @@
         return mController.getMediaDevices().size();
     }
 
-    void onItemClick(MediaDevice device) {
-        mController.connectDevice(device);
-        device.setState(MediaDeviceState.STATE_CONNECTING);
-        notifyDataSetChanged();
-    }
-
-    void onItemClick(int customizedItem) {
-        if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
-            mController.launchBluetoothPairing();
-        }
-    }
-
     @Override
     CharSequence getItemTitle(MediaDevice device) {
         if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
@@ -117,6 +107,10 @@
         @Override
         void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin) {
             super.onBind(device, topMargin, bottomMargin);
+            final boolean currentlyConnected = isCurrentlyConnected(device);
+            if (currentlyConnected) {
+                mConnectedItem = mFrameLayout;
+            }
             if (mController.isTransferring()) {
                 if (device.getState() == MediaDeviceState.STATE_CONNECTING
                         && !mController.hasAdjustVolumeUserRestriction()) {
@@ -133,16 +127,16 @@
                             false /* showSeekBar*/, false /* showProgressBar */,
                             true /* showSubtitle */);
                     mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
-                    mFrameLayout.setOnClickListener(v -> onItemClick(device));
+                    mFrameLayout.setOnClickListener(v -> onItemClick(v, device));
                 } else if (!mController.hasAdjustVolumeUserRestriction()
-                        && isCurrentConnected(device)) {
+                        && currentlyConnected) {
                     setTwoLineLayout(device, null /* title */, true /* bFocused */,
                             true /* showSeekBar*/, false /* showProgressBar */,
                             false /* showSubtitle */);
                     initSeekbar(device);
                 } else {
                     setSingleLineLayout(getItemTitle(device), false /* bFocused */);
-                    mFrameLayout.setOnClickListener(v -> onItemClick(device));
+                    mFrameLayout.setOnClickListener(v -> onItemClick(v, device));
                 }
             }
         }
@@ -160,5 +154,24 @@
                 mFrameLayout.setOnClickListener(v -> onItemClick(CUSTOMIZED_ITEM_PAIR_NEW));
             }
         }
+
+        private void onItemClick(View view, MediaDevice device) {
+            if (mController.isTransferring()) {
+                return;
+            }
+
+            playSwitchingAnim(mConnectedItem, view);
+            mController.connectDevice(device);
+            device.setState(MediaDeviceState.STATE_CONNECTING);
+            if (!isAnimating()) {
+                notifyDataSetChanged();
+            }
+        }
+
+        private void onItemClick(int customizedItem) {
+            if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
+                mController.launchBluetoothPairing();
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 01dc6c4..2d3e77d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media.dialog;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.graphics.Typeface;
 import android.text.TextUtils;
@@ -33,6 +35,7 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.Interpolators;
 import com.android.systemui.R;
 
 /**
@@ -50,6 +53,7 @@
 
     private boolean mIsDragging;
     private int mMargin;
+    private boolean mIsAnimating;
 
     Context mContext;
     View mHolderView;
@@ -75,7 +79,7 @@
         return device.getName();
     }
 
-    boolean isCurrentConnected(MediaDevice device) {
+    boolean isCurrentlyConnected(MediaDevice device) {
         return TextUtils.equals(device.getId(),
                 mController.getCurrentConnectedMediaDevice().getId());
     }
@@ -84,10 +88,17 @@
         return mIsDragging;
     }
 
+    boolean isAnimating() {
+        return mIsAnimating;
+    }
+
     /**
      * ViewHolder for binding device view.
      */
     abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+
+        private static final int ANIM_DURATION = 200;
+
         final FrameLayout mFrameLayout;
         final TextView mTitleText;
         final TextView mTwoLineTitleText;
@@ -123,17 +134,16 @@
         private void setMargin(boolean topMargin, boolean bottomMargin) {
             ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mFrameLayout
                     .getLayoutParams();
-            if (topMargin) {
-                params.topMargin = mMargin;
-            }
-            if (bottomMargin) {
-                params.bottomMargin = mMargin;
-            }
+            params.topMargin = topMargin ? mMargin : 0;
+            params.bottomMargin = bottomMargin ? mMargin : 0;
             mFrameLayout.setLayoutParams(params);
         }
+
         void setSingleLineLayout(CharSequence title, boolean bFocused) {
-            mTitleText.setVisibility(View.VISIBLE);
             mTwoLineLayout.setVisibility(View.GONE);
+            mProgressBar.setVisibility(View.GONE);
+            mTitleText.setVisibility(View.VISIBLE);
+            mTitleText.setTranslationY(0);
             mTitleText.setText(title);
             if (bFocused) {
                 mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL));
@@ -146,9 +156,11 @@
                 boolean showSeekBar, boolean showProgressBar, boolean showSubtitle) {
             mTitleText.setVisibility(View.GONE);
             mTwoLineLayout.setVisibility(View.VISIBLE);
+            mSeekBar.setAlpha(1);
             mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
             mProgressBar.setVisibility(showProgressBar ? View.VISIBLE : View.GONE);
             mSubTitleText.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
+            mTwoLineTitleText.setTranslationY(0);
             if (device == null) {
                 mTwoLineTitleText.setText(title);
             } else {
@@ -189,5 +201,53 @@
                 }
             });
         }
+
+        void playSwitchingAnim(@NonNull View from, @NonNull View to) {
+            final float delta = (float) (mContext.getResources().getDimensionPixelSize(
+                    R.dimen.media_output_dialog_title_anim_y_delta));
+            final SeekBar fromSeekBar = from.requireViewById(R.id.volume_seekbar);
+            final TextView toTitleText = to.requireViewById(R.id.title);
+            if (fromSeekBar.getVisibility() != View.VISIBLE || toTitleText.getVisibility()
+                    != View.VISIBLE) {
+                return;
+            }
+            mIsAnimating = true;
+            // Animation for title text
+            toTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL));
+            toTitleText.animate()
+                    .setDuration(ANIM_DURATION)
+                    .translationY(-delta)
+                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                    .setListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            to.requireViewById(R.id.volume_indeterminate_progress).setVisibility(
+                                    View.VISIBLE);
+                        }
+                    });
+            // Animation for seek bar
+            fromSeekBar.animate()
+                    .alpha(0)
+                    .setDuration(ANIM_DURATION)
+                    .setListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            final TextView fromTitleText = from.requireViewById(
+                                    R.id.two_line_title);
+                            fromTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+                            fromTitleText.animate()
+                                    .setDuration(ANIM_DURATION)
+                                    .translationY(delta)
+                                    .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+                                    .setListener(new AnimatorListenerAdapter() {
+                                        @Override
+                                        public void onAnimationEnd(Animator animation) {
+                                            mIsAnimating = false;
+                                            notifyDataSetChanged();
+                                        }
+                                    });
+                        }
+                    });
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index a589b07..e3e399b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -170,7 +170,7 @@
             mHeaderSubtitle.setText(subTitle);
             mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
         }
-        if (!mAdapter.isDragging()) {
+        if (!mAdapter.isDragging() && !mAdapter.isAnimating()) {
             mAdapter.notifyDataSetChanged();
         }
         // Show when remote media session is available
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 0e376bd..2d460aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -192,6 +192,7 @@
     public void onItemClick_clickDevice_verifyConnectDevice() {
         assertThat(mMediaDevice2.getState()).isEqualTo(
                 LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
         mViewHolder.mFrameLayout.performClick();