/** * Wire * Copyright (C) 2016 Wire Swiss GmbH * * 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 3 of the License, 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.waz.zclient.views; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import android.view.animation.LinearInterpolator; import com.waz.zclient.R; import com.waz.zclient.ui.utils.ColorUtils; import com.waz.zclient.ui.utils.MathUtils; import com.waz.zclient.utils.ViewUtils; public class SendingAnimationView extends View implements ValueAnimator.AnimatorUpdateListener { private static final int DEFAULT_RADIUS = -1; private static final int DEFAULT_STROKE_WIDTH_DP = 1; private static final int DEFAULT_ANIMATION_DURATION = 1500; private static final float CIRCLE_FILL_ANIMATION_PART = 0.8f; private static final int DEFAULT_PENDING_ANGLE_STOP = 270; private Paint circlePaint; private Paint fullCirclePaint; private Paint backgroundCirclePaint; private int strokeWidth; private RectF rectF; private float angle; private int animationDuration; private int radius; private int inputRadius; private ValueAnimator circleStartAnimator; private ValueAnimator circleEndAnimator; private ValueAnimator scalingAnimator; private ValueAnimator nextAnimation; private int backgroundCircleRadius = DEFAULT_RADIUS; private int backgroundCirclePadding = 0; private float scale; private int pendingAngleStop; public SendingAnimationView(Context context) { super(context); init(null); } public SendingAnimationView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public SendingAnimationView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } private void init(AttributeSet attrs) { resetValues(); radius = DEFAULT_RADIUS; animationDuration = DEFAULT_ANIMATION_DURATION; pendingAngleStop = DEFAULT_PENDING_ANGLE_STOP; int backgroundColor = ColorUtils.injectAlpha(204, Color.WHITE); int backgroundCircleColor = Color.TRANSPARENT; int circleColor = Color.WHITE; if (attrs != null) { TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.SendingAnimationView); if (ta != null) { circleColor = ta.getColor(R.styleable.SendingAnimationView_progressColor, circleColor); backgroundColor = ta.getColor(R.styleable.SendingAnimationView_fullCircleColor, backgroundColor); backgroundCircleRadius = ta.getDimensionPixelSize(R.styleable.SendingAnimationView_backgroundCircleRadius, DEFAULT_ANIMATION_DURATION); backgroundCircleColor = ta.getColor(R.styleable.SendingAnimationView_backgroundColor, backgroundCircleColor); backgroundCirclePadding = ta.getDimensionPixelSize(R.styleable.SendingAnimationView_circlePadding, 0); inputRadius = ta.getDimensionPixelSize(R.styleable.SendingAnimationView_circleRadius, radius); pendingAngleStop = ta.getInt(R.styleable.SendingAnimationView_pendingStop, pendingAngleStop); animationDuration = ta.getInt(R.styleable.SendingAnimationView_pendingDuration, animationDuration); ta.recycle(); } } circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint.setStyle(Paint.Style.STROKE); circlePaint.setColor(circleColor); circlePaint.setStrokeWidth(strokeWidth); fullCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); fullCirclePaint.setStyle(Paint.Style.STROKE); fullCirclePaint.setColor(backgroundColor); fullCirclePaint.setStrokeWidth(strokeWidth); backgroundCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); backgroundCirclePaint.setStyle(Paint.Style.FILL); backgroundCirclePaint.setColor(backgroundCircleColor); recalculateRect(); } private void resetValues() { nextAnimation = null; angle = 0f; scale = 1f; strokeWidth = ViewUtils.toPx(getContext(), DEFAULT_STROKE_WIDTH_DP); } private void initAnimations() { circleStartAnimator = ObjectAnimator.ofFloat(0, pendingAngleStop); circleStartAnimator.addUpdateListener(this); circleStartAnimator.setDuration((long) (animationDuration * (CIRCLE_FILL_ANIMATION_PART / 4f * 3))); circleStartAnimator.setInterpolator(new LinearInterpolator()); circleStartAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { SendingAnimationView.super.setVisibility(VISIBLE); } @Override public void onAnimationEnd(Animator animation) { if (nextAnimation != null) { nextAnimation.start(); } } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); circleEndAnimator = ObjectAnimator.ofFloat(DEFAULT_PENDING_ANGLE_STOP, 360); circleEndAnimator.addUpdateListener(this); circleEndAnimator.setDuration((long) (animationDuration * (CIRCLE_FILL_ANIMATION_PART / 4f))); circleEndAnimator.setInterpolator(new LinearInterpolator()); circleEndAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { SendingAnimationView.super.setVisibility(VISIBLE); } @Override public void onAnimationEnd(Animator animation) { scalingAnimator.start(); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); scalingAnimator = ObjectAnimator.ofFloat(1, 0); scalingAnimator.addUpdateListener(this); scalingAnimator.setDuration((long) (animationDuration * (1f - CIRCLE_FILL_ANIMATION_PART))); scalingAnimator.setInterpolator(new LinearInterpolator()); scalingAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { resetValues(); SendingAnimationView.super.setVisibility(GONE); } @Override public void onAnimationCancel(Animator animation) { resetValues(); SendingAnimationView.super.setVisibility(GONE); } @Override public void onAnimationRepeat(Animator animation) {} }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); recalculateRect(); } private void recalculateRect() { if (rectF == null) { rectF = new RectF(); } rectF.set(strokeWidth + getPaddingLeft() + backgroundCirclePadding, strokeWidth + getPaddingTop() + backgroundCirclePadding, getWidth() - getPaddingLeft() - getPaddingRight() - strokeWidth - backgroundCirclePadding, getHeight() - getPaddingBottom() - getPaddingTop() - strokeWidth - backgroundCirclePadding); recalculateRadius(); } private void recalculateRadius() { if (inputRadius != DEFAULT_RADIUS) { radius = inputRadius - strokeWidth; float centerX = rectF.centerX(); float centerY = rectF.centerY(); rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius); return; } float width = (rectF.right - rectF.left); float height = (rectF.bottom - rectF.top); radius = (int) (Math.min(width, height) / 2f); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (MathUtils.floatEqual(rectF.centerX(), 0f) || MathUtils.floatEqual(rectF.centerY(), 0f)) { recalculateRect(); } float centerX = rectF.centerX(); float centerY = rectF.centerY(); canvas.drawCircle(centerX, centerY, backgroundCircleRadius == DEFAULT_RADIUS ? radius + strokeWidth + backgroundCirclePadding : backgroundCircleRadius, backgroundCirclePaint); canvas.drawCircle(centerX, centerY, scale * radius, fullCirclePaint); if (circlePaint.getStyle() == Paint.Style.STROKE) { canvas.drawArc(rectF, 0, angle, false, circlePaint); } else { canvas.drawCircle(centerX, centerY, scale * radius, circlePaint); } } @Override public void setVisibility(int visibility) { setAnimate(visibility == VISIBLE); } @SuppressLint("NewApi") private void setAnimate(boolean animate) { if (animate) { if (circleStartAnimator == null) { initAnimations(); } if (!circleStartAnimator.isRunning() && MathUtils.floatEqual(angle, 0f)) { circleStartAnimator.start(); } } else { if (circleStartAnimator == null) { super.setVisibility(GONE); } else if (MathUtils.floatEqual(angle, DEFAULT_PENDING_ANGLE_STOP)) { circleEndAnimator.start(); } else if (circleStartAnimator.isRunning()) { nextAnimation = circleEndAnimator; } } } @Override public void onAnimationUpdate(ValueAnimator animation) { if (animation.equals(circleStartAnimator) || animation.equals(circleEndAnimator)) { angle = (float) animation.getAnimatedValue(); } else { scale = (float) animation.getAnimatedValue(); if (circlePaint.getStyle() == Paint.Style.STROKE) { circlePaint.setStyle(Paint.Style.FILL); } } invalidate(); } }