/*
* Copyright (C) 2015 RECRUIT LIFESTYLE CO., LTD.
*
* 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.miris.ui.view;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.support.v4.view.ViewCompat;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.BounceInterpolator;
import com.miris.ui.comp.DropBounceInterpolator;
public class WaveView extends View implements ViewTreeObserver.OnPreDrawListener {
private static final long DROP_CIRCLE_ANIMATOR_DURATION = 500;
private static final long DROP_VERTEX_ANIMATION_DURATION = 500;
private static final long DROP_BOUNCE_ANIMATOR_DURATION = 500;
private static final int DROP_REMOVE_ANIMATOR_DURATION = 200;
private static final int WAVE_ANIMATOR_DURATION = 1000;
private static final float MAX_WAVE_HEIGHT = 0.2f;
private static final int SHADOW_COLOR = 0xFF000000;
private float mDropCircleRadius = 100;
private Paint mPaint;
private Path mWavePath;
private Path mDropTangentPath;
private Path mDropCirclePath;
private Paint mShadowPaint;
private Path mShadowPath;
private RectF mDropRect;
private int mWidth;
private float mCurrentCircleCenterY;
private int mMaxDropHeight;
private boolean mIsManualRefreshing = false;
private boolean mDropHeightUpdated = false;
private int mUpdateMaxDropHeight;
private ValueAnimator mDropVertexAnimator;
private ValueAnimator mDropBounceVerticalAnimator;
private ValueAnimator mDropBounceHorizontalAnimator;
private ValueAnimator mDropCircleAnimator;
private ValueAnimator mDisappearCircleAnimator;
private ValueAnimator mWaveReverseAnimator;
private static final float[][] BEGIN_PHASE_POINTS = {
{ 0.1655f, 0 },
{ 0.4188f, -0.0109f },
{ 0.4606f, -0.0049f },
{ 0.4893f, 0.f },
{ 0.4893f, 0.f },
{ 0.5f, 0.f }
};
private static final float[][] APPEAR_PHASE_POINTS = {
{ 0.1655f, 0.f },
{ 0.5237f, 0.0553f },
{ 0.4557f, 0.0936f },
{ 0.3908f, 0.1302f },
{ 0.4303f, 0.2173f },
{ 0.5f, 0.2173f }
};
private static final float[][] EXPAND_PHASE_POINTS = {
{ 0.1655f, 0.f },
{ 0.5909f, 0.0000f },
{ 0.4557f, 0.1642f },
{ 0.3941f, 0.2061f },
{ 0.4303f, 0.2889f },
{ 0.5f, 0.2889f }
};
private ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener =
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
postInvalidate();
}
};
public WaveView(Context context) {
super(context);
getViewTreeObserver().addOnPreDrawListener(this);
initView();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mDropCircleRadius = w / 14.4f;
updateMaxDropHeight((int) Math.min(Math.min(w, h), getHeight() - mDropCircleRadius));
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
if (mDropHeightUpdated) {
updateMaxDropHeight(mUpdateMaxDropHeight);
}
return false;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(mWavePath, mShadowPaint);
canvas.drawPath(mWavePath, mPaint);
mWavePath.rewind();
mDropTangentPath.rewind();
mDropCirclePath.rewind();
float circleCenterY = (Float) mDropCircleAnimator.getAnimatedValue();
float circleCenterX = mWidth / 2.f;
mDropRect.setEmpty();
float scale = (Float) mDisappearCircleAnimator.getAnimatedValue();
float vertical = (Float) mDropBounceVerticalAnimator.getAnimatedValue();
float horizontal = (Float) mDropBounceHorizontalAnimator.getAnimatedValue();
mDropRect.set(circleCenterX - mDropCircleRadius * (1 + vertical) * scale
+ mDropCircleRadius * horizontal / 2,
circleCenterY + mDropCircleRadius * (1 + horizontal) * scale
- mDropCircleRadius * vertical / 2,
circleCenterX + mDropCircleRadius * (1 + vertical) * scale
- mDropCircleRadius * horizontal / 2,
circleCenterY - mDropCircleRadius * (1 + horizontal) * scale
+ mDropCircleRadius * vertical / 2);
float vertex = (Float) mDropVertexAnimator.getAnimatedValue();
mDropTangentPath.moveTo(circleCenterX, vertex);
double q =
(Math.pow(mDropCircleRadius, 2) + circleCenterY * vertex - Math.pow(circleCenterY, 2)) / (
vertex - circleCenterY);
double b = -2.0 * mWidth / 2;
double c =
Math.pow(q - circleCenterY, 2) + Math.pow(circleCenterX, 2) - Math.pow(mDropCircleRadius,
2);
double p1 = (-b + Math.sqrt(b * b - 4 * c)) / 2;
double p2 = (-b - Math.sqrt(b * b - 4 * c)) / 2;
mDropTangentPath.lineTo((float) p1, (float) q);
mDropTangentPath.lineTo((float) p2, (float) q);
mDropTangentPath.close();
mShadowPath.set(mDropTangentPath);
mShadowPath.addOval(mDropRect, Path.Direction.CCW);
mDropCirclePath.addOval(mDropRect, Path.Direction.CCW);
if (mDropVertexAnimator.isRunning()) {
canvas.drawPath(mShadowPath, mShadowPaint);
} else {
canvas.drawPath(mDropCirclePath, mShadowPaint);
}
canvas.drawPath(mDropTangentPath, mPaint);
canvas.drawPath(mDropCirclePath, mPaint);
}
@Override
protected void onDetachedFromWindow() {
if (mDisappearCircleAnimator != null) {
mDisappearCircleAnimator.end();
mDisappearCircleAnimator.removeAllUpdateListeners();
}
if (mDropCircleAnimator != null) {
mDropCircleAnimator.end();
mDropCircleAnimator.removeAllUpdateListeners();
}
if (mDropVertexAnimator != null) {
mDropVertexAnimator.end();
mDropVertexAnimator.removeAllUpdateListeners();
}
if (mWaveReverseAnimator != null) {
mWaveReverseAnimator.end();
mWaveReverseAnimator.removeAllUpdateListeners();
}
if (mDropBounceHorizontalAnimator != null) {
mDropBounceHorizontalAnimator.end();
mDropBounceHorizontalAnimator.removeAllUpdateListeners();
}
if (mDropBounceVerticalAnimator != null) {
mDropBounceVerticalAnimator.end();
mDropBounceVerticalAnimator.removeAllUpdateListeners();
}
super.onDetachedFromWindow();
}
private void initView() {
setUpPaint();
setUpPath();
resetAnimator();
mDropRect = new RectF();
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
private void setUpPaint() {
mPaint = new Paint();
mPaint.setColor(0xff2196F3);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mShadowPaint = new Paint();
mShadowPaint.setShadowLayer(10.0f, 0.0f, 2.0f, SHADOW_COLOR);
}
private void setUpPath() {
mWavePath = new Path();
mDropTangentPath = new Path();
mDropCirclePath = new Path();
mShadowPath = new Path();
}
private void resetAnimator() {
mDropVertexAnimator = ValueAnimator.ofFloat(0.f, 0.f);
mDropBounceVerticalAnimator = ValueAnimator.ofFloat(0.f, 0.f);
mDropBounceHorizontalAnimator = ValueAnimator.ofFloat(0.f, 0.f);
mDropCircleAnimator = ValueAnimator.ofFloat(-1000.f, -1000.f);
mDropCircleAnimator.start();
mDisappearCircleAnimator = ValueAnimator.ofFloat(1.f, 1.f);
mDisappearCircleAnimator.setDuration(1);
mDisappearCircleAnimator.start();
}
private void onPreDragWave() {
if (mWaveReverseAnimator != null) {
if (mWaveReverseAnimator.isRunning()) {
mWaveReverseAnimator.cancel();
}
}
}
public void manualRefresh() {
if (mIsManualRefreshing) {
return;
}
mIsManualRefreshing = true;
mDropCircleAnimator = ValueAnimator.ofFloat(mMaxDropHeight, mMaxDropHeight);
mDropCircleAnimator.start();
mDropVertexAnimator = ValueAnimator.ofFloat(mMaxDropHeight - mDropCircleRadius,
mMaxDropHeight - mDropCircleRadius);
mDropVertexAnimator.start();
mCurrentCircleCenterY = mMaxDropHeight;
postInvalidate();
}
public void beginPhase(float move1) {
onPreDragWave();
mWavePath.moveTo(0, 0);
mWavePath.cubicTo(mWidth * BEGIN_PHASE_POINTS[0][0], BEGIN_PHASE_POINTS[0][1],
mWidth * BEGIN_PHASE_POINTS[1][0], mWidth * (BEGIN_PHASE_POINTS[1][1] + move1),
mWidth * BEGIN_PHASE_POINTS[2][0], mWidth * (BEGIN_PHASE_POINTS[2][1] + move1));
mWavePath.cubicTo(mWidth * BEGIN_PHASE_POINTS[3][0],
mWidth * (BEGIN_PHASE_POINTS[3][1] + move1), mWidth * BEGIN_PHASE_POINTS[4][0],
mWidth * (BEGIN_PHASE_POINTS[4][1] + move1), mWidth * BEGIN_PHASE_POINTS[5][0],
mWidth * (BEGIN_PHASE_POINTS[5][1] + move1));
mWavePath.cubicTo(mWidth - mWidth * BEGIN_PHASE_POINTS[4][0],
mWidth * (BEGIN_PHASE_POINTS[4][1] + move1), mWidth - mWidth * BEGIN_PHASE_POINTS[3][0],
mWidth * (BEGIN_PHASE_POINTS[3][1] + move1), mWidth - mWidth * BEGIN_PHASE_POINTS[2][0],
mWidth * (BEGIN_PHASE_POINTS[2][1] + move1));
mWavePath.cubicTo(mWidth - mWidth * BEGIN_PHASE_POINTS[1][0],
mWidth * (BEGIN_PHASE_POINTS[1][1] + move1), mWidth - mWidth * BEGIN_PHASE_POINTS[0][0],
BEGIN_PHASE_POINTS[0][1], mWidth, 0);
ViewCompat.postInvalidateOnAnimation(this);
}
public void appearPhase(float move1, float move2) {
onPreDragWave();
mWavePath.moveTo(0, 0);
mWavePath.cubicTo(mWidth * APPEAR_PHASE_POINTS[0][0], mWidth * APPEAR_PHASE_POINTS[0][1],
mWidth * Math.min(BEGIN_PHASE_POINTS[1][0] + move2, APPEAR_PHASE_POINTS[1][0]),
mWidth * Math.max(BEGIN_PHASE_POINTS[1][1] + move1 - move2, APPEAR_PHASE_POINTS[1][1]),
mWidth * Math.max(BEGIN_PHASE_POINTS[2][0] - move2, APPEAR_PHASE_POINTS[2][0]),
mWidth * Math.max(BEGIN_PHASE_POINTS[2][1] + move1 - move2, APPEAR_PHASE_POINTS[2][1]));
mWavePath.cubicTo(
mWidth * Math.max(BEGIN_PHASE_POINTS[3][0] - move2, APPEAR_PHASE_POINTS[3][0]),
mWidth * Math.min(BEGIN_PHASE_POINTS[3][1] + move1 + move2, APPEAR_PHASE_POINTS[3][1]),
mWidth * Math.max(BEGIN_PHASE_POINTS[4][0] - move2, APPEAR_PHASE_POINTS[4][0]),
mWidth * Math.min(BEGIN_PHASE_POINTS[4][1] + move1 + move2, APPEAR_PHASE_POINTS[4][1]),
mWidth * APPEAR_PHASE_POINTS[5][0],
mWidth * Math.min(BEGIN_PHASE_POINTS[0][1] + move1 + move2, APPEAR_PHASE_POINTS[5][1]));
mWavePath.cubicTo(
mWidth - mWidth * Math.max(BEGIN_PHASE_POINTS[4][0] - move2, APPEAR_PHASE_POINTS[4][0]),
mWidth * Math.min(BEGIN_PHASE_POINTS[4][1] + move1 + move2, APPEAR_PHASE_POINTS[4][1]),
mWidth - mWidth * Math.max(BEGIN_PHASE_POINTS[3][0] - move2, APPEAR_PHASE_POINTS[3][0]),
mWidth * Math.min(BEGIN_PHASE_POINTS[3][1] + move1 + move2, APPEAR_PHASE_POINTS[3][1]),
mWidth - mWidth * Math.max(BEGIN_PHASE_POINTS[2][0] - move2, APPEAR_PHASE_POINTS[2][0]),
mWidth * Math.max(BEGIN_PHASE_POINTS[2][1] + move1 - move2, APPEAR_PHASE_POINTS[2][1]));
mWavePath.cubicTo(
mWidth - mWidth * Math.min(BEGIN_PHASE_POINTS[1][0] + move2, APPEAR_PHASE_POINTS[1][0]),
mWidth * Math.max(BEGIN_PHASE_POINTS[1][1] + move1 - move2, APPEAR_PHASE_POINTS[1][1]),
mWidth - mWidth * APPEAR_PHASE_POINTS[0][0], mWidth * APPEAR_PHASE_POINTS[0][1], mWidth, 0);
mCurrentCircleCenterY =
mWidth * Math.min(BEGIN_PHASE_POINTS[3][1] + move1 + move2, APPEAR_PHASE_POINTS[3][1])
+ mDropCircleRadius;
ViewCompat.postInvalidateOnAnimation(this);
}
public void expandPhase(float move1, float move2, float move3) {
onPreDragWave();
mWavePath.moveTo(0, 0);
mWavePath.cubicTo(mWidth * EXPAND_PHASE_POINTS[0][0], mWidth * EXPAND_PHASE_POINTS[0][1],
mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[1][0] + move2, APPEAR_PHASE_POINTS[1][0]) + move3,
EXPAND_PHASE_POINTS[1][0]), mWidth * Math.max(
Math.max(BEGIN_PHASE_POINTS[1][1] + move1 - move2, APPEAR_PHASE_POINTS[1][1]) - move3,
EXPAND_PHASE_POINTS[1][1]),
mWidth * Math.max(BEGIN_PHASE_POINTS[2][0] - move2, EXPAND_PHASE_POINTS[2][0]),
mWidth * Math.min(
Math.max(BEGIN_PHASE_POINTS[2][1] + move1 - move2, APPEAR_PHASE_POINTS[2][1]) + move3,
EXPAND_PHASE_POINTS[2][1]));
mWavePath.cubicTo(mWidth * Math.min(
Math.max(BEGIN_PHASE_POINTS[3][0] - move2, APPEAR_PHASE_POINTS[3][0]) + move3,
EXPAND_PHASE_POINTS[3][0]), mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[3][1] + move1 + move2, APPEAR_PHASE_POINTS[3][1]) + move3,
EXPAND_PHASE_POINTS[3][1]),
mWidth * Math.max(BEGIN_PHASE_POINTS[4][0] - move2, EXPAND_PHASE_POINTS[4][0]),
mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[4][1] + move1 + move2, APPEAR_PHASE_POINTS[4][1]) + move3,
EXPAND_PHASE_POINTS[4][1]), mWidth * EXPAND_PHASE_POINTS[5][0], mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[0][1] + move1 + move2, APPEAR_PHASE_POINTS[5][1]) + move3,
EXPAND_PHASE_POINTS[5][1]));
mWavePath.cubicTo(
mWidth - mWidth * Math.max(BEGIN_PHASE_POINTS[4][0] - move2, EXPAND_PHASE_POINTS[4][0]),
mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[4][1] + move1 + move2, APPEAR_PHASE_POINTS[4][1]) + move3,
EXPAND_PHASE_POINTS[4][1]), mWidth - mWidth * Math.min(
Math.max(BEGIN_PHASE_POINTS[3][0] - move2, APPEAR_PHASE_POINTS[3][0]) + move3,
EXPAND_PHASE_POINTS[3][0]), mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[3][1] + move1 + move2, APPEAR_PHASE_POINTS[3][1]) + move3,
EXPAND_PHASE_POINTS[3][1]),
mWidth - mWidth * Math.max(BEGIN_PHASE_POINTS[2][0] - move2, EXPAND_PHASE_POINTS[2][0]),
mWidth * Math.min(
Math.max(BEGIN_PHASE_POINTS[2][1] + move1 - move2, APPEAR_PHASE_POINTS[2][1]) + move3,
EXPAND_PHASE_POINTS[2][1]));
mWavePath.cubicTo(mWidth - mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[1][0] + move2, APPEAR_PHASE_POINTS[1][0]) + move3,
EXPAND_PHASE_POINTS[1][0]), mWidth * Math.max(
Math.max(BEGIN_PHASE_POINTS[1][1] + move1 - move2, APPEAR_PHASE_POINTS[1][1]) - move3,
EXPAND_PHASE_POINTS[1][1]), mWidth - mWidth * EXPAND_PHASE_POINTS[0][0],
mWidth * EXPAND_PHASE_POINTS[0][1], mWidth, 0);
mCurrentCircleCenterY = mWidth * Math.min(
Math.min(BEGIN_PHASE_POINTS[3][1] + move1 + move2, APPEAR_PHASE_POINTS[3][1]) + move3,
EXPAND_PHASE_POINTS[3][1]) + mDropCircleRadius;
ViewCompat.postInvalidateOnAnimation(this);
}
private void updateMaxDropHeight(int height) {
if (500 * (mWidth / 1440.f) > height) {
Log.w("WaveView", "DropHeight is more than " + 500 * (mWidth / 1440.f));
return;
}
//mMaxDropHeight = (int) Math.min(height, getHeight() - mDropCircleRadius);
mMaxDropHeight = (int) Math.min(height, getHeight()/2 - mDropCircleRadius);
if (mIsManualRefreshing) {
mIsManualRefreshing = false;
manualRefresh();
}
}
public void startDropAnimation() {
mDisappearCircleAnimator = ValueAnimator.ofFloat(1.f, 1.f);
mDisappearCircleAnimator.setDuration(1);
mDisappearCircleAnimator.start();
mDropCircleAnimator = ValueAnimator.ofFloat(500 * (mWidth / 1440.f), mMaxDropHeight);
mDropCircleAnimator.setDuration(DROP_CIRCLE_ANIMATOR_DURATION);
mDropCircleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentCircleCenterY = (float) animation.getAnimatedValue();
ViewCompat.postInvalidateOnAnimation(WaveView.this);
}
});
mDropCircleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mDropCircleAnimator.start();
mDropVertexAnimator = ValueAnimator.ofFloat(0.f, mMaxDropHeight - mDropCircleRadius);
mDropVertexAnimator.setDuration(DROP_VERTEX_ANIMATION_DURATION);
mDropVertexAnimator.addUpdateListener(mAnimatorUpdateListener);
mDropVertexAnimator.start();
mDropBounceVerticalAnimator = ValueAnimator.ofFloat(0.f, 1.f);
mDropBounceVerticalAnimator.setDuration(DROP_BOUNCE_ANIMATOR_DURATION);
mDropBounceVerticalAnimator.addUpdateListener(mAnimatorUpdateListener);
mDropBounceVerticalAnimator.setInterpolator(new DropBounceInterpolator());
mDropBounceVerticalAnimator.setStartDelay(DROP_VERTEX_ANIMATION_DURATION);
mDropBounceVerticalAnimator.start();
mDropBounceHorizontalAnimator = ValueAnimator.ofFloat(0.f, 1.f);
mDropBounceHorizontalAnimator.setDuration(DROP_BOUNCE_ANIMATOR_DURATION);
mDropBounceHorizontalAnimator.addUpdateListener(mAnimatorUpdateListener);
mDropBounceHorizontalAnimator.setInterpolator(new DropBounceInterpolator());
mDropBounceHorizontalAnimator.setStartDelay(
(long) (DROP_VERTEX_ANIMATION_DURATION + DROP_BOUNCE_ANIMATOR_DURATION * 0.25));
mDropBounceHorizontalAnimator.start();
}
public void startDisappearCircleAnimation() {
mDisappearCircleAnimator = ValueAnimator.ofFloat(1.f, 0.f);
mDisappearCircleAnimator.addUpdateListener(mAnimatorUpdateListener);
mDisappearCircleAnimator.setDuration(DROP_REMOVE_ANIMATOR_DURATION);
mDisappearCircleAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
resetAnimator();
mIsManualRefreshing = false;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mDisappearCircleAnimator.start();
}
public void startWaveAnimation(float h) {
h = Math.min(h, MAX_WAVE_HEIGHT) * mWidth;
mWaveReverseAnimator = ValueAnimator.ofFloat(h, 0.f);
mWaveReverseAnimator.setDuration(WAVE_ANIMATOR_DURATION);
mWaveReverseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float h = (Float) valueAnimator.getAnimatedValue();
mWavePath.moveTo(0, 0);
mWavePath.quadTo(0.25f * mWidth, 0, 0.333f * mWidth, h * 0.5f);
mWavePath.quadTo(mWidth * 0.5f, h * 1.4f, 0.666f * mWidth, h * 0.5f);
mWavePath.quadTo(0.75f * mWidth, 0, mWidth, 0);
postInvalidate();
}
});
mWaveReverseAnimator.setInterpolator(new BounceInterpolator());
mWaveReverseAnimator.start();
}
public void animationDropCircle() {
if (mDisappearCircleAnimator.isRunning()) {
return;
}
startDropAnimation();
startWaveAnimation(0.1f);
}
public float getCurrentCircleCenterY() {
return mCurrentCircleCenterY;
}
public void setMaxDropHeight(int maxDropHeight) {
if (mDropHeightUpdated) {
updateMaxDropHeight(maxDropHeight);
} else {
mUpdateMaxDropHeight = maxDropHeight;
mDropHeightUpdated = true;
}
}
public boolean isDisappearCircleAnimatorRunning() {
return mDisappearCircleAnimator.isRunning();
}
public void setShadowRadius(int radius) {
mShadowPaint.setShadowLayer(radius, 0.0f, 2.0f, SHADOW_COLOR);
}
public void setWaveColor(int color) {
mPaint.setColor(color);
invalidate();
}
}