package com.asha;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.BounceInterpolator;
import android.view.animation.Transformation;
import com.asha.ChromeLikeSwipeLayout.IOnExpandViewListener;
import java.util.List;
import static com.asha.ChromeLikeSwipeLayout.dp2px;
/**
* Created by hzqiujiadi on 15/11/18.
* hzqiujiadi ashqalcn@gmail.com
*/
public class ChromeLikeLayout extends ViewGroup implements IOnExpandViewListener {
private static final String TAG = "ChromeLikeView";
private static final float sMagicNumber = 0.55228475f;
private static final int sDefaultCircleColor = 0xFFFFCC11;
private static final int sDefaultBackgroundColor = 0xFF333333;
private static final float sThreshold = 0.5f;
private static final float sAnimCancelThreshold = 0.75f;
private Threshold mAnimCancelThreshold;
private Paint mPaint;
private Path mPath;
private float mDegrees;
private float mTranslate;
private int mCurrentFlag;
private int mRadius = dp2px(40);
private int mGap = dp2px(15);
private IOnRippleListener mRippleListener;
private GummyAnimatorHelper mGummyAnimatorHelper = new GummyAnimatorHelper();
private RippleAnimatorHelper mRippleAnimatorHelper = new RippleAnimatorHelper();
private TouchHelper mTouchHelper;
public ChromeLikeLayout(Context context) {
this(context,null);
}
public ChromeLikeLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ChromeLikeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private int getItemWidth(){
return mRadius*2 + mGap;
}
private float getMovingThreshold() {
return getItemWidth() * sThreshold;
}
private int getCircleStartX(){
int contentWidth = getItemWidth();
int totalWidth = getMeasuredWidth();
int totalContextWidth = contentWidth * (getChildCount() - 1);
return (totalWidth - totalContextWidth) >> 1;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int startXOffset = getCircleStartX();
int startYOffset = (b - t);
for (int i = 0 ; i < getChildCount() ; i++ ){
View view = getChildAt(i);
final int left = startXOffset + i * getItemWidth() - view.getMeasuredWidth()/2;
final int right = left + view.getMeasuredWidth();
final int top = (startYOffset - view.getMeasuredHeight())>>1;
final int bottom = top + view.getMeasuredHeight();
view.layout(left,top,right,bottom);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(getMeasuredWidth(),getMeasuredHeight());
}
private void init() {
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchHelper = new TouchHelper(configuration.getScaledTouchSlop());
mAnimCancelThreshold = new Threshold(getMovingThreshold() * sAnimCancelThreshold);
setBackgroundColor(sDefaultBackgroundColor);
mPaint = new Paint();
mPaint.setColor(sDefaultCircleColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mPath = new Path();
reset();
setWillNotDraw(false);
}
public void setIcons(List<Integer> drawables){
this.removeAllViews();
for ( int res : drawables ){
View v = new View(getContext());
v.setBackgroundResource(res);
addView(v, LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
}
}
public void setRippleDuration(int duration){
mRippleAnimatorHelper.setDuration(duration);
}
public void setGummyDuration(int duration){
mGummyAnimatorHelper.setDuration(duration);
}
public void setRadius(int radius) {
this.mRadius = radius;
}
public void onActionDown(){
reset();
}
public void onActionMove(boolean isExpanded, TouchManager touchManager){
// feed MotionEvent after first expanded
int motionX = touchManager.getMotionX();
if ( !mTouchHelper.isExpanded() && isExpanded ){
mTouchHelper.feed(motionX);
return;
}
// reset the mTouchHelper if view is collapsed
if ( !isExpanded ) {
mTouchHelper.reset();
return;
}
// now, the view must be expanded.
// feed the MotionEvent
// to update mTouchHelper status.
mTouchHelper.feed(motionX);
// if not in moving status
if ( !mTouchHelper.isMoving() ){
updateAlpha(1);
updatePath( 0, 0, mRadius, false );
updateIconScale(1);
return;
}
if ( mGummyAnimatorHelper.isAnimationStarted() ){
float currentX = mTouchHelper.getCurrentX();
if (mAnimCancelThreshold.absOverflow(currentX)) {
mGummyAnimatorHelper.end();
mTouchHelper.resetToReady(currentX);
}
return;
}
// we can't move to left if the first circle is selected
// neither to right if the last circle is selected
if ( mCurrentFlag == prevOfCurrentFlag() )
mTouchHelper.testLeftEdge();
if ( mCurrentFlag == nextOfCurrentFlag() )
mTouchHelper.testRightEdge();
// now, the values can be trusted
float currentX = mTouchHelper.getCurrentX();
float prevX = mTouchHelper.getPrevX();
updateAlpha(1);
updatePath( currentX, prevX, mRadius, false );
updateIconScale(1);
// if diff > threshold, update translate
if ( Math.abs( currentX - prevX ) > getMovingThreshold() ){
if ( currentX > prevX ) updateCurrentFlag(nextOfCurrentFlag());
else updateCurrentFlag(prevOfCurrentFlag());
mAnimCancelThreshold.reset();
mGummyAnimatorHelper.launchAnim(
currentX
, prevX
, mTranslate
, flag2TargetTranslate() );
}
}
public void onActionUpOrCancel(boolean isExpanded){
if ( getChildCount() == 0 ) return;
if ( !mTouchHelper.isExpanded() ) return;
mTouchHelper.reset();
if ( isExpanded ){
boolean isRippleAnimEnabled = getChildCount() > 0;
if ( isRippleAnimEnabled ){
if ( mRippleAnimatorHelper.isAnimationStarted() ) return;
if ( mGummyAnimatorHelper.isAnimationStarted() ){
mGummyAnimatorHelper.end();
}
mRippleAnimatorHelper.launchAnim(mRadius,getMeasuredWidth());
} else {
if ( mRippleListener != null ) mRippleListener.onRippleAnimFinished(-1);
}
}
}
private void reset(){
onExpandView(0,false);
updateAlpha(1);
updateCurrentFlag((getChildCount() - 1) >> 1);
mTranslate = flag2TargetTranslate();
}
private void updateAlpha( float alpha ){
mPaint.setAlpha(Math.round(255 * alpha));
}
private void updatePath(float currentX, float prevX, int radius, boolean animate){
updatePath(currentX,0,prevX,0,radius,animate);
}
private void updatePath(float currentX, float currentY, float prevX, float prevY, int radius, boolean animate ){
float distance = distance(prevX, prevY, currentX, currentY);
float tempDegree = points2Degrees(prevX, prevY, currentX, currentY);
if ( animate ){
if ( Math.abs( mDegrees - tempDegree ) > 5 ) distance = -distance;
} else {
//if ( distance < mTouchSlop ) distance = 0;
mDegrees = tempDegree;
}
float longRadius = radius + distance;
float shortRadius = radius - distance * 0.1f;
mPath.reset();
mPath.lineTo(0, -radius);
mPath.cubicTo(radius * sMagicNumber, -radius
, longRadius, -radius * sMagicNumber
, longRadius, 0);
mPath.lineTo(0, 0);
mPath.lineTo(0, radius);
mPath.cubicTo(radius * sMagicNumber, radius
, longRadius, radius * sMagicNumber
, longRadius, 0);
mPath.lineTo(0, 0);
mPath.lineTo(0, -radius);
mPath.cubicTo(-radius * sMagicNumber, -radius
, -shortRadius, -radius * sMagicNumber
, -shortRadius, 0);
mPath.lineTo(0, 0);
mPath.lineTo(0, radius);
mPath.cubicTo(-radius * sMagicNumber, radius
, -shortRadius, radius * sMagicNumber
, -shortRadius, 0);
mPath.lineTo(0, 0);
//postInvalidate();
invalidate();
}
private void updateIconScale( float fraction ){
float iconFraction = iconOffsetFraction(fraction);
for (int i = 0 ; i < getChildCount(); i++ ){
View v = getChildAt(i);
ViewCompat.setScaleX(v,iconFraction);
ViewCompat.setScaleY(v,iconFraction);
}
}
private void updateCurrentFlag(int flag){
mCurrentFlag = flag;
boolean isPressed;
for (int i = 0; i < getChildCount(); i++ ){
View view = getChildAt(i);
isPressed = i == mCurrentFlag;
view.setPressed(isPressed);
}
}
private int nextOfCurrentFlag(){
int tmp = mCurrentFlag;
tmp++;
return Math.min(tmp,getChildCount()-1);
}
private int prevOfCurrentFlag(){
int tmp = mCurrentFlag;
tmp--;
return Math.max(tmp,0);
}
@Override
protected void onDraw(Canvas canvas) {
if ( getChildCount() == 0 ) return;
int centerY = getMeasuredHeight() >> 1;
canvas.save();
canvas.translate(mTranslate, centerY);
canvas.rotate(mDegrees);
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
private static float distance(float x1,float y1, float x2, float y2){
return (float) Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
private int flag2TargetTranslate(){
int startXOffset = getCircleStartX();
return startXOffset + getItemWidth() * mCurrentFlag;
}
private static float points2Degrees(float x1, float y1, float x2, float y2){
double angle = Math.atan2(y2-y1,x2-x1);
return (float) Math.toDegrees(angle);
}
@Override
public void onExpandView(float fraction, boolean isFromCancel) {
float circleFraction = circleOffsetFraction(fraction);
if (isFromCancel) updateAlpha(circleFraction);
updatePath(0,0,Math.round(mRadius*circleFraction),true);
updateIconScale(fraction);
}
private static final float sFactorScaleCircle = 0.75f;
private static final float sFactorScaleIcon = 0.3f;
private float circleOffsetFraction( float fraction ){
return offsetFraction(fraction, sFactorScaleCircle);
}
private float iconOffsetFraction( float fraction ){
return offsetFraction(fraction, sFactorScaleIcon);
}
private float offsetFraction(float fraction, float factor){
float result = (fraction - factor) / (1 - factor);
result = result > 0 ? result : 0;
return result;
}
public void setRippleListener(IOnRippleListener mRippleListener) {
this.mRippleListener = mRippleListener;
}
public void setCircleColor(int circleColor) {
mPaint.setColor(circleColor);
}
public void setGap(int gap) {
this.mGap = gap;
}
public interface IOnRippleListener {
void onRippleAnimFinished(int index);
}
public static class TouchHelper {
private final int mTouchSlop;
private int mStatus;
private final int STATUS_NONE = 0;
private final int STATUS_EXPANDED = 1;
private final int STATUS_READY = 2;
private final int STATUS_MOVING = 3;
private float mReadyPrevX;
private float mMovingPrevX;
private float mMovingCurrentX;
public TouchHelper(int mTouchSlop) {
this.mTouchSlop = mTouchSlop;
}
public boolean isMoving(){
return mStatus == STATUS_MOVING;
}
public boolean isExpanded(){
return mStatus > STATUS_NONE;
}
public void feed(float motionX){
int status = mStatus;
//float tmpX = MotionEventCompat.getX(event,pointerIndex);
switch ( status ){
case STATUS_NONE:
case STATUS_EXPANDED:
mReadyPrevX = motionX;
mStatus = STATUS_READY;
break;
case STATUS_READY:
if ( Math.abs(motionX - mReadyPrevX) > mTouchSlop ){
mMovingPrevX = motionX;
mMovingCurrentX = motionX;
mStatus = STATUS_MOVING;
}
break;
case STATUS_MOVING:
mMovingCurrentX = motionX;
break;
}
}
public float getPrevX(){
return mMovingPrevX;
}
public float getCurrentX(){
return mMovingCurrentX;
}
public void resetToReady(float animFromX){
mStatus = STATUS_READY;
mReadyPrevX = animFromX;
}
public void reset(){
mStatus = STATUS_NONE;
mReadyPrevX = 0;
mMovingPrevX = 0;
}
public void testLeftEdge() {
if ( mMovingCurrentX < mMovingPrevX )
mMovingPrevX = mMovingCurrentX;
}
public void testRightEdge() {
if ( mMovingCurrentX > mMovingPrevX )
mMovingPrevX = mMovingCurrentX;
}
}
public static class Threshold {
private boolean mInit;
private float mPrev;
private float mThreshold;
public Threshold(float threshold) {
this.mThreshold = threshold;
}
private boolean checkAbsOverflow(float now){
if (Math.abs(now - mPrev) > mThreshold) return true;
else return false;
}
public boolean absOverflow(float value){
if (!mInit){
mPrev = value;
mInit = true;
return false;
}
return checkAbsOverflow(value);
}
public void reset(){
mInit = false;
mPrev = 0;
}
}
/***
*
* Ripple animation
*
* */
private class RippleAnimatorHelper extends AnimationListenerAdapter {
private float mAnimFromRadius;
private float mAnimToRadius;
private boolean mAnimationStarted;
private boolean mEventDispatched;
private int mDuration = 300;
public void onAnimationUpdate(float interpolation) {
int currentRadius = FloatEvaluator.evaluate(interpolation,mAnimFromRadius,mAnimToRadius).intValue();
updatePath(0, 0, currentRadius, true);
updateAlpha(1-interpolation);
}
public void launchAnim(float fromRadius, float toRadius) {
mAnimFromRadius = fromRadius;
mAnimToRadius = toRadius;
Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
onAnimationUpdate(interpolatedTime);
}
};
animation.setDuration(mDuration);
animation.setInterpolator(new FastOutSlowInInterpolator());
animation.setAnimationListener(this);
View target = ChromeLikeLayout.this.getChildAt(mCurrentFlag);
if ( target == null ) return;
target.clearAnimation();
target.startAnimation(animation);
mAnimationStarted = true;
mEventDispatched = false;
}
public boolean isAnimationStarted() {
return mAnimationStarted;
}
@Override
public void onAnimationEnd(Animation animation) {
mAnimationStarted = false;
if ( !mEventDispatched && mRippleListener != null ){
mRippleListener.onRippleAnimFinished(mCurrentFlag);
mEventDispatched = true;
}
}
public void setDuration(int duration) {
this.mDuration = duration;
}
}
/***
*
* Gummy animation
*
* */
private class GummyAnimatorHelper extends AnimationListenerAdapter {
private float mAnimFromX;
private float mAnimToX;
private float mAnimFromTranslate;
private float mAnimToTranslate;
private boolean mAnimationStarted;
private int mDuration = 300;
public void onAnimationUpdate(float interpolation) {
Float currentX = FloatEvaluator.evaluate(interpolation,mAnimFromX,mAnimToX);
mTranslate = FloatEvaluator.evaluate(interpolation, mAnimFromTranslate, mAnimToTranslate);
updatePath(currentX, mAnimToX, mRadius, true);
}
public void launchAnim(float fromX, float toX, float fromTranslate, float toTranslate) {
mAnimFromX = fromX;
mAnimToX = toX;
mAnimFromTranslate = fromTranslate;
mAnimToTranslate = toTranslate;
Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
onAnimationUpdate(interpolatedTime);
}
};
animation.setDuration(mDuration);
animation.setInterpolator(new BounceInterpolator());
animation.setAnimationListener(this);
ChromeLikeLayout.this.clearAnimation();
ChromeLikeLayout.this.startAnimation(animation);
mAnimationStarted = true;
}
public boolean isAnimationStarted() {
return mAnimationStarted;
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mAnimationStarted){
mTouchHelper.resetToReady(mAnimFromX);
mAnimationStarted = false;
}
}
public void setDuration(int duration) {
this.mDuration = duration;
}
public void end() {
// false immediately
mAnimationStarted = false;
// clear animation
ChromeLikeLayout.this.clearAnimation();
// anim to end immediately
onAnimationUpdate(1);
}
}
private static class FloatEvaluator {
public static Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
}