/* * Copyright (C) 2010 Josh Guilfoyle <jasta@devtcg.org> * * 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 2, 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. */ package org.devtcg.five.widget; import org.devtcg.five.Constants; import org.devtcg.five.R; import android.content.Context; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnTouchListener; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; /** * This is the main view that resides underneath the player screen's header. * Includes album artwork, reflection effect of the artwork, prev/pause/next * controls, and progress controls. */ public class PlayerControls extends FrameLayout implements OnClickListener, OnTouchListener { private Drawable mDivider; private ImageView mArtwork; private View mProgressControls; private View mMainControls; private TextView mPlaybackPos; private TextView mPlaybackDur; private TextView mPlaylistPos; private SeekBar mPlaybackInfo; private ImageButton mControlPrev; private ImageButton mControlPause; private ImageButton mControlNext; private int mBufferPercent; private GestureDetector mGestureDetector; private OnClickListener mControlClickListener; /** * How many milliseconds to display the progress controls? This timer begins * after a user-initiated gesture or after the playback buffer becomes full. */ private static final int PROGRESS_CONTROLS_TIMEOUT = 5500; /** * Minimum distance a fling must move along the X axis in order * to be considered a gesture to jump to a new playlist position. */ private static final int MINIMUM_GESTURE_DISTANCE = 50; public PlayerControls(Context context) { this(context, null); } public PlayerControls(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PlayerControls(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mDivider = context.getResources().getDrawable(R.drawable.divider_horizontal_dark); LayoutInflater.from(context).inflate(R.layout.player_controls, this); mArtwork = (ImageView)findViewById(R.id.album_artwork); mProgressControls = findViewById(R.id.progress_controls); mMainControls = findViewById(R.id.main_controls); mPlaybackPos = (TextView)findViewById(R.id.playback_position); mPlaybackDur = (TextView)findViewById(R.id.playback_duration); mPlaylistPos = (TextView)findViewById(R.id.playlist_position); mPlaybackInfo = (SeekBar)findViewById(R.id.playback_info); mPlaybackInfo.setKeyProgressIncrement(10); mControlPrev = (ImageButton)findViewById(R.id.control_prev); mControlPause = (ImageButton)findViewById(R.id.control_pause); mControlNext = (ImageButton)findViewById(R.id.control_next); mControlPrev.setOnClickListener(mControlClick); mControlPause.setOnClickListener(mControlClick); mControlNext.setOnClickListener(mControlClick); mGestureDetector = new GestureDetector(mGestureListener); setOnClickListener(this); setOnTouchListener(this); } public boolean progressControlsAreShown() { return mProgressControls.getVisibility() == View.VISIBLE; } public void hideProgressControls() { if (progressControlsAreShown()) { mProgressControls.setVisibility(View.GONE); invalidate(); } } public void showProgressControls() { if (mBufferPercent < 100) showProgressControls(0); else showProgressControls(PROGRESS_CONTROLS_TIMEOUT); } public void showProgressControls(int timeout) { if (!progressControlsAreShown()) { mProgressControls.setVisibility(View.VISIBLE); invalidate(); } removeCallbacks(mEventuallyHideProgress); if (timeout > 0) postDelayed(mEventuallyHideProgress, timeout); } private final Runnable mEventuallyHideProgress = new Runnable() { public void run() { hideProgressControls(); } }; public void onClick(View v) { if (!progressControlsAreShown()) showProgressControls(); else hideProgressControls(); } public boolean onTouch(View v, MotionEvent event) { return mGestureDetector.onTouchEvent(event); } public void setBufferPercent(int percent) { mBufferPercent = percent; } public void setAlbumCover(Uri uri) { mArtwork.setImageURI(uri); } public void setAlbumCover(int resId) { mArtwork.setImageResource(resId); } public void setOnControlClickListener(OnClickListener l) { mControlClickListener = l; } public SeekBar getSeekBar() { return mPlaybackInfo; } public ImageButton getPauseButton() { return mControlPause; } private static String formatTime(int sec) { int minutes = sec / 60; int seconds = sec % 60; StringBuilder b = new StringBuilder(); b.append(minutes).append(':'); if (seconds < 10) b.append('0'); b.append(seconds); return b.toString(); } public void setNotPlaying() { setAlbumCover(R.drawable.not_playing_cover); mPlaylistPos.setText(""); setTrackPosition(0, -1); } public void setPlaylistPosition(int playlistPos, int playlistSize) { mPlaylistPos.setText(playlistPos + " of " + playlistSize); } public void setTrackPosition(int playbackPos, int totalDuration) { mPlaybackPos.setText(formatTime(playbackPos)); if (totalDuration >= 0) mPlaybackDur.setText('-' + formatTime(totalDuration)); else mPlaybackDur.setText("-:--"); if (playbackPos == 0) { mPlaybackInfo.setEnabled(true); mPlaybackInfo.setProgress(0); mPlaybackInfo.setSecondaryProgress(0); } } private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { int dx = (int)(e2.getX() - e1.getX()); if (Math.abs(dx) > MINIMUM_GESTURE_DISTANCE && Math.abs(velocityX) > Math.abs(velocityY)) { /* Some may consider this logic backward, but I hope to * introduce an animation soon which makes it seem * clearer that you're "dragging" in the next song, not * flicking a directional gesture. That is, flinging * to the left actually moves to the next song and * vice versa. */ if (velocityX > 0) mControlClickListener.onClick(mControlPrev); else mControlClickListener.onClick(mControlNext); return true; } return false; } }; private final OnClickListener mControlClick = new OnClickListener() { public void onClick(View v) { mControlClickListener.onClick(v); } }; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { /* * First measure the main controls. The album artwork fills the rest of * the available height. */ measureChildWithMargins(mMainControls, widthMeasureSpec, 0, heightMeasureSpec, 0); int width = resolveSize(mMainControls.getMeasuredWidth(), widthMeasureSpec); int height = resolveSize(mMainControls.getMeasuredHeight(), heightMeasureSpec); /* Fill the remaining space. */ mArtwork.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY, height - mMainControls.getMeasuredHeight())); /* * The progress controls "float", and so measurement doesn't depend on * any other factor than its own visibility. */ if (mProgressControls.getVisibility() != View.GONE) measureChildWithMargins(mProgressControls, widthMeasureSpec, 0, heightMeasureSpec, 0); setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int parentTop = 0; int parentBottom = bottom - top; int parentLeft = 0; int parentRight = right - left; mArtwork.layout(parentLeft, parentTop, parentLeft + mArtwork.getMeasuredWidth(), parentTop + mArtwork.getMeasuredHeight()); mMainControls.layout(parentLeft, parentBottom - mMainControls.getMeasuredHeight(), parentLeft + mMainControls.getMeasuredWidth(), parentBottom); View topView; if (mProgressControls.getVisibility() == View.GONE) topView = mMainControls; else { topView = mProgressControls; mProgressControls.layout(parentLeft, mMainControls.getTop() - mProgressControls.getMeasuredHeight(), parentLeft + mMainControls.getMeasuredWidth(), mMainControls.getTop()); } mDivider.setBounds(parentLeft, topView.getTop(), parentRight, topView.getTop() + mDivider.getIntrinsicHeight()); } @Override public void dispatchDraw(Canvas canvas) { final long drawingTime = getDrawingTime(); /* * Can't use super.dispatchDraw(canvas) because we need to control the * order that the reflection is drawn. Maybe at some point we can extend * our own ImageView to do this, but for now it's just easier to fix-up * the drawing order manually. */ drawChild(canvas, mArtwork, drawingTime); /* Flip the album artwork. */ canvas.save(); canvas.scale(1, -1); canvas.translate(0, -(mArtwork.getHeight() * 2)); mArtwork.draw(canvas); canvas.restore(); if (mProgressControls.getVisibility() == View.VISIBLE) drawChild(canvas, mProgressControls, drawingTime); drawChild(canvas, mMainControls, drawingTime); mDivider.draw(canvas); } }