Java tutorial
/* * Copyright (C) 2016 JokAr * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.jokar.rxupload.widget; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; import com.github.jokar.rxupload.R; /** * Created by thibaultguegan on 15/02/15. */ public class ProgressDownloadView extends View { private static final String LOG_TAG = ProgressDownloadView.class.getSimpleName(); public static final long ANIMATION_DURATION_BASE = 1250; private int mWidth, mHeight, bubbleAnchorX, bubbleAnchorY, mBubbleWidth, mBubbleHeight, mPadding; private Path mPathBlack, mPathWhite, mPathBubble; private Paint mPaintBlack, mPaintWhite, mPaintBubble, mPaintText; private float mDensity = getResources().getDisplayMetrics().density; private float mProgress = 0, mTarget = 0, mSpeedAngle = 0, mBubbleAngle = 0, mFailAngle = 0, mFlipFactor; private State mState = State.STATE_WORKING; private enum State { STATE_WORKING, STATE_FAILED, STATE_SUCCESS } /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. */ public ProgressDownloadView(Context context) { this(context, null); } /** * MARK: Constructor */ public ProgressDownloadView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ProgressDownloadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mPadding = (int) (30 * mDensity); mBubbleWidth = (int) (45 * mDensity); mBubbleHeight = (int) (35 * mDensity); setPadding(mPadding, 0, mPadding, 0); mPaintBlack = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBlack.setStyle(Paint.Style.STROKE); mPaintBlack.setStrokeWidth(5 * mDensity); mPaintBlack.setColor(ContextCompat.getColor(getContext(), R.color.red_wood)); mPaintBlack.setStrokeCap(Paint.Cap.ROUND); //mPaintBlack.setPathEffect(new CornerPathEffect(5*mDensity)); mPaintWhite = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintWhite.setStyle(Paint.Style.STROKE); mPaintWhite.setStrokeWidth(5 * mDensity); mPaintWhite.setColor(Color.RED); mPaintWhite.setStrokeCap(Paint.Cap.ROUND); //mPaintWhite.setPathEffect(new CornerPathEffect(5*mDensity)); mPaintBubble = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintBubble.setColor(Color.RED); mPaintBubble.setStyle(Paint.Style.FILL); mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG); mPaintText.setColor(Color.WHITE); mPaintText.setStyle(Paint.Style.FILL); mPaintText.setTextSize(12 * mDensity); } /** * MARK: Overrides */ @Override protected void onDraw(Canvas canvas) { if (mPathWhite != null && mPathBlack != null) { float textX = Math.max(getPaddingLeft() - (int) (mBubbleWidth / 4.0f), mProgress * mWidth / 100 - (int) (mBubbleWidth / 4.0f)); float textY = mHeight / 2 - mBubbleHeight / 2 + calculateDeltaY(); switch (mState) { case STATE_WORKING: // Save and restore prevent the rest of the canvas to not be rotated canvas.save(); float speed = (getProgress() - mTarget) / 20; mBubbleAngle += speed * 10; if (mBubbleAngle > 20) { mBubbleAngle = 20; } if (mBubbleAngle < -20) { mBubbleAngle = -20; } if (Math.abs(speed) < 1) { mSpeedAngle -= mBubbleAngle / 20; mSpeedAngle *= .9f; } mBubbleAngle += mSpeedAngle; canvas.rotate(mBubbleAngle, bubbleAnchorX, bubbleAnchorY); canvas.drawPath(mPathBubble, mPaintBubble); canvas.drawText(String.valueOf((int) mProgress) + " %", textX, textY, mPaintText); canvas.restore(); break; case STATE_FAILED: canvas.save(); canvas.rotate(mFailAngle, bubbleAnchorX, bubbleAnchorY); canvas.drawPath(mPathBubble, mPaintBubble); canvas.rotate(mFailAngle, bubbleAnchorX, textY - mBubbleHeight / 7); // mPaintText.setColor(getResources().getColor(R.color.red_wine)); textX = Math.max(getPaddingLeft() - (int) (mBubbleWidth / 3.2f), mProgress * mWidth / 100 - (int) (mBubbleWidth / 3.2f)); canvas.drawText(getResources().getString(R.string.failed), textX, textY, mPaintText); canvas.restore(); break; case STATE_SUCCESS: canvas.save(); // mPaintText.setColor(getResources().getColor(R.color.green_grass)); textX = Math.max(getPaddingLeft() - (int) (mBubbleWidth / 3.2f), mProgress * mWidth / 100 - (int) (mBubbleWidth / 3.2f)); Matrix flipMatrix = new Matrix(); flipMatrix.setScale(mFlipFactor, 1, bubbleAnchorX, bubbleAnchorY); canvas.concat(flipMatrix); canvas.drawPath(mPathBubble, mPaintBubble); canvas.concat(flipMatrix); canvas.drawText(getResources().getString(R.string.done), textX, textY, mPaintText); canvas.restore(); break; } canvas.drawPath(mPathBlack, mPaintBlack); canvas.drawPath(mPathWhite, mPaintWhite); } } @Override protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) { super.onSizeChanged(xNew, yNew, xOld, yOld); mWidth = xNew - getPaddingRight(); mHeight = yNew; Log.d(LOG_TAG, String.format("width and height measured are %d and %d", mWidth, mHeight)); // Call this if the enter animation is not used //setPercentage(mProgress); } /** * MARK: Update drawings */ private void makePathBlack() { if (mPathBlack == null) { mPathBlack = new Path(); } Path p = new Path(); p.moveTo(Math.max(getPaddingLeft(), mProgress * mWidth / 100), mHeight / 2 + calculateDeltaY()); p.lineTo(mWidth, mHeight / 2); mPathBlack.set(p); } private void makePathWhite() { if (mPathWhite == null) { mPathWhite = new Path(); } Path p = new Path(); p.moveTo(getPaddingLeft(), mHeight / 2); p.lineTo(Math.max(getPaddingLeft(), mProgress * mWidth / 100), mHeight / 2 + calculateDeltaY()); mPathWhite.set(p); } private void makePathBubble() { if (mPathBubble == null) { mPathBubble = new Path(); } int width = mBubbleWidth; int height = mBubbleHeight; int arrowWidth = width / 3; //Rect r = new Rect(Math.max(getPaddingLeft()-width/2-arrowWidth/4, mProgress*mWidth/100-width/2-arrowWidth/4), mHeight/2-height + calculatedeltaY(), Math.max(getPaddingLeft()+width/2-arrowWidth/4, mProgress*mWidth/100+width/2-arrowWidth/4), mHeight/2+height-height + calculatedeltaY()); Rect r = new Rect((int) (Math.max(getPaddingLeft() - width / 2, mProgress * mWidth / 100 - width / 2)), (int) (mHeight / 2 - height + calculateDeltaY()), (int) (Math.max(getPaddingLeft() + width / 2, mProgress * mWidth / 100 + width / 2)), (int) (mHeight / 2 + height - height + calculateDeltaY())); int arrowHeight = (int) (arrowWidth / 1.5f); int radius = 8; Path path = new Path(); // Down arrow path.moveTo(r.left + r.width() / 2 - arrowWidth / 2, r.top + r.height() - arrowHeight); bubbleAnchorX = r.left + r.width() / 2; bubbleAnchorY = r.top + r.height(); path.lineTo(bubbleAnchorX, bubbleAnchorY); path.lineTo(r.left + r.width() / 2 + arrowWidth / 2, r.top + r.height() - arrowHeight); // Go to bottom-right path.lineTo(r.left + r.width() - radius, r.top + r.height() - arrowHeight); // Bottom-right arc path.arcTo(new RectF(r.left + r.width() - 2 * radius, r.top + r.height() - arrowHeight - 2 * radius, r.left + r.width(), r.top + r.height() - arrowHeight), 90, -90); // Go to upper-right path.lineTo(r.left + r.width(), r.top + arrowHeight); // Upper-right arc path.arcTo(new RectF(r.left + r.width() - 2 * radius, r.top, r.right, r.top + 2 * radius), 0, -90); // Go to upper-left path.lineTo(r.left + radius, r.top); // Upper-left arc path.arcTo(new RectF(r.left, r.top, r.left + 2 * radius, r.top + 2 * radius), 270, -90); // Go to bottom-left path.lineTo(r.left, r.top + r.height() - arrowHeight - radius); // Bottom-left arc path.arcTo(new RectF(r.left, r.top + r.height() - arrowHeight - 2 * radius, r.left + 2 * radius, r.top + r.height() - arrowHeight), 180, -90); path.close(); mPathBubble.set(path); } /** * MARK: Animation functions */ private float calculateDeltaY() { int wireTension = 15; if (mProgress <= 50) { return (mProgress * mWidth / wireTension) / 50 + Math.abs((mTarget - getProgress()) / wireTension) + Math.abs(mBubbleAngle); } else { return ((100 - mProgress) * mWidth / wireTension) / 50 + Math.abs((mTarget - getProgress()) / wireTension) + Math.abs(mBubbleAngle); } } public void setPercentage(float newProgress) { if (newProgress < 0 || newProgress > 100) { throw new IllegalArgumentException("setPercentage not between 0 and 100"); } mState = State.STATE_WORKING; mTarget = newProgress; ObjectAnimator anim = ObjectAnimator.ofFloat(this, "progress", getProgress(), mTarget); anim.setInterpolator(new DecelerateInterpolator()); anim.setDuration((long) (ANIMATION_DURATION_BASE + Math.abs(mTarget * 10 - getProgress() * 10) / 2)); anim.start(); } public void setProgress(float progress) { mProgress = progress; makePathBlack(); makePathWhite(); makePathBubble(); invalidate(); } public void setFailAngle(float failAngle) { mFailAngle = failAngle; makePathBlack(); makePathWhite(); makePathBubble(); invalidate(); } public void setFlip(float flipValue) { mFlipFactor = flipValue; makePathBlack(); makePathWhite(); makePathBubble(); invalidate(); } public float getProgress() { return mProgress; } public void drawFail() { mState = State.STATE_FAILED; ObjectAnimator failAnim = ObjectAnimator.ofFloat(this, "failAngle", 0, 180); failAnim.setInterpolator(new OvershootInterpolator()); //This one doesn't do much actually, we just use it to take advantage of associating two different interpolators ObjectAnimator anim = ObjectAnimator.ofFloat(this, "progress", getProgress(), mTarget); anim.setInterpolator(new AccelerateInterpolator()); AnimatorSet set = new AnimatorSet(); set.setDuration((long) (ANIMATION_DURATION_BASE / 1.7f)); set.playTogether(failAnim, anim); set.start(); } public void drawSuccess() { mTarget = 100; final ObjectAnimator successAnim = ObjectAnimator.ofFloat(this, "flip", 1, -1); successAnim.setInterpolator(new OvershootInterpolator()); successAnim.setDuration(ANIMATION_DURATION_BASE); ObjectAnimator anim = ObjectAnimator.ofFloat(this, "progress", getProgress(), mTarget); anim.setInterpolator(new DecelerateInterpolator()); anim.setDuration((long) (ANIMATION_DURATION_BASE + Math.abs(mTarget * 10 - getProgress() * 10) / 2)); anim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { mState = State.STATE_SUCCESS; successAnim.start(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); anim.start(); } }