Start of work for a structural modification of the player

This commit is contained in:
CappielloAntonio 2022-01-02 20:53:11 +01:00
parent 9fdc9ff44d
commit 0fbf3a4cdf
18 changed files with 900 additions and 484 deletions

View file

@ -0,0 +1,34 @@
package com.cappielloantonio.play.adapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.cappielloantonio.play.ui.fragment.PlayerControllerFragment;
import com.cappielloantonio.play.ui.fragment.PlayerQueueFragment;
public class PlayerBodyAdapter extends FragmentStateAdapter {
private static final String TAG = "PlayerNowPlayingSongInfoAdapter";
public PlayerBodyAdapter(@NonNull Fragment fragment) {
super(fragment);
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return new PlayerControllerFragment();
case 1:
return new PlayerQueueFragment();
}
return new PlayerControllerFragment();
}
@Override
public int getItemCount() {
return 2;
}
}

View file

@ -27,15 +27,13 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
private static final String TAG = "SongResultSearchAdapter";
private final LayoutInflater mInflater;
private final PlayerBottomSheetFragment playerBottomSheetFragment;
private final Context context;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private List<Song> songs;
public PlayerSongQueueAdapter(Context context, PlayerBottomSheetFragment playerBottomSheetFragment) {
public PlayerSongQueueAdapter(Context context) {
this.context = context;
this.playerBottomSheetFragment = playerBottomSheetFragment;
this.mInflater = LayoutInflater.from(context);
this.songs = new ArrayList<>();
}

View file

@ -0,0 +1,88 @@
package com.cappielloantonio.play.helper.recyclerview
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}

View file

@ -152,18 +152,18 @@ public class MainActivity extends BaseActivity {
break;
case BottomSheetBehavior.STATE_COLLAPSED:
if (playerBottomSheetFragment != null) {
playerBottomSheetFragment.goBackToFirstPage();
playerBottomSheetFragment.scrollOnTop();
// playerBottomSheetFragment.goBackToFirstPage();
// playerBottomSheetFragment.scrollOnTop();
}
case BottomSheetBehavior.STATE_SETTLING:
if (playerBottomSheetFragment != null) {
playerBottomSheetFragment.scrollOnTop();
// playerBottomSheetFragment.scrollOnTop();
}
break;
case BottomSheetBehavior.STATE_EXPANDED:
if (playerBottomSheetFragment != null) {
playerBottomSheetFragment.scrollOnTop();
setBottomSheetDraggableState(playerBottomSheetFragment.isViewPagerInFirstPage());
// playerBottomSheetFragment.scrollOnTop();
// setBottomSheetDraggableState(playerBottomSheetFragment.isViewPagerInFirstPage());
}
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_HALF_EXPANDED:

View file

@ -23,6 +23,7 @@ import androidx.media3.common.Player;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken;
import androidx.media3.ui.PlayerControlView;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
@ -31,6 +32,7 @@ import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.adapter.PlayerBodyAdapter;
import com.cappielloantonio.play.adapter.PlayerNowPlayingSongAdapter;
import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter;
import com.cappielloantonio.play.databinding.FragmentPlayerBottomSheetBinding;
@ -51,20 +53,11 @@ public class PlayerBottomSheetFragment extends Fragment {
private static final String TAG = "PlayerBottomSheetFragment";
private FragmentPlayerBottomSheetBinding bind;
private ImageView playerMoveDownBottomSheet;
private ViewPager2 playerSongCoverViewPager;
private RecyclerView playerQueueRecyclerView;
private ToggleButton buttonFavorite;
private ImageButton playerCommandUnfoldButton;
private CardView playerCommandCardview;
private TextView playerSongTitleLabel;
private TextView playerArtistNameLabel;
private MainActivity activity;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private PlayerSongQueueAdapter playerSongQueueAdapter;
// TODO: Collegare la seekbar all'exo_progress
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -74,19 +67,12 @@ public class PlayerBottomSheetFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentPlayerBottomSheetBinding.inflate(inflater, container, false);
View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
init();
initCoverLyricsSlideView();
initQueueRecyclerView();
initMediaListenable();
initMusicCommandUnfoldButton();
initArtistLabelButton();
initViewPager();
return view;
}
@ -97,7 +83,6 @@ public class PlayerBottomSheetFragment extends Fragment {
initializeMediaBrowser();
bindMediaController();
setMediaBrowserListenableFuture();
}
@Override
@ -112,19 +97,9 @@ public class PlayerBottomSheetFragment extends Fragment {
bind = null;
}
@SuppressLint("UnsafeOptInUsageError")
private void init() {
playerMoveDownBottomSheet = bind.getRoot().findViewById(R.id.player_move_down_bottom_sheet);
playerSongCoverViewPager = bind.getRoot().findViewById(R.id.player_song_cover_view_pager);
playerQueueRecyclerView = bind.getRoot().findViewById(R.id.player_queue_recycler_view);
buttonFavorite = bind.getRoot().findViewById(R.id.button_favorite);
playerCommandUnfoldButton = bind.getRoot().findViewById(R.id.player_command_unfold_button);
playerCommandCardview = bind.getRoot().findViewById(R.id.player_command_cardview);
playerSongTitleLabel = bind.getRoot().findViewById(R.id.player_song_title_label);
playerArtistNameLabel = bind.getRoot().findViewById(R.id.player_artist_name_label);
playerMoveDownBottomSheet.setOnClickListener(view -> activity.collapseBottomSheet());
bind.playerBodyLayout.setProgressUpdateListener((position, bufferedPosition) -> bind.playerHeaderLayout.playerHeaderSeekBar.setProgress((int) (position / 1000), true));
private void initViewPager() {
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
bind.playerBodyLayout.playerBodyBottomSheetViewPager.setAdapter(new PlayerBodyAdapter(this));
}
@SuppressLint("UnsafeOptInUsageError")
@ -136,18 +111,12 @@ public class PlayerBottomSheetFragment extends Fragment {
MediaController.releaseFuture(mediaBrowserListenableFuture);
}
private void setMediaBrowserListenableFuture() {
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
@SuppressLint("UnsafeOptInUsageError")
private void bindMediaController() {
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
bind.playerBodyLayout.setPlayer(mediaBrowser);
setMediaControllerListener(mediaBrowser);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
@ -190,9 +159,6 @@ public class PlayerBottomSheetFragment extends Fragment {
bind.playerHeaderLayout.playerHeaderSongTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title)));
bind.playerHeaderLayout.playerHeaderSongArtistLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist)));
playerSongTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title)));
playerArtistNameLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist)));
if (mediaMetadata.extras != null) CustomGlideRequest.Builder
.from(requireContext(), mediaMetadata.extras.getString("id"), CustomGlideRequest.SONG_PIC, null)
.build()
@ -226,143 +192,7 @@ public class PlayerBottomSheetFragment extends Fragment {
bind.playerHeaderLayout.playerHeaderNextSongButton.setAlpha(isEnabled ? (float) 1.0 : (float) 0.3);
}
private void initCoverLyricsSlideView() {
playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
playerSongCoverViewPager.setAdapter(new PlayerNowPlayingSongAdapter(this));
playerSongCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (position == 0) {
activity.setBottomSheetDraggableState(true);
} else if (position == 1) {
activity.setBottomSheetDraggableState(false);
}
}
});
}
private void initQueueRecyclerView() {
playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
playerQueueRecyclerView.setHasFixedSize(true);
playerSongQueueAdapter = new PlayerSongQueueAdapter(requireContext(), this);
playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
playerBottomSheetViewModel.getQueueSong().observe(requireActivity(), queue -> {
if (queue != null) {
playerSongQueueAdapter.setItems(MappingUtil.mapQueue(queue));
}
});
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) {
int originalPosition = -1;
int fromPosition = -1;
int toPosition = -1;
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
if (originalPosition == -1) {
originalPosition = viewHolder.getBindingAdapterPosition();
}
fromPosition = viewHolder.getBindingAdapterPosition();
toPosition = target.getBindingAdapterPosition();
/*
* Per spostare un elemento nella coda devo:
* - Spostare graficamente la traccia da una posizione all'altra con Collections.swap()
* - Spostare nel db la traccia, tramite QueueRepository
* - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong()
*
* In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento
* In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db
* Al rilascio dell'elemento chiamo il metodo clearView()
*/
Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition);
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
return false;
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (originalPosition != -1 && fromPosition != -1 && toPosition != -1) {
MediaManager.swap(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), originalPosition, toPosition);
}
originalPosition = -1;
fromPosition = -1;
toPosition = -1;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
MediaManager.remove(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), viewHolder.getBindingAdapterPosition());
viewHolder.getBindingAdapter().notifyDataSetChanged();
}
}).attachToRecyclerView(playerQueueRecyclerView);
}
private void initMediaListenable() {
playerBottomSheetViewModel.getLiveSong().observe(requireActivity(), song -> {
if (song != null) {
buttonFavorite.setChecked(song.isFavorite());
buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite(requireContext(), song));
buttonFavorite.setOnLongClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putParcelable("song_object", song);
RatingDialog dialog = new RatingDialog();
dialog.setArguments(bundle);
dialog.show(requireActivity().getSupportFragmentManager(), null);
return true;
});
playerBottomSheetViewModel.refreshSongInfo(requireActivity(), song);
}
});
}
private void initMusicCommandUnfoldButton() {
playerCommandUnfoldButton.setOnClickListener(view -> {
if (playerCommandCardview.getVisibility() == View.INVISIBLE || playerCommandCardview.getVisibility() == View.GONE) {
playerCommandCardview.setVisibility(View.VISIBLE);
} else {
playerCommandCardview.setVisibility(View.GONE);
}
});
}
private void initArtistLabelButton() {
playerArtistNameLabel.setOnClickListener(view -> playerBottomSheetViewModel.getLiveArtist().observe(requireActivity(), artist -> {
Bundle bundle = new Bundle();
bundle.putParcelable("artist_object", artist);
NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle);
activity.collapseBottomSheet();
}));
}
public View getPlayerHeader() {
return requireView().findViewById(R.id.player_header_layout);
}
public void scrollOnTop() {
bind.playerNestedScrollView.fullScroll(ScrollView.FOCUS_UP);
}
public void goBackToFirstPage() {
playerSongCoverViewPager.setCurrentItem(0);
}
public boolean isViewPagerInFirstPage() {
return playerSongCoverViewPager.getCurrentItem() == 0;
}
}

View file

@ -0,0 +1,206 @@
package com.cappielloantonio.play.ui.fragment;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import androidx.navigation.fragment.NavHostFragment;
import androidx.viewpager2.widget.ViewPager2;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.adapter.PlayerNowPlayingSongAdapter;
import com.cappielloantonio.play.databinding.InnerFragmentPlayerControllerBinding;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.ui.dialog.RatingDialog;
import com.cappielloantonio.play.util.MusicUtil;
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
public class PlayerControllerFragment extends Fragment {
private static final String TAG = "PlayerCoverFragment";
private InnerFragmentPlayerControllerBinding bind;
private ImageView playerMoveDownBottomSheet;
private ViewPager2 playerSongCoverViewPager;
private ToggleButton buttonFavorite;
private ImageButton playerCommandUnfoldButton;
private CardView playerCommandCardview;
private TextView playerSongTitleLabel;
private TextView playerArtistNameLabel;
private MainActivity activity;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = InnerFragmentPlayerControllerBinding.inflate(inflater, container, false);
View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
init();
initCoverLyricsSlideView();
initMediaListenable();
initMusicCommandUnfoldButton();
initArtistLabelButton();
return view;
}
@Override
public void onStart() {
super.onStart();
initializeBrowser();
bindMediaController();
}
@Override
public void onStop() {
releaseBrowser();
super.onStop();
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
@SuppressLint("UnsafeOptInUsageError")
private void init() {
playerMoveDownBottomSheet = bind.getRoot().findViewById(R.id.player_move_down_bottom_sheet);
playerSongCoverViewPager = bind.getRoot().findViewById(R.id.player_song_cover_view_pager);
buttonFavorite = bind.getRoot().findViewById(R.id.button_favorite);
playerCommandUnfoldButton = bind.getRoot().findViewById(R.id.player_command_unfold_button);
playerCommandCardview = bind.getRoot().findViewById(R.id.player_command_cardview);
playerSongTitleLabel = bind.getRoot().findViewById(R.id.player_song_title_label);
playerArtistNameLabel = bind.getRoot().findViewById(R.id.player_artist_name_label);
playerMoveDownBottomSheet.setOnClickListener(view -> activity.collapseBottomSheet());
}
@SuppressLint("UnsafeOptInUsageError")
private void initializeBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
private void releaseBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
@SuppressLint("UnsafeOptInUsageError")
private void bindMediaController() {
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
bind.playerControlBodyLayout.setPlayer(mediaBrowser);
setMediaControllerListener(mediaBrowser);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}, MoreExecutors.directExecutor());
}
@SuppressLint("UnsafeOptInUsageError")
private void setMediaControllerListener(MediaBrowser mediaBrowser) {
setMetadata(mediaBrowser.getMediaMetadata());
mediaBrowser.addListener(new Player.Listener() {
@Override
public void onMediaMetadataChanged(@NonNull MediaMetadata mediaMetadata) {
setMetadata(mediaMetadata);
}
});
}
private void setMetadata(MediaMetadata mediaMetadata) {
if (mediaMetadata.extras != null) playerBottomSheetViewModel.setLiveSong(requireActivity(), mediaMetadata.extras.getString("id"));
if (mediaMetadata.extras != null) playerBottomSheetViewModel.setLiveArtist(requireActivity(), mediaMetadata.extras.getString("artistId"));
playerSongTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title)));
playerArtistNameLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist)));
}
private void initCoverLyricsSlideView() {
playerSongCoverViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
playerSongCoverViewPager.setAdapter(new PlayerNowPlayingSongAdapter(this));
playerSongCoverViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (position == 0) {
activity.setBottomSheetDraggableState(true);
} else if (position == 1) {
activity.setBottomSheetDraggableState(false);
}
}
});
}
private void initMediaListenable() {
playerBottomSheetViewModel.getLiveSong().observe(requireActivity(), song -> {
if (song != null) {
buttonFavorite.setChecked(song.isFavorite());
buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite(requireContext(), song));
buttonFavorite.setOnLongClickListener(v -> {
Bundle bundle = new Bundle();
bundle.putParcelable("song_object", song);
RatingDialog dialog = new RatingDialog();
dialog.setArguments(bundle);
dialog.show(requireActivity().getSupportFragmentManager(), null);
return true;
});
playerBottomSheetViewModel.refreshSongInfo(requireActivity(), song);
}
});
}
private void initMusicCommandUnfoldButton() {
playerCommandUnfoldButton.setOnClickListener(view -> {
if (playerCommandCardview.getVisibility() == View.INVISIBLE || playerCommandCardview.getVisibility() == View.GONE) {
playerCommandCardview.setVisibility(View.VISIBLE);
} else {
playerCommandCardview.setVisibility(View.GONE);
}
});
}
private void initArtistLabelButton() {
playerArtistNameLabel.setOnClickListener(view -> playerBottomSheetViewModel.getLiveArtist().observe(requireActivity(), artist -> {
Bundle bundle = new Bundle();
bundle.putParcelable("artist_object", artist);
NavHostFragment.findNavController(this).navigate(R.id.artistPageFragment, bundle);
activity.collapseBottomSheet();
}));
}
}

View file

@ -10,36 +10,28 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.cappielloantonio.play.databinding.FragmentPlayerCoverBinding;
import com.cappielloantonio.play.databinding.InnerFragmentPlayerCoverBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
public class PlayerCoverFragment extends Fragment {
private static final String TAG = "PlayerCoverFragment";
private FragmentPlayerCoverBinding bind;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private InnerFragmentPlayerCoverBinding bind;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
bind = FragmentPlayerCoverBinding.inflate(inflater, container, false);
View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
return view;
bind = InnerFragmentPlayerCoverBinding.inflate(inflater, container, false);
return bind.getRoot();
}
@Override

View file

@ -11,19 +11,19 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.FragmentPlayerLyricsBinding;
import com.cappielloantonio.play.databinding.InnerFragmentPlayerLyricsBinding;
import com.cappielloantonio.play.util.MusicUtil;
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
public class PlayerLyricsFragment extends Fragment {
private static final String TAG = "PlayerLyricsFragment";
private FragmentPlayerLyricsBinding bind;
private InnerFragmentPlayerLyricsBinding bind;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
bind = FragmentPlayerLyricsBinding.inflate(inflater, container, false);
bind = InnerFragmentPlayerLyricsBinding.inflate(inflater, container, false);
View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);

View file

@ -0,0 +1,157 @@
package com.cappielloantonio.play.ui.fragment;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.adapter.PlayerSongQueueAdapter;
import com.cappielloantonio.play.databinding.InnerFragmentPlayerQueueBinding;
import com.cappielloantonio.play.service.MediaManager;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.util.MappingUtil;
import com.cappielloantonio.play.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
public class PlayerQueueFragment extends Fragment {
private static final String TAG = "PlayerCoverFragment";
private InnerFragmentPlayerQueueBinding bind;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private PlayerSongQueueAdapter playerSongQueueAdapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
bind = InnerFragmentPlayerQueueBinding.inflate(inflater, container, false);
View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
init();
initQueueRecyclerView();
return view;
}
@Override
public void onStart() {
super.onStart();
initializeBrowser();
}
@Override
public void onResume() {
super.onResume();
setMediaBrowserListenableFuture();
}
@Override
public void onStop() {
releaseBrowser();
super.onStop();
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
@SuppressLint("UnsafeOptInUsageError")
private void init() {
}
@SuppressLint("UnsafeOptInUsageError")
private void initializeBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
private void releaseBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
private void setMediaBrowserListenableFuture() {
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
private void initQueueRecyclerView() {
bind.playerQueueRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.playerQueueRecyclerView.setHasFixedSize(true);
playerSongQueueAdapter = new PlayerSongQueueAdapter(requireContext());
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
playerBottomSheetViewModel.getQueueSong().observe(requireActivity(), queue -> {
if (queue != null) {
playerSongQueueAdapter.setItems(MappingUtil.mapQueue(queue));
}
});
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) {
int originalPosition = -1;
int fromPosition = -1;
int toPosition = -1;
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
if (originalPosition == -1) {
originalPosition = viewHolder.getBindingAdapterPosition();
}
fromPosition = viewHolder.getBindingAdapterPosition();
toPosition = target.getBindingAdapterPosition();
/*
* Per spostare un elemento nella coda devo:
* - Spostare graficamente la traccia da una posizione all'altra con Collections.swap()
* - Spostare nel db la traccia, tramite QueueRepository
* - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong()
*
* In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento
* In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db
* Al rilascio dell'elemento chiamo il metodo clearView()
*/
Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition);
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
return false;
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (originalPosition != -1 && fromPosition != -1 && toPosition != -1) {
MediaManager.swap(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), originalPosition, toPosition);
}
originalPosition = -1;
fromPosition = -1;
toPosition = -1;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
MediaManager.remove(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), viewHolder.getBindingAdapterPosition());
viewHolder.getBindingAdapter().notifyDataSetChanged();
}
}).attachToRecyclerView(bind.playerQueueRecyclerView);
}
}