diff options
author | Joshua Brown <keyboardr@google.com> | 2018-04-27 13:20:02 -0700 |
---|---|---|
committer | Joshua Brown <keyboardr@google.com> | 2018-05-02 11:40:05 -0700 |
commit | 30ff1007ab6729566d053649d3e973ac9fd33627 (patch) | |
tree | 523c2a5effee661a20c9b1b6be3b0b6a8876b86d | |
parent | 3b78b872b97b59880ad46c96114bc349570fc333 (diff) | |
download | Media-30ff1007ab6729566d053649d3e973ac9fd33627.tar.gz |
DO NOT MERGE Show/hide playback queue
Flattens layouts so that they can be easily transitioned with ConstraintSets. Extracts metadata logic into MetadataController.
Bug: 78784683
Test: Manual
Change-Id: I55f2de884e278fc38fe23a64a84a48d156959747
-rw-r--r-- | res/layout/fragment_metadata.xml | 57 | ||||
-rw-r--r-- | res/layout/fragment_metadata_with_queue.xml | 45 | ||||
-rw-r--r-- | res/layout/fragment_playback.xml | 69 | ||||
-rw-r--r-- | res/layout/fragment_playback_with_queue.xml | 43 | ||||
-rw-r--r-- | res/layout/media_activity.xml | 3 | ||||
-rw-r--r-- | res/layout/metadata_compact.xml | 27 | ||||
-rw-r--r-- | res/layout/metadata_normal.xml | 62 | ||||
-rw-r--r-- | res/layout/playback_controls.xml | 26 | ||||
-rw-r--r-- | res/transition/queue_in.xml | 19 | ||||
-rw-r--r-- | res/transition/queue_out.xml | 19 | ||||
-rw-r--r-- | res/values/attrs.xml | 10 | ||||
-rw-r--r-- | res/values/dimens.xml | 5 | ||||
-rw-r--r-- | src/com/android/car/media/MetadataController.java | 153 | ||||
-rw-r--r-- | src/com/android/car/media/PlaybackFragment.java | 140 | ||||
-rw-r--r-- | src/com/android/car/media/widgets/MetadataView.java | 155 |
15 files changed, 532 insertions, 301 deletions
diff --git a/res/layout/fragment_metadata.xml b/res/layout/fragment_metadata.xml new file mode 100644 index 0000000..e22410b --- /dev/null +++ b/res/layout/fragment_metadata.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018, 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. +--> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:showIn="@layout/fragment_playback"> + + <ImageView + android:id="@+id/album_art" + android:layout_width="@dimen/playback_album_art_size_large" + android:layout_height="@dimen/playback_album_art_size_large" + android:layout_marginEnd="@dimen/car_keyline_2" + android:layout_marginStart="@dimen/car_keyline_2" + android:contentDescription="@string/album_art" + android:scaleType="centerCrop" + android:transitionName="@string/album_art" + app:layout_constraintBottom_toTopOf="@id/metadata_subcontainer" + app:layout_constraintEnd_toEndOf="@+id/margin_end" + app:layout_constraintStart_toStartOf="@+id/margin_start" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:src="@drawable/ic_person"/> + + <include android:id="@+id/metadata_subcontainer" + layout="@layout/metadata_normal" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_padding_5" + app:layout_constraintBottom_toTopOf="@+id/playback_controls" + app:layout_constraintEnd_toEndOf="@+id/album_art" + app:layout_constraintStart_toStartOf="@+id/album_art" + app:layout_constraintTop_toBottomOf="@+id/album_art"/> + + <androidx.car.widget.PagedListView + android:id="@+id/queue_list" + android:layout_width="0dp" + android:layout_height="0dp" + android:visibility="gone" + app:dividerEndMargin="@dimen/car_keyline_1" + app:dividerStartMargin="@dimen/car_keyline_1" + app:listDividerColor="@color/car_list_divider_inverse"/> +</merge> diff --git a/res/layout/fragment_metadata_with_queue.xml b/res/layout/fragment_metadata_with_queue.xml new file mode 100644 index 0000000..ba204a2 --- /dev/null +++ b/res/layout/fragment_metadata_with_queue.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:showIn="@layout/fragment_playback_with_queue"> + + + <ImageView + android:id="@+id/album_art" + android:layout_width="@dimen/playback_album_art_size_normal" + android:layout_height="@dimen/playback_album_art_size_normal" + android:layout_marginTop="@dimen/car_padding_4" + android:layout_marginStart="@dimen/car_keyline_1" + android:contentDescription="@string/album_art" + android:scaleType="centerCrop" + android:transitionName="@string/album_art" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="@+id/margin_start" + tools:src="@drawable/ic_person"/> + + <include android:id="@+id/metadata_subcontainer" + layout="@layout/metadata_normal" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/car_padding_4" + android:layout_marginEnd="@dimen/car_keyline_1" + app:layout_constraintBottom_toBottomOf="@+id/album_art" + app:layout_constraintEnd_toEndOf="@+id/margin_end" + app:layout_constraintStart_toEndOf="@+id/album_art" + app:layout_constraintTop_toTopOf="@+id/album_art"/> + + <androidx.car.widget.PagedListView + android:id="@+id/queue_list" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="@dimen/car_padding_4" + app:dividerEndMargin="@dimen/car_keyline_1" + app:dividerStartMargin="@dimen/car_keyline_1" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:layout_constraintBottom_toTopOf="@+id/playback_controls" + app:layout_constraintTop_toBottomOf="@+id/album_art" + app:listDividerColor="@color/car_list_divider_inverse"/> + +</merge> diff --git a/res/layout/fragment_playback.xml b/res/layout/fragment_playback.xml index 52f7ab2..bda3f01 100644 --- a/res/layout/fragment_playback.xml +++ b/res/layout/fragment_playback.xml @@ -18,64 +18,27 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/playback_container" - android:animateLayoutChanges="true" android:layout_width="match_parent" android:layout_height="match_parent"> - <LinearLayout - android:id="@+id/metadata_container" - android:orientation="horizontal" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:paddingTop="@dimen/car_padding_4" - android:paddingBottom="@dimen/car_padding_4" - android:layout_marginLeft="@dimen/car_margin" - android:layout_marginRight="@dimen/car_margin" - android:gravity="center_vertical" - app:layout_constraintTop_toTopOf="parent"> - - <ImageView - android:id="@+id/album_art" - android:contentDescription="@string/album_art" - android:layout_marginStart="@dimen/car_keyline_1" - android:layout_marginEnd="@dimen/car_keyline_1" - android:layout_width="@dimen/playback_album_art_height" - android:layout_height="@dimen/playback_album_art_width" - android:scaleType="centerCrop" - android:transitionName="@string/album_art"/> - - <com.android.car.media.widgets.MetadataView - android:id="@+id/metadata" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/car_keyline_1" - android:layout_weight="1" - android:focusable="false" - app:style="normal"/> - - </LinearLayout> - - <androidx.car.widget.PagedListView - android:id="@+id/queue_list" - android:layout_width="match_parent" + <android.support.constraint.Guideline + android:id="@+id/margin_start" + android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginBottom="@dimen/car_padding_4" - app:listDividerColor="@color/car_list_divider_inverse" - app:dividerStartMargin="@dimen/car_keyline_1" - app:dividerEndMargin="@dimen/car_keyline_1" - app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:layout_constraintTop_toBottomOf="@+id/metadata_container" - app:layout_constraintBottom_toTopOf="@+id/playback_controls"/> + android:orientation="vertical" + app:layout_constraintGuide_begin="@dimen/car_margin"/> - <com.android.car.media.common.PlaybackControlsActionBar - android:id="@+id/playback_controls" - android:layout_width="match_parent" + <android.support.constraint.Guideline + android:id="@+id/margin_end" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/car_margin" - android:layout_marginRight="@dimen/car_margin" - android:layout_marginTop="@dimen/car_padding_1" - android:layout_marginBottom="@dimen/car_padding_1" - app:columns="5" - app:layout_constraintBottom_toBottomOf="parent"/> + android:orientation="vertical" + app:layout_constraintGuide_end="@dimen/car_margin"/> + + <include + layout="@layout/fragment_metadata"/> + + <include + layout="@layout/playback_controls"/> </android.support.constraint.ConstraintLayout> diff --git a/res/layout/fragment_playback_with_queue.xml b/res/layout/fragment_playback_with_queue.xml new file mode 100644 index 0000000..f684603 --- /dev/null +++ b/res/layout/fragment_playback_with_queue.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018, 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. +--> +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/playback_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <android.support.constraint.Guideline + android:id="@+id/margin_start" + android:layout_width="0dp" + android:layout_height="0dp" + android:orientation="vertical" + app:layout_constraintGuide_begin="@dimen/car_margin"/> + + <android.support.constraint.Guideline + android:id="@+id/margin_end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_end="@dimen/car_margin"/> + + <include + layout="@layout/fragment_metadata_with_queue"/> + + <include + layout="@layout/playback_controls"/> + +</android.support.constraint.ConstraintLayout> diff --git a/res/layout/media_activity.xml b/res/layout/media_activity.xml index c9be97d..bf26aa9 100644 --- a/res/layout/media_activity.xml +++ b/res/layout/media_activity.xml @@ -78,8 +78,7 @@ android:id="@+id/metadata" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="0.5" - app:style="compact"/> + android:layout_weight="0.5"/> </LinearLayout> </android.support.constraint.ConstraintLayout> diff --git a/res/layout/metadata_compact.xml b/res/layout/metadata_compact.xml index 3d772a1..1a70254 100644 --- a/res/layout/metadata_compact.xml +++ b/res/layout/metadata_compact.xml @@ -15,36 +15,41 @@ limitations under the License. --> <merge - xmlns:android="http://schemas.android.com/apk/res/android" > + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/car_padding_1" - android:textAppearance="@style/TextAppearance.Car.Body1.Light" - android:maxLines="1" android:ellipsize="end" - android:includeFontPadding="false"/> + android:includeFontPadding="false" + android:maxLines="1" + android:textAppearance="@style/TextAppearance.Car.Body1.Light" + tools:text="Body 1 Header"/> <TextView android:id="@+id/subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/title" android:layout_marginBottom="@dimen/car_padding_1" - android:textAppearance="@style/TextAppearance.Car.Body2.Light" - android:maxLines="1" android:ellipsize="end" - android:includeFontPadding="false"/> + android:includeFontPadding="false" + android:maxLines="1" + android:textAppearance="@style/TextAppearance.Car.Body2.Light" + tools:text="Body 2"/> <SeekBar android:id="@+id/seek_bar" android:layout_width="match_parent" android:layout_height="@dimen/playback_seekbar_height" android:layout_below="@+id/subtitle" - android:paddingStart="0dp" android:paddingEnd="0dp" - android:visibility="invisible" + android:paddingStart="0dp" android:progressDrawable="@drawable/seekbar_background" - android:thumb="@drawable/seekbar_thumb"/> + android:thumb="@drawable/seekbar_thumb" + android:visibility="invisible" + tools:visibility="visible" + tools:progress="70"/> -</merge>
\ No newline at end of file +</merge> diff --git a/res/layout/metadata_normal.xml b/res/layout/metadata_normal.xml index cdb63c4..a98e3e0 100644 --- a/res/layout/metadata_normal.xml +++ b/res/layout/metadata_normal.xml @@ -14,47 +14,67 @@ See the License for the specific language governing permissions and limitations under the License. --> -<merge - xmlns:android="http://schemas.android.com/apk/res/android" > +<android.support.constraint.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/metadata_subcontainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="false"> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/car_padding_1" - android:textAppearance="@style/TextAppearance.Car.Body1.Light" - android:maxLines="@integer/playback_title_text_max_lines" android:ellipsize="end" - android:includeFontPadding="false"/> + android:includeFontPadding="false" + android:maxLines="@integer/playback_title_text_max_lines" + android:textAppearance="@style/TextAppearance.Car.Body1.Light" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Body 1 Header"/> <TextView android:id="@+id/subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@+id/title" - android:layout_marginBottom="@dimen/car_padding_4" - android:textAppearance="@style/TextAppearance.Car.Body2.Light" - android:maxLines="@integer/playback_subtitle_text_max_lines" + android:layout_marginBottom="@dimen/car_padding_2" + android:layout_marginTop="@dimen/car_padding_1" android:ellipsize="end" - android:includeFontPadding="false"/> + android:includeFontPadding="false" + android:maxLines="@integer/playback_subtitle_text_max_lines" + android:textAppearance="@style/TextAppearance.Car.Body2.Light" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/title" + tools:text="Body 2"/> <TextView android:id="@+id/time" - android:layout_width="wrap_content" + android:layout_width="160dp" android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:layout_below="@+id/title" - android:textAppearance="@style/TextAppearance.Car.Body2.Light" - android:maxLines="1" + android:layout_marginTop="@dimen/car_padding_1" android:ellipsize="end" - android:includeFontPadding="false"/> + android:includeFontPadding="false" + android:maxLines="@integer/playback_subtitle_text_max_lines" + android:gravity="end" + android:textAppearance="@style/TextAppearance.Car.Body2.Light" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/title" + tools:text="3:27 / 4:03"/> <SeekBar android:id="@+id/seek_bar" android:layout_width="match_parent" android:layout_height="@dimen/playback_seekbar_height" - android:layout_below="@+id/subtitle" - android:paddingStart="0dp" + android:layout_marginTop="@dimen/car_padding_2" + android:background="@color/car_grey_900" android:paddingEnd="0dp" - android:visibility="invisible" + android:paddingStart="0dp" android:progressDrawable="@drawable/seekbar_background" - android:thumb="@drawable/seekbar_thumb"/> + android:thumb="@drawable/seekbar_thumb" + android:visibility="invisible" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/subtitle" + tools:progress="70" + tools:visibility="visible"/> -</merge>
\ No newline at end of file +</android.support.constraint.ConstraintLayout> diff --git a/res/layout/playback_controls.xml b/res/layout/playback_controls.xml new file mode 100644 index 0000000..cbd41a7 --- /dev/null +++ b/res/layout/playback_controls.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2018, 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. +--> +<com.android.car.media.common.PlaybackControlsActionBar + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/playback_controls" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/car_margin" + android:layout_marginBottom="@dimen/car_padding_1" + app:columns="5" + app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/res/transition/queue_in.xml b/res/transition/queue_in.xml new file mode 100644 index 0000000..9e7f798 --- /dev/null +++ b/res/transition/queue_in.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:transitionOrdering="sequential"> + <arcMotion/> + <fade android:fadingMode="fade_out"/> + <transitionSet android:transitionOrdering="together"> + <changeBounds> + <targets> + <target android:targetId="@+id/album_art"/> + </targets> + </changeBounds> + <changeBounds android:startDelay="50"> + <targets> + <target android:excludeId="@+id/album_art"/> + </targets> + </changeBounds> + </transitionSet> + <fade android:fadingMode="fade_in"/> +</transitionSet> diff --git a/res/transition/queue_out.xml b/res/transition/queue_out.xml new file mode 100644 index 0000000..f0c1e3e --- /dev/null +++ b/res/transition/queue_out.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:transitionOrdering="sequential"> + <arcMotion/> + <fade android:fadingMode="fade_out"/> + <transitionSet android:transitionOrdering="together"> + <changeBounds android:startDelay="100"> + <targets> + <target android:targetId="@+id/album_art"/> + </targets> + </changeBounds> + <changeBounds> + <targets> + <target android:excludeId="@+id/album_art"/> + </targets> + </changeBounds> + </transitionSet> + <fade android:fadingMode="fade_in"/> +</transitionSet> diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 4048b22..134f7f5 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -15,16 +15,6 @@ limitations under the License. --> <resources> - <declare-styleable name="MetadataView"> - <!-- Style of media item metadata view --> - <attr name="style" format="enum"> - <!-- Compact version (to be included in the bottom bar during browsing --> - <enum name="compact" value="0"/> - <!-- Normal version (to be included in the playback screen --> - <enum name="normal" value="1"/> - </attr> - </declare-styleable> - <declare-styleable name="AppBarView"> <!-- Maximum number of tabs to show on the app bar --> <attr name="max_tabs" format="integer"/> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index a6f025f..65c654d 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -44,8 +44,9 @@ <!-- Scale to apply to images before blurring them to create the playback background --> <item name="playback_background_blur_scale" format="float" type="dimen">1</item> <!-- Size of the album art thumbnail --> - <dimen name="playback_album_art_height">156dp</dimen> - <dimen name="playback_album_art_width">156dp</dimen> + <dimen name="playback_album_art_size_normal">156dp</dimen> + <!-- Size of the album art thumbnail when large --> + <dimen name="playback_album_art_size_large">574dp</dimen> <!-- Tab height --> <dimen name="browse_tab_height">76dp</dimen> diff --git a/src/com/android/car/media/MetadataController.java b/src/com/android/car/media/MetadataController.java new file mode 100644 index 0000000..f5c382a --- /dev/null +++ b/src/com/android/car/media/MetadataController.java @@ -0,0 +1,153 @@ +package com.android.car.media; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.View; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.android.car.media.common.MediaItemMetadata; +import com.android.car.media.common.PlaybackModel; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Objects; + +/** + * Common controller for displaying current track's metadata. + */ +public class MetadataController { + + private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US); + + @NonNull + private final TextView mTitle; + @NonNull + private final TextView mSubtitle; + @Nullable + private final TextView mTime; + @NonNull + private final SeekBar mSeekBar; + @Nullable + private final ImageView mAlbumArt; + + @Nullable + private PlaybackModel mModel; + @Nullable + private MediaItemMetadata mCurrentMetadata; + + private final PlaybackModel.PlaybackObserver mPlaybackObserver = + new PlaybackModel.PlaybackObserver() { + @Override + public void onPlaybackStateChanged() { + updateState(); + } + + @Override + public void onSourceChanged() { + updateState(); + updateMetadata(); + } + + @Override + public void onMetadataChanged() { + updateMetadata(); + } + }; + + /** + * Create a new MetadataController that operates on the provided Views + * @param title Displays the track's title. Must not be {@code null}. + * @param subtitle Displays the track's artist. Must not be {@code null}. + * @param time Displays the track's progress as text. May be {@code null}. + * @param seekBar Displays the track's progress visually. Must not be {@code null}. + * @param albumArt Displays the track's album art. May be {@code null}. + */ + public MetadataController(@NonNull TextView title, @NonNull TextView subtitle, + @Nullable TextView time, @NonNull SeekBar seekBar, @Nullable ImageView albumArt) { + mTitle = title; + mSubtitle = subtitle; + mTime = time; + mSeekBar = seekBar; + mAlbumArt = albumArt; + } + + /** + * Registers the {@link PlaybackModel} this widget will use to follow playback state. + * Consumers of this class must unregister the {@link PlaybackModel} by calling this method with + * null. + * + * @param model {@link PlaybackModel} to subscribe, or null to unsubscribe. + */ + public void setModel(@Nullable PlaybackModel model) { + if (mModel != null) { + mModel.unregisterObserver(mPlaybackObserver); + } + mModel = model; + if (mModel != null) { + mModel.registerObserver(mPlaybackObserver); + } + } + + private void updateState() { + updateProgress(); + + if (mModel != null && mModel.isPlaying()) { + mSeekBar.post(mSeekBarRunnable); + } else { + mSeekBar.removeCallbacks(mSeekBarRunnable); + } + } + + private void updateMetadata() { + MediaItemMetadata metadata = mModel != null ? mModel.getMetadata() : null; + if (Objects.equals(mCurrentMetadata, metadata)) { + return; + } + mCurrentMetadata = metadata; + mTitle.setText(metadata != null ? metadata.getTitle() : null); + mSubtitle.setText(metadata != null ? metadata.getSubtitle() : null); + if (mAlbumArt != null) { + MediaItemMetadata.updateImageView(mAlbumArt.getContext(), metadata, mAlbumArt, 0); + } + } + + private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 1000; + + private final Runnable mSeekBarRunnable = new Runnable() { + @Override + public void run() { + if (mModel == null || !mModel.isPlaying()) { + return; + } + updateProgress(); + mSeekBar.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS); + + } + }; + + private void updateProgress() { + if (mModel == null) { + mTime.setVisibility(View.INVISIBLE); + mSeekBar.setVisibility(View.INVISIBLE); + return; + } + long maxProgress = mModel.getMaxProgress(); + int visibility = maxProgress > 0 ? View.VISIBLE : View.INVISIBLE; + if (mTime != null) { + String time = String.format("%s / %s", + TIME_FORMAT.format(new Date(mModel.getProgress())), + TIME_FORMAT.format(new Date(maxProgress))); + mTime.setVisibility(visibility); + mTime.setText(time); + } + mSeekBar.setVisibility(visibility); + mSeekBar.setMax((int) mModel.getMaxProgress()); + mSeekBar.setProgress((int) mModel.getProgress()); + } + + +} diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java index 6b4994a..b133ff8 100644 --- a/src/com/android/car/media/PlaybackFragment.java +++ b/src/com/android/car/media/PlaybackFragment.java @@ -18,30 +18,33 @@ package com.android.car.media; import android.content.Context; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.constraint.ConstraintLayout; +import android.support.constraint.ConstraintSet; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.car.widget.ListItem; +import androidx.car.widget.ListItemAdapter; +import androidx.car.widget.ListItemProvider; +import androidx.car.widget.PagedListView; +import androidx.car.widget.TextListItem; import com.android.car.media.common.MediaItemMetadata; import com.android.car.media.common.MediaSource; import com.android.car.media.common.PlaybackControls; import com.android.car.media.common.PlaybackModel; -import com.android.car.media.widgets.MetadataView; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.List; -import java.util.Locale; -import java.util.Objects; - -import androidx.car.widget.ListItem; -import androidx.car.widget.ListItemAdapter; -import androidx.car.widget.ListItemProvider; -import androidx.car.widget.PagedListView; -import androidx.car.widget.TextListItem; /** * A {@link Fragment} that implements both the playback and the content forward browsing experience. @@ -50,34 +53,32 @@ import androidx.car.widget.TextListItem; */ public class PlaybackFragment extends Fragment { private static final String TAG = "PlaybackFragment"; - private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US); private PlaybackModel mModel; private PlaybackControls mPlaybackControls; - private ImageView mAlbumArt; - private MetadataView mMetadataView; - private PagedListView mQueueList; private QueueItemsAdapter mQueueAdapter; - private MediaItemMetadata mCurrentMetadata; - private boolean mQueueIsVisible; - private PlaybackModel.PlaybackObserver mPlaybackObserver = new PlaybackModel.PlaybackObserver() { - @Override - public void onPlaybackStateChanged() { - updateState(); - } - @Override - public void onSourceChanged() { - updateState(); - updateMetadata(); - updateAccentColor(); - } + private MetadataController mMetadataController; + private ConstraintLayout mRootView; - @Override - public void onMetadataChanged() { - updateMetadata(); - } - }; + private boolean mQueueIsVisible; + private PlaybackModel.PlaybackObserver mPlaybackObserver = + new PlaybackModel.PlaybackObserver() { + @Override + public void onPlaybackStateChanged() { + updateState(); + } + + @Override + public void onSourceChanged() { + updateAccentColor(); + updateState(); + } + + @Override + public void onMetadataChanged() { + } + }; private ListItemProvider mQueueItemsProvider = new ListItemProvider() { @Override public ListItem get(int position) { @@ -95,6 +96,7 @@ public class PlaybackFragment extends Fragment { textListItem.setOnClickListener(v -> onQueueItemClicked(item)); return textListItem; } + @Override public int size() { if (!mModel.hasQueue()) { @@ -103,6 +105,7 @@ public class PlaybackFragment extends Fragment { return mModel.getQueue().size(); } }; + private static class QueueItemsAdapter extends ListItemAdapter { QueueItemsAdapter(Context context, ListItemProvider itemProvider) { super(context, itemProvider, BackgroundStyle.SOLID); @@ -114,70 +117,91 @@ public class PlaybackFragment extends Fragment { this.notifyDataSetChanged(); } } + private PlaybackControls.Listener mPlaybackControlsListener = new PlaybackControls.Listener() { @Override public void onToggleQueue() { mQueueIsVisible = !mQueueIsVisible; mPlaybackControls.setQueueVisible(mQueueIsVisible); + setQueueVisible(mQueueIsVisible); } }; @Override - public View onCreateView(LayoutInflater inflater, final ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_playback, container, false); + mRootView = view.findViewById(R.id.playback_container); mModel = new PlaybackModel(getContext()); - mPlaybackControls = view.findViewById(R.id.playback_controls); + + initPlaybackControls(view.findViewById(R.id.playback_controls)); + initQueue(view.findViewById(R.id.queue_list)); + initMetadataController(view); + return view; + } + + private void initPlaybackControls(PlaybackControls playbackControls) { + mPlaybackControls = playbackControls; mPlaybackControls.setModel(mModel); mPlaybackControls.setListener(mPlaybackControlsListener); - ViewGroup playbackContainer = view.findViewById(R.id.playback_container); - mPlaybackControls.setAnimationViewGroup(playbackContainer); - mAlbumArt = view.findViewById(R.id.album_art); - mMetadataView = view.findViewById(R.id.metadata); - mQueueList = view.findViewById(R.id.queue_list); - RecyclerView recyclerView = mQueueList.getRecyclerView(); + mPlaybackControls.setAnimationViewGroup(mRootView); + } + + private void initQueue(PagedListView queueList) { + RecyclerView recyclerView = queueList.getRecyclerView(); recyclerView.setVerticalFadingEdgeEnabled(true); recyclerView.setFadingEdgeLength(getResources() .getDimensionPixelSize(R.dimen.car_padding_4)); mQueueAdapter = new QueueItemsAdapter(getContext(), mQueueItemsProvider); - mQueueList.setAdapter(mQueueAdapter); + queueList.setAdapter(mQueueAdapter); + } - return view; + private void initMetadataController(View view) { + ImageView albumArt = view.findViewById(R.id.album_art); + TextView title = view.findViewById(R.id.title); + TextView subtitle = view.findViewById(R.id.subtitle); + SeekBar seekbar = view.findViewById(R.id.seek_bar); + TextView time = view.findViewById(R.id.time); + mMetadataController = new MetadataController(title, subtitle, time, seekbar, albumArt); + mMetadataController.setModel(mModel); + } + + + @Override + public void onDestroyView() { + super.onDestroyView(); + mPlaybackControls.setModel(null); + mMetadataController.setModel(null); + mMetadataController = null; } @Override public void onStart() { super.onStart(); mModel.registerObserver(mPlaybackObserver); - mMetadataView.setModel(mModel); } @Override public void onStop() { super.onStop(); mModel.unregisterObserver(mPlaybackObserver); - mMetadataView.setModel(null); - mCurrentMetadata = null; } - @Override - public void onDestroyView() { - super.onDestroyView(); - mPlaybackControls.setModel(null); + public void setQueueVisible(boolean visible) { + Transition transition = TransitionInflater.from(getContext()).inflateTransition( + visible ? R.transition.queue_in : R.transition.queue_out); + TransitionManager.beginDelayedTransition(mRootView, transition); + ConstraintSet constraintSet = new ConstraintSet(); + constraintSet.clone(mRootView.getContext(), + visible ? R.layout.fragment_playback_with_queue : R.layout.fragment_playback); + constraintSet.applyTo(mRootView); + } private void updateState() { mQueueAdapter.refresh(); } - private void updateMetadata() { - MediaItemMetadata metadata = mModel.getMetadata(); - if (Objects.equals(mCurrentMetadata, metadata)) { - return; - } - mCurrentMetadata = metadata; - MediaItemMetadata.updateImageView(getContext(), metadata, mAlbumArt, 0); - } private void updateAccentColor() { int defaultColor = getResources().getColor(android.R.color.background_dark, null); diff --git a/src/com/android/car/media/widgets/MetadataView.java b/src/com/android/car/media/widgets/MetadataView.java index 4b3fb38..6d9e72a 100644 --- a/src/com/android/car/media/widgets/MetadataView.java +++ b/src/com/android/car/media/widgets/MetadataView.java @@ -1,117 +1,43 @@ package com.android.car.media.widgets; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; -import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.LayoutInflater; -import android.view.View; import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; +import com.android.car.media.MetadataController; import com.android.car.media.R; -import com.android.car.media.common.MediaItemMetadata; import com.android.car.media.common.PlaybackModel; -import java.lang.annotation.Retention; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Objects; - /** * A view that can be used to display the metadata and playback progress of a media item. * This view can be styled using the "MetadataView" styleable attributes. */ public class MetadataView extends RelativeLayout { - private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US); - - @Nullable - private MediaItemMetadata mCurrentMetadata; - private TextView mTitle; - private TextView mTime; - private TextView mSubtitle; - private SeekBar mSeekbar; - @Nullable - private PlaybackModel mModel; - - private PlaybackModel.PlaybackObserver mPlaybackObserver = - new PlaybackModel.PlaybackObserver() { - @Override - public void onPlaybackStateChanged() { - updateState(); - } - - @Override - public void onSourceChanged() { - updateState(); - updateMetadata(); - } - - @Override - public void onMetadataChanged() { - updateMetadata(); - } - }; - - /** - * The possible styles of this widget. - */ - @IntDef({ - MetadataView.Style.COMPACT, - MetadataView.Style.NORMAL - }) - @Retention(SOURCE) - public @interface Style { - /** Compact style (small space between elements, no time progress indicator) */ - int COMPACT = 0; - /** Normal style (normal spacing and progress indicator) */ - int NORMAL = 1; - } + private final MetadataController mMetadataController; public MetadataView(Context context) { - super(context); - init(context, null, 0, 0); + this(context, null); } public MetadataView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context, attrs, 0, 0); + this(context, attrs, 0); } public MetadataView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context, attrs, defStyleAttr, 0); + this(context, attrs, defStyleAttr, 0); } public MetadataView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - init(context, attrs, defStyleAttr, defStyleRes); - } - - private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - TypedArray ta = context.obtainStyledAttributes( - attrs, R.styleable.MetadataView, defStyleAttr, defStyleRes); - @Style int style = ta.getInteger(R.styleable.MetadataView_style, Style.NORMAL); - ta.recycle(); - - int layoutId = style == Style.COMPACT - ? R.layout.metadata_compact - : R.layout.metadata_normal; - - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutId, this, true); - - mTitle = findViewById(R.id.title); - mSubtitle = findViewById(R.id.subtitle); - mSeekbar = findViewById(R.id.seek_bar); - mTime = findViewById(R.id.time); + LayoutInflater.from(context).inflate(R.layout.metadata_compact, this, true); + TextView title = findViewById(R.id.title); + TextView subtitle = findViewById(R.id.subtitle); + SeekBar seekBar = findViewById(R.id.seek_bar); + mMetadataController = new MetadataController(title, subtitle, null, seekBar, null); } /** @@ -122,66 +48,7 @@ public class MetadataView extends RelativeLayout { * @param model {@link PlaybackModel} to subscribe, or null to unsubscribe. */ public void setModel(@Nullable PlaybackModel model) { - if (mModel != null) { - mModel.unregisterObserver(mPlaybackObserver); - } - mModel = model; - if (mModel != null) { - mModel.registerObserver(mPlaybackObserver); - } - } - - private void updateState() { - updateProgress(); - - if (mModel != null && mModel.isPlaying()) { - mSeekbar.post(mSeekBarRunnable); - } else { - mSeekbar.removeCallbacks(mSeekBarRunnable); - } + mMetadataController.setModel(model); } - private void updateMetadata() { - MediaItemMetadata metadata = mModel != null ? mModel.getMetadata() : null; - if (Objects.equals(mCurrentMetadata, metadata)) { - return; - } - mCurrentMetadata = metadata; - mTitle.setText(metadata != null ? metadata.getTitle() : null); - mSubtitle.setText(metadata != null ? metadata.getSubtitle() : null); - } - - private static final long SEEK_BAR_UPDATE_TIME_INTERVAL_MS = 1000; - - private final Runnable mSeekBarRunnable = new Runnable() { - @Override - public void run() { - if (mModel == null || !mModel.isPlaying()) { - return; - } - updateProgress(); - mSeekbar.postDelayed(this, SEEK_BAR_UPDATE_TIME_INTERVAL_MS); - - } - }; - - private void updateProgress() { - if (mModel == null) { - mTime.setVisibility(View.INVISIBLE); - mSeekbar.setVisibility(View.INVISIBLE); - return; - } - long maxProgress = mModel.getMaxProgress(); - int visibility = maxProgress > 0 ? View.VISIBLE : View.INVISIBLE; - if (mTime != null) { - String time = String.format("%s / %s", - TIME_FORMAT.format(new Date(mModel.getProgress())), - TIME_FORMAT.format(new Date(maxProgress))); - mTime.setVisibility(visibility); - mTime.setText(time); - } - mSeekbar.setVisibility(visibility); - mSeekbar.setMax((int) mModel.getMaxProgress()); - mSeekbar.setProgress((int) mModel.getProgress()); - } } |