package in.shadowfax.proswipebutton; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.os.Handler; import android.support.annotation.ColorInt; import android.support.annotation.Dimension; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Animation; import android.view.animation.TranslateAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import static in.shadowfax.proswipebutton.Constants.BTN_INIT_RADIUS; import static in.shadowfax.proswipebutton.Constants.BTN_MORPHED_RADIUS; import static in.shadowfax.proswipebutton.Constants.DEFAULT_SWIPE_DISTANCE; import static in.shadowfax.proswipebutton.Constants.DEFAULT_TEXT_SIZE; import static in.shadowfax.proswipebutton.Constants.MORPH_ANIM_DURATION; import static in.shadowfax.proswipebutton.UiUtils.animateFadeHide; import static in.shadowfax.proswipebutton.UiUtils.animateFadeShow; import static in.shadowfax.proswipebutton.UiUtils.dpToPx; /** * Created by shadow-admin on 24/10/17. */ public class ProSwipeButton extends RelativeLayout { private Context context; private View view; private GradientDrawable gradientDrawable; private RelativeLayout contentContainer; private TextView contentTv; private ImageView arrow1; private ImageView arrow2; private LinearLayout arrowHintContainer; private ProgressBar progressBar; //// TODO: 26/10/17 Add touch blocking /* User configurable settings */ private CharSequence btnText = "BUTTON"; @ColorInt private int textColorInt; @ColorInt private int bgColorInt; @ColorInt private int arrowColorInt; private float btnRadius = BTN_INIT_RADIUS; @Dimension private float textSize = DEFAULT_TEXT_SIZE; @Nullable private OnSwipeListener swipeListener = null; private float swipeDistance = DEFAULT_SWIPE_DISTANCE; public ProSwipeButton(Context context) { super(context); this.context = context; init(); } public ProSwipeButton(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; setAttrs(context, attrs); init(); } public ProSwipeButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.context = context; setAttrs(context, attrs); init(); } private void setAttrs(Context context, AttributeSet attrs) { TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.ProSwipeButton, 0, 0); try { final String btnString = a.getString(R.styleable.ProSwipeButton_btn_text); if (btnString != null) btnText = btnString; textColorInt = a.getColor(R.styleable.ProSwipeButton_text_color, ContextCompat.getColor(context, android.R.color.white)); bgColorInt = a.getColor(R.styleable.ProSwipeButton_bg_color, ContextCompat.getColor(context, R.color.proswipebtn_red)); arrowColorInt = a.getColor(R.styleable.ProSwipeButton_arrow_color, ContextCompat.getColor(context, R.color.proswipebtn_translucent_white)); btnRadius = a.getFloat(R.styleable.ProSwipeButton_btn_radius, BTN_INIT_RADIUS); textSize = a.getDimensionPixelSize(R.styleable.ProSwipeButton_text_size, (int) DEFAULT_TEXT_SIZE); } finally { a.recycle(); } } public void init() { LayoutInflater inflater = LayoutInflater.from(context); view = inflater.inflate(R.layout.view_proswipebtn, this, true); } @Override protected void onFinishInflate() { super.onFinishInflate(); contentContainer = view.findViewById(R.id.relativeLayout_swipeBtn_contentContainer); arrowHintContainer = view.findViewById(R.id.linearLayout_swipeBtn_hintContainer); contentTv = view.findViewById(R.id.tv_btnText); arrow1 = view.findViewById(R.id.iv_arrow1); arrow2 = view.findViewById(R.id.iv_arrow2); tintArrowHint(); contentTv.setText(btnText); contentTv.setTextColor(textColorInt); contentTv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); gradientDrawable = new GradientDrawable(); gradientDrawable.setShape(GradientDrawable.RECTANGLE); gradientDrawable.setCornerRadius(btnRadius); setBackgroundColor(bgColorInt); updateBackground(); setupTouchListener(); } private void setupTouchListener() { setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_MOVE: // Movement logic here if (event.getX() > arrowHintContainer.getWidth() / 2 && event.getX() + arrowHintContainer.getWidth() / 2 < getWidth() && (event.getX() < arrowHintContainer.getX() + arrowHintContainer.getWidth() || arrowHintContainer.getX() != 0)) { // snaps the hint to user touch, only if the touch is within hint width or if it has already been displaced arrowHintContainer.setX(event.getX() - arrowHintContainer.getWidth() / 2); } if (arrowHintContainer.getX() + arrowHintContainer.getWidth() > getWidth() && arrowHintContainer.getX() + arrowHintContainer.getWidth() / 2 < getWidth()) { // allows the hint to go up to a max of btn container width arrowHintContainer.setX(getWidth() - arrowHintContainer.getWidth()); } if (event.getX() < arrowHintContainer.getWidth() / 2 && arrowHintContainer.getX() > 0) { // allows the hint to go up to a min of btn container starting arrowHintContainer.setX(0); } return true; case MotionEvent.ACTION_UP: //Release logic here if (arrowHintContainer.getX() + arrowHintContainer.getWidth() > getWidth() * swipeDistance) { // swipe completed, fly the hint away! performSuccessfulSwipe(); } else if (arrowHintContainer.getX() <= 0) { // upon click without swipe startFwdAnim(); } else { // swipe not completed, pull back the hint animateHintBack(); } return true; } return false; } }); } private void performSuccessfulSwipe() { if (swipeListener != null) swipeListener.onSwipeConfirm(); morphToCircle(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); startFwdAnim(); } private void animateHintBack() { final ValueAnimator positionAnimator = ValueAnimator.ofFloat(arrowHintContainer.getX(), 0); positionAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); positionAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float x = (Float) positionAnimator.getAnimatedValue(); arrowHintContainer.setX(x); } }); positionAnimator.setDuration(200); positionAnimator.start(); } private void startFwdAnim() { if (isEnabled()) { TranslateAnimation animation = new TranslateAnimation(0, getMeasuredWidth(), 0, 0); animation.setInterpolator(new AccelerateDecelerateInterpolator()); animation.setDuration(1000); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { startHintInitAnim(); } @Override public void onAnimationRepeat(Animation animation) { } }); arrowHintContainer.startAnimation(animation); } } /** * animate entry of hint from the left-most edge */ private void startHintInitAnim() { TranslateAnimation anim = new TranslateAnimation(-arrowHintContainer.getWidth(), 0, 0, 0); anim.setDuration(500); arrowHintContainer.startAnimation(anim); } /** * Just like performOnClick() in a standard button, * this will call the attached OnSwipeListener * and morph the btn to a circle */ public void performOnSwipe() { performSuccessfulSwipe(); } public void morphToCircle() { animateFadeHide(context, arrowHintContainer); setOnTouchListener(null); ObjectAnimator cornerAnimation = ObjectAnimator.ofFloat(gradientDrawable, "cornerRadius", BTN_INIT_RADIUS, BTN_MORPHED_RADIUS); animateFadeHide(context, contentTv); ValueAnimator widthAnimation; widthAnimation = ValueAnimator.ofInt(getWidth(), dpToPx(50)); widthAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int val = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = contentContainer.getLayoutParams(); layoutParams.width = val; contentContainer.setLayoutParams(layoutParams); } }); ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), dpToPx(50)); heightAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int val = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = contentContainer.getLayoutParams(); layoutParams.height = val; contentContainer.setLayoutParams(layoutParams); } }); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(MORPH_ANIM_DURATION); animatorSet.playTogether(cornerAnimation, widthAnimation, heightAnimation); animatorSet.start(); showProgressBar(); } private void morphToRect() { setupTouchListener(); ObjectAnimator cornerAnimation = ObjectAnimator.ofFloat(gradientDrawable, "cornerRadius", BTN_MORPHED_RADIUS, BTN_INIT_RADIUS); ValueAnimator widthAnimation; widthAnimation = ValueAnimator.ofInt(dpToPx(50), getWidth()); widthAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int val = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = contentContainer.getLayoutParams(); layoutParams.width = val; contentContainer.setLayoutParams(layoutParams); } }); ValueAnimator heightAnimation = ValueAnimator.ofInt(dpToPx(50), getWidth()); heightAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { int val = (Integer) valueAnimator.getAnimatedValue(); ViewGroup.LayoutParams layoutParams = contentContainer.getLayoutParams(); layoutParams.height = val; contentContainer.setLayoutParams(layoutParams); } }); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.setDuration(MORPH_ANIM_DURATION); animatorSet.playTogether(cornerAnimation, widthAnimation, heightAnimation); animatorSet.start(); } public void updateBackground() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { contentContainer.setBackground(gradientDrawable); } else { // noinspection deprecation contentContainer.setBackgroundDrawable(gradientDrawable); } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (!enabled) { gradientDrawable.setColor(ContextCompat.getColor(context, R.color.proswipebtn_disabled_grey)); updateBackground(); this.setAlpha(0.5f); } else { setBackgroundColor(getBackgroundColor()); this.setAlpha(1f); } } private void showProgressBar() { progressBar = new ProgressBar(context); progressBar.getIndeterminateDrawable().setColorFilter(ContextCompat.getColor(context, android.R.color.white), android.graphics.PorterDuff.Mode.SRC_IN); animateFadeHide(context, contentTv); contentContainer.addView(progressBar); } public void showResultIcon(boolean isSuccess, boolean shouldReset) { animateFadeHide(context, progressBar); final AppCompatImageView failureIcon = new AppCompatImageView(context); RelativeLayout.LayoutParams icLayoutParams = new RelativeLayout.LayoutParams(dpToPx(50), dpToPx(50)); failureIcon.setLayoutParams(icLayoutParams); failureIcon.setVisibility(GONE); int icon; if (isSuccess) icon = R.drawable.ic_check_circle_36dp; else icon = R.drawable.ic_cancel_full_24dp; failureIcon.setImageResource(icon); contentContainer.addView(failureIcon); animateFadeShow(context, failureIcon); if (shouldReset) { // expand the btn again new Handler().postDelayed(new Runnable() { @Override public void run() { animateFadeHide(context, failureIcon); morphToRect(); arrowHintContainer.setX(0); animateFadeShow(context, arrowHintContainer); animateFadeShow(context, contentTv); } }, 1000); } } public void showResultIcon(boolean isSuccess) { showResultIcon(isSuccess, !isSuccess); } private void tintArrowHint() { arrow1.setColorFilter(arrowColorInt, PorterDuff.Mode.MULTIPLY); arrow2.setColorFilter(arrowColorInt, PorterDuff.Mode.MULTIPLY); } public interface OnSwipeListener { void onSwipeConfirm(); } public void setText(CharSequence text) { this.btnText = text; contentTv.setText(text); } public CharSequence getText() { return this.btnText; } public void setTextColor(@ColorInt int textColor) { this.textColorInt = textColor; contentTv.setTextColor(textColor); } @ColorInt public int getTextColor() { return this.textColorInt; } public void setBackgroundColor(@ColorInt int bgColor) { this.bgColorInt = bgColor; gradientDrawable.setColor(bgColor); updateBackground(); } @ColorInt public int getBackgroundColor() { return this.bgColorInt; } public void setCornerRadius(float cornerRadius) { this.btnRadius = cornerRadius; } public float getCornerRadius() { return this.btnRadius; } public int getArrowColorRes() { return this.arrowColorInt; } /** * Include alpha in arrowColor for transparency (ex: #33FFFFFF) */ public void setArrowColor(int arrowColor) { this.arrowColorInt = arrowColor; tintArrowHint(); } public void setTextSize(@Dimension float textSize) { this.textSize = textSize; contentTv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); } @Dimension public float getTextSize() { return this.textSize; } /** * How much of the button must the user swipe to trigger the OnSwipeListener successfully * * @param swipeDistance float from 0.0 to 1.0 where 1.0 means user must swipe the button fully from end to end. Default is 0.85. */ public void setSwipeDistance(@Dimension float swipeDistance) { if (swipeDistance > 1.0f) { swipeDistance = 1.0f; } if (swipeDistance < 0.0f) { swipeDistance = 0.0f; } this.swipeDistance = swipeDistance; } @Dimension public float getSwipeDistance() { return this.swipeDistance; } public void setOnSwipeListener(@Nullable OnSwipeListener customSwipeListener) { this.swipeListener = customSwipeListener; } }