From 1a6774ca2fbe0c0fe2cb8f4b9bc9cc890d193e6a Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 20 Mar 2016 13:21:25 +0100 Subject: [PATCH] squash-merge of slide-up branch --- res/layout/full_playback.xml | 33 +- res/layout/full_playback_alt.xml | 34 +- res/layout/library_content.xml | 85 +++-- res/values/slidingview_attr.xml | 7 + .../android/vanilla/FullPlaybackActivity.java | 12 +- .../android/vanilla/PlaybackService.java | 22 +- .../android/vanilla/ShowQueueFragment.java | 233 +++++++++++++ .../android/vanilla/SlidingView.java | 315 ++++++++++++++++++ 8 files changed, 677 insertions(+), 64 deletions(-) create mode 100644 res/values/slidingview_attr.xml create mode 100644 src/ch/blinkenlights/android/vanilla/ShowQueueFragment.java create mode 100644 src/ch/blinkenlights/android/vanilla/SlidingView.java diff --git a/res/layout/full_playback.xml b/res/layout/full_playback.xml index b456eb2e..ed06698c 100644 --- a/res/layout/full_playback.xml +++ b/res/layout/full_playback.xml @@ -20,7 +20,8 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + - - - + android:layout_marginTop="36dip" + android:orientation="horizontal" + vanilla:slider_handle_id="@+id/queue_slider"> + + + + + + + diff --git a/res/layout/full_playback_alt.xml b/res/layout/full_playback_alt.xml index f46976c5..3d0d15e0 100644 --- a/res/layout/full_playback_alt.xml +++ b/res/layout/full_playback_alt.xml @@ -20,7 +20,8 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + - - - + android:layout_marginTop="88dip" + android:orientation="horizontal" + vanilla:slider_handle_id="@+id/queue_slider"> + + + + + + + diff --git a/res/layout/library_content.xml b/res/layout/library_content.xml index b549d43c..5e3a55c7 100644 --- a/res/layout/library_content.xml +++ b/res/layout/library_content.xml @@ -20,36 +20,63 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - - - - + - + + + + + + + - - - - + android:layout_height="fill_parent" + android:layout_gravity="bottom|left" + android:orientation="horizontal" + vanilla:slider_handle_id="@+id/bottombar_controls" + vanilla:slider_slave_id="@+id/content"> + + + + + + + + + diff --git a/res/values/slidingview_attr.xml b/res/values/slidingview_attr.xml new file mode 100644 index 00000000..e6dbf489 --- /dev/null +++ b/res/values/slidingview_attr.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java b/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java index e24982b7..4e8e2d68 100644 --- a/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java +++ b/src/ch/blinkenlights/android/vanilla/FullPlaybackActivity.java @@ -64,7 +64,7 @@ public class FullPlaybackActivity extends PlaybackActivity private TextView mOverlayText; private View mControlsTop; - private View mControlsBottom; + private View mSlidingView; private SeekBar mSeekBar; private TableLayout mInfoTable; @@ -161,7 +161,7 @@ public class FullPlaybackActivity extends PlaybackActivity coverView.setOnLongClickListener(this); mCoverView = coverView; - mControlsBottom = findViewById(R.id.controls_bottom); + mSlidingView = findViewById(R.id.sliding_view); View previousButton = findViewById(R.id.previous); previousButton.setOnClickListener(this); mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause); @@ -389,13 +389,13 @@ public class FullPlaybackActivity extends PlaybackActivity openLibrary(null); break; case MENU_ENQUEUE_ALBUM: - PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_ALBUM); + PlaybackService.get(this).enqueueFromSong(PlaybackService.get(this).getSong(0), MediaUtils.TYPE_ALBUM); break; case MENU_ENQUEUE_ARTIST: - PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_ARTIST); + PlaybackService.get(this).enqueueFromSong(PlaybackService.get(this).getSong(0), MediaUtils.TYPE_ARTIST); break; case MENU_ENQUEUE_GENRE: - PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_GENRE); + PlaybackService.get(this).enqueueFromSong(PlaybackService.get(this).getSong(0), MediaUtils.TYPE_GENRE); break; case MENU_SONG_FAVORITE: Song song = (PlaybackService.get(this)).getSong(0); @@ -513,7 +513,7 @@ public class FullPlaybackActivity extends PlaybackActivity { int mode = visible ? View.VISIBLE : View.GONE; mControlsTop.setVisibility(mode); - mControlsBottom.setVisibility(mode); + mSlidingView.setVisibility(mode); mControlsVisible = visible; if (visible) { diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index 53527408..7ed48ea9 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -1777,37 +1777,37 @@ public final class PlaybackService extends Service } /** - * Enqueues all the songs with the same album/artist/genre as the current + * Enqueues all the songs with the same album/artist/genre as the passed * song. * * This will clear the queue and place the first song from the group after * the playing song. * + * @param song The song to base the query on * @param type The media type, one of MediaUtils.TYPE_ALBUM, TYPE_ARTIST, * or TYPE_GENRE */ - public void enqueueFromCurrent(int type) + public void enqueueFromSong(Song song, int type) { - Song current = mCurrentSong; - if (current == null) + if (song == null) return; long id; switch (type) { case MediaUtils.TYPE_ARTIST: - id = current.artistId; + id = song.artistId; break; case MediaUtils.TYPE_ALBUM: - id = current.albumId; + id = song.albumId; break; case MediaUtils.TYPE_GENRE: - id = MediaUtils.queryGenreForSong(getContentResolver(), current.id); + id = MediaUtils.queryGenreForSong(getContentResolver(), song.id); break; default: throw new IllegalArgumentException("Unsupported media type: " + type); } - String selection = "_id!=" + current.id; + String selection = "_id!=" + song.id; QueryTask query = MediaUtils.buildQuery(type, id, Song.FILLED_PROJECTION, selection); query.mode = SongTimeline.MODE_FLUSH_AND_PLAY_NEXT; addSongs(query); @@ -2248,13 +2248,13 @@ public final class PlaybackService extends Service break; } case EnqueueAlbum: - enqueueFromCurrent(MediaUtils.TYPE_ALBUM); + enqueueFromSong(mCurrentSong, MediaUtils.TYPE_ALBUM); break; case EnqueueArtist: - enqueueFromCurrent(MediaUtils.TYPE_ARTIST); + enqueueFromSong(mCurrentSong, MediaUtils.TYPE_ARTIST); break; case EnqueueGenre: - enqueueFromCurrent(MediaUtils.TYPE_GENRE); + enqueueFromSong(mCurrentSong, MediaUtils.TYPE_GENRE); break; case ClearQueue: clearQueue(); diff --git a/src/ch/blinkenlights/android/vanilla/ShowQueueFragment.java b/src/ch/blinkenlights/android/vanilla/ShowQueueFragment.java new file mode 100644 index 00000000..55f589d6 --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/ShowQueueFragment.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.vanilla; + +import android.annotation.SuppressLint; +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import com.mobeta.android.dslv.DragSortListView; + + +public class ShowQueueFragment extends Fragment + implements TimelineCallback, + AdapterView.OnItemClickListener, + DragSortListView.DropListener, + DragSortListView.RemoveListener, + MenuItem.OnMenuItemClickListener + { + + private DragSortListView mListView; + private ShowQueueAdapter mListAdapter; + private PlaybackService mService; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + View view = inflater.inflate(R.layout.showqueue_listview, container, false); + Context context = getActivity(); + + mListView = (DragSortListView) view.findViewById(R.id.list); + mListAdapter = new ShowQueueAdapter(context, R.layout.draggable_row); + mListView.setAdapter(mListAdapter); + mListView.setDropListener(this); + mListView.setRemoveListener(this); + mListView.setOnItemClickListener(this); + mListView.setOnCreateContextMenuListener(this); + + PlaybackService.addTimelineCallback(this); + return view; + } + + @Override + public void onDestroyView() { + PlaybackService.removeTimelineCallback(this); + super.onDestroyView(); + } + + @Override + public void onResume() { + super.onResume(); + + // Check if playback service has already been created + if (mService == null && PlaybackService.hasInstance()) + mService = PlaybackService.get(getActivity()); + + if (mService != null) + refreshSongQueueList(true); + } + + + private final static int MENU_PLAY = 100; + private final static int MENU_ENQUEUE_ALBUM = 101; + private final static int MENU_ENQUEUE_ARTIST = 102; + private final static int MENU_ENQUEUE_GENRE = 103; + private final static int MENU_REMOVE = 104; + + /** + * Called by Android on long press. Builds the long press context menu. + */ + @Override + public void onCreateContextMenu(ContextMenu menu, View listView, ContextMenu.ContextMenuInfo absInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)absInfo; + Intent intent = new Intent(); + intent.putExtra("id", info.id); + intent.putExtra("position", info.position); + Song song = mService.getSongByQueuePosition(info.position); + + menu.setHeaderTitle(song.title); + menu.add(0, MENU_PLAY, 0, R.string.play).setIntent(intent).setOnMenuItemClickListener(this); + menu.add(0, MENU_ENQUEUE_ALBUM, 0, R.string.enqueue_current_album).setIntent(intent).setOnMenuItemClickListener(this); + menu.add(0, MENU_ENQUEUE_ARTIST, 0, R.string.enqueue_current_artist).setIntent(intent).setOnMenuItemClickListener(this); + menu.add(0, MENU_ENQUEUE_GENRE, 0, R.string.enqueue_current_genre).setIntent(intent).setOnMenuItemClickListener(this); + menu.add(0, MENU_REMOVE, 0, R.string.remove).setIntent(intent).setOnMenuItemClickListener(this); + } + + /** + * Called by Android after the User selected a MenuItem. + * + * @param item The selected menu item. + */ + @Override + public boolean onMenuItemClick(MenuItem item) { + Intent intent = item.getIntent(); + int itemId = item.getItemId(); + int pos = intent.getIntExtra("position", -1); + + Song song = mService.getSongByQueuePosition(pos); + switch (item.getItemId()) { + case MENU_PLAY: + onItemClick(null, null, pos, -1); + break; + case MENU_ENQUEUE_ALBUM: + mService.enqueueFromSong(song, MediaUtils.TYPE_ALBUM); + break; + case MENU_ENQUEUE_ARTIST: + mService.enqueueFromSong(song, MediaUtils.TYPE_ARTIST); + break; + case MENU_ENQUEUE_GENRE: + mService.enqueueFromSong(song, MediaUtils.TYPE_GENRE); + break; + case MENU_REMOVE: + remove(pos); + break; + default: + throw new IllegalArgumentException("Unhandled menu id received!"); + // we could actually dispatch this to the hosting activity, but we do not need this for now. + } + return true; + } + + + /** + * Fired from adapter listview if user moved an item + * @param from the item index that was dragged + * @param to the index where the item was dropped + */ + @Override + public void drop(int from, int to) { + if (from != to) { + mService.moveSongPosition(from, to); + } + } + + /** + * Fired from adapter listview after user removed a song + * @param which index to remove from queue + */ + @Override + public void remove(int which) { + mService.removeSongPosition(which); + } + + /** + * Called when an item in the listview gets clicked + */ + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + mService.jumpToQueuePosition(position); + } + + /** + * Triggers a refresh of the queueview + * @param scroll enable or disable jumping to the currently playing item + */ + public void refreshSongQueueList(final boolean scroll) { + getActivity().runOnUiThread(new Runnable(){ + public void run() { + int i, stotal, spos; + stotal = mService.getTimelineLength(); /* Total number of songs in queue */ + spos = mService.getTimelinePosition(); /* Current position in queue */ + + mListAdapter.clear(); /* Flush all existing entries... */ + mListAdapter.highlightRow(spos); /* and highlight current position */ + + for(i=0 ; i + * We suppress the new api lint check as lint thinks + * {@link android.widget.AbsListView#setSelectionFromTop(int, int)} was only added in + * {@link Build.VERSION_CODES#JELLY_BEAN}, but it was actually added in API + * level 1
+ * + * Android reference: AbsListView.setSelectionFromTop() + * @param currentSongPosition The position in {@link #mListView} of the current song + */ + @SuppressLint("NewApi") + private void scrollToCurrentSong(int currentSongPosition){ + mListView.setSelectionFromTop(currentSongPosition, 0); /* scroll to currently playing song */ + } + + // Used Callbacks of TImelineCallback + public void onTimelineChanged() { + if (mService == null) + mService = PlaybackService.get(getActivity()); + refreshSongQueueList(false); + } + + // Unused Callbacks of TimelineCallback + public void onPositionInfoChanged() { + } + public void onMediaChange() { + } + public void recreate() { + } + public void replaceSong(int delta, Song song) { + } + public void setSong(long uptime, Song song) { + } + public void setState(long uptime, int state) { + } +} diff --git a/src/ch/blinkenlights/android/vanilla/SlidingView.java b/src/ch/blinkenlights/android/vanilla/SlidingView.java new file mode 100644 index 00000000..f6beb513 --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/SlidingView.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.vanilla; + +import android.app.Activity; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.animation.DecelerateInterpolator; +import android.view.GestureDetector; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnTouchListener; +import android.view.MotionEvent; +import android.widget.FrameLayout; + +import java.util.ArrayList; + +public class SlidingView extends FrameLayout + implements View.OnTouchListener + { + /** + * Ignore drag until we made 30 px progress. + */ + private final float MAX_PROGRESS = 30; + /** + * The maximum (initial) offset of the view + */ + private float mMaxOffsetY = 0; + /** + * The previous Y coordinate, used to calculate the movement diff. + */ + private float mPreviousY = 0; + /** + * The total progress in pixels of this drag + */ + private float mProgressPx = 0; + /** + * Signals the direction of the fling + */ + private int mFlingDirection = 0; + /** + * TRUE if we started to move this view + */ + private boolean mDidScroll = false; + /** + * Reference to the gesture detector + */ + private GestureDetector mDetector; + /** + * An external View we are managing during layout changes. + */ + private View mSlaveView; + /** + * The resource id to listen for touch events + */ + private int mSliderHandleId = 0; + /** + * The current expansion stage + */ + int mCurrentStage = 0; + /** + * List with all possible stages and their offsets + */ + ArrayList mStages = new ArrayList(); + + + public SlidingView(Context context) { + this(context, null); + } + + public SlidingView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setBackgroundColor(ThemeHelper.getDefaultCoverColors(context)[0]); + + mDetector = new GestureDetector(new GestureListener()); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SlidingViewPreferences); + mSliderHandleId = a.getResourceId(R.styleable.SlidingViewPreferences_slider_handle_id, 0); + int slaveId = a.getResourceId(R.styleable.SlidingViewPreferences_slider_slave_id, 0); + a.recycle(); + + // This is probably a parent view: so we need the context but can search + // it before we got inflated: + mSlaveView = ((Activity)context).findViewById(slaveId); + } + + /** + * Fully expands the slide + */ + public void expandSlide() { + setExpansionStage(mStages.size()-1); + } + + /** + * Hides the slide + */ + public void hideSlide() { + setExpansionStage(0); + } + + /** + * Transforms to the new expansion state + * + * @param stage the stage to transform to + */ + private void setExpansionStage(int stage) { + mCurrentStage = stage; + int pxOff = mStages.get(stage); + this + .animate() + .translationY(pxOff) + .setListener(new AnimationListener()) + .setInterpolator(new DecelerateInterpolator()); + } + + /** + * Changes the parent view to fit given stage + * + * @param stage the stage to transform to + */ + private void setSlaveViewStage(int stage) { + if (mSlaveView == null) + return; + + int totalOffset = 0; + for (int i = 0; i <= stage; i++) { + totalOffset += getChildAt(i).getHeight(); + } + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)mSlaveView.getLayoutParams(); + params.bottomMargin = totalOffset; + mSlaveView.setLayoutParams(params); + } + + /** + * Called after the view was inflated, binds an onTouchListener to all child + * elements of the child view + */ + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + View handle = findViewById(mSliderHandleId); + + if (handle != null) { + if (handle instanceof ViewGroup) { + ViewGroup group = (ViewGroup)handle; + for (int i = 0; i < group.getChildCount(); i++) { + group.getChildAt(i).setOnTouchListener(this); + } + } else { + handle.setOnTouchListener(this); + } + } + } + + + /** + * Attempts to stack all views orizontally in the available space + */ + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + int viewHeight = getMeasuredHeight(); + int childCount = getChildCount(); + int topOffset = 0; + View lastChild = null; + + mStages.clear(); + + for (int i = 0; i < childCount ; i++) { + lastChild = getChildAt(i); + int childWidth = lastChild.getMeasuredWidth(); + int childHeight = lastChild.getMeasuredHeight(); + int childBottom = childHeight + topOffset; + + // No child should consume space outside of our view + if (topOffset > viewHeight) + topOffset = viewHeight; + if (childBottom > viewHeight) + childBottom = viewHeight; + + lastChild.layout(0, topOffset, childWidth, childBottom); + mStages.add(viewHeight - childBottom); + topOffset += childHeight; + } + + if (lastChild != null && mMaxOffsetY == 0) { + // Sizes are now fixed: Overwrite any (possible) FILL_PARENT or WRAP_CONTENT + // value with the measured size + // This should only happen on the first run (mMaxOffsetY == 0) + for (int i = 0; i < childCount ; i++) { + View child = getChildAt(i); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)child.getLayoutParams(); + params.height = child.getHeight(); + params.width = child.getWidth(); + child.setLayoutParams(params); + } + } + + if (changed) { + mMaxOffsetY = mStages.get(0); + setTranslationY(mMaxOffsetY); + setExpansionStage(0); + } + } + + + @Override + public boolean onTouch(View v, MotionEvent event){ + // Fix up the event offset as we are moving the view itself. + // This is required to get flings correctly detected + event.setLocation(event.getRawX(), event.getRawY()); + + mDetector.onTouchEvent(event); + float y = event.getRawY(); + float dy = y - mPreviousY; // diff Y + float vy = getTranslationY(); // view Y + + switch(event.getActionMasked()) { + case MotionEvent.ACTION_UP : { + if (mDidScroll == false) { // Dispatch event if we never scrolled + v.onTouchEvent(event); + } else { + int nstages = mStages.size(); + int tstage = 0; + int tbonus = (int)mProgressPx * mFlingDirection; // we add the progress as virtual bonus on fling + int toff = (int)mMaxOffsetY; + for (int i=0; i mMaxOffsetY) + usedY = mMaxOffsetY; + + if (mProgressPx < MAX_PROGRESS) { + // we did not reach a minimum of progress: do not scroll yet + usedY = vy; + } else if (mDidScroll == false) { + mDidScroll = true; + event.setAction(MotionEvent.ACTION_CANCEL); + v.onTouchEvent(event); + setSlaveViewStage(0); // parent can use full view, will be reset on ACTION_UP handlers + } + + setTranslationY(usedY); + break; + } + } + mPreviousY = y; + return true; + } + + class GestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { + mFlingDirection = (velocityY > 0 ? 1 : -1); + return true; + } + } + + class AnimationListener extends AnimatorListenerAdapter { + @Override + public void onAnimationEnd(Animator animation) { + setSlaveViewStage(mCurrentStage); + } + @Override + public void onAnimationCancel(Animator animation) { + onAnimationEnd(animation); + } + } + +}