package com.marverenic.music.fragments; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.ItemDecoration; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.marverenic.adapter.DragDropAdapter; import com.marverenic.adapter.DragDropDecoration; import com.marverenic.music.JockeyApplication; import com.marverenic.music.R; import com.marverenic.music.adapter.LibraryEmptyState; import com.marverenic.music.adapter.QueueSection; import com.marverenic.music.adapter.SpacerSingleton; import com.marverenic.music.model.Song; import com.marverenic.music.player.PlayerController; import com.marverenic.music.view.DragBackgroundDecoration; import com.marverenic.music.view.DragDividerDecoration; import com.marverenic.music.view.InsetDecoration; import com.marverenic.music.view.QueueAnimator; import com.marverenic.music.view.SnappingScroller; import com.trello.rxlifecycle.FragmentEvent; import java.util.List; import javax.inject.Inject; import timber.log.Timber; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; public class QueueFragment extends BaseFragment { @Inject PlayerController mPlayerController; private int lastPlayIndex; private RecyclerView mRecyclerView; private DragDropAdapter mAdapter; private QueueSection mQueueSection; private SpacerSingleton[] mBottomSpacers; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); JockeyApplication.getComponent(this).inject(this); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.list, container, false); mRecyclerView = (RecyclerView) view.findViewById(R.id.list); boolean isLandscape = getContext().getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; mRecyclerView.setNestedScrollingEnabled(!isLandscape); setupRecyclerView(); mPlayerController.getQueue() .compose(bindToLifecycle()) .subscribe(this::setupAdapter, throwable -> { Timber.e(throwable, "Failed to update queue"); }); // Remove the list padding on landscape tablets Configuration config = getResources().getConfiguration(); if (config.orientation == ORIENTATION_LANDSCAPE && config.smallestScreenWidthDp >= 600) { view.setPadding(0, 0, 0, 0); } return view; } @Override public void onAttach(Context context) { super.onAttach(context); if (mQueueSection != null) mRecyclerView.post(this::setupSpacers); } private void setupAdapter(List<Song> queue) { if (mQueueSection == null) { mAdapter = new DragDropAdapter(); mAdapter.setHasStableIds(true); mAdapter.attach(mRecyclerView); mRecyclerView.setItemAnimator(new QueueAnimator()); mQueueSection = new QueueSection(this, mPlayerController, queue); mAdapter.setDragSection(mQueueSection); // Wait for a layout pass before calculating bottom spacing since it is dependent on the // height of the RecyclerView (which has not been computed yet) if (isAdded()) mRecyclerView.post(this::setupSpacers); mAdapter.setEmptyState(new LibraryEmptyState(getActivity()) { @Override public String getEmptyMessage() { return getString(R.string.empty_queue); } @Override public String getEmptyMessageDetail() { return getString(R.string.empty_queue_detail); } @Override public String getEmptyAction1Label() { return ""; } @Override public String getEmptyAction2Label() { return ""; } }); mPlayerController.getQueuePosition() .take(1) .compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW)) .subscribe(this::setQueuePosition, throwable -> { Timber.e(throwable, "Failed to scroll to queue position"); }); mPlayerController.getQueuePosition() .skip(1) .compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW)) .subscribe(this::onQueuePositionChanged, throwable -> { Timber.e(throwable, "Failed to scroll to queue position"); }); } else if (!mQueueSection.getData().equals(queue)) { mQueueSection.setData(queue); mAdapter.notifyDataSetChanged(); mPlayerController.getQueuePosition() .skip(1) .take(1) .compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW)) .subscribe(this::setQueuePosition, throwable -> { Timber.e(throwable, "Failed to scroll to queue position"); }); } } private void setupSpacers() { if (mBottomSpacers != null || !isAdded()) { return; } int itemHeight = (int) getResources().getDimension(R.dimen.list_height); int dividerHeight = (int) getResources().getDisplayMetrics().density; int listHeight = mRecyclerView.getMeasuredHeight(); int listItemHeight = itemHeight + dividerHeight; int numberOfSpacers = (int) Math.ceil(listHeight / (float) listItemHeight) - 1; numberOfSpacers = Math.max(0, numberOfSpacers); int remainingListHeight = listHeight - listItemHeight; mBottomSpacers = new SpacerSingleton[numberOfSpacers]; for (int i = 0; i < numberOfSpacers; i++) { int height; if (remainingListHeight % listItemHeight > 0) { height = remainingListHeight % listItemHeight; } else { height = listItemHeight; } remainingListHeight -= height; mBottomSpacers[i] = new SpacerSingleton(height); mAdapter.addSection(mBottomSpacers[i]); } mAdapter.notifyDataSetChanged(); smoothScrollToNowPlaying(); } private void setupRecyclerView() { Drawable shadow = ContextCompat.getDrawable(getContext(), R.drawable.list_drag_shadow); ItemDecoration background = new DragBackgroundDecoration(R.id.song_drag_root); ItemDecoration divider = new DragDividerDecoration(getContext(), true, R.id.instance_blank); ItemDecoration dragShadow = new DragDropDecoration((NinePatchDrawable) shadow); mRecyclerView.addItemDecoration(background); mRecyclerView.addItemDecoration(divider); mRecyclerView.addItemDecoration(dragShadow); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); boolean portrait = getResources().getConfiguration().orientation != ORIENTATION_LANDSCAPE; boolean tablet = getResources().getConfiguration().smallestScreenWidthDp >= 600; if (portrait || !tablet) { // Add an inner shadow at the top of the list mRecyclerView.addItemDecoration(new InsetDecoration( ContextCompat.getDrawable(getContext(), R.drawable.inset_top_shadow), (int) getResources().getDimension(R.dimen.inset_top_shadow_height), Gravity.TOP)); } else { // Add an inner shadow at the bottom of the list mRecyclerView.addItemDecoration(new InsetDecoration( ContextCompat.getDrawable(getContext(), R.drawable.inset_bottom_shadow), getResources().getDimensionPixelSize(R.dimen.inset_bottom_shadow_height), Gravity.BOTTOM)); } } private void setQueuePosition(int currentIndex) { lastPlayIndex = currentIndex; scrollToNowPlaying(); } private void onQueuePositionChanged(int currentIndex) { lastPlayIndex = currentIndex; if (shouldScrollToCurrent()) { smoothScrollToNowPlaying(); } } /** * @return true if the currently playing song is above or below the current item by the * list's height, if the queue has been restarted, or if repeat all is enabled and * the user wrapped from the front of the queue to the end of the queue */ private boolean shouldScrollToCurrent() { int queueSize = mQueueSection.getData().size(); View topView = mRecyclerView.getChildAt(0); View bottomView = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1); int topIndex = mRecyclerView.getChildAdapterPosition(topView); int bottomIndex = mRecyclerView.getChildAdapterPosition(bottomView); return Math.abs(topIndex - lastPlayIndex) <= (bottomIndex - topIndex) || (queueSize - bottomIndex <= 2 && lastPlayIndex == 0) || (bottomIndex - queueSize <= 2 && lastPlayIndex == queueSize - 1); } private void updateSpacers() { if (mBottomSpacers == null) { return; } int queueSize = mQueueSection.getData().size(); int visibleSpacers = mBottomSpacers.length - (queueSize - lastPlayIndex) + 1; visibleSpacers = Math.max(0, visibleSpacers); int prevVisibleSpacers = 0; for (int i = 0; i < mBottomSpacers.length; i++) { if (mBottomSpacers[i].showSection()) { prevVisibleSpacers++; } mBottomSpacers[i].setShowSection(i < visibleSpacers); } if (visibleSpacers > prevVisibleSpacers) { int addedSpacers = visibleSpacers - prevVisibleSpacers; mAdapter.notifyItemRangeInserted(queueSize, addedSpacers); } else if (visibleSpacers < prevVisibleSpacers) { int firstSpacerIndex = queueSize + visibleSpacers; int removedSpacers = prevVisibleSpacers - visibleSpacers; mAdapter.notifyItemRangeRemoved(firstSpacerIndex, removedSpacers); } } private void smoothScrollToNowPlaying() { updateSpacers(); RecyclerView.SmoothScroller scroller = new SnappingScroller(getContext(), SnappingScroller.SNAP_TO_START); scroller.setTargetPosition(lastPlayIndex); mRecyclerView.getLayoutManager().startSmoothScroll(scroller); } private void scrollToNowPlaying() { updateSpacers(); ((LinearLayoutManager) mRecyclerView.getLayoutManager()) .scrollToPositionWithOffset(lastPlayIndex, 0); } }