squash-merge of slide-up branch

This commit is contained in:
Adrian Ulrich 2016-03-20 13:21:25 +01:00
parent 563b01ee6c
commit 1a6774ca2f
8 changed files with 677 additions and 64 deletions

View File

@ -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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--> -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:vanilla="http://schemas.android.com/apk/res/ch.blinkenlights.android.vanilla" >
<ch.blinkenlights.android.vanilla.CoverView <ch.blinkenlights.android.vanilla.CoverView
android:id="@+id/cover_view" android:id="@+id/cover_view"
android:layout_height="fill_parent" android:layout_height="fill_parent"
@ -55,14 +56,28 @@ THE SOFTWARE.
android:layout_gravity="center" android:layout_gravity="center"
android:paddingRight="5dip" /> android:paddingRight="5dip" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/controls_bottom" <ch.blinkenlights.android.vanilla.SlidingView
android:layout_height="wrap_content" android:id="@+id/sliding_view"
android:layout_height="fill_parent"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_gravity="bottom|left" android:layout_gravity="bottom|left"
android:layout_margin="0dip" android:layout_marginTop="36dip"
android:orientation="horizontal"
vanilla:slider_handle_id="@+id/queue_slider">
<LinearLayout
android:id="@+id/queue_slider"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:background="?overlay_background_color" android:background="?overlay_background_color"
android:orientation="horizontal"> android:orientation="horizontal">
<include layout="@layout/controls" /> <include layout="@layout/controls" />
</LinearLayout> </LinearLayout>
<fragment class="ch.blinkenlights.android.vanilla.ShowQueueFragment"
android:id="@+id/queue"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</ch.blinkenlights.android.vanilla.SlidingView>
</merge> </merge>

View File

@ -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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--> -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:vanilla="http://schemas.android.com/apk/res/ch.blinkenlights.android.vanilla" >
<ch.blinkenlights.android.vanilla.CoverView <ch.blinkenlights.android.vanilla.CoverView
android:id="@+id/cover_view" android:id="@+id/cover_view"
android:layout_height="fill_parent" android:layout_height="fill_parent"
@ -139,14 +140,29 @@ THE SOFTWARE.
android:layout_gravity="center" /> android:layout_gravity="center" />
</LinearLayout> </LinearLayout>
</TableLayout> </TableLayout>
<LinearLayout
android:id="@+id/controls_bottom"
android:layout_height="wrap_content" <ch.blinkenlights.android.vanilla.SlidingView
android:id="@+id/sliding_view"
android:layout_height="fill_parent"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_gravity="bottom|left" android:layout_gravity="bottom|left"
android:layout_margin="0dip" android:layout_marginTop="88dip"
android:orientation="horizontal"
vanilla:slider_handle_id="@+id/queue_slider">
<LinearLayout
android:id="@+id/queue_slider"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:background="?overlay_background_color" android:background="?overlay_background_color"
android:orientation="horizontal"> android:orientation="horizontal">
<include layout="@layout/controls" /> <include layout="@layout/controls" />
</LinearLayout> </LinearLayout>
<fragment class="ch.blinkenlights.android.vanilla.ShowQueueFragment"
android:id="@+id/queue"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</ch.blinkenlights.android.vanilla.SlidingView>
</merge> </merge>

View File

@ -20,20 +20,21 @@ 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--> -->
<LinearLayout
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:vanilla="http://schemas.android.com/apk/res/ch.blinkenlights.android.vanilla" >
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content" android:id="@+id/content"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent"> android:layout_height="fill_parent">
<android.support.iosched.tabs.VanillaTabLayout <android.support.iosched.tabs.VanillaTabLayout
android:id="@+id/sliding_tabs" android:id="@+id/sliding_tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:elevation="4dp" android:elevation="4dp"
android:background="@color/tabs_background" /> android:background="@color/tabs_background" />
<android.support.v4.view.ViewPager <android.support.v4.view.ViewPager
android:id="@+id/pager" android:id="@+id/pager"
android:layout_width="fill_parent" android:layout_width="fill_parent"
@ -50,6 +51,32 @@ THE SOFTWARE.
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="fill_parent" /> android:layout_height="fill_parent" />
</HorizontalScrollView> </HorizontalScrollView>
</LinearLayout>
<ch.blinkenlights.android.vanilla.SlidingView
android:id="@+id/sliding_view"
android:layout_width="fill_parent"
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">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:background="?overlay_background_color"
android:orientation="horizontal">
<include layout="@layout/bottombar_controls" android:id="@+id/bottombar_controls" /> <include layout="@layout/bottombar_controls" android:id="@+id/bottombar_controls" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:background="?overlay_background_color"
android:orientation="horizontal">
<include layout="@layout/controls" />
</LinearLayout>
<fragment class="ch.blinkenlights.android.vanilla.ShowQueueFragment"
android:id="@+id/queue"
android:layout_width="fill_parent" android:layout_height="fill_parent" />
</ch.blinkenlights.android.vanilla.SlidingView>
</merge>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SlidingViewPreferences">
<attr name="slider_handle_id" format="integer"/>
<attr name="slider_slave_id" format="integer"/>
</declare-styleable>
</resources>

View File

@ -64,7 +64,7 @@ public class FullPlaybackActivity extends PlaybackActivity
private TextView mOverlayText; private TextView mOverlayText;
private View mControlsTop; private View mControlsTop;
private View mControlsBottom; private View mSlidingView;
private SeekBar mSeekBar; private SeekBar mSeekBar;
private TableLayout mInfoTable; private TableLayout mInfoTable;
@ -161,7 +161,7 @@ public class FullPlaybackActivity extends PlaybackActivity
coverView.setOnLongClickListener(this); coverView.setOnLongClickListener(this);
mCoverView = coverView; mCoverView = coverView;
mControlsBottom = findViewById(R.id.controls_bottom); mSlidingView = findViewById(R.id.sliding_view);
View previousButton = findViewById(R.id.previous); View previousButton = findViewById(R.id.previous);
previousButton.setOnClickListener(this); previousButton.setOnClickListener(this);
mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause); mPlayPauseButton = (ImageButton)findViewById(R.id.play_pause);
@ -389,13 +389,13 @@ public class FullPlaybackActivity extends PlaybackActivity
openLibrary(null); openLibrary(null);
break; break;
case MENU_ENQUEUE_ALBUM: case MENU_ENQUEUE_ALBUM:
PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_ALBUM); PlaybackService.get(this).enqueueFromSong(PlaybackService.get(this).getSong(0), MediaUtils.TYPE_ALBUM);
break; break;
case MENU_ENQUEUE_ARTIST: case MENU_ENQUEUE_ARTIST:
PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_ARTIST); PlaybackService.get(this).enqueueFromSong(PlaybackService.get(this).getSong(0), MediaUtils.TYPE_ARTIST);
break; break;
case MENU_ENQUEUE_GENRE: case MENU_ENQUEUE_GENRE:
PlaybackService.get(this).enqueueFromCurrent(MediaUtils.TYPE_GENRE); PlaybackService.get(this).enqueueFromSong(PlaybackService.get(this).getSong(0), MediaUtils.TYPE_GENRE);
break; break;
case MENU_SONG_FAVORITE: case MENU_SONG_FAVORITE:
Song song = (PlaybackService.get(this)).getSong(0); Song song = (PlaybackService.get(this)).getSong(0);
@ -513,7 +513,7 @@ public class FullPlaybackActivity extends PlaybackActivity
{ {
int mode = visible ? View.VISIBLE : View.GONE; int mode = visible ? View.VISIBLE : View.GONE;
mControlsTop.setVisibility(mode); mControlsTop.setVisibility(mode);
mControlsBottom.setVisibility(mode); mSlidingView.setVisibility(mode);
mControlsVisible = visible; mControlsVisible = visible;
if (visible) { if (visible) {

View File

@ -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. * song.
* *
* This will clear the queue and place the first song from the group after * This will clear the queue and place the first song from the group after
* the playing song. * the playing song.
* *
* @param song The song to base the query on
* @param type The media type, one of MediaUtils.TYPE_ALBUM, TYPE_ARTIST, * @param type The media type, one of MediaUtils.TYPE_ALBUM, TYPE_ARTIST,
* or TYPE_GENRE * or TYPE_GENRE
*/ */
public void enqueueFromCurrent(int type) public void enqueueFromSong(Song song, int type)
{ {
Song current = mCurrentSong; if (song == null)
if (current == null)
return; return;
long id; long id;
switch (type) { switch (type) {
case MediaUtils.TYPE_ARTIST: case MediaUtils.TYPE_ARTIST:
id = current.artistId; id = song.artistId;
break; break;
case MediaUtils.TYPE_ALBUM: case MediaUtils.TYPE_ALBUM:
id = current.albumId; id = song.albumId;
break; break;
case MediaUtils.TYPE_GENRE: case MediaUtils.TYPE_GENRE:
id = MediaUtils.queryGenreForSong(getContentResolver(), current.id); id = MediaUtils.queryGenreForSong(getContentResolver(), song.id);
break; break;
default: default:
throw new IllegalArgumentException("Unsupported media type: " + type); 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); QueryTask query = MediaUtils.buildQuery(type, id, Song.FILLED_PROJECTION, selection);
query.mode = SongTimeline.MODE_FLUSH_AND_PLAY_NEXT; query.mode = SongTimeline.MODE_FLUSH_AND_PLAY_NEXT;
addSongs(query); addSongs(query);
@ -2248,13 +2248,13 @@ public final class PlaybackService extends Service
break; break;
} }
case EnqueueAlbum: case EnqueueAlbum:
enqueueFromCurrent(MediaUtils.TYPE_ALBUM); enqueueFromSong(mCurrentSong, MediaUtils.TYPE_ALBUM);
break; break;
case EnqueueArtist: case EnqueueArtist:
enqueueFromCurrent(MediaUtils.TYPE_ARTIST); enqueueFromSong(mCurrentSong, MediaUtils.TYPE_ARTIST);
break; break;
case EnqueueGenre: case EnqueueGenre:
enqueueFromCurrent(MediaUtils.TYPE_GENRE); enqueueFromSong(mCurrentSong, MediaUtils.TYPE_GENRE);
break; break;
case ClearQueue: case ClearQueue:
clearQueue(); clearQueue();

View File

@ -0,0 +1,233 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<stotal; i++) {
mListAdapter.add(mService.getSongByQueuePosition(i));
}
if(scroll)
scrollToCurrentSong(spos);
}
});
}
/**
* Scrolls to the current song<br/>
* 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<br/>
* <a href="https://developer.android.com/reference/android/widget/AbsListView.html#setSelectionFromTop%28int,%20int%29">
* Android reference: AbsListView.setSelectionFromTop()</a>
* @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) {
}
}

View File

@ -0,0 +1,315 @@
/*
* Copyright (C) 2016 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Integer> mStages = new ArrayList<Integer>();
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<nstages; i++) {
int tdiff = Math.abs((int)vy + tbonus - mStages.get(i));
if (tdiff < toff) {
toff = tdiff;
tstage = i;
}
}
setExpansionStage(tstage);
}
break;
}
case MotionEvent.ACTION_DOWN : {
v.onTouchEvent(event);
mProgressPx = 0;
mFlingDirection = 0;
mDidScroll = false;
break;
}
case MotionEvent.ACTION_MOVE : {
mProgressPx += Math.abs(dy);
float usedY = vy + dy;
if (usedY < 0)
usedY = 0;
if (usedY > 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);
}
}
}