com.bitants.wally.views.swipeclearlayout.SwipeClearLayout.java Source code

Java tutorial

Introduction

Here is the source code for com.bitants.wally.views.swipeclearlayout.SwipeClearLayout.java

Source

/*
 * Copyright (C) 2014 Freddie (Musenkishi) Lust-Hed
 *
 * 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.bitants.wally.views.swipeclearlayout;

import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ClipDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;

import com.musenkishi.wally.R;

/**
 * The SwipeRefreshLayout should be used whenever the user can refresh the
 * contents of a view via a vertical swipe gesture. The activity that
 * instantiates this view should add an OnRefreshListener to be notified
 * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout
 * will notify the listener each and every time the gesture is completed again;
 * the listener is responsible for correctly determining when to actually
 * initiate a refresh of its content. If the listener determines there should
 * not be a refresh, it must call setRefreshing(false) to cancel any visual
 * indication of a refresh. If an activity wishes to show just the progress
 * animation, it should call setRefreshing(true). To disable the gesture and
 * progress animation, call setEnabled(false) on the view.
 * <p>
 * This layout should be made the parent of the view that will be refreshed as a
 * result of the gesture and can only support one direct child. This view will
 * also be made the target of the gesture and will be forced to match both the
 * width and the height supplied in this layout. The SwipeRefreshLayout does not
 * provide accessibility events; instead, a menu item must be provided to allow
 * refresh of the content wherever this gesture is used.
 * </p>
 */
public class SwipeClearLayout extends RelativeLayout {
    private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
    private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
    private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
    private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
    private static final int REFRESH_TRIGGER_DISTANCE = 120;
    private static final int CIRCLE_SIZE = 48;
    private static final int CIRCLE_DEFAULT_COLOR = Color.MAGENTA;
    private static final int DEFAULT_ANIMATION_DURATION = 300 * 2;

    private View circle;
    private int circleTopMargin = 0;
    private int circleColor;
    private int duration = DEFAULT_ANIMATION_DURATION;

    private ProgressBar progressBar;

    private View target; //the content that gets pulled down
    private int originalOffsetTop;
    private OnRefreshListener listener;
    private OnSwipeListener onSwipeListener;
    private MotionEvent downEvent;
    private int from;
    private boolean refreshing = false;
    private int touchSlop;
    private float distanceToTriggerSync = -1;
    private float prevY;
    private int mediumAnimationDuration;
    private float currPercentage = 0;
    private int currentTargetOffsetTop;
    // Target is returning to its start offset because it was cancelled or a
    // refresh was triggered.
    private boolean returningToStart;
    private final DecelerateInterpolator decelerateInterpolator;
    private final AccelerateInterpolator accelerateInterpolator;
    private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.enabled };

    private final Animation animateToStartPosition = new Animation() {
        @Override
        public void applyTransformation(float interpolatedTime, Transformation t) {
            int targetTop = 0;
            if (from != originalOffsetTop) {
                targetTop = (from + (int) ((originalOffsetTop - from) * interpolatedTime));
            }
            int offset = targetTop - circle.getTop();
            final int currentTop = circle.getTop();
            if (offset + currentTop < 0) {
                offset = 0 - currentTop;
            }
            setTargetOffsetTopAndBottom(offset);
        }
    };

    private final AnimationListener returnToStartPositionListener = new BaseAnimationListener() {
        @Override
        public void onAnimationEnd(Animation animation) {
            // Once the target content has returned to its start position, reset
            // the target offset to 0
            currentTargetOffsetTop = 0;
        }
    };

    private final Runnable returnToStartPosition = new Runnable() {

        @Override
        public void run() {
            returningToStart = true;
            animateOffsetToStartPosition(currentTargetOffsetTop + getPaddingTop(), returnToStartPositionListener);
        }

    };

    // Cancel the refresh gesture and animate everything back to its original state.
    private final Runnable cancel = new Runnable() {

        @Override
        public void run() {
            returningToStart = true;
            // Timeout fired since the user last moved their finger; animate the
            // trigger to 0 and put the target back at its original position
            animateOffsetToStartPosition(currentTargetOffsetTop + getPaddingTop(), returnToStartPositionListener);
        }

    };
    private View filledView;

    /**
     * Simple constructor to use when creating a SwipeRefreshLayout from code.
     * @param context
     */
    public SwipeClearLayout(Context context) {
        this(context, null);
    }

    /**
     * Constructor that is called when inflating SwipeRefreshLayout from XML.
     * @param context
     * @param attrs
     */
    public SwipeClearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

        mediumAnimationDuration = getResources().getInteger(android.R.integer.config_mediumAnimTime);

        setWillNotDraw(false);
        final DisplayMetrics metrics = getResources().getDisplayMetrics();

        circle = generateCircle(context, attrs, metrics);
        progressBar = new ProgressBar(context, attrs);

        decelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
        accelerateInterpolator = new AccelerateInterpolator(ACCELERATE_INTERPOLATION_FACTOR);

        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
        setEnabled(a.getBoolean(0, true));
        initAttrs(context, attrs);
        a.recycle();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        final Resources.Theme theme = context.getTheme();
        if (theme != null) {
            final TypedArray typedArray = theme.obtainStyledAttributes(attrs, R.styleable.SwipeClearLayout, 0, 0);
            if (typedArray != null) {
                try {
                    circleTopMargin = (int) typedArray.getDimension(R.styleable.SwipeClearLayout_circleTopMargin,
                            0);
                    circleColor = typedArray.getColor(R.styleable.SwipeClearLayout_circleColor,
                            CIRCLE_DEFAULT_COLOR);
                    duration = typedArray.getInteger(R.styleable.SwipeClearLayout_duration,
                            DEFAULT_ANIMATION_DURATION);
                } finally {
                    typedArray.recycle();
                }
            }
        }
    }

    private View generateCircle(Context context, AttributeSet attrs, DisplayMetrics metrics) {
        ImageView view = new ImageView(context, attrs);
        GradientDrawable circle = (GradientDrawable) getResources().getDrawable(R.drawable.circle);
        circle.setColor(CIRCLE_DEFAULT_COLOR);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            view.setBackground(circle);
        } else {
            view.setBackgroundDrawable(circle);
        }
        int size = (int) (metrics.density * CIRCLE_SIZE);
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(size, size);
        view.setLayoutParams(params);
        view.setImageResource(R.drawable.clip_random);
        view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
        view.setRotation(90.0f);
        return view;
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        removeCallbacks(cancel);
        removeCallbacks(returnToStartPosition);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        removeCallbacks(returnToStartPosition);
        removeCallbacks(cancel);
    }

    private void animateOffsetToStartPosition(int from, AnimationListener listener) {
        this.from = from;
        animateToStartPosition.reset();
        animateToStartPosition.setDuration(mediumAnimationDuration);
        animateToStartPosition.setAnimationListener(listener);
        animateToStartPosition.setInterpolator(decelerateInterpolator);
        //        target.startAnimation(animateToStartPosition);
        circle.startAnimation(animateToStartPosition);
    }

    /**
     * Set the listener to be notified when a refresh is triggered via the swipe
     * gesture.
     */
    public void setOnRefreshListener(OnRefreshListener listener) {
        this.listener = listener;
    }

    /**
     * Set the listener to be notified when a swipe is triggered by the user.
     */
    public void setOnSwipeListener(OnSwipeListener listener) {
        onSwipeListener = listener;
    }

    private void setTriggerPercentage(float percent) {
        if (percent == 0f) {
            // No-op. A null trigger means it's uninitialized, and setting it to zero-percent
            // means we're trying to reset state, so there's nothing to reset in this case.
            currPercentage = 0;
            return;
        }
        currPercentage = percent;
    }

    /**
     * Notify the widget that refresh state has changed. Do not call this when
     * refresh is triggered by a swipe gesture.
     *
     * @param refreshing Whether or not the view should show refresh progress.
     */
    public void setRefreshing(boolean refreshing) {
        if (this.refreshing != refreshing) {
            ensureTarget();
            currPercentage = 0;
            this.refreshing = refreshing;
            if (this.refreshing) {
                animateCircle();
                progressBar.animate().alpha(1.0f).setStartDelay(duration / 2).setDuration(duration / 2).start();
            } else {
                progressBar.animate().alpha(0.0f).setStartDelay(0).setDuration(200).start();
                getChildAt(0).setAlpha(1.0f);
            }
        }
    }

    /**
     * Set the color for the swipe circle.
     * @param colorResId Color resource.
     */
    public void setCircleColorResourceId(int colorResId) {
        ensureTarget();
        final Resources resources = getResources();
        circleColor = resources.getColor(colorResId);
        ((GradientDrawable) circle.getBackground()).setColor(circleColor);
    }

    /**
     * Set the color for the swipe circle.
     * @param color Color.
     */
    public void setCircleColor(int color) {
        ensureTarget();
        circleColor = color;
        ((GradientDrawable) circle.getBackground()).setColor(circleColor);
    }

    /**
     * Set the top margin of the circle.
     */
    public void setCircleTopMargin(int topMargin) {
        circleTopMargin = topMargin;
    }

    public ProgressBar getProgressBar() {
        return progressBar;
    }

    public void setProgressBar(ProgressBar progressBar) {
        this.progressBar = progressBar;
    }

    /**
     * @return Whether the SwipeClearWidget is actively showing refresh
     *         progress.
     */
    public boolean isRefreshing() {
        return refreshing;
    }

    private void ensureTarget() {
        // Don't bother getting the parent height if the parent hasn't been laid out yet.
        if (target == null) {
            if (getChildCount() > 4 && !isInEditMode()) {
                throw new IllegalStateException("SwipeClearLayout can host only one direct child");
            }
            target = getChildAt(0);
            originalOffsetTop = circle.getTop() + getPaddingTop();
        }
        if (distanceToTriggerSync == -1) {
            if (getParent() != null && ((View) getParent()).getHeight() > 0) {
                final DisplayMetrics metrics = getResources().getDisplayMetrics();
                distanceToTriggerSync = (int) Math.min(((View) getParent()).getHeight() * MAX_SWIPE_DISTANCE_FACTOR,
                        REFRESH_TRIGGER_DISTANCE * metrics.density);
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        final int width = getMeasuredWidth();
        final int height = getMeasuredHeight();
        if (getChildCount() == 0) {
            return;
        }
        final View child = getChildAt(0);
        final int childLeft = getPaddingLeft();
        final int childTop = currentTargetOffsetTop + getPaddingTop();
        final int childWidth = width - getPaddingLeft() - getPaddingRight();
        final int childHeight = height - getPaddingTop() - getPaddingBottom();
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);

        if (getChildAt(1) == null) {
            final DisplayMetrics metrics = getResources().getDisplayMetrics();
            int size = (int) (metrics.density * CIRCLE_SIZE);
            @SuppressLint("DrawAllocation")
            final RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(size, size);
            //The circle should start to grow from the top of SwipeClearLayout, hence "-(size/2)".
            //            params.topMargin = circleTopMargin - size;
            params.addRule(CENTER_HORIZONTAL, TRUE);
            circle.setLayoutParams(params);
            addView(circle, 1);
            circle.setScaleX(1.0f);
            circle.setScaleY(1.0f);
            ((GradientDrawable) circle.getBackground()).setColor(circleColor);
            circle.setTranslationY(-size);
        }

        if (getChildAt(2) == null) {
            addView(progressBar, 2);
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) progressBar.getLayoutParams();
            params.addRule(CENTER_IN_PARENT, TRUE);
            progressBar.setLayoutParams(params);
            progressBar.setAlpha(0.0f);
        }
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (getChildCount() > 4 && !isInEditMode()) {
            throw new IllegalStateException("SwipeClearLayout can host only one direct child");
        }
        if (getChildCount() > 0) {
            getChildAt(0).measure(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
                            MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
                            MeasureSpec.EXACTLY));
        }
    }

    /**
     * @return Whether it is possible for the child view of this layout to
     *         scroll up. Override this if the child view is a custom view.
     */
    public boolean canChildScrollUp() {
        if (Build.VERSION.SDK_INT < 14) {
            if (target instanceof AbsListView) {
                final AbsListView absListView = (AbsListView) target;
                return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0
                        || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
            } else {
                return target.getScrollY() > 0;
            }
        } else {
            return ViewCompat.canScrollVertically(target, -1);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();
        boolean handled = false;
        if (returningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) {
            returningToStart = false;
        }
        if (isEnabled() && !returningToStart && !canChildScrollUp()) {
            handled = onTouchEvent(ev);
        }
        return !handled ? super.onInterceptTouchEvent(ev) : handled;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean b) {
        // Nope.
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        final int action = event.getAction();
        boolean handled = false;
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            currPercentage = 0;
            downEvent = MotionEvent.obtain(event);
            prevY = downEvent.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            if (downEvent != null && !returningToStart) {
                final float eventY = event.getY();
                float yDiff = eventY - downEvent.getY();
                if (yDiff > touchSlop) {
                    // User velocity passed min velocity; trigger a refresh
                    if (yDiff > distanceToTriggerSync) {
                        // User movement passed distance; trigger a refresh
                        startRefresh();
                        handled = true;
                        break;
                    } else {
                        // Just track the user's movement
                        setTriggerPercentage(
                                accelerateInterpolator.getInterpolation(yDiff / distanceToTriggerSync));
                        float offsetTop = yDiff;
                        if (prevY > eventY) {
                            offsetTop = yDiff - touchSlop;
                        }
                        updateContentOffsetTop((int) (offsetTop));
                        //                            if (prevY > eventY && (target.getTop() < touchSlop)) {
                        if (prevY > eventY && (circle.getTop() < touchSlop)) {
                            // If the user puts the view back at the top, we
                            // don't need to. This shouldn't be considered
                            // cancelling the gesture as the user can restart from the top.
                            removeCallbacks(cancel);
                        } else {
                            updatePositionTimeout();
                        }
                        prevY = event.getY();
                        handled = true;
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            if (downEvent != null) {
                downEvent.recycle();
                downEvent = null;
            }
            break;
        }
        return handled;
    }

    private void startRefresh() {
        removeCallbacks(cancel);
        returnToStartPosition.run();
        setRefreshing(true);
        listener.onRefresh();
    }

    private void animateCircle() {
        float currentScale = circle.getScaleX();

        if (currentScale == 0.0f) {
            circle.setScaleX(1.0f);
            circle.setScaleY(1.0f);
            currentScale = 1.0f;
        }

        int currentPixelSize = (int) (circle.getHeight() * currentScale);

        if (currentPixelSize == 0) {
            return;
        }

        int width = getMeasuredWidth();
        int height = getMeasuredHeight();

        int hypotenuse = (int) Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));

        float goalScale = (hypotenuse / currentPixelSize) * 2;

        circle.setAlpha(1.0f);
        circle.animate().scaleX(goalScale).scaleY(goalScale).setDuration(duration / 2)
                .setListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                    }

                    @Override
                    public void onAnimationEnd(Animator animator) {

                        if (isRefreshing()) {
                            getChildAt(0).setAlpha(0.0f); //list should be invisible until it stops refreshing
                        }

                        if (getChildAt(3) == null) {
                            filledView = new View(getContext());
                            ColorDrawable colorDrawable = new ColorDrawable(circleColor);
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                                filledView.setBackground(colorDrawable);
                            } else {
                                filledView.setBackgroundDrawable(colorDrawable);
                            }
                            addView(filledView, 3);
                            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) filledView
                                    .getLayoutParams();
                            params.width = RelativeLayout.LayoutParams.MATCH_PARENT;
                            params.height = RelativeLayout.LayoutParams.MATCH_PARENT;
                            filledView.setLayoutParams(params);
                        }
                        filledView.setVisibility(VISIBLE);
                        filledView.setAlpha(1.0f);
                        filledView.animate().alpha(0.0f).setDuration(duration / 2)
                                .setListener(new Animator.AnimatorListener() {
                                    @Override
                                    public void onAnimationStart(Animator animator) {
                                    }

                                    @Override
                                    public void onAnimationEnd(Animator animator) {
                                        //                        removeViewAt(3);
                                    }

                                    @Override
                                    public void onAnimationCancel(Animator animator) {
                                    }

                                    @Override
                                    public void onAnimationRepeat(Animator animator) {
                                    }
                                }).start();
                        circle.setScaleX(1.0f);
                        circle.setScaleY(1.0f);
                        circle.setAlpha(1.0f);
                        circle.setTranslationY(-circle.getHeight());
                    }

                    @Override
                    public void onAnimationCancel(Animator animator) {

                    }

                    @Override
                    public void onAnimationRepeat(Animator animator) {

                    }
                }).start();
    }

    private void updateContentOffsetTop(int targetTop) {
        final int currentTop = circle.getTop();
        if (targetTop > distanceToTriggerSync) {
            targetTop = (int) distanceToTriggerSync;
        } else if (targetTop < 0) {
            targetTop = 0;
        }
        setTargetOffsetTopAndBottom(targetTop - currentTop);
    }

    private void setTargetOffsetTopAndBottom(int offset) {
        circle.offsetTopAndBottom(offset);
        currentTargetOffsetTop = circle.getTop();
        int percent = (int) ((currentTargetOffsetTop / distanceToTriggerSync) * 100);
        if (onSwipeListener != null) {
            onSwipeListener.onSwipe(percent, currentTargetOffsetTop);
        }
        ViewCompat.setElevation(circle, percent);
        ImageView imageView = (ImageView) circle;
        ClipDrawable clipDrawable = (ClipDrawable) imageView.getDrawable();

        if (percent < 50) {
            clipDrawable.setLevel(0);
        } else {
            clipDrawable.setLevel((percent - 50) * 2 * 100);
        }
    }

    private void updatePositionTimeout() {
        removeCallbacks(cancel);
        postDelayed(cancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
    }

    /**
     * Classes that wish to be notified when the swipe gesture correctly
     * triggers a refresh should implement this interface.
     */
    public interface OnRefreshListener {
        public void onRefresh();
    }

    /**
     * Classes that wish to be notified how much there's left until a
     * refresh is triggered should implement this interface.
     */
    public interface OnSwipeListener {
        /**
         * Called when user starts pulling the layout for a refresh.
         * @param progress How much is left (in percent) until a refresh is triggered.
         * @param pixels How much the list has moved in pixels
         */
        abstract void onSwipe(int progress, int pixels);
    }

    /**
     * Simple AnimationListener to avoid having to implement unneeded methods in
     * AnimationListeners.
     */
    private class BaseAnimationListener implements AnimationListener {
        @Override
        public void onAnimationStart(Animation animation) {
        }

        @Override
        public void onAnimationEnd(Animation animation) {
        }

        @Override
        public void onAnimationRepeat(Animation animation) {
        }
    }
}