package com.android.view; /* 实现Music 自定义布局 */ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import com.android.play.R; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; /** * Created by mertsimsek on 17/07/15. */ public class MusicPlayerView extends View { /** * Rect for get time height and width */ private Rect mRectText; /** * Paint for drawing left and passed time. */ private Paint mPaintTime; /** * RectF for draw circle progress. */ private RectF rectF; /** * Paint for circle progress left */ private Paint mPaintProgressEmpty; /** * Paint for circle progress loaded */ private Paint mPaintProgressLoaded; /** * Modified OnClickListener. We do not want all view click. * notify onClick() only button area touched. */ private OnClickListener onClickListener; /** * Button paint for play/pause control button */ private Paint mPaintButton; /** * Play/Pause button region for handle onTouch */ private Region mButtonRegion; /** * Play icon will be converted to Bitmap */ private Bitmap mBitmapPlay; /** * Pause icon will be converted to Bitmap */ private Bitmap mBitmapPause; /** * Paint for drawing play/pause icons to canvas. */ private Paint mPaintPlayPause; /** * Paint to draw cover photo to canvas */ private Paint mPaintCover; /** * Bitmap for shader. */ private Bitmap mBitmapCover; /** * Shader for make drawable circle */ private BitmapShader mShader; /** * Scale image to view width/height */ private float mCoverScale; /** * Image Height and Width values. */ private int mHeight; private int mWidth; /** * Center values for cover image. */ private float mCenterX; private float mCenterY; /** * Cover image is rotating. That is why we hold that value. */ private int mRotateDegrees; /** * Handler for posting runnable object */ private Handler mHandlerRotate; /** * Runnable for turning image (default velocity is 10) */ private Runnable mRunnableRotate; /** * Handler for posting runnable object */ private Handler mHandlerProgress; /** * Runnable for turning image (default velocity is 10) */ private Runnable mRunnableProgress; /** * isRotating */ private boolean isRotating; /** * Handler will post runnable object every @ROTATE_DELAY seconds. */ private static int ROTATE_DELAY = 10; /** * 1 sn = 1000 ms */ private static int PROGRESS_SECOND_MS = 1000; /** * mRotateDegrees count increase 1 by 1 default. * I used that parameter as velocity. */ private static int VELOCITY = 1; /** * Default color code for cover */ private int mCoverColor = Color.GRAY; /** * Play/Pause button radius.(default = 120) */ private float mButtonRadius = 120f; /** * Play/Pause button color(Default = dark gray) */ private int mButtonColor = Color.DKGRAY; /** * Color code for progress left. */ private int mProgressEmptyColor = 0x20FFFFFF; /** * Color code for progress loaded. */ private int mProgressLoadedColor = 0xFF00815E; /** * Time text size */ private int mTextSize = 40; /** * Default text color */ private int mTextColor = 0xFFFFFFFF; /** * Current progress value */ private int currentProgress = 0; /** * Max progress value */ private int maxProgress = 100; /** * Auto progress value start progressing when * cover image start rotating. */ private boolean isAutoProgress = true; /** * Progressview and time will be visible/invisible depends on this */ private boolean mProgressVisibility = true; /** * Constructor * * @param context */ public MusicPlayerView(Context context) { super(context); init(context, null); } /** * Constructor * * @param context * @param attrs */ public MusicPlayerView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } /** * Constructor * * @param context * @param attrs * @param defStyleAttr */ public MusicPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } /** * Constructor * * @param context * @param attrs * @param defStyleAttr * @param defStyleRes */ public MusicPlayerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } /** * Initializes resource values, create objects which we need them later. * Object creation must not called onDraw() method, otherwise it won't be * smooth. * * @param context * @param attrs */ private void init(Context context, AttributeSet attrs) { //Get Image resource from xml TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.playerview); Drawable mDrawableCover = a.getDrawable(R.styleable.playerview_cover); if (mDrawableCover != null) mBitmapCover = drawableToBitmap(mDrawableCover); mButtonColor = a.getColor(R.styleable.playerview_buttonColor, mButtonColor); mProgressEmptyColor = a.getColor(R.styleable.playerview_progressEmptyColor, mProgressEmptyColor); mProgressLoadedColor = a.getColor(R.styleable.playerview_progressLoadedColor, mProgressLoadedColor); mTextColor = a.getColor(R.styleable.playerview_textColor, mTextColor); mTextSize = a.getDimensionPixelSize(R.styleable.playerview_textSize, mTextSize); a.recycle(); mRotateDegrees = 0; //Handler and Runnable object for turn cover image by updating rotation degrees mHandlerRotate = new Handler(); mRunnableRotate = new Runnable() { @Override public void run() { Log.i("fenghaitao","====mRunnableRotate======="+isRotating); if (isRotating) { Log.i("fenghaitao", "====mRunnableRotate===currentProgress="+currentProgress+" ,====maxProgress=="+maxProgress); if(currentProgress > maxProgress){ currentProgress = 0; setProgress(currentProgress); stop(); } updateCoverRotate(); mHandlerRotate.postDelayed(mRunnableRotate, ROTATE_DELAY); } } }; //Handler and Runnable object for progressing. mHandlerProgress = new Handler(); mRunnableProgress = new Runnable() { @Override public void run() { Log.i("fenghaitao", "=========mRunnableProgress========="+isRotating); if(isRotating){ currentProgress += 1; mHandlerProgress.postDelayed(mRunnableProgress, PROGRESS_SECOND_MS); } } }; //Play/Pause button circle paint mPaintButton = new Paint(); mPaintButton.setAntiAlias(true); mPaintButton.setStyle(Paint.Style.FILL); mPaintButton.setColor(mButtonColor); //Play/Pause button icons paint and bitmaps mPaintPlayPause = new Paint(); mPaintPlayPause.setAntiAlias(true); mBitmapPlay = BitmapFactory.decodeResource(getResources(), R.drawable.icon_play); mBitmapPause = BitmapFactory.decodeResource(getResources(), R.drawable.icon_pause); //Progress paint object creation mPaintProgressEmpty = new Paint(); mPaintProgressEmpty.setAntiAlias(true); mPaintProgressEmpty.setColor(mProgressEmptyColor); mPaintProgressEmpty.setStyle(Paint.Style.STROKE); mPaintProgressEmpty.setStrokeWidth(12.0f); mPaintProgressLoaded = new Paint(); mPaintProgressEmpty.setAntiAlias(true); mPaintProgressLoaded.setColor(mProgressLoadedColor); mPaintProgressLoaded.setStyle(Paint.Style.STROKE); mPaintProgressLoaded.setStrokeWidth(12.0f); mPaintTime = new Paint(); mPaintTime.setColor(mTextColor); mPaintTime.setAntiAlias(true); mPaintTime.setTextSize(mTextSize); //rectF and rect initializes rectF = new RectF(); mRectText = new Rect(); } /** * Calculate mWidth, mHeight, mCenterX, mCenterY values and * scale resource bitmap. Create shader. This is not called multiple times. * * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mWidth = MeasureSpec.getSize(widthMeasureSpec); mHeight = MeasureSpec.getSize(heightMeasureSpec); int minSide = Math.min(mWidth,mHeight); mWidth = minSide; mHeight = minSide; this.setMeasuredDimension(mWidth, mHeight); mCenterX = mWidth / 2f; mCenterY = mHeight / 2f; //set RectF left, top, right, bottom coordiantes rectF.set(20.0f, 20.0f, mWidth - 20.0f, mHeight - 20.0f); //button size is about to 1/4 of image size then we divide it to 8. mButtonRadius = mWidth / 8.0f; //We resize icons with button radius. icons need to be inside circle. mBitmapPlay = getResizedBitmap(mBitmapPlay, mButtonRadius - 20.0f, mButtonRadius - 20.0f); mBitmapPause = getResizedBitmap(mBitmapPause, mButtonRadius - 20.0f, mButtonRadius - 20.0f); mButtonRegion = new Region((int) (mCenterX - mButtonRadius), (int) (mCenterY - mButtonRadius), (int) (mCenterX + mButtonRadius), (int) (mCenterY + mButtonRadius)); createShader(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** * This is where magic happens as you know. * * @param canvas */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Log.i("fenghaitao","============onDraw=="); if (mShader == null) return; //Draw cover image float radius = mCenterX <= mCenterY ? mCenterX - 75.0f: mCenterY - 75.0f; canvas.rotate(mRotateDegrees, mCenterX, mCenterY); canvas.drawCircle(mCenterX, mCenterY, radius, mPaintCover); //Rotate back to make play/pause button stable(No turn) canvas.rotate(-mRotateDegrees, mCenterX, mCenterY); //Draw Play/Pause button canvas.drawCircle(mCenterX, mCenterY, mButtonRadius, mPaintButton); canvas.drawBitmap(isRotating() ? mBitmapPause : mBitmapPlay, mCenterX - mBitmapPause.getWidth() / 2f, mCenterY - mBitmapPause.getHeight() / 2f, mPaintPlayPause); if(mProgressVisibility){ //Draw empty progress canvas.drawArc(rectF, 145, 250, false, mPaintProgressEmpty); Log.i("fenghaitao", "=====calculatePastProgressDegree="+calculatePastProgressDegree()); //Draw loaded progress canvas.drawArc(rectF, 145, calculatePastProgressDegree(), false, mPaintProgressLoaded); //Draw left time text String leftTime = secondsToTime(calculateLeftSeconds()); Log.i("fenghaitao","========lefttime=="+leftTime); mPaintTime.getTextBounds(leftTime, 0, leftTime.length(), mRectText); canvas.drawText(leftTime, (float) (mCenterX * Math.cos(Math.toRadians(35.0))) + mWidth / 2.0f - mRectText.width() / 1.5f, (float) (mCenterX * Math.sin(Math.toRadians(35.0))) + mHeight / 2.0f + mRectText.height() + 15.0f, mPaintTime); //Draw passed time text String passedTime = secondsToTime(calculatePassedSeconds()); Log.i("fenghaitao", "=======passedTime=="+passedTime); mPaintTime.getTextBounds(passedTime, 0, passedTime.length(), mRectText); canvas.drawText(passedTime, (float) (mCenterX * -Math.cos(Math.toRadians(35.0))) + mWidth / 2.0f - mRectText.width() / 3.0f, (float) (mCenterX * Math.sin(Math.toRadians(35.0))) + mHeight / 2.0f + mRectText.height() + 15.0f, mPaintTime); } } /** * We need to convert drawable (which we get from attributes) to bitmap * to prepare if for BitmapShader * * @param drawable * @return */ private Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } int width = drawable.getIntrinsicWidth(); width = width > 0 ? width : 1; int height = drawable.getIntrinsicHeight(); height = height > 0 ? height : 1; Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } /** * Create shader and set shader to mPaintCover */ private void createShader() { if (mWidth == 0) return; //if mBitmapCover is null then create default colored cover if (mBitmapCover == null) { mBitmapCover = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); mBitmapCover.eraseColor(mCoverColor); } mCoverScale = ((float) mWidth) / (float) mBitmapCover.getWidth(); mBitmapCover = Bitmap.createScaledBitmap(mBitmapCover, (int) (mBitmapCover.getWidth() * mCoverScale), (int) (mBitmapCover.getHeight() * mCoverScale), true); mShader = new BitmapShader(mBitmapCover, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mPaintCover = new Paint(); mPaintCover.setAntiAlias(true); mPaintCover.setShader(mShader); } /** * Update rotate degree of cover and invalide onDraw(); */ public void updateCoverRotate() { mRotateDegrees += VELOCITY; mRotateDegrees = mRotateDegrees % 360; postInvalidate(); } /** * Checks is rotating * * @return */ public boolean isRotating() { return isRotating; } /** * Start turning image */ public void start() { isRotating = true; mHandlerRotate.removeCallbacksAndMessages(null); mHandlerRotate.postDelayed(mRunnableRotate, ROTATE_DELAY); if(isAutoProgress){ mHandlerProgress.removeCallbacksAndMessages(null); mHandlerProgress.postDelayed(mRunnableProgress, PROGRESS_SECOND_MS); } } /** * Stop turning image */ public void stop() { isRotating = false; } /** * Set velocity.When updateCoverRotate() method called, * increase degree by velocity value. * * @param velocity */ public void setVelocity(int velocity) { if (velocity > 0) VELOCITY = velocity; } /** * set cover image resource * * @param coverDrawable */ public void setCoverDrawable(int coverDrawable) { Drawable drawable = getContext().getDrawable(coverDrawable); mBitmapCover = drawableToBitmap(drawable); createShader(); postInvalidate(); } /** * gets image URL and load it to cover image.It uses Picasso Library. * * @param imageUrl */ public void setCoverURL(String imageUrl) { Picasso.with(getContext()).load(imageUrl).into(target); } /** * When picasso load into target. Overrider methods are called. * Invalidate view onBitmapLoaded. */ private Target target = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { mBitmapCover = bitmap; createShader(); postInvalidate(); } @Override public void onBitmapFailed(Drawable errorDrawable) { } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { } }; /** * This is detect when mButtonRegion is clicked. Which means * play/pause action happened. * * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { return true; } case MotionEvent.ACTION_UP: { if (mButtonRegion.contains((int) x, (int) y)) { if (onClickListener != null) onClickListener.onClick(this); } } break; } return super.onTouchEvent(event); } /** * onClickListener.onClick will be called when button clicked. * We dont want all view click. We only want button area click. * That is why we override it. * * @param l */ @Override public void setOnClickListener(OnClickListener l) { onClickListener = l; } /** * Resize bitmap with @newHeight and @newWidth parameters * * @param bm * @param newHeight * @param newWidth * @return */ private Bitmap getResizedBitmap(Bitmap bm, float newHeight, float newWidth) { int width = bm.getWidth(); int height = bm.getHeight(); float scaleWidth = ((float) newWidth) / width; float scaleHeight = ((float) newHeight) / height; Matrix matrix = new Matrix(); matrix.postScale(scaleWidth, scaleHeight); Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false); return resizedBitmap; } /** * Sets button color * * @param color */ public void setButtonColor(int color) { mButtonColor = color; mPaintButton.setColor(mButtonColor); postInvalidate(); } /** * sets progress empty color * @param color */ public void setProgressEmptyColor(int color){ mProgressEmptyColor = color; mPaintProgressEmpty.setColor(mProgressEmptyColor); postInvalidate(); } /** * sets progress loaded color * @param color */ public void setProgressLoadedColor(int color){ mProgressLoadedColor = color; mPaintProgressLoaded.setColor(mProgressLoadedColor); postInvalidate(); } /** * Sets total seconds of music * @param maxProgress */ public void setMax(int maxProgress){ this.maxProgress = maxProgress; postInvalidate(); } /** * Sets current seconds of music * @param currentProgress */ public void setProgress(int currentProgress){ if(0 <= currentProgress && currentProgress<= maxProgress){ this.currentProgress = currentProgress; postInvalidate(); } } /** * Get current progress seconds * @return */ public int getProgress(){ return currentProgress; } /** * Calculate left seconds * @return */ private int calculateLeftSeconds(){ Log.i("fenghaitao","======maxProgress="+maxProgress+" , =====currentPro="+currentProgress); return maxProgress - currentProgress; } /** * Return passed seconds * @return */ private int calculatePassedSeconds(){ return currentProgress; } /** * Convert seconds to time * @param seconds * @return */ private String secondsToTime(int seconds){ String time = ""; String minutesText = String.valueOf(seconds / 60); if(minutesText.length() == 1) minutesText = "0" + minutesText; String secondsText = String.valueOf(seconds % 60); if(secondsText.length() == 1) secondsText = "0" + secondsText; time = minutesText + ":" + secondsText; return time; } /** * Calculate passed progress degree * @return */ private int calculatePastProgressDegree(){ return (250 * currentProgress) / maxProgress; } /** * If you do not want to automatic progress, you can disable it * and implement your own handler by using setProgress method repeatedly. * @param isAutoProgress */ public void setAutoProgress(boolean isAutoProgress){ this.isAutoProgress = isAutoProgress; } /** * Sets time text color * @param color */ public void setTimeColor(int color){ mTextColor = color; mPaintTime.setColor(mTextColor); postInvalidate(); } public void setProgressVisibility(boolean mProgressVisibility){ this.mProgressVisibility = mProgressVisibility; postInvalidate(); } }