2 * Copyright (c) 2013, The Linux Foundation. All rights reserved.
3 * Copyright (C) 2015 The CyanogenMod Project
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 package org.lineageos.eleven.widgets;
19 import android.content.Context;
20 import android.util.AttributeSet;
21 import android.util.Log;
22 import android.view.MotionEvent;
23 import android.view.ViewConfiguration;
24 import android.widget.FrameLayout;
25 import android.widget.ProgressBar;
27 import org.lineageos.eleven.R;
28 import org.lineageos.eleven.utils.MusicUtils;
31 * This class handles the playpause button as well as the circular progress bar
32 * it self-updates the progress bar but the containing activity/fragment
33 * needs to add code to pause/resume this button to prevent unnecessary
34 * updates while the activity/fragment is not visible
36 public class PlayPauseProgressButton extends FrameLayout {
37 private static String TAG = PlayPauseProgressButton.class.getSimpleName();
38 private static boolean DEBUG = false;
39 private static final int REVOLUTION_IN_DEGREES = 360;
40 private static final int HALF_REVOLUTION_IN_DEGREES = REVOLUTION_IN_DEGREES / 2;
42 private ProgressBar mProgressBar;
43 private PlayPauseButton mPlayPauseButton;
44 private Runnable mUpdateProgress;
45 private boolean mPaused;
47 private final int mSmallDistance;
48 private float mDragPercentage = 0.0f;
49 private boolean mDragEnabled = false;
50 private boolean mDragging = false;
51 private float mDownAngle;
52 private float mDragAngle;
56 private long mCurrentSongDuration;
57 private long mCurrentSongProgress;
59 public PlayPauseProgressButton(Context context, AttributeSet attrs) {
60 super(context, attrs);
62 // set enabled to false as default so that calling enableAndShow will execute
65 // set paused to false since we shouldn't be typically created while not visible
68 mSmallDistance = ViewConfiguration.get(context).getScaledTouchSlop();
72 protected void onFinishInflate() {
73 super.onFinishInflate();
75 mPlayPauseButton = (PlayPauseButton)findViewById(R.id.action_button_play);
76 mProgressBar = (ProgressBar)findViewById(R.id.circularProgressBar);
80 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
81 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
83 // Make the play pause button size dependent on the container size
84 int horizontalPadding = getMeasuredWidth() / 4;
85 int verticalPadding = getMeasuredHeight() / 4;
86 mPlayPauseButton.setPadding(
87 horizontalPadding, horizontalPadding,
88 verticalPadding, verticalPadding);
90 // rotate the progress bar 90 degrees counter clockwise so that the
91 // starting position is at the top
92 mProgressBar.setPivotX(mProgressBar.getMeasuredWidth() / 2);
93 mProgressBar.setPivotY(mProgressBar.getMeasuredHeight() / 2);
94 mProgressBar.setRotation(-90);
98 * Enable and shows the container
100 public void enableAndShow() {
104 // make our view visible
105 setVisibility(VISIBLE);
109 * Disables and sets the visibility to gone for the container
111 public void disableAndHide() {
120 * Sets whether the user can drag the progress in a circular motion to seek the track
122 public void setDragEnabled(boolean enabled) {
123 mDragEnabled = enabled;
127 * @return true if the user is actively dragging to seek
129 public boolean isDragging() {
130 return mDragEnabled && mDragging;
134 * @return how far the user has dragged in the track in ms
136 public long getDragProgressInMs() {
137 return (long)(mDragPercentage * mCurrentSongDuration);
141 public void setEnabled(boolean enabled) {
142 // if the enabled state isn't changed, quit
143 if (enabled == isEnabled()) return;
145 super.setEnabled(enabled);
147 // signal our state has changed
152 * Pauses the progress bar periodic update logic
154 public void pause() {
158 // signal our state has changed
164 * Resumes the progress bar periodic update logic
166 public void resume() {
170 // signal our state has changed
176 * @return play pause button
178 public PlayPauseButton getPlayPauseButton() {
179 return mPlayPauseButton;
183 * Signaled if the state has changed (either the enabled or paused flag)
184 * When the state changes, we either kick off the updates or remove them based on those flags
186 private void onStateChanged() {
187 // if we are enabled and not paused
188 if (isEnabled() && !mPaused) {
189 // update the state of the progress bar and play/pause button
192 // kick off update states
195 // otherwise remove our update
201 * Updates the state of the progress bar and the play pause button
203 private void updateState() {
204 mCurrentSongDuration = MusicUtils.duration();
205 mCurrentSongProgress = MusicUtils.position();
209 progress = (int) (mDragPercentage * mProgressBar.getMax());
210 } else if (mCurrentSongDuration > 0) {
211 progress = (int) (mProgressBar.getMax() * mCurrentSongProgress / mCurrentSongDuration);
214 mProgressBar.setProgress(progress);
215 mPlayPauseButton.updateState();
219 * Creates and posts the update runnable to the handler
221 private void postUpdate() {
222 if (mUpdateProgress == null) {
223 mUpdateProgress = new Runnable() {
227 postDelayed(mUpdateProgress, isDragging() ? MusicUtils.UPDATE_FREQUENCY_FAST_MS
228 : MusicUtils.UPDATE_FREQUENCY_MS);
233 // remove any existing callbacks
234 removeCallbacks(mUpdateProgress);
236 // post ourselves as a delayed
237 post(mUpdateProgress);
241 * Removes the runnable from the handler
243 private void removeUpdate() {
244 if (mUpdateProgress != null) {
245 removeCallbacks(mUpdateProgress);
250 protected void onSizeChanged(int w, int h, int oldW, int oldH) {
251 mWidth = Math.min(w, h);
255 public boolean onInterceptTouchEvent(MotionEvent ev) {
260 return onTouchEvent(ev);
264 public boolean onTouchEvent(MotionEvent event) {
265 final float x = event.getX();
266 final float y = event.getY();
268 if (!mDragEnabled || mCurrentSongDuration <= 0) {
272 switch (event.getActionMasked()) {
273 case MotionEvent.ACTION_DOWN:
274 mDownX = event.getX();
275 mDownY = event.getY();
276 mDownAngle = angle(mDownX, mDownY);
277 mDragAngle = REVOLUTION_IN_DEGREES
278 * (mCurrentSongProgress / (float) mCurrentSongDuration);
279 mDragPercentage = mDragAngle / REVOLUTION_IN_DEGREES;
282 case MotionEvent.ACTION_MOVE:
283 // if the user has moved a certain distance
284 if (Math.sqrt(Math.pow(event.getX() - mDownX, 2)
285 + Math.pow(event.getY() - mDownY, 2)) < mSmallDistance) {
289 // if we weren't previously dragging, immediately kick off an update to reflect
296 getParent().requestDisallowInterceptTouchEvent(true);
298 // calculate the amount of angle we've moved
299 final float deltaAngle = getDelta(x, y);
300 mDragAngle = cropAngle(mDragAngle + deltaAngle);
301 mDragPercentage = mDragAngle / REVOLUTION_IN_DEGREES;
304 Log.d(TAG, "Delta Angle: " + deltaAngle + ", Target Angle: " + mDownAngle);
308 case MotionEvent.ACTION_UP:
309 case MotionEvent.ACTION_CANCEL:
310 // if we were dragging, seek to where we dragged to
312 MusicUtils.seek((long)(mDragPercentage * mCurrentSongDuration));
322 * Crops the angle between 0 and 360 - if the angle is < 0, it will return 0, if it is more than
323 * 360 it will return 360
325 private static float cropAngle(float angle) {
326 return Math.min(REVOLUTION_IN_DEGREES, Math.max(0.0f, angle));
330 * Wraps the angle between -180 and 180. This assumes that the passed in
331 * angle is >= -360 and <= 360
333 private static float wrapHalfRevolution(float angle) {
334 if (angle < -HALF_REVOLUTION_IN_DEGREES) {
335 return angle + REVOLUTION_IN_DEGREES;
336 } else if (angle > HALF_REVOLUTION_IN_DEGREES) {
337 return angle - REVOLUTION_IN_DEGREES;
344 * Gets the change in angle from the down angle and updates the down angle to the current angle
346 private float getDelta(float x, float y) {
347 float angle = angle(x, y);
348 float deltaAngle = wrapHalfRevolution(angle - mDownAngle);
354 * Calculates the angle at the point passed in based on the center of the button
356 private float angle(float x, float y) {
357 float center = mWidth / 2.0f;
369 float angle = (float) (Math.atan(y / x) / Math.PI * 180.0);