arun.com.chromer.webheads.ui.views.BaseWebHead.java Source code

Java tutorial

Introduction

Here is the source code for arun.com.chromer.webheads.ui.views.BaseWebHead.java

Source

/*
 * Lynket
 *
 * Copyright (C) 2019 Arunkumar
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package arun.com.chromer.webheads.ui.views;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.text.SpannableString;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;

import com.mikepenz.iconics.IconicsDrawable;

import arun.com.chromer.R;
import arun.com.chromer.data.website.model.Website;
import arun.com.chromer.settings.Preferences;
import arun.com.chromer.util.ColorUtil;
import arun.com.chromer.util.Utils;
import butterknife.BindView;
import butterknife.ButterKnife;
import cn.nekocode.badge.BadgeDrawable;
import timber.log.Timber;

import static android.graphics.Color.TRANSPARENT;
import static android.graphics.Color.WHITE;
import static android.graphics.PixelFormat.TRANSLUCENT;
import static android.view.Gravity.LEFT;
import static android.view.Gravity.TOP;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
import static android.widget.ImageView.ScaleType.CENTER;
import static arun.com.chromer.shared.Constants.NO_COLOR;
import static arun.com.chromer.util.ColorUtil.getForegroundWhiteOrBlack;
import static arun.com.chromer.util.Utils.dpToPx;
import static cn.nekocode.badge.BadgeDrawable.TYPE_NUMBER;
import static com.mikepenz.community_material_typeface_library.CommunityMaterial.Icon.cmd_close;

/**
 * ViewGroup that holds the web head UI elements. Allows configuring various parameters in relation
 * to UI like favicon, text indicator and is responsible for inflating all the content.
 */
public abstract class BaseWebHead extends FrameLayout {
    // Helper instance to know screen boundaries that web head is allowed to travel
    static ScreenBounds screenBounds;
    // Counter to keep count of active web heads
    static int WEB_HEAD_COUNT = 0;
    // Class variables to keep track of where the master was last touched down
    static int masterDownX;
    static int masterDownY;
    // Static window manager instance to update, add and remove web heads
    private static WindowManager windowManager;
    // X icon drawable used when closing
    private static Drawable xDrawable;
    // Badge indicator
    private static BadgeDrawable badgeDrawable;
    // Class variables to keep track of master movements
    private static int masterX;
    private static int masterY;
    // Window parameters used to track and update web heads post creation;
    final WindowManager.LayoutParams windowParams;
    // Color of web head when removed
    int deleteColor = NO_COLOR;
    // The preferredUrl of the website that this web head represents, not allowed to change
    private final String url;
    // Website data that this web head represents
    protected Website website;

    @BindView(R.id.favicon)
    protected ImageView favicon;
    @BindView(R.id.indicator)
    protected TextView indicator;
    @BindView(R.id.circleBackground)
    protected ElevatedCircleView circleBg;
    @BindView(R.id.revealView)
    protected CircleView revealView;
    @BindView(R.id.badge)
    protected TextView badgeView;

    // Display dimensions
    int dispWidth, dispHeight;
    // The content view group which host all our elements
    FrameLayout contentRoot;
    // Flag to know if the user moved manually or if the web heads is still resting
    boolean userManuallyMoved;
    // If web head was issued with destroy before.
    boolean destroyed;
    // Master Wayne
    boolean master;
    // If this web head is being queued to be displayed on screen.
    boolean inQueue;
    // Flag to know if this web head was created for opening in new tab
    private boolean fromNewTab;
    protected boolean spawnCoordSet;
    // Color of the web head
    @ColorInt
    int webHeadColor;

    @SuppressLint("RtlHardcoded")
    BaseWebHead(@NonNull final Context context, @NonNull final String url) {
        super(context);
        WEB_HEAD_COUNT++;
        this.url = url;
        website = new Website();
        website.url = url;

        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        inflateContent(context);
        windowParams = createWindowParams();
        windowParams.gravity = TOP | LEFT;
        initDisplayMetrics();
        windowManager.addView(this, windowParams);
        if (xDrawable == null) {
            xDrawable = new IconicsDrawable(context).icon(cmd_close).color(WHITE).sizeDp(18);
        }
        if (deleteColor == NO_COLOR) {
            deleteColor = ContextCompat.getColor(context, R.color.remove_web_head_color);
        }
        // Needed to prevent overly dark shadow.
        if (WEB_HEAD_COUNT > 2) {
            setWebHeadElevation(dpToPx(5));
        }
    }

    public static void clearMasterPosition() {
        masterY = 0;
        masterX = 0;
    }

    protected abstract void onMasterChanged(boolean master);

    /**
     * Event for sub class to get notified once spawn location is set.
     *
     * @param x X
     * @param y Y
     */
    protected abstract void onSpawnLocationSet(int x, int y);

    private void inflateContent(@NonNull Context context) {
        // size
        if (Preferences.get(context).webHeadsSize() == 2) {
            contentRoot = (FrameLayout) LayoutInflater.from(getContext())
                    .inflate(R.layout.widget_web_head_layout_small, this, false);
        } else
            contentRoot = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.widget_web_head_layout,
                    this, false);
        addView(contentRoot);
        ButterKnife.bind(this);

        webHeadColor = Preferences.get(context).webHeadColor();
        indicator.setText(Utils.getFirstLetter(url));
        indicator.setTextColor(getForegroundWhiteOrBlack(webHeadColor));
        initRevealView(webHeadColor);

        if (badgeDrawable == null) {
            badgeDrawable = new BadgeDrawable.Builder().type(TYPE_NUMBER)
                    .badgeColor(ContextCompat.getColor(getContext(), R.color.accent)).textColor(WHITE)
                    .number(WEB_HEAD_COUNT).build();
        } else {
            badgeDrawable.setNumber(WEB_HEAD_COUNT);
        }
        badgeView.setVisibility(VISIBLE);
        badgeView.setText(new SpannableString(badgeDrawable.toSpannable()));
        updateBadgeColors(webHeadColor);

        if (!Utils.isLollipopAbove()) {
            final int pad = dpToPx(5);
            badgeView.setPadding(pad, pad, pad, pad);
        }
    }

    private void initDisplayMetrics() {
        final DisplayMetrics metrics = new DisplayMetrics();
        windowManager.getDefaultDisplay().getMetrics(metrics);
        dispWidth = metrics.widthPixels;
        dispHeight = metrics.heightPixels;
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        masterX = 0;
        masterY = 0;
        initDisplayMetrics();
    }

    protected void setInitialSpawnLocation() {
        Timber.d("Initial spawn location set.");
        if (screenBounds == null) {
            screenBounds = new ScreenBounds(dispWidth, dispHeight, getWidth());
        }
        if (!spawnCoordSet) {
            int x, y = dispHeight / 3;
            if (masterX != 0 || masterY != 0) {
                x = masterX;
                y = masterY;
            } else {
                if (Preferences.get(getContext()).webHeadsSpawnLocation() == 1) {
                    x = screenBounds.right;
                } else {
                    x = screenBounds.left;
                }
            }
            spawnCoordSet = true;
            onSpawnLocationSet(x, y);
        }
    }

    /**
     * Used to get an instance of remove web head
     *
     * @return an instance of {@link Trashy}
     */
    Trashy getTrashy() {
        return Trashy.get(getContext());
    }

    /**
     * Wrapper around window manager to update this view. Called to move the web head usually.
     */
    void updateView() {
        try {
            if (master) {
                masterX = windowParams.x;
                masterY = windowParams.y;
            }
            windowManager.updateViewLayout(this, windowParams);
        } catch (IllegalArgumentException e) {
            Timber.e("Update called after view was removed");
        }
    }

    public WindowManager.LayoutParams getWindowParams() {
        return windowParams;
    }

    boolean isLastWebHead() {
        return WEB_HEAD_COUNT == 0;
    }

    private void setWebHeadElevation(final int elevationPx) {
        if (Utils.isLollipopAbove()) {
            if (circleBg != null && revealView != null) {
                circleBg.setElevation(elevationPx);
                revealView.setElevation(elevationPx + 1);
            }
        }
    }

    @NonNull
    public Animator getRevealAnimator(@ColorInt final int newWebHeadColor) {
        revealView.clearAnimation();
        initRevealView(newWebHeadColor);

        final AnimatorSet animator = new AnimatorSet();
        animator.playTogether(ObjectAnimator.ofFloat(revealView, "scaleX", 1f),
                ObjectAnimator.ofFloat(revealView, "scaleY", 1f), ObjectAnimator.ofFloat(revealView, "alpha", 1f));
        revealView.setLayerType(LAYER_TYPE_HARDWARE, null);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                webHeadColor = newWebHeadColor;
                updateBadgeColors(webHeadColor);
                if (indicator != null && circleBg != null && revealView != null) {
                    circleBg.setColor(newWebHeadColor);
                    indicator.setTextColor(getForegroundWhiteOrBlack(newWebHeadColor));
                    revealView.setLayerType(LAYER_TYPE_NONE, null);
                    revealView.setScaleX(0f);
                    revealView.setScaleY(0f);
                }
            }
        });
        animator.setInterpolator(new LinearOutSlowInInterpolator());
        animator.setDuration(250);
        return animator;
    }

    /**
     * Opposite of {@link #getRevealAnimator(int)}. Reveal goes from max scale to 0 appearing to be
     * revealing in.
     *
     * @param newWebHeadColor New themeColor of reveal
     * @param start           Runnable to run on start
     * @param end             Runnable to run on end
     */
    void revealInAnimation(@ColorInt final int newWebHeadColor, @NonNull final Runnable start,
            @NonNull final Runnable end) {
        if (revealView == null || circleBg == null) {
            start.run();
            end.run();
        }
        revealView.clearAnimation();
        revealView.setColor(circleBg.getColor());
        revealView.setScaleX(1f);
        revealView.setScaleY(1f);
        revealView.setAlpha(1f);
        circleBg.setColor(newWebHeadColor);
        final AnimatorSet animator = new AnimatorSet();
        animator.playTogether(ObjectAnimator.ofFloat(revealView, "scaleX", 0f),
                ObjectAnimator.ofFloat(revealView, "scaleY", 0f));
        revealView.setLayerType(LAYER_TYPE_HARDWARE, null);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                start.run();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                webHeadColor = newWebHeadColor;
                indicator.setTextColor(getForegroundWhiteOrBlack(newWebHeadColor));
                revealView.setLayerType(LAYER_TYPE_NONE, null);
                revealView.setScaleX(0f);
                revealView.setScaleY(0f);
                end.run();
            }

        });
        animator.setInterpolator(new LinearOutSlowInInterpolator());
        animator.setDuration(400);
        animator.setStartDelay(100);
        animator.start();
    }

    /**
     * Resets the reveal color so that it is ready to being reveal animation
     *
     * @param revealColor The color to appear during animation
     */
    private void initRevealView(@ColorInt int revealColor) {
        revealView.setColor(revealColor);
        revealView.setScaleX(0f);
        revealView.setScaleY(0f);
        revealView.setAlpha(0.8f);
    }

    /**
     * Applies a cross fade animation to transform the current favicon to an X icon. Ensures favicon
     * is visible by hiding indicators.
     */
    void crossFadeFaviconToX() {
        favicon.setVisibility(VISIBLE);
        favicon.clearAnimation();
        favicon.setScaleType(CENTER);
        final TransitionDrawable icon = new TransitionDrawable(
                new Drawable[] { new ColorDrawable(TRANSPARENT), xDrawable });
        favicon.setImageDrawable(icon);
        icon.setCrossFadeEnabled(true);
        icon.startTransition(50);
        favicon.animate().withLayer().rotation(180).setDuration(250)
                .setInterpolator(new LinearOutSlowInInterpolator()).start();
    }

    @SuppressWarnings("SameParameterValue")
    @ColorInt
    public int getWebHeadColor(boolean ignoreFavicons) {
        if (ignoreFavicons) {
            return webHeadColor;
        } else {
            if (getFaviconBitmap() != null) {
                return webHeadColor;
            } else
                return NO_COLOR;
        }
    }

    public void setWebHeadColor(@ColorInt int webHeadColor) {
        getRevealAnimator(webHeadColor).start();
    }

    void updateBadgeColors(@ColorInt int webHeadColor) {
        final int badgeColor = ColorUtil.getClosestAccentColor(webHeadColor);
        badgeDrawable.setBadgeColor(badgeColor);
        badgeDrawable.setTextColor(getForegroundWhiteOrBlack(badgeColor));
        badgeView.invalidate();
    }

    @NonNull
    public String getUrl() {
        return url;
    }

    @NonNull
    public String getUnShortenedUrl() {
        return website.preferredUrl();
    }

    @Nullable
    public Bitmap getFaviconBitmap() {
        try {
            final RoundedBitmapDrawable roundedBitmapDrawable = (RoundedBitmapDrawable) getFaviconDrawable();
            return roundedBitmapDrawable != null ? roundedBitmapDrawable.getBitmap() : null;
        } catch (Exception e) {
            Timber.e("Error while getting favicon bitmap: %s", e.getMessage());
        }
        return null;
    }

    @Nullable
    private Drawable getFaviconDrawable() {
        try {
            TransitionDrawable drawable = (TransitionDrawable) favicon.getDrawable();
            if (drawable != null) {
                return drawable.getDrawable(1);
            } else
                return null;
        } catch (ClassCastException e) {
            Timber.e("Error while getting favicon drawable: %s", e.getMessage());
        }
        return null;
    }

    public void setFaviconDrawable(@NonNull final Drawable faviconDrawable) {
        if (indicator != null && favicon != null) {
            indicator.animate().alpha(0).withLayer().start();
            TransitionDrawable transitionDrawable = new TransitionDrawable(
                    new Drawable[] { new ColorDrawable(TRANSPARENT), faviconDrawable });
            favicon.setVisibility(VISIBLE);
            favicon.setImageDrawable(transitionDrawable);
            transitionDrawable.setCrossFadeEnabled(true);
            transitionDrawable.startTransition(500);
        }
    }

    @SuppressWarnings("UnusedParameters")
    void destroySelf(boolean receiveCallback) {
        destroyed = true;
        Trashy.disappear();
        removeView(contentRoot);
        if (windowManager != null)
            try {
                windowManager.removeView(this);
            } catch (Exception ignored) {
            }
    }

    public boolean isMaster() {
        return master;
    }

    public void setMaster(boolean master) {
        this.master = master;
        if (!master) {
            badgeView.setVisibility(INVISIBLE);
        } else {
            badgeView.setVisibility(VISIBLE);
            badgeDrawable.setNumber(WEB_HEAD_COUNT);
            setInQueue(false);
        }
        onMasterChanged(master);
    }

    public void setInQueue(boolean inQueue) {
        this.inQueue = inQueue;
        if (inQueue) {
            setVisibility(GONE);
        } else {
            setVisibility(VISIBLE);
        }
    }

    @NonNull
    private WindowManager.LayoutParams createWindowParams() {
        if (Utils.ANDROID_OREO) {
            return new WindowManager.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, TYPE_APPLICATION_OVERLAY,
                    FLAG_NOT_FOCUSABLE | FLAG_LAYOUT_NO_LIMITS | FLAG_HARDWARE_ACCELERATED, TRANSLUCENT);
        } else
            //noinspection deprecation
            return new WindowManager.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, TYPE_SYSTEM_ALERT,
                    FLAG_NOT_FOCUSABLE | FLAG_LAYOUT_NO_LIMITS | FLAG_HARDWARE_ACCELERATED, TRANSLUCENT);
    }

    /**
     * Returns website POJO containing useful data.
     *
     * @return website data.
     */
    @NonNull
    public Website getWebsite() {
        return website;
    }

    public void setWebsite(@NonNull Website website) {
        // Timber.d(website.toString());
        this.website = website;
    }

    /**
     * Helper class to hold screen boundaries
     */
    class ScreenBounds {
        /**
         * Amount of web head that will be displaced off of the screen horizontally
         */
        private static final double DISPLACE_PERC = 0.7;

        public int left;
        public int right;
        public int top;
        public int bottom;

        ScreenBounds(int dispWidth, int dispHeight, int webHeadWidth) {
            if (webHeadWidth == 0 || dispWidth == 0 || dispHeight == 0) {
                throw new IllegalArgumentException("Width of web head or screen size cannot be 0");
            }
            right = (int) (dispWidth - (webHeadWidth * DISPLACE_PERC));
            left = (int) (webHeadWidth * (1 - DISPLACE_PERC)) * -1;
            top = dpToPx(25);
            bottom = (int) (dispHeight * 0.85);
        }
    }
}