OSDN Git Service

Add ability to seek through track with circular motion
authorLinus Lee <llee@cyngn.com>
Tue, 28 Jul 2015 00:07:05 +0000 (17:07 -0700)
committerLinus Lee <llee@cyngn.com>
Wed, 12 Aug 2015 01:25:59 +0000 (18:25 -0700)
With this patch, if you do a circular gesture motion
on the large play button, you can seek the track instead
of using the long press of the next/previous buttons

Also change the progress bar to 1000 to allow for smaller updates

Change-Id: I38512b2439227686bc6363e77b1858dbb5764a9f

res/layout/main_playback_controls.xml
res/layout/play_pause_progress_button.xml
src/com/cyanogenmod/eleven/ui/activities/HomeActivity.java
src/com/cyanogenmod/eleven/ui/fragments/AudioPlayerFragment.java
src/com/cyanogenmod/eleven/utils/MusicUtils.java
src/com/cyanogenmod/eleven/widgets/PlayPauseProgressButton.java

index 8c11211..54385cd 100644 (file)
@@ -44,8 +44,8 @@
             android:layout_height="match_parent"
             android:clickable="false"
             android:indeterminate="false"
-            android:max="100"
-            android:progress="100"
+            android:max="1000"
+            android:progress="1000"
             android:progressDrawable="@drawable/now_playing_progress_background" />
 
         <ProgressBar
@@ -55,7 +55,7 @@
             android:layout_height="match_parent"
             android:clickable="false"
             android:indeterminate="false"
-            android:max="100"
+            android:max="1000"
             android:progressDrawable="@drawable/now_playing_progress" />
     </com.cyanogenmod.eleven.widgets.PlayPauseProgressButton>
 
index 13805ea..0631cad 100644 (file)
@@ -39,8 +39,8 @@
         android:layout_height="match_parent"
         android:clickable="false"
         android:indeterminate="false"
-        android:max="100"
-        android:progress="100"
+        android:max="1000"
+        android:progress="1000"
         android:progressDrawable="@drawable/circular_drawable_background" />
 
     <ProgressBar
@@ -50,7 +50,7 @@
         android:layout_height="match_parent"
         android:clickable="false"
         android:indeterminate="false"
-        android:max="100"
+        android:max="1000"
         android:progressDrawable="@drawable/circular_drawable" />
 
 </com.cyanogenmod.eleven.widgets.PlayPauseProgressButton>
index d946039..a66c6c0 100644 (file)
@@ -38,6 +38,7 @@ import com.cyanogenmod.eleven.R;
 import com.cyanogenmod.eleven.cache.ImageFetcher;
 import com.cyanogenmod.eleven.ui.fragments.AlbumDetailFragment;
 import com.cyanogenmod.eleven.ui.fragments.ArtistDetailFragment;
+import com.cyanogenmod.eleven.ui.fragments.AudioPlayerFragment;
 import com.cyanogenmod.eleven.ui.fragments.IChildFragment;
 import com.cyanogenmod.eleven.ui.fragments.ISetupActionBar;
 import com.cyanogenmod.eleven.ui.fragments.PlaylistDetailFragment;
@@ -215,7 +216,11 @@ public class HomeActivity extends SlidingPanelActivity implements
             color = getResources().getColor(R.color.visualizer_fill_color);
         }
 
-        getAudioPlayerFragment().setVisualizerColor(color);
+        // check for null since updatestatusBarColor is a async task
+        AudioPlayerFragment fragment = getAudioPlayerFragment();
+        if (fragment != null) {
+            fragment.setVisualizerColor(color);
+        }
     }
 
     private void updateStatusBarColor(int color) {
index efeaa62..e54eabd 100644 (file)
@@ -156,8 +156,6 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
 
     private boolean mIsPaused = false;
 
-    private boolean mFromTouch = false;
-
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
@@ -352,6 +350,7 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
      */
     private void initPlaybackControls() {
         mPlayPauseProgressButton = (PlayPauseProgressButton)mRootView.findViewById(R.id.playPauseProgressButton);
+        mPlayPauseProgressButton.setDragEnabled(true);
         mShuffleButton = (ShuffleButton)mRootView.findViewById(R.id.action_button_shuffle);
         mRepeatButton = (RepeatButton)mRootView.findViewById(R.id.action_button_repeat);
         mPreviousButton = (RepeatingImageButton)mRootView.findViewById(R.id.action_button_previous);
@@ -538,49 +537,48 @@ public class AudioPlayerFragment extends Fragment implements ServiceConnection {
     }
 
     private void refreshCurrentTimeText(final long pos) {
-        mCurrentTime.setText(MusicUtils.makeShortTimeString(getActivity(), pos / 1000));
+        if (mPlayPauseProgressButton.isDragging()) {
+            mCurrentTime.setText(MusicUtils.makeShortTimeString(getActivity(),
+                    mPlayPauseProgressButton.getDragProgressInMs() / 1000));
+        } else {
+            mCurrentTime.setText(MusicUtils.makeShortTimeString(getActivity(), pos / 1000));
+        }
     }
 
     /* Used to update the current time string */
     private long refreshCurrentTime() {
         if (mService == null) {
-            return 500;
+            return MusicUtils.UPDATE_FREQUENCY_MS;
         }
         try {
             final long pos = MusicUtils.position();
             if (pos >= 0 && MusicUtils.duration() > 0) {
                 refreshCurrentTimeText(pos);
 
-                if (mFromTouch) {
-                    return 500;
+                if (mPlayPauseProgressButton.isDragging()) {
+                    mCurrentTime.setVisibility(View.VISIBLE);
+                    return MusicUtils.UPDATE_FREQUENCY_FAST_MS;
                 } else if (MusicUtils.isPlaying()) {
                     mCurrentTime.setVisibility(View.VISIBLE);
+
+                    // calculate the number of milliseconds until the next full second,
+                    // so the counter can be updated at just the right time
+                    return Math.max(20, 1000 - pos % 1000);
                 } else {
                     // blink the counter
                     final int vis = mCurrentTime.getVisibility();
                     mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE
                             : View.INVISIBLE);
-                    return 500;
                 }
             } else {
                 mCurrentTime.setText("--:--");
             }
-
-            // calculate the number of milliseconds until the next full second,
-            // so
-            // the counter can be updated at just the right time
-            final long remaining = 1000 - pos % 1000;
-            if (remaining < 20) {
-                return 20;
-            }
-
-            return remaining;
         } catch (final Exception ignored) {
             if (ignored.getMessage() != null) {
                 Log.e(TAG, ignored.getMessage());
             }
         }
-        return 500;
+        return MusicUtils.UPDATE_FREQUENCY_MS;
     }
 
     /**
index 6279a78..38e9ac0 100644 (file)
@@ -87,6 +87,9 @@ public final class MusicUtils {
     public static final String MUSIC_ONLY_SELECTION = MediaStore.Audio.AudioColumns.IS_MUSIC + "=1"
                     + " AND " + MediaStore.Audio.AudioColumns.TITLE + " != ''"; //$NON-NLS-2$
 
+    public static final long UPDATE_FREQUENCY_MS = 500;
+    public static final long UPDATE_FREQUENCY_FAST_MS = 30;
+
     static {
         mConnectionMap = new WeakHashMap<Context, ServiceBinder>();
         sEmptyList = new long[0];
index 5c086be..8b8788f 100644 (file)
@@ -1,5 +1,6 @@
 /*
-* Copyright (C) 2014 The CyanogenMod Project
+* Copyright (c) 2013, The Linux Foundation. All rights reserved.
+* Copyright (C) 2015 The CyanogenMod Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
@@ -17,6 +18,9 @@ package com.cyanogenmod.eleven.widgets;
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 import android.widget.FrameLayout;
 import android.widget.ProgressBar;
 
@@ -30,12 +34,28 @@ import com.cyanogenmod.eleven.utils.MusicUtils;
  * updates while the activity/fragment is not visible
  */
 public class PlayPauseProgressButton extends FrameLayout {
-    private static final long UPDATE_FREQUENCY_MS = 500;
+    private static String TAG = PlayPauseProgressButton.class.getSimpleName();
+    private static boolean DEBUG = false;
+    private static final int REVOLUTION_IN_DEGREES = 360;
+    private static final int HALF_REVOLUTION_IN_DEGREES = REVOLUTION_IN_DEGREES / 2;
+
     private ProgressBar mProgressBar;
     private PlayPauseButton mPlayPauseButton;
     private Runnable mUpdateProgress;
     private boolean mPaused;
 
+    private final int mSmallDistance;
+    private float mDragPercentage = 0.0f;
+    private boolean mDragEnabled = false;
+    private boolean mDragging = false;
+    private float mDownAngle;
+    private float mDragAngle;
+    private float mDownX;
+    private float mDownY;
+    private int mWidth;
+    private long mCurrentSongDuration;
+    private long mCurrentSongProgress;
+
     public PlayPauseProgressButton(Context context, AttributeSet attrs) {
         super(context, attrs);
 
@@ -44,6 +64,8 @@ public class PlayPauseProgressButton extends FrameLayout {
 
         // set paused to false since we shouldn't be typically created while not visible
         mPaused = false;
+
+        mSmallDistance = ViewConfiguration.get(context).getScaledTouchSlop();
     }
 
     @Override
@@ -94,6 +116,27 @@ public class PlayPauseProgressButton extends FrameLayout {
         setVisibility(GONE);
     }
 
+    /**
+     * Sets whether the user can drag the progress in a circular motion to seek the track
+     */
+    public void setDragEnabled(boolean enabled) {
+        mDragEnabled = enabled;
+    }
+
+    /**
+     * @return true if the user is actively dragging to seek
+     */
+    public boolean isDragging() {
+        return mDragEnabled && mDragging;
+    }
+
+    /**
+     * @return how far the user has dragged in the track in ms
+     */
+    public long getDragProgressInMs() {
+        return (long)(mDragPercentage * mCurrentSongDuration);
+    }
+
     @Override
     public void setEnabled(boolean enabled) {
         // if the enabled state isn't changed, quit
@@ -158,18 +201,17 @@ public class PlayPauseProgressButton extends FrameLayout {
      * Updates the state of the progress bar and the play pause button
      */
     private void updateState() {
-        final long duration = MusicUtils.duration();
-
-        if (duration > 0) {
-            final long pos = MusicUtils.position();
-
-            int progress = (int) (mProgressBar.getMax() * pos / duration);
-            mProgressBar.setProgress(progress);
-        } else {
-            // this is when there are no tracks loaded or some kind of error condition
-            mProgressBar.setProgress(0);
+        mCurrentSongDuration = MusicUtils.duration();
+        mCurrentSongProgress = MusicUtils.position();
+
+        int progress = 0;
+        if (isDragging()) {
+            progress = (int) (mDragPercentage * mProgressBar.getMax());
+        } else if (mCurrentSongDuration > 0) {
+            progress = (int) (mProgressBar.getMax() * mCurrentSongProgress / mCurrentSongDuration);
         }
 
+        mProgressBar.setProgress(progress);
         mPlayPauseButton.updateState();
     }
 
@@ -182,7 +224,8 @@ public class PlayPauseProgressButton extends FrameLayout {
                 @Override
                 public void run() {
                     updateState();
-                    postDelayed(mUpdateProgress, UPDATE_FREQUENCY_MS);
+                    postDelayed(mUpdateProgress, isDragging() ? MusicUtils.UPDATE_FREQUENCY_FAST_MS
+                            : MusicUtils.UPDATE_FREQUENCY_MS);
                 }
             };
         }
@@ -191,7 +234,7 @@ public class PlayPauseProgressButton extends FrameLayout {
         removeCallbacks(mUpdateProgress);
 
         // post ourselves as a delayed
-        postDelayed(mUpdateProgress, UPDATE_FREQUENCY_MS);
+        post(mUpdateProgress);
     }
 
     /**
@@ -202,4 +245,133 @@ public class PlayPauseProgressButton extends FrameLayout {
             removeCallbacks(mUpdateProgress);
         }
     }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldW, int oldH) {
+        mWidth = Math.min(w, h);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!mDragEnabled) {
+            return false;
+        }
+
+        return onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+
+        if (!mDragEnabled || mCurrentSongDuration <= 0) {
+            return false;
+        }
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mDownX = event.getX();
+                mDownY = event.getY();
+                mDownAngle = angle(mDownX, mDownY);
+                mDragAngle = REVOLUTION_IN_DEGREES
+                        * (mCurrentSongProgress / (float) mCurrentSongDuration);
+                mDragPercentage = mDragAngle / REVOLUTION_IN_DEGREES;
+                mDragging = false;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // if the user has moved a certain distance
+                if (Math.sqrt(Math.pow(event.getX() - mDownX, 2)
+                        + Math.pow(event.getY() - mDownY, 2)) < mSmallDistance) {
+                    return false;
+                }
+
+                // if we weren't previously dragging, immediately kick off an update to reflect
+                // the change faster
+                if (!mDragging) {
+                    postUpdate();
+                }
+
+                mDragging = true;
+                getParent().requestDisallowInterceptTouchEvent(true);
+
+                // calculate the amount of angle we've moved
+                final float deltaAngle = getDelta(x, y);
+                mDragAngle = cropAngle(mDragAngle + deltaAngle);
+                mDragPercentage = mDragAngle / REVOLUTION_IN_DEGREES;
+
+                if (DEBUG) {
+                    Log.d(TAG, "Delta Angle: " + deltaAngle + ", Target Angle: " + mDownAngle);
+                }
+
+                return true;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                // if we were dragging, seek to where we dragged to
+                if (mDragging) {
+                    MusicUtils.seek((long)(mDragPercentage * mCurrentSongDuration));
+                }
+                mDragging = false;
+            default:
+                break;
+        }
+        return mDragging;
+    }
+
+    /**
+     * Crops the angle between 0 and 360 - if the angle is < 0, it will return 0, if it is more than
+     * 360 it will return 360
+     */
+    private static float cropAngle(float angle) {
+        return Math.min(REVOLUTION_IN_DEGREES, Math.max(0.0f, angle));
+    }
+
+    /**
+     * Wraps the angle between -180 and 180. This assumes that the passed in
+     * angle is >= -360 and <= 360
+     */
+    private static float wrapHalfRevolution(float angle) {
+        if (angle < -HALF_REVOLUTION_IN_DEGREES) {
+            return angle + REVOLUTION_IN_DEGREES;
+        } else if (angle > HALF_REVOLUTION_IN_DEGREES) {
+            return angle - REVOLUTION_IN_DEGREES;
+        }
+
+        return angle;
+    }
+
+    /**
+     * Gets the change in angle from the down angle and updates the down angle to the current angle
+     */
+    private float getDelta(float x, float y) {
+        float angle = angle(x, y);
+        float deltaAngle = wrapHalfRevolution(angle - mDownAngle);
+        mDownAngle = angle;
+        return deltaAngle;
+    }
+
+    /**
+     * Calculates the angle at the point passed in based on the center of the button
+     */
+    private float angle(float x, float y) {
+        float center = mWidth / 2.0f;
+        x -= center;
+        y -= center;
+
+        if (x == 0.0f) {
+            if (y > 0.0f) {
+                return 180.0f;
+            } else {
+                return 0.0f;
+            }
+        }
+
+        float angle = (float) (Math.atan(y / x) / Math.PI * 180.0);
+        if (x > 0.0f) {
+            angle += 90;
+        } else {
+            angle += 270;
+        }
+        return angle;
+    }
 }