com.cleveroad.audiowidget.AudioWidget.java Source code

Java tutorial

Introduction

Here is the source code for com.cleveroad.audiowidget.AudioWidget.java

Source

package com.cleveroad.audiowidget;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Vibrator;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.view.Gravity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;

import com.afollestad.appthemeengine.Config;
import com.rks.musicx.R;
import com.rks.musicx.misc.utils.Helper;

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.Random;
import java.util.WeakHashMap;

/*
 * 2017 Rajneesh Singh
 * 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.
 */

public class AudioWidget {

    private static final long VIBRATION_DURATION = 100;
    private final PlayPauseButton playPauseButton;

    private final ExpandCollapseWidget expandCollapseWidget;

    private final RemoveWidgetView removeWidgetView;
    private final Controller controller;
    private final WindowManager windowManager;
    private final Vibrator vibrator;
    private final Handler handler;
    private final Point screenSize;
    private final Context context;
    private final TouchManager playPauseButtonManager;
    private final TouchManager expandedWidgetManager;
    private final TouchManager.BoundsChecker ppbToExpBoundsChecker;
    private final TouchManager.BoundsChecker expToPpbBoundsChecker;
    private final Map<Integer, WeakReference<Drawable>> albumCoverCache = new WeakHashMap<>();
    private final RectF removeBounds;
    private final Point hiddenRemWidPos;
    private final Point visibleRemWidPos;
    private final OnControlsClickListenerWrapper onControlsClickListener;
    private PlaybackState playbackState;
    private int animatedRemBtnYPos = -1;
    private float widgetWidth, widgetHeight, radius;
    private boolean shown;
    private boolean released;
    private boolean removeWidgetShown;
    private OnWidgetStateChangedListener onWidgetStateChangedListener;
    private int accentColor;
    private int primaryColor;
    private String ateKey;

    @SuppressWarnings("deprecation")
    private AudioWidget(@NonNull Builder builder) {
        this.context = builder.context.getApplicationContext();
        this.vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
        this.handler = new Handler();
        this.screenSize = new Point();
        this.removeBounds = new RectF();
        this.hiddenRemWidPos = new Point();
        this.visibleRemWidPos = new Point();
        this.controller = newController();
        this.windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getSize(screenSize);
        screenSize.y -= statusBarHeight() + navigationBarHeight();

        Configuration configuration = prepareConfiguration(builder);
        playPauseButton = new PlayPauseButton(configuration);
        expandCollapseWidget = new ExpandCollapseWidget(configuration);
        removeWidgetView = new RemoveWidgetView(configuration);
        int offsetCollapsed = context.getResources().getDimensionPixelOffset(R.dimen.aw_edge_offset_collapsed);
        int offsetExpanded = context.getResources().getDimensionPixelOffset(R.dimen.aw_edge_offset_expanded);
        playPauseButtonManager = new TouchManager(playPauseButton,
                playPauseButton.newBoundsChecker(
                        builder.edgeOffsetXCollapsedSet ? builder.edgeOffsetXCollapsed : offsetCollapsed,
                        builder.edgeOffsetYCollapsedSet ? builder.edgeOffsetYCollapsed : offsetCollapsed))
                                .screenWidth(screenSize.x).screenHeight(screenSize.y);
        expandedWidgetManager = new TouchManager(expandCollapseWidget,
                expandCollapseWidget.newBoundsChecker(
                        builder.edgeOffsetXExpandedSet ? builder.edgeOffsetXExpanded : offsetExpanded,
                        builder.edgeOffsetYExpandedSet ? builder.edgeOffsetYExpanded : offsetExpanded))
                                .screenWidth(screenSize.x).screenHeight(screenSize.y);

        playPauseButtonManager.callback(new PlayPauseButtonCallback());
        expandedWidgetManager.callback(new ExpandCollapseWidgetCallback());
        expandCollapseWidget.onWidgetStateChangedListener(new OnWidgetStateChangedListener() {
            @Override
            public void onWidgetStateChanged(@NonNull State state) {
                if (state == State.COLLAPSED) {
                    playPauseButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
                    try {
                        windowManager.removeView(expandCollapseWidget);
                    } catch (IllegalArgumentException e) {
                        // view not attached to window
                    }
                    playPauseButton.enableProgressChanges(true);
                }
                if (onWidgetStateChangedListener != null) {
                    onWidgetStateChangedListener.onWidgetStateChanged(state);
                }
            }

            @Override
            public void onWidgetPositionChanged(int cx, int cy) {

            }
        });
        onControlsClickListener = new OnControlsClickListenerWrapper();
        expandCollapseWidget.onControlsClickListener(onControlsClickListener);
        ppbToExpBoundsChecker = playPauseButton.newBoundsChecker(
                builder.edgeOffsetXExpandedSet ? builder.edgeOffsetXExpanded : offsetExpanded,
                builder.edgeOffsetYExpandedSet ? builder.edgeOffsetYExpanded : offsetExpanded);
        expToPpbBoundsChecker = expandCollapseWidget.newBoundsChecker(
                builder.edgeOffsetXCollapsedSet ? builder.edgeOffsetXCollapsed : offsetCollapsed,
                builder.edgeOffsetYCollapsedSet ? builder.edgeOffsetYCollapsed : offsetCollapsed);
    }

    private Configuration prepareConfiguration(@NonNull Builder builder) {
        /*int darkColor = builder.darkColorSet ? builder.darkColor : ContextCompat.getColor(context, R.color.colorPrimary);
        int lightColor = builder.lightColorSet ? builder.lightColor : ContextCompat.getColor(context, R.color.colorAccent);
        int progressColor = builder.progressColorSet ? builder.progressColor : ContextCompat.getColor(context, R.color.colorPrimaryDarkTheme);
        int expandColor = builder.expandWidgetColorSet ? builder.expandWidgetColor : ContextCompat.getColor(context, R.color.colorPrimary);
        int crossColor = builder.crossColorSet ? builder.crossColor : ContextCompat.getColor(context, R.color.colorAccent);
        int crossOverlappedColor = builder.crossOverlappedColorSet ? builder.crossOverlappedColor : ContextCompat.getColor(context, R.color.colorPrimaryLight);*/
        int shadowColor = builder.shadowColorSet ? builder.shadowColor
                : ContextCompat.getColor(context, R.color.shadowbg);

        Drawable playDrawable = builder.playDrawable != null ? builder.playDrawable
                : ContextCompat.getDrawable(context, R.drawable.aw_ic_play);
        Drawable pauseDrawable = builder.pauseDrawable != null ? builder.pauseDrawable
                : ContextCompat.getDrawable(context, R.drawable.aw_ic_pause);
        Drawable prevDrawable = builder.prevDrawable != null ? builder.prevDrawable
                : ContextCompat.getDrawable(context, R.drawable.aw_ic_prev);
        Drawable nextDrawable = builder.nextDrawable != null ? builder.nextDrawable
                : ContextCompat.getDrawable(context, R.drawable.aw_ic_next);
        Drawable playlistDrawable = builder.playlistDrawable != null ? builder.playlistDrawable
                : ContextCompat.getDrawable(context, R.mipmap.ic_launcher);
        Drawable albumDrawable = builder.defaultAlbumDrawable != null ? builder.defaultAlbumDrawable
                : ContextCompat.getDrawable(context, R.mipmap.ic_launcher);

        int buttonPadding = builder.buttonPaddingSet ? builder.buttonPadding
                : context.getResources().getDimensionPixelSize(R.dimen.aw_button_padding);
        float crossStrokeWidth = builder.crossStrokeWidthSet ? builder.crossStrokeWidth
                : context.getResources().getDimension(R.dimen.aw_cross_stroke_width);
        float progressStrokeWidth = builder.progressStrokeWidthSet ? builder.progressStrokeWidth
                : context.getResources().getDimension(R.dimen.aw_progress_stroke_width);
        float shadowRadius = builder.shadowRadiusSet ? builder.shadowRadius
                : context.getResources().getDimension(R.dimen.aw_shadow_radius);
        float shadowDx = builder.shadowDxSet ? builder.shadowDx
                : context.getResources().getDimension(R.dimen.aw_shadow_dx);
        float shadowDy = builder.shadowDySet ? builder.shadowDy
                : context.getResources().getDimension(R.dimen.aw_shadow_dy);
        float bubblesMinSize = builder.bubblesMinSizeSet ? builder.bubblesMinSize
                : context.getResources().getDimension(R.dimen.aw_bubbles_min_size);
        float bubblesMaxSize = builder.bubblesMaxSizeSet ? builder.bubblesMaxSize
                : context.getResources().getDimension(R.dimen.aw_bubbles_max_size);
        int prevNextExtraPadding = context.getResources()
                .getDimensionPixelSize(R.dimen.aw_prev_next_button_extra_padding);

        widgetHeight = context.getResources().getDimensionPixelSize(R.dimen.aw_player_height);
        widgetWidth = context.getResources().getDimensionPixelSize(R.dimen.aw_player_width);
        radius = widgetHeight / 2f;
        playbackState = new PlaybackState();
        ateKey = Helper.getATEKey(context);
        accentColor = Config.accentColor(context, ateKey);
        primaryColor = Config.primaryColor(context, ateKey);
        return new Configuration.Builder().context(context).playbackState(playbackState).random(new Random())
                .accDecInterpolator(new AccelerateDecelerateInterpolator()).darkColor(primaryColor)
                .playColor(primaryColor).progressColor(accentColor).expandedColor(primaryColor)
                .widgetWidth(widgetWidth).radius(radius).playlistDrawable(playlistDrawable)
                .playDrawable(playDrawable).prevDrawable(prevDrawable).nextDrawable(nextDrawable)
                .pauseDrawable(pauseDrawable).albumDrawable(albumDrawable).buttonPadding(buttonPadding)
                .prevNextExtraPadding(prevNextExtraPadding).crossStrokeWidth(crossStrokeWidth)
                .progressStrokeWidth(progressStrokeWidth).shadowRadius(shadowRadius).shadowDx(shadowDx)
                .shadowDy(shadowDy).shadowColor(shadowColor).bubblesMinSize(bubblesMinSize)
                .bubblesMaxSize(bubblesMaxSize).crossColor(accentColor).crossOverlappedColor(accentColor).build();
    }

    private int statusBarHeight() {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    private int navigationBarHeight() {
        if (hasNavigationBar()) {
            int result = 0;
            int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
            if (resourceId > 0) {
                result = context.getResources().getDimensionPixelSize(resourceId);
            }
            return result;
        }
        return 0;
    }

    private boolean hasNavigationBar() {
        boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK);
        boolean hasHomeKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME);
        int id = context.getResources().getIdentifier("config_showNavigationBar", "bool", "android");
        return !hasBackKey && !hasHomeKey || id > 0 && context.getResources().getBoolean(id);
    }

    @NonNull
    private Controller newController() {
        return new Controller() {

            @Override
            public void start() {
                playbackState.start(this);
            }

            @Override
            public void pause() {
                playbackState.pause(this);
            }

            @Override
            public void stop() {
                playbackState.stop(this);
            }

            @Override
            public int duration() {
                return playbackState.duration();
            }

            @Override
            public void duration(int duration) {
                playbackState.duration(duration);
            }

            @Override
            public int position() {
                return playbackState.position();
            }

            @Override
            public void position(int position) {
                playbackState.position(position);
            }

            @Override
            public void onControlsClickListener(@Nullable OnControlsClickListener onControlsClickListener) {
                AudioWidget.this.onControlsClickListener.onControlsClickListener(onControlsClickListener);
            }

            @Override
            public void onWidgetStateChangedListener(
                    @Nullable OnWidgetStateChangedListener onWidgetStateChangedListener) {
                AudioWidget.this.onWidgetStateChangedListener = onWidgetStateChangedListener;
            }

            @Override
            public void albumCover(@Nullable Drawable albumCover) {
                expandCollapseWidget.albumCover(albumCover);
                playPauseButton.albumCover(albumCover);
            }

            @Override
            public void albumCoverBitmap(@Nullable Bitmap bitmap) {
                if (bitmap == null) {
                    return;
                }
                WeakReference<Drawable> wrDrawable = albumCoverCache.get(bitmap.hashCode());
                if (wrDrawable != null) {
                    Drawable drawable = wrDrawable.get();
                    if (drawable != null) {
                        expandCollapseWidget.albumCover(drawable);
                        playPauseButton.albumCover(drawable);
                        return;
                    }
                }
                Drawable albumCover = new BitmapDrawable(context.getResources(), bitmap);
                expandCollapseWidget.albumCover(albumCover);
                playPauseButton.albumCover(albumCover);
                albumCoverCache.put(bitmap.hashCode(), new WeakReference<>(albumCover));
            }
        };
    }

    public void show(int cx, int cy) {
        shown = true;
        widgetHeight = context.getResources().getDimensionPixelSize(R.dimen.aw_player_height);
        widgetWidth = context.getResources().getDimensionPixelSize(R.dimen.aw_player_width);
        float remWidX = screenSize.x / 2f - radius * RemoveWidgetView.SCALE_LARGE;
        hiddenRemWidPos.set((int) remWidX, (int) (screenSize.y + widgetHeight + navigationBarHeight()));
        visibleRemWidPos.set((int) remWidX,
                (int) (screenSize.y - radius - (hasNavigationBar() ? 0 : widgetHeight)));
        try {
            show(removeWidgetView, hiddenRemWidPos.x, hiddenRemWidPos.y);
        } catch (IllegalArgumentException e) {
            // widget not removed yet, animation in progress
        }
        show(playPauseButton, (int) (cx - widgetHeight), (int) (cy - widgetHeight));
        playPauseButtonManager.animateToBounds();
    }

    public void hide() {
        hideInternal(true);
    }

    private void hideInternal(boolean byPublic) {
        if (!shown) {
            return;
        }
        shown = false;
        released = true;
        if (byPublic) {
            try {
                windowManager.removeView(playPauseButton);
                windowManager.removeView(expandCollapseWidget);
                windowManager.removeView(removeWidgetView);
            } catch (IllegalArgumentException e) {
                // view not attached to window
            }
            if (onWidgetStateChangedListener != null) {
                onWidgetStateChangedListener.onWidgetStateChanged(State.REMOVED);
                // onWidgetStateChangedListener.onWidgetPositionChanged(100, 100);
            }
        }
    }

    public boolean isShown() {
        return shown;
    }

    public void expand() {
        removeWidgetShown = false;
        playPauseButton.enableProgressChanges(false);
        playPauseButton.postDelayed(this::checkSpaceAndShowExpanded, PlayPauseButton.PROGRESS_CHANGES_DURATION);
    }

    public void collapse() {
        expandCollapseWidget.setCollapseListener(playPauseButton::setAlpha);

        WindowManager.LayoutParams params = (WindowManager.LayoutParams) expandCollapseWidget.getLayoutParams();
        int cx = params.x + expandCollapseWidget.getWidth() / 2;
        if (cx > screenSize.x / 2) {
            expandCollapseWidget.expandDirection(ExpandCollapseWidget.DIRECTION_LEFT);
        } else {
            expandCollapseWidget.expandDirection(ExpandCollapseWidget.DIRECTION_RIGHT);
        }
        updatePlayPauseButtonPosition();
        if (expandCollapseWidget.collapse()) {
            playPauseButtonManager.animateToBounds();
            expandedWidgetManager.animateToBounds(expToPpbBoundsChecker, null);
        }
    }

    private void updatePlayPauseButtonPosition() {
        WindowManager.LayoutParams widgetParams = (WindowManager.LayoutParams) expandCollapseWidget
                .getLayoutParams();
        WindowManager.LayoutParams params = (WindowManager.LayoutParams) playPauseButton.getLayoutParams();
        if (expandCollapseWidget.expandDirection() == ExpandCollapseWidget.DIRECTION_RIGHT) {
            params.x = (int) (widgetParams.x - radius);
        } else {
            params.x = (int) (widgetParams.x + widgetWidth - widgetHeight - radius);
        }
        params.y = widgetParams.y;
        try {
            windowManager.updateViewLayout(playPauseButton, params);
        } catch (IllegalArgumentException e) {
            // view not attached to window
        }
        if (onWidgetStateChangedListener != null) {
            onWidgetStateChangedListener.onWidgetPositionChanged((int) (params.x + widgetHeight),
                    (int) (params.y + widgetHeight));
        }
    }

    @SuppressWarnings("deprecation")
    private void checkSpaceAndShowExpanded() {
        WindowManager.LayoutParams params = (WindowManager.LayoutParams) playPauseButton.getLayoutParams();
        int x = params.x;
        int y = params.y;
        int expandDirection;
        if (x + widgetHeight > screenSize.x / 2) {
            expandDirection = ExpandCollapseWidget.DIRECTION_LEFT;
        } else {
            expandDirection = ExpandCollapseWidget.DIRECTION_RIGHT;
        }

        playPauseButtonManager.animateToBounds(ppbToExpBoundsChecker, () -> {
            WindowManager.LayoutParams params1 = (WindowManager.LayoutParams) playPauseButton.getLayoutParams();
            int x1 = params1.x;
            int y1 = params1.y;
            if (expandDirection == ExpandCollapseWidget.DIRECTION_LEFT) {
                x1 -= widgetWidth - widgetHeight * 1.5f;
            } else {
                x1 += widgetHeight / 2f;
            }
            show(expandCollapseWidget, x1, y1);
            playPauseButton.setLayerType(View.LAYER_TYPE_NONE, null);

            expandCollapseWidget.setExpandListener(percent -> playPauseButton.setAlpha(1f - percent));
            expandCollapseWidget.expand(expandDirection);
        });
    }

    @NonNull
    public Controller controller() {
        return controller;
    }

    public void setAlbumArt(@NonNull Bitmap bitmap) {
        if (controller != null) {
            controller.albumCoverBitmap(bitmap);
        }
    }

    public void Pause() {
        if (controller != null) {
            controller.pause();
        }
    }

    public void Start() {
        if (controller != null) {
            controller.start();
        }
    }

    public void Stop() {
        if (controller != null) {
            controller.stop();
        }
    }

    public void Pos(int pos) {
        if (controller != null) {
            controller.position(pos);
        }
    }

    public void cleanUp() {
        if (controller != null) {
            controller.onControlsClickListener(null);
            controller.onWidgetStateChangedListener(null);
        }
    }

    public void Dur(int dur) {
        if (controller != null) {
            controller.duration(dur);
        }
    }

    private void show(View view, int left, int top) {
        if (view.getParent() != null || view.getWindowToken() != null) {
            windowManager.removeView(view);
        }
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
                PixelFormat.TRANSLUCENT);
        params.gravity = Gravity.START | Gravity.TOP;
        params.x = left;
        params.y = top;
        windowManager.addView(view, params);
    }

    public Map<Integer, WeakReference<Drawable>> getAlbumCoverCache() {
        return albumCoverCache;
    }

    public Controller getController() {
        return controller;
    }

    public enum State {
        COLLAPSED, EXPANDED, REMOVED
    }

    public interface Controller {

        void start();

        void pause();

        void stop();

        int duration();

        void duration(int duration);

        int position();

        void position(int position);

        void onControlsClickListener(@Nullable OnControlsClickListener onControlsClickListener);

        void onWidgetStateChangedListener(@Nullable OnWidgetStateChangedListener onWidgetStateChangedListener);

        void albumCover(@Nullable Drawable albumCover);

        void albumCoverBitmap(@Nullable Bitmap albumCover);
    }

    public interface OnControlsClickListener {

        boolean onPlaylistClicked();

        void onPlaylistLongClicked();

        void onPreviousClicked();

        void onPreviousLongClicked();

        boolean onPlayPauseClicked();

        void onPlayPauseLongClicked();

        void onNextClicked();

        void onNextLongClicked();

        void onAlbumClicked();

        void onAlbumLongClicked();
    }

    public interface OnWidgetStateChangedListener {

        void onWidgetStateChanged(@NonNull State state);

        void onWidgetPositionChanged(int cx, int cy);
    }

    abstract static class BoundsCheckerWithOffset implements TouchManager.BoundsChecker {

        private int offsetX, offsetY;

        public BoundsCheckerWithOffset(int offsetX, int offsetY) {
            this.offsetX = offsetX;
            this.offsetY = offsetY;
        }

        @Override
        public final float stickyLeftSide(float screenWidth) {
            return stickyLeftSideImpl(screenWidth) + offsetX;
        }

        @Override
        public final float stickyRightSide(float screenWidth) {
            return stickyRightSideImpl(screenWidth) - offsetX;
        }

        @Override
        public final float stickyTopSide(float screenHeight) {
            return stickyTopSideImpl(screenHeight) + offsetY;
        }

        @Override
        public final float stickyBottomSide(float screenHeight) {
            return stickyBottomSideImpl(screenHeight) - offsetY;
        }

        protected abstract float stickyLeftSideImpl(float screenWidth);

        protected abstract float stickyRightSideImpl(float screenWidth);

        protected abstract float stickyTopSideImpl(float screenHeight);

        protected abstract float stickyBottomSideImpl(float screenHeight);
    }

    public static class Builder {

        private final Context context;

        @ColorInt
        private int darkColor;
        @ColorInt
        private int lightColor;
        @ColorInt
        private int progressColor;
        @ColorInt
        private int crossColor;
        @ColorInt
        private int crossOverlappedColor;
        @ColorInt
        private int shadowColor;
        @ColorInt
        private int expandWidgetColor;
        private int buttonPadding;
        private float crossStrokeWidth;
        private float progressStrokeWidth;
        private float shadowRadius;
        private float shadowDx;
        private float shadowDy;
        private float bubblesMinSize;
        private float bubblesMaxSize;
        private Drawable playDrawable;
        private Drawable prevDrawable;
        private Drawable nextDrawable;
        private Drawable playlistDrawable;
        private Drawable defaultAlbumDrawable;
        private Drawable pauseDrawable;
        private boolean darkColorSet;
        private boolean lightColorSet;
        private boolean progressColorSet;
        private boolean crossColorSet;
        private boolean crossOverlappedColorSet;
        private boolean shadowColorSet;
        private boolean expandWidgetColorSet;
        private boolean buttonPaddingSet;
        private boolean crossStrokeWidthSet;
        private boolean progressStrokeWidthSet;
        private boolean shadowRadiusSet;
        private boolean shadowDxSet;
        private boolean shadowDySet;
        private boolean bubblesMinSizeSet;
        private boolean bubblesMaxSizeSet;
        private int edgeOffsetXCollapsed;
        private int edgeOffsetYCollapsed;
        private int edgeOffsetXExpanded;
        private int edgeOffsetYExpanded;
        private boolean edgeOffsetXCollapsedSet;
        private boolean edgeOffsetYCollapsedSet;
        private boolean edgeOffsetXExpandedSet;
        private boolean edgeOffsetYExpandedSet;

        public Builder(@NonNull Context context) {
            this.context = context;
        }

        public Builder darkColor(@ColorInt int darkColor) {
            this.darkColor = darkColor;
            darkColorSet = true;
            return this;
        }

        public Builder lightColor(@ColorInt int lightColor) {
            this.lightColor = lightColor;
            lightColorSet = true;
            return this;
        }

        public Builder progressColor(@ColorInt int progressColor) {
            this.progressColor = progressColor;
            progressColorSet = true;
            return this;
        }

        public Builder crossColor(@ColorInt int crossColor) {
            this.crossColor = crossColor;
            crossColorSet = true;
            return this;
        }

        public Builder crossOverlappedColor(@ColorInt int crossOverlappedColor) {
            this.crossOverlappedColor = crossOverlappedColor;
            crossOverlappedColorSet = true;
            return this;
        }

        public Builder shadowColor(@ColorInt int shadowColor) {
            this.shadowColor = shadowColor;
            shadowColorSet = true;
            return this;
        }

        public Builder expandWidgetColor(@ColorInt int expandWidgetColor) {
            this.expandWidgetColor = expandWidgetColor;
            expandWidgetColorSet = true;
            return this;
        }

        public Builder buttonPadding(int buttonPadding) {
            this.buttonPadding = buttonPadding;
            buttonPaddingSet = true;
            return this;
        }

        public Builder crossStrokeWidth(float crossStrokeWidth) {
            this.crossStrokeWidth = crossStrokeWidth;
            crossStrokeWidthSet = true;
            return this;
        }

        public Builder progressStrokeWidth(float progressStrokeWidth) {
            this.progressStrokeWidth = progressStrokeWidth;
            progressStrokeWidthSet = true;
            return this;
        }

        public Builder shadowRadius(float shadowRadius) {
            this.shadowRadius = shadowRadius;
            shadowRadiusSet = true;
            return this;
        }

        public Builder shadowDx(float shadowDx) {
            this.shadowDx = shadowDx;
            shadowDxSet = true;
            return this;
        }

        public Builder shadowDy(float shadowDy) {
            this.shadowDy = shadowDy;
            shadowDySet = true;
            return this;
        }

        public Builder bubblesMinSize(float bubblesMinSize) {
            this.bubblesMinSize = bubblesMinSize;
            bubblesMinSizeSet = true;
            return this;
        }

        public Builder bubblesMaxSize(float bubblesMaxSize) {
            this.bubblesMaxSize = bubblesMaxSize;
            bubblesMaxSizeSet = true;
            return this;
        }

        public Builder playDrawable(@NonNull Drawable playDrawable) {
            this.playDrawable = playDrawable;
            return this;
        }

        public Builder prevTrackDrawale(@NonNull Drawable prevDrawable) {
            this.prevDrawable = prevDrawable;
            return this;
        }

        public Builder nextTrackDrawable(@NonNull Drawable nextDrawable) {
            this.nextDrawable = nextDrawable;
            return this;
        }

        public Builder playlistDrawable(@NonNull Drawable playlistDrawable) {
            this.playlistDrawable = playlistDrawable;
            return this;
        }

        public Builder defaultAlbumDrawable(@NonNull Drawable defaultAlbumCover) {
            this.defaultAlbumDrawable = defaultAlbumCover;
            return this;
        }

        public Builder pauseDrawable(@NonNull Drawable pauseDrawable) {
            this.pauseDrawable = pauseDrawable;
            return this;
        }

        public Builder edgeOffsetXCollapsed(int edgeOffsetX) {
            this.edgeOffsetXCollapsed = edgeOffsetX;
            edgeOffsetXCollapsedSet = true;
            return this;
        }

        public Builder edgeOffsetYCollapsed(int edgeOffsetY) {
            this.edgeOffsetYCollapsed = edgeOffsetY;
            edgeOffsetYCollapsedSet = true;
            return this;
        }

        public Builder edgeOffsetYExpanded(int edgeOffsetY) {
            this.edgeOffsetYExpanded = edgeOffsetY;
            edgeOffsetYExpandedSet = true;
            return this;
        }

        public Builder edgeOffsetXExpanded(int edgeOffsetX) {
            this.edgeOffsetXExpanded = edgeOffsetX;
            edgeOffsetXExpandedSet = true;
            return this;
        }

        public AudioWidget build() {
            if (buttonPaddingSet) {
                checkOrThrow(buttonPadding, "Button padding");
            }
            if (shadowRadiusSet) {
                checkOrThrow(shadowRadius, "Shadow radius");
            }
            if (shadowDxSet) {
                checkOrThrow(shadowDx, "Shadow dx");
            }
            if (shadowDySet) {
                checkOrThrow(shadowDy, "Shadow dy");
            }
            if (bubblesMinSizeSet) {
                checkOrThrow(bubblesMinSize, "Bubbles min size");
            }
            if (bubblesMaxSizeSet) {
                checkOrThrow(bubblesMaxSize, "Bubbles max size");
            }
            if (bubblesMinSizeSet && bubblesMaxSizeSet && bubblesMaxSize < bubblesMinSize) {
                throw new IllegalArgumentException("Bubbles max size must be greater than bubbles min size");
            }
            if (crossStrokeWidthSet) {
                checkOrThrow(crossStrokeWidth, "Cross stroke width");
            }
            if (progressStrokeWidthSet) {
                checkOrThrow(progressStrokeWidth, "Progress stroke width");
            }
            return new AudioWidget(this);
        }

        private void checkOrThrow(int number, String name) {
            if (number < 0)
                throw new IllegalArgumentException(name + " must be equals or greater zero.");
        }

        private void checkOrThrow(float number, String name) {
            if (number < 0)
                throw new IllegalArgumentException(name + " must be equals or greater zero.");
        }

    }

    private class PlayPauseButtonCallback extends TouchManager.SimpleCallback {

        private static final long REMOVE_BTN_ANIM_DURATION = 200;
        private final ValueAnimator.AnimatorUpdateListener animatorUpdateListener;
        private boolean readyToRemove;

        PlayPauseButtonCallback() {
            animatorUpdateListener = animation -> {
                if (!removeWidgetShown) {
                    return;
                }
                animatedRemBtnYPos = (int) ((float) animation.getAnimatedValue());
                //updateRemoveBtnPosition();
            };
        }

        @Override
        public void onClick(float x, float y) {
            playPauseButton.onClick();
            if (onControlsClickListener != null) {
                onControlsClickListener.onPlayPauseClicked();
            }
        }

        @Override
        public void onLongClick(float x, float y) {
            released = true;
            expand();
        }

        @Override
        public void onTouched(float x, float y) {
            super.onTouched(x, y);
            released = false;
            handler.postDelayed(() -> {
                if (!released) {
                    removeWidgetShown = true;
                    ValueAnimator animator = ValueAnimator.ofFloat(hiddenRemWidPos.y, visibleRemWidPos.y);
                    animator.setDuration(REMOVE_BTN_ANIM_DURATION);
                    animator.addUpdateListener(animatorUpdateListener);
                    animator.start();
                }
            }, Configuration.LONG_CLICK_THRESHOLD);
            playPauseButton.onTouchDown();
        }

        @Override
        public void onMoved(float diffX, float diffY) {
            super.onMoved(diffX, diffY);
            boolean curReadyToRemove = isReadyToRemove();
            if (curReadyToRemove != readyToRemove) {
                readyToRemove = curReadyToRemove;
                removeWidgetView.setOverlapped(readyToRemove);
                if (readyToRemove && vibrator.hasVibrator()) {
                    vibrator.vibrate(VIBRATION_DURATION);
                }
            }
            updateRemoveBtnPosition();
        }

        private void updateRemoveBtnPosition() {
            if (removeWidgetShown) {
                WindowManager.LayoutParams playPauseBtnParams = (WindowManager.LayoutParams) playPauseButton
                        .getLayoutParams();
                WindowManager.LayoutParams removeBtnParams = (WindowManager.LayoutParams) removeWidgetView
                        .getLayoutParams();

                double tgAlpha = (screenSize.x / 2. - playPauseBtnParams.x)
                        / (visibleRemWidPos.y - playPauseBtnParams.y);
                double rotationDegrees = 360 - Math.toDegrees(Math.atan(tgAlpha));

                float distance = (float) Math.sqrt(Math.pow(animatedRemBtnYPos - playPauseBtnParams.y, 2)
                        + Math.pow(visibleRemWidPos.x - hiddenRemWidPos.x, 2));
                float maxDistance = (float) Math.sqrt(Math.pow(screenSize.x, 2) + Math.pow(screenSize.y, 2));
                distance /= maxDistance;

                if (animatedRemBtnYPos == -1) {
                    animatedRemBtnYPos = visibleRemWidPos.y;
                }

                removeBtnParams.x = (int) DrawableUtils.rotateX(visibleRemWidPos.x,
                        animatedRemBtnYPos - radius * distance, hiddenRemWidPos.x, animatedRemBtnYPos,
                        (float) rotationDegrees);
                removeBtnParams.y = (int) DrawableUtils.rotateY(visibleRemWidPos.x,
                        animatedRemBtnYPos - radius * distance, hiddenRemWidPos.x, animatedRemBtnYPos,
                        (float) rotationDegrees);

                try {
                    windowManager.updateViewLayout(removeWidgetView, removeBtnParams);
                } catch (IllegalArgumentException e) {
                    // view not attached to window
                }
            }
        }

        @Override
        public void onReleased(float x, float y) {
            super.onReleased(x, y);
            playPauseButton.onTouchUp();
            released = true;
            if (removeWidgetShown) {
                ValueAnimator animator = ValueAnimator.ofFloat(visibleRemWidPos.y, hiddenRemWidPos.y);
                animator.setDuration(REMOVE_BTN_ANIM_DURATION);
                animator.addUpdateListener(animatorUpdateListener);
                animator.addListener(new AnimatorListenerAdapter() {

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        removeWidgetShown = false;
                        if (!shown) {
                            try {
                                windowManager.removeView(removeWidgetView);
                            } catch (IllegalArgumentException e) {
                                // view not attached to window
                            }
                        }
                    }
                });
                animator.start();
            }
            if (isReadyToRemove()) {
                hideInternal(true);
            } else {
                if (onWidgetStateChangedListener != null) {
                    WindowManager.LayoutParams params = (WindowManager.LayoutParams) playPauseButton
                            .getLayoutParams();
                    onWidgetStateChangedListener.onWidgetPositionChanged((int) (params.x + widgetHeight),
                            (int) (params.y + widgetHeight));
                }
            }
        }

        @Override
        public void onAnimationCompleted() {
            super.onAnimationCompleted();
            if (onWidgetStateChangedListener != null) {
                WindowManager.LayoutParams params = (WindowManager.LayoutParams) playPauseButton.getLayoutParams();
                onWidgetStateChangedListener.onWidgetPositionChanged((int) (params.x + widgetHeight),
                        (int) (params.y + widgetHeight));
            }
        }

        private boolean isReadyToRemove() {
            WindowManager.LayoutParams removeParams = (WindowManager.LayoutParams) removeWidgetView
                    .getLayoutParams();
            removeBounds.set(removeParams.x, removeParams.y, removeParams.x + widgetHeight,
                    removeParams.y + widgetHeight);
            WindowManager.LayoutParams params = (WindowManager.LayoutParams) playPauseButton.getLayoutParams();
            float cx = params.x + widgetHeight;
            float cy = params.y + widgetHeight;
            return removeBounds.contains(cx, cy);
        }
    }

    private class ExpandCollapseWidgetCallback extends TouchManager.SimpleCallback {

        @Override
        public void onTouched(float x, float y) {
            super.onTouched(x, y);
            expandCollapseWidget.onTouched(x, y);
        }

        @Override
        public void onReleased(float x, float y) {
            super.onReleased(x, y);
            expandCollapseWidget.onReleased(x, y);
        }

        @Override
        public void onClick(float x, float y) {
            super.onClick(x, y);
            expandCollapseWidget.onClick(x, y);
        }

        @Override
        public void onLongClick(float x, float y) {
            super.onLongClick(x, y);
            expandCollapseWidget.onLongClick(x, y);
        }

        @Override
        public void onTouchOutside() {
            if (!expandCollapseWidget.isAnimationInProgress()) {
                collapse();
            }
        }

        @Override
        public void onMoved(float diffX, float diffY) {
            super.onMoved(diffX, diffY);
            updatePlayPauseButtonPosition();
        }

        @Override
        public void onAnimationCompleted() {
            super.onAnimationCompleted();
            updatePlayPauseButtonPosition();
        }
    }

    private class OnControlsClickListenerWrapper implements OnControlsClickListener {

        private OnControlsClickListener onControlsClickListener;

        public OnControlsClickListenerWrapper onControlsClickListener(OnControlsClickListener inner) {
            this.onControlsClickListener = inner;
            return this;
        }

        @Override
        public boolean onPlaylistClicked() {
            if (onControlsClickListener == null || !onControlsClickListener.onPlaylistClicked()) {
                collapse();
                return true;
            }
            return false;
        }

        @Override
        public void onPlaylistLongClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onPlaylistLongClicked();
            }
        }

        @Override
        public void onPreviousClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onPreviousClicked();
            }
        }

        @Override
        public void onPreviousLongClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onPreviousLongClicked();
            }
        }

        @Override
        public boolean onPlayPauseClicked() {
            if (onControlsClickListener == null || !onControlsClickListener.onPlayPauseClicked()) {
                if (playbackState.state() != Configuration.STATE_PLAYING) {
                    playbackState.start(AudioWidget.this);
                } else {
                    playbackState.pause(AudioWidget.this);
                }
                return true;
            }
            return false;
        }

        @Override
        public void onPlayPauseLongClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onPlayPauseLongClicked();
            }
        }

        @Override
        public void onNextClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onNextClicked();
            }
        }

        @Override
        public void onNextLongClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onNextLongClicked();
            }
        }

        @Override
        public void onAlbumClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onAlbumClicked();
            }
        }

        @Override
        public void onAlbumLongClicked() {
            if (onControlsClickListener != null) {
                onControlsClickListener.onAlbumLongClicked();
            }
        }
    }
}