Java tutorial
/* * Copyright 2014 - 2016 Michael Rapp * * 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 de.mrapp.android.sidebar; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources.NotFoundException; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcelable; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Pair; import android.view.KeyEvent; 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.Animation.AnimationListener; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import java.util.LinkedHashSet; import java.util.Set; import de.mrapp.android.sidebar.animation.ContentViewResizeAnimation; import de.mrapp.android.sidebar.animation.ContentViewScrollAnimation; import de.mrapp.android.sidebar.animation.SidebarViewAnimation; import de.mrapp.android.sidebar.inflater.Inflater; import de.mrapp.android.sidebar.inflater.InflaterFactory; import de.mrapp.android.sidebar.savedstate.SidebarSavedState; import de.mrapp.android.sidebar.util.DragHelper; import de.mrapp.android.sidebar.view.ContentView; import de.mrapp.android.sidebar.view.SidebarView; import de.mrapp.android.util.ElevationUtil; import static de.mrapp.android.util.Condition.ensureAtLeast; import static de.mrapp.android.util.Condition.ensureAtMaximum; import static de.mrapp.android.util.Condition.ensureGreater; import static de.mrapp.android.util.Condition.ensureNotNull; import static de.mrapp.android.util.Condition.ensureSmaller; import static de.mrapp.android.util.DisplayUtil.dpToPixels; import static de.mrapp.android.util.DisplayUtil.pixelsToDp; /** * A custom view, which allows to show a sidebar, which overlaps the view's main content and can be * shown or hidden in an animated manner. The sidebar may be located at left or right edge of the * parent view and its state can be changed by either calling an appropriate method or via dragging * on the device's touch screen. Furthermore there are a lot of attributes, which allow to specify * the appearance and behavior of the sidebar. * * @author Michael Rapp * @since 1.0.0 */ public class Sidebar extends ViewGroup { /** * The default location of the sidebar. */ protected static final Location DEFAULT_LOCATION = Location.RIGHT; /** * The default speed of the animation, which is used to show or hide the sidebar, in dp per * millisecond. */ protected static final float DEFAULT_ANIMATION_SPEED = 1.5f; /** * The default width of the sidebar in relation to the width of its parent view. */ protected static final float DEFAULT_SIDEBAR_WIDTH = 0.75f; /** * The default maximum width of the sidebar in dp or -1, if the sidebar's width should not be * restricted. */ protected static final int DEFAULT_MAX_SIDEBAR_WIDTH = -1; /** * The default amount of space in relation to the width of the parent view, the sidebar is * visible, even if it is currently hidden. */ protected static final float DEFAULT_SIDEBAR_OFFSET = 0.125f; /** * The default maximum offset of the sidebar in dp or -1, if the sidebar's offset should not be * restricted. */ protected static final int DEFAULT_MAX_SIDEBAR_OFFSET = -1; /** * The default content mode, which specifies how the content view is handled when the sidebar * becomes shown or hidden. */ protected static final ContentMode DEFAULT_CONTENT_MODE = ContentMode.SCROLL; /** * The default ratio between the distance, the sidebar is moved by, when it becomes shown or * hidden, in relation to the distance, the content is moved by. */ protected static final float DEFAULT_SCROLL_RATIO = 0.5f; /** * The default drag mode, which specifies the region, where drag gestures are recognized, when * the sidebar is currently hidden. */ protected static final DragMode DEFAULT_DRAG_MODE_WHEN_HIDDEN = DragMode.SIDEBAR_ONLY; /** * The default drag mode, which specifies the region, where drag gestures are recognized, when * the sidebar is currently shown. */ protected static final DragMode DEFAULT_DRAG_MODE_WHEN_SHOWN = DragMode.BOTH; /** * The default distance, the sidebar has to be dragged until its state changes, in relation to * the whole distance. */ protected static final float DEFAULT_DRAG_THRESHOLD = 0.25f; /** * The default sensitivity, which specifies the distance after which dragging has an effect on * the sidebar, in relation to an internal value range. */ protected static final float DEFAULT_DRAG_SENSITIVITY = 0.25f; /** * Specifies, whether by default the sidebar should be hidden, when the device's back button is * clicked, or not. */ protected static final boolean DEFAULT_HIDE_ON_BACK_BUTTON = true; /** * Specifies, whether by default the sidebar should be hidden, when the main content is clicked * by the user, or not. */ protected static final boolean DEFAULT_HIDE_ON_CONTENT_CLICK = true; /** * Specifies, whether by default the sidebar should be shown, when it is clicked by the user, or * not. */ protected static final boolean DEFAULT_SHOW_ON_SIDEBAR_CLICK = true; /** * The default color of the overlay, which is shown in front of the main content, when the * sidebar is shown. */ protected static final int DEFAULT_CONTENT_OVERLAY_COLOR = Color.BLACK; /** * The default transparency of the overlay, which is shown in front of the main content, when * the sidebar is shown. */ protected static final float DEFAULT_CONTENT_OVERLAY_TRANSPARENCY = 0.5f; /** * The default elevation of the sidebar in dp. */ protected static final int DEFAULT_SIDEBAR_ELEVATION = 16; /** * True, if the sidebar should be shown by default, false otherwise. */ protected static final boolean SHOW_SIDEBAR_BY_DEFAULT = false; /** * The minimum value of the internal value range, which specifies after which distance dragging * has an effect on the sidebar. */ private static final int MIN_DRAG_SENSITIVITY = 10; /** * The maximum value of the internal value range, which specifies after which distance dragging * has an effect on the sidebar. */ private static final int MAX_DRAG_SENSITIVITY = 260; /** * The tolerance in dp, which is used to determine, if a drag gesture has been started at the * edge of the sidebar's parent view, when using the drag mode <code>DragMode.EDGE</code>. */ private static final int EDGE_DRAGGING_TOLERANCE = 4; /** * The location of the sidebar. */ private Location location; /** * The speed of the animation, which is used to show or hide the sidebar, in pixels per * millisecond. */ private float animationSpeed; /** * The width of the sidebar in relation to the width of its parent view. */ private float sidebarWidth; /** * The maximum width of the sidebar in dp or -1, if the sidebar's width should not be * restricted. */ private int maxSidebarWidth; /** * The amount of space in relation to the width of the parent view, the sidebar is visible, even * if it is currently hidden. */ private float sidebarOffset; /** * The maximum offset of the sidebar in dp or -1, if the sidebar's offset should not be * restricted. */ private int maxSidebarOffset; /** * The content mode, which specifies how the content view is handled when the sidebar becomes * shown or hidden. */ private ContentMode contentMode; /** * The ratio between the distance, the sidebar is moved by, when it becomes shown or hidden, in * relation to the distance, the content is moved by. */ private float scrollRatio; /** * The drag mode, which specifies the region, where drag gestures are recognized, when the * sidebar is currently hidden. */ private DragMode dragModeWhenHidden; /** * The drag mode, which specifies the region, where drag gestures are recognized, when the * sidebar is currently shown. */ private DragMode dragModeWhenShown; /** * The distance, the sidebar has to be dragged until its state changes, in relation to the whole * distance. */ private float dragThreshold; /** * The sensitivity, which specifies the distance after which dragging has an effect on the * sidebar, in relation to an internal value range. */ private float dragSensitivity; /** * True, if the sidebar should be hidden, when the device's back button is clicked, false * otherwise. */ private boolean hideOnBackButton; /** * True, if the sidebar should be hidden, when the main content is clicked by the user, false * otherwise. */ private boolean hideOnContentClick; /** * True, if the sidebar should be shown, when it is clicked by the user, false otherwise. */ private boolean showOnSidebarClick; /** * The color of the overlay, which is shown in front of the main content, when the sidebar is * shown. */ private int contentOverlayColor; /** * The transparency of the overlay, which is shown in front of the main content, when the * sidebar is shown. */ private float contentOverlayTransparency; /** * The elevation of the sidebar in dp. */ private int sidebarElevation; /** * True, if the sidebar is currently shown, false otherwise. */ private boolean shown; /** * The background of the sidebar or null, if the default background is used. */ private transient Drawable sidebarBackground; /** * A set, which contains the listeners, which have registered to be notified, when the sidebar * becomes shown or hidden. */ private transient Set<SidebarListener> listeners; /** * The view, which contains the sidebar and its shadow. */ private transient SidebarView sidebarView; /** * The view, which contains the content and its overlay. */ private transient ContentView contentView; /** * A helper variable, which contains the sidebar's width in pixels. */ private transient int mSidebarWidth; /** * A helper variable, which contains the content's width in pixels. */ private transient int mContentWidth; /** * A helper variable, which contains the sidebar's offset in pixels. */ private transient int mOffset; /** * An instance of the class {@link DragHelper}, which is used to recognize drag gestures. */ private transient DragHelper dragHelper; /** * Initializes the sidebar. * * @param attributeSet * The attribute set, the attributes should be obtained from, as an instance of the type * {@link AttributeSet} or null, if no attributes should be obtained */ private void initialize(@Nullable final AttributeSet attributeSet) { this.listeners = new LinkedHashSet<>(); this.shown = false; this.dragHelper = new DragHelper(calculateDragSensitivity()); this.setFocusableInTouchMode(true); obtainStyledAttributes(attributeSet); } /** * Obtains all attributes from a specific attribute set. * * @param attributeSet * The attribute set, the attributes should be obtained from, as an instance of the type * {@link AttributeSet} or null, if no attributes should be obtained */ private void obtainStyledAttributes(@Nullable final AttributeSet attributeSet) { TypedArray typedArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.Sidebar); try { obtainContentOverlayColor(typedArray); obtainContentOverlayTransparency(typedArray); obtainSidebarElevation(typedArray); obtainLocation(typedArray); obtainSidebarBackground(typedArray); obtainSidebarView(typedArray); obtainContentView(typedArray); obtainAnimationSpeed(typedArray); obtainSidebarWidth(typedArray); obtainMaxSidebarWidth(typedArray); obtainSidebarOffset(typedArray); obtainMaxSidebarOffset(typedArray); obtainContentMode(typedArray); obtainScrollRatio(typedArray); obtainDragModeWhenHidden(typedArray); obtainDragModeWhenShown(typedArray); obtainDragThreshold(typedArray); obtainDragSensitivity(typedArray); obtainHideOnBackButton(typedArray); obtainHideOnContentClick(typedArray); obtainShowOnSidebarClick(typedArray); obtainShowSidebar(typedArray); } finally { typedArray.recycle(); } } /** * Obtains the background of the sidebar from a specific typed array. * * @param typedArray * The typed array, the background of the sidebar should be obtained from, as an * instance of the class {@link TypedArray}. The typed array may not be null */ private void obtainSidebarBackground(@NonNull final TypedArray typedArray) { setSidebarBackground(typedArray.getResourceId(R.styleable.Sidebar_sidebarBackground, -1)); } /** * Obtains the color of the overlay, which is shown in front of the main content, when the * sidebar is shown, from a specific typed array. * * @param typedArray * The typed array, the color of the overlay should be obtained from, as an instance of * the class {@link TypedArray}. The typed array may not be null */ private void obtainContentOverlayColor(@NonNull final TypedArray typedArray) { setContentOverlayColor( typedArray.getColor(R.styleable.Sidebar_contentOverlayColor, DEFAULT_CONTENT_OVERLAY_COLOR)); } /** * Obtains the transparency of the overlay, which is shown in front of the main content, when * the sidebar is shown, from a specific typed array. * * @param typedArray * The typed array, the transparency of the overlay should be obtained from, as an * instance of the class {@link TypedArray}. The typed array may not be null */ private void obtainContentOverlayTransparency(@NonNull final TypedArray typedArray) { setContentOverlayTransparency(typedArray.getFraction(R.styleable.Sidebar_contentOverlayTransparency, 1, 1, DEFAULT_CONTENT_OVERLAY_TRANSPARENCY)); } /** * Obtains the elevation of the sidebar from a specific typed array. * * @param typedArray * The typed array, the elevation of the sidebar should be obtained from, as an instance * of the class {@link TypedArray}. The typed array may not be null */ private void obtainSidebarElevation(@NonNull final TypedArray typedArray) { setSidebarElevation(pixelsToDp(getContext(), typedArray.getDimensionPixelSize( R.styleable.Sidebar_sidebarElevation, dpToPixels(getContext(), DEFAULT_SIDEBAR_ELEVATION)))); } /** * Obtains the sidebar view from a specific typed array. * * @param typedArray * The typed array, the sidebar view should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainSidebarView(@NonNull final TypedArray typedArray) { try { setSidebarView(typedArray.getResourceId(R.styleable.Sidebar_sidebarView, -1)); } catch (NotFoundException e) { // No need to handle } } /** * Obtains the content view from a specific typed array. * * @param typedArray * The typed array, the content view should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainContentView(@NonNull final TypedArray typedArray) { try { setContentView(typedArray.getResourceId(R.styleable.Sidebar_contentView, -1)); } catch (NotFoundException e) { // No need to handle } } /** * Obtains the location of the sidebar from a specific typed array. * * @param typedArray * The typed array, the location of the sidebar should be obtained from, as an instance * of the class {@link TypedArray}. The typed array may not be null */ private void obtainLocation(@NonNull final TypedArray typedArray) { setLocation( Location.fromValue(typedArray.getInt(R.styleable.Sidebar_location, DEFAULT_LOCATION.getValue()))); } /** * Obtains the speed of the animation, which is used to show or hide the sidebar, from a * specific typed array. * * @param typedArray * The typed array, the speed of the animation should be obtained from, as an instance * of the class {@link TypedArray}. The typed array may not be null */ private void obtainAnimationSpeed(@NonNull final TypedArray typedArray) { setAnimationSpeed(typedArray.getFloat(R.styleable.Sidebar_animationSpeed, DEFAULT_ANIMATION_SPEED)); } /** * Obtains the width of the sidebar in relation to the width of its parent view, from a specific * typed array. * * @param typedArray * The typed array, the width of the sidebar should be obtained from, as an instance of * the class {@link TypedArray}. The typed array may not be null */ private void obtainSidebarWidth(@NonNull final TypedArray typedArray) { setSidebarWidth(typedArray.getFraction(R.styleable.Sidebar_sidebarWidth, 1, 1, DEFAULT_SIDEBAR_WIDTH)); } /** * Obtains the maximum width of the sidebar from a specific typed array. * * @param typedArray * The typed array, the maximum width of the sidebar should be obtained from, as an * instance of the class {@link TypedArray}. The typed array may not be null */ private void obtainMaxSidebarWidth(@NonNull final TypedArray typedArray) { setMaxSidebarWidthInPixels( typedArray.getDimensionPixelSize(R.styleable.Sidebar_maxSidebarWidth, DEFAULT_MAX_SIDEBAR_WIDTH)); } /** * Obtains the amount of space in relation to the width of the parent view, the sidebar is * visible, even if it is currently hidden, from a specific typed array. * * @param typedArray * The typed array, the offset should be obtained from, as an instance of the class * {@link TypedArray}. The typed array may not be null */ private void obtainSidebarOffset(@NonNull final TypedArray typedArray) { setSidebarOffset(typedArray.getFraction(R.styleable.Sidebar_sidebarOffset, 1, 1, DEFAULT_SIDEBAR_OFFSET)); } /** * Obtains the maximum offset of the sidebar from a specific typed array. * * @param typedArray * The typed array, the maximum offset should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainMaxSidebarOffset(@NonNull final TypedArray typedArray) { setMaxSidebarOffsetInPixels( typedArray.getDimensionPixelSize(R.styleable.Sidebar_maxSidebarOffset, DEFAULT_MAX_SIDEBAR_OFFSET)); } /** * Obtains the content mode, which specifies how the content view is handled when the sidebar * becomes shown or hidden, from a specific typed array. * * @param typedArray * The typed array, the content mode should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainContentMode(@NonNull final TypedArray typedArray) { setContentMode(ContentMode .fromValue(typedArray.getInt(R.styleable.Sidebar_contentMode, DEFAULT_CONTENT_MODE.getValue()))); } /** * Obtains the ratio between the distance, the sidebar is moved by when it becomes shown or * hidden, in relation to the distance, the content is moved by, from a specific typed array. * * @param typedArray * The typed array, the scroll ratio should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainScrollRatio(@NonNull final TypedArray typedArray) { setScrollRatio(typedArray.getFraction(R.styleable.Sidebar_scrollRatio, 1, 1, DEFAULT_SCROLL_RATIO)); } /** * Obtains the drag mode, which specifies the region, where drag gestures are recognized, when * the sidebar is currently hidden, from a specific typed array. * * @param typedArray * The typed array, the drag mode should be obtained from, as an instance of the class * {@link TypedArray}. The typed array may not be null */ private void obtainDragModeWhenHidden(@NonNull final TypedArray typedArray) { setDragModeWhenHidden(DragMode.fromValue(typedArray.getInt(R.styleable.Sidebar_dragModeWhenHidden, DEFAULT_DRAG_MODE_WHEN_HIDDEN.getValue()))); } /** * Obtains the drag mode, which specifies the region, where drag gestures are recognized, when * the sidebar is currently shown, from a specific typed array. * * @param typedArray * The typed array, the drag mode should be obtained from, as an instance of the class * {@link TypedArray}. The typed array may not be null */ private void obtainDragModeWhenShown(@NonNull final TypedArray typedArray) { setDragModeWhenShown(DragMode.fromValue( typedArray.getInt(R.styleable.Sidebar_dragModeWhenShown, DEFAULT_DRAG_MODE_WHEN_SHOWN.getValue()))); } /** * Obtains the distance, the sidebar has to be dragged until its state changes, in relation to * the whole distance, from a specific typed array. * * @param typedArray * The typed array, the threshold should be obtained from, as an instance of the class * {@link TypedArray}. The typed array may not be null */ private void obtainDragThreshold(@NonNull final TypedArray typedArray) { setDragThreshold(typedArray.getFraction(R.styleable.Sidebar_dragThreshold, 1, 1, DEFAULT_DRAG_THRESHOLD)); } /** * Obtains the sensitivity, which specifies the distance after which dragging has an effect on * the sidebar, from a specific typed array. * * @param typedArray * The typed array, the sensitivity should be obtained from, as an instance of the class * {@link TypedArray}. The typed array may not be null */ private void obtainDragSensitivity(@NonNull final TypedArray typedArray) { setDragSensitivity( typedArray.getFraction(R.styleable.Sidebar_dragSensitivity, 1, 1, DEFAULT_DRAG_SENSITIVITY)); } /** * Obtains the boolean value, which specifies, whether the sidebar should be hidden, when the * device's back button is clicked, from a specific typed array. * * @param typedArray * The typed array, the boolean value should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainHideOnBackButton(@NonNull final TypedArray typedArray) { hideOnBackButton(typedArray.getBoolean(R.styleable.Sidebar_hideOnBackButton, DEFAULT_HIDE_ON_BACK_BUTTON)); } /** * Obtains the boolean value, which specifies, whether the sidebar should be hidden, when the * main content is clicked by the user, from a specific typed array. * * @param typedArray * The typed array, the boolean value should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainHideOnContentClick(@NonNull final TypedArray typedArray) { hideOnContentClick( typedArray.getBoolean(R.styleable.Sidebar_hideOnContentClick, DEFAULT_HIDE_ON_CONTENT_CLICK)); } /** * Obtains the boolean value, which specifies, whether the sidebar should be shown, when it is * clicked by the user, from a specific typed array. * * @param typedArray * The typed array, the boolean value should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainShowOnSidebarClick(@NonNull final TypedArray typedArray) { showOnSidebarClick( typedArray.getBoolean(R.styleable.Sidebar_showOnSidebarClick, DEFAULT_SHOW_ON_SIDEBAR_CLICK)); } /** * Obtains the boolean value, which specifies, whether the sidebar should be shown by default, * from a specific typed array. * * @param typedArray * The typed array, the boolean value should be obtained from, as an instance of the * class {@link TypedArray}. The typed array may not be null */ private void obtainShowSidebar(@NonNull final TypedArray typedArray) { if (typedArray.getBoolean(R.styleable.Sidebar_showSidebar, SHOW_SIDEBAR_BY_DEFAULT)) { showSidebar(); } } /** * Sets the maximum width of the sidebar in pixels. * * @param maxSidebarWidth * The maximum width, which should be set, in pixels as an {@link Integer} value */ private void setMaxSidebarWidthInPixels(final int maxSidebarWidth) { ensureAtLeast(maxSidebarWidth, -1, "The max sidebar width must be at least -1"); this.maxSidebarWidth = maxSidebarWidth; measureSidebarWidth(); requestLayout(); } /** * Sets the maximum offset of the sidebar in pixels. * * @param maxSidebarOffset * The maximum offset, which should be set, in pixels as an {@link Integer} value */ private void setMaxSidebarOffsetInPixels(final int maxSidebarOffset) { ensureAtLeast(maxSidebarOffset, -1, "The max sidebar offset must be at least -1"); this.maxSidebarOffset = maxSidebarOffset; measureSidebarOffset(); requestLayout(); } /** * Inflates and adds the sidebar view by using a specific inflater. * * @param inflater * The inflater, which should be used, as an instance of the type {@link Inflater}. The * inflater may not be null */ private void inflateSidebarView(@NonNull final Inflater inflater) { if (sidebarView != null) { removeView(sidebarView); } sidebarView = new SidebarView(getContext(), inflater, getLocation(), sidebarBackground, sidebarElevation); addView(sidebarView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); bringSidebarToFront(); } /** * Inflates and adds the content view by using a specific inflater. * * @param inflater * The inflater, which should be used, as an instance of the type {@link Inflater}. The * inflater may not be null */ private void inflateContentView(@NonNull final Inflater inflater) { if (contentView != null) { removeView(contentView); } contentView = new ContentView(getContext(), inflater, getContentOverlayColor()); addView(contentView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); bringSidebarToFront(); } /** * Brings the sidebar view to the front, if it has already been inflated. */ private void bringSidebarToFront() { if (sidebarView != null) { sidebarView.bringToFront(); } } /** * Animates the sidebar to become shown. * * @param distance * The distance, the sidebar has to be moved by, as a {@link Float} value * @param interpolator * The interpolator, which should be used by the animation, as an instance of the type * {@link Interpolator}. The interpolator may not be null */ private void animateShowSidebar(final float distance, @NonNull final Interpolator interpolator) { animateShowSidebar(distance, animationSpeed, interpolator); } /** * Animates the sidebar to become shown. * * @param distance * The distance, the sidebar has to be moved by, as a {@link Float} value * @param animationSpeed * The speed of the animation in pixels per milliseconds as a {@link Float} value * @param interpolator * The interpolator, which should be used by the animation, as an instance of the type * {@link Interpolator}. The interpolator may not be null */ private void animateShowSidebar(final float distance, final float animationSpeed, @NonNull final Interpolator interpolator) { animateSidebar(true, distance, animationSpeed, createAnimationListener(true), interpolator); } /** * Animates the sidebar to become hidden. * * @param distance * The distance, the sidebar has to be moved by, as a {@link Float} value * @param interpolator * The interpolator, which should be used by the animation, as an instance of the type * {@link Interpolator}. The interpolator may not be null */ private void animateHideSidebar(final float distance, @NonNull final Interpolator interpolator) { animateHideSidebar(distance, animationSpeed, interpolator); } /** * Animates the sidebar to become hidden. * * @param distance * The distance, the sidebar has to be moved by, as a {@link Float} value * @param animationSpeed * The speed of the animation in pixels per millisecond as a {@link Float} value * @param interpolator * The interpolator, which should be used by the animation, as an instance of the type * {@link Interpolator}. The interpolator may not be null */ private void animateHideSidebar(final float distance, final float animationSpeed, @NonNull final Interpolator interpolator) { animateSidebar(false, distance, animationSpeed, createAnimationListener(false), interpolator); } /** * Animates the sidebar to be moved by a specific distance. * * @param show * True, if the sidebar should be shown at the end of the animation, false otherwise * @param distance * The distance, the sidebar has to be moved by, as a {@link Float} value. If the * distance is negative, the sidebar will be moved to the left, if the distance is * positive, it will be moved to the right * @param animationSpeed * The speed of the animation in pixels per millisecond as a {@link Float} value * @param animationListener * The listener, which should be notified about the animation's progress, as an instance * of the type {@link AnimationListener}. The listener may not be null * @param interpolator * The interpolator, which should be used by the animation, as an instance of the type * {@link Interpolator}. The interpolator may not be null */ private void animateSidebar(final boolean show, final float distance, final float animationSpeed, @NonNull final AnimationListener animationListener, @NonNull final Interpolator interpolator) { if (!isDragging() && !isAnimationRunning()) { long duration = calculateAnimationDuration(distance, animationSpeed); Animation contentViewAnimation; if (getContentMode() == ContentMode.SCROLL) { contentViewAnimation = new ContentViewScrollAnimation(contentView, duration, distance, scrollRatio, 1 - getContentOverlayTransparency(), show); } else { contentViewAnimation = new ContentViewResizeAnimation(contentView, duration, distance, getLocation(), 1 - getContentOverlayTransparency(), show); } Animation sidebarViewAnimation = new SidebarViewAnimation(distance, duration, animationListener); contentViewAnimation.setInterpolator(interpolator); sidebarViewAnimation.setInterpolator(interpolator); contentView.startAnimation(contentViewAnimation); sidebarView.startAnimation(sidebarViewAnimation); } } /** * Creates and returns a listener, which allows to handle the end of an animation, which has * been used to show or hide the sidebar. * * @param show * True, if the sidebar should be shown at the end of the animation, false otherwise * @return The listener, which has been created, as an instance of the type {@link * AnimationListener} */ private AnimationListener createAnimationListener(final boolean show) { return new AnimationListener() { @Override public void onAnimationStart(final Animation animation) { } @Override public void onAnimationRepeat(final Animation animation) { } @Override public void onAnimationEnd(final Animation animation) { contentView.clearAnimation(); sidebarView.clearAnimation(); requestLayout(); shown = show; if (shown) { notifyOnSidebarShown(); } else { notifyOnSidebarHidden(); } } }; } /** * Notifies all listeners, which have been registered to be notified, when the sidebar becomes * shown or hidden, about when the sidebar has become shown. */ private void notifyOnSidebarShown() { for (SidebarListener listener : listeners) { listener.onSidebarShown(this); } } /** * Notifies all listeners, which have been registered to be notified, when the sidebar becomes * show or hidden, about when the sidebar has become hidden. */ private void notifyOnSidebarHidden() { for (SidebarListener listener : listeners) { listener.onSidebarHidden(this); } } /** * Measures the sidebar's actual width in pixels, depending on the width of the parent view. */ private void measureSidebarWidth() { mSidebarWidth = Math.round(getWidth() * sidebarWidth); if (maxSidebarWidth != -1) { mSidebarWidth = Math.min(maxSidebarWidth, mSidebarWidth); } if (sidebarView != null) { sidebarView.getLayoutParams().width = mSidebarWidth + sidebarView.getShadowWidth(); } } /** * Measures the sidebar's actual offset in pixels, depending on the width of the parent view. */ private void measureSidebarOffset() { mOffset = Math.round(getWidth() * sidebarOffset); if (maxSidebarOffset != -1) { mOffset = Math.min(maxSidebarOffset, mOffset); } mContentWidth = getWidth() - mOffset; if (contentView != null) { contentView.getLayoutParams().width = mContentWidth; } } /** * Calculates and returns the distance, the sidebar has to be moved by when it should become * shown or hidden, depending on its current position. * * @param show * True, if the sidebar should become shown, false otherwise * @return The distance, the sidebar has to be moved by, as a {@link Float} value */ private float calculateAnimationDistance(final boolean show) { float distance; if (getLocation() == Location.LEFT) { if (show) { distance = mSidebarWidth + sidebarView.getShadowWidth() - sidebarView.getRight(); } else { distance = mOffset + sidebarView.getShadowWidth() - sidebarView.getRight(); } } else { if (show) { distance = getWidth() - mSidebarWidth - sidebarView.getShadowWidth() - sidebarView.getLeft(); } else { distance = mContentWidth - sidebarView.getShadowWidth() - sidebarView.getLeft(); } } return distance; } /** * Calculates the duration of the animation, which is used to hide or show the sidebar, * depending on a specific distance and speed. * * @param distance * The distance, the sidebar has to be moved by, as a {@link Float} value * @param animationSpeed * The speed of the animation in pixels per millisecond as a {@link Float} value * @return The duration of the animation in milliseconds as an {@link Integer} value */ private int calculateAnimationDuration(final float distance, final float animationSpeed) { return Math.round(Math.abs(distance) / animationSpeed); } /** * Calculates and returns the position of the sidebar's left and right edge, depending on its * location and whether it is currently shown or not. * * @return The position of the sidebar's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateSidebarConstraints() { return calculateSidebarConstraints(isSidebarShown()); } /** * Calculates and returns the position of the sidebar's left and right edge, depending on its * location and a specific boolean value, which specifies, whether it should be shown or not. * * @param shown * True, if the sidebar should be shown, false otherwise * @return The position of the sidebar's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateSidebarConstraints(final boolean shown) { int leftEdge; int rightEdge; if (getLocation() == Location.LEFT) { if (shown) { leftEdge = 0; } else { leftEdge = mOffset - mSidebarWidth; } } else { if (shown) { leftEdge = getWidth() - mSidebarWidth - sidebarView.getShadowWidth(); } else { leftEdge = getWidth() - mOffset - sidebarView.getShadowWidth(); } } rightEdge = leftEdge + mSidebarWidth + sidebarView.getShadowWidth(); return new Pair<>(leftEdge, rightEdge); } /** * Calculates and returns the position of the content's left and right edge, depending on the * sidebar's location, whether the sidebar is currently shown and the current content mode. * * @return The position of the content's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateContentConstraints() { if (getContentMode() == ContentMode.SCROLL) { return calculateScrolledContentConstraints(); } else { return calculateResizedContentConstraints(); } } /** * Calculates and returns the position of the content's left and right edge, depending on the * sidebar's location and whether the sidebar is currently shown, using the content mode * <code>SCROLL</code>. * * @return The position of the content's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateScrolledContentConstraints() { int leftEdge; int rightEdge; if (getLocation() == Location.LEFT) { if (isSidebarShown()) { leftEdge = mOffset + Math.round((mSidebarWidth - mOffset) * scrollRatio); } else { leftEdge = mOffset; } } else { if (isSidebarShown()) { leftEdge = Math.round((-mSidebarWidth + mOffset) * scrollRatio); } else { leftEdge = 0; } } rightEdge = leftEdge + mContentWidth; return new Pair<>(leftEdge, rightEdge); } /** * Calculates and returns the position of the content's left and right edge, depending on the * sidebar's location and whether the sidebar is currently shown, using the content mode * <code>RESIZE</code>. * * @return The position of the content's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateResizedContentConstraints() { int leftEdge; int rightEdge; if (getLocation() == Location.LEFT) { rightEdge = getWidth(); if (isSidebarShown()) { leftEdge = mSidebarWidth; } else { leftEdge = mOffset; } } else { leftEdge = 0; if (isSidebarShown()) { rightEdge = getWidth() - mSidebarWidth; } else { rightEdge = getWidth() - mOffset; } } return new Pair<>(leftEdge, rightEdge); } /** * Calculates and returns the position of the sidebar's left and right edge, depending on its * location, while the user performs a drag gesture. * * @return The position of the sidebar's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateSidebarConstraintsWhileDragging() { Pair<Integer, Integer> shownSidebarConstraints = calculateSidebarConstraints(true); Pair<Integer, Integer> hiddenSidebarConstraints = calculateSidebarConstraints(false); int leftEdge = calculateSidebarConstraints().first + dragHelper.getDistance(); if (getLocation() == Location.LEFT) { leftEdge = Math.max(hiddenSidebarConstraints.first, leftEdge); leftEdge = Math.min(shownSidebarConstraints.first, leftEdge); } else { leftEdge = Math.max(shownSidebarConstraints.first, leftEdge); leftEdge = Math.min(hiddenSidebarConstraints.first, leftEdge); } int rightEdge = leftEdge + mSidebarWidth + sidebarView.getShadowWidth(); return new Pair<>(leftEdge, rightEdge); } /** * Calculates and returns the position of the content's left and right edge, depending on the * sidebar's location and the current content mode, while the user performs a drag gesture. * * @param sidebarConstraints * The current position of the sidebar's left and right edge, as an instance of the * class {@link Pair} * @return The position of the content's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateContentConstraintsWhileDragging( final Pair<Integer, Integer> sidebarConstraints) { if (getContentMode() == ContentMode.SCROLL) { return calculateScrolledContentConstraintsWhileDragging(sidebarConstraints); } else { return calculateResizedContentConstraintsWhileDragging(sidebarConstraints); } } /** * Calculates and returns the position of the content's left and right edge, depending on the * sidebar's location, using the content mode <code>SCROLL</code>, while the user performs a * drag gesture. * * @param sidebarConstraints * The current position of the sidebar's left and right edge, as an instance of the * class {@link Pair} * @return The position of the content's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateScrolledContentConstraintsWhileDragging( final Pair<Integer, Integer> sidebarConstraints) { int leftEdge; int rightEdge; if (getLocation() == Location.LEFT) { leftEdge = mOffset + Math .round((sidebarConstraints.second - sidebarView.getShadowWidth() - mOffset) * scrollRatio); } else { leftEdge = Math .round((sidebarConstraints.first + sidebarView.getShadowWidth() - mContentWidth) * scrollRatio); } rightEdge = leftEdge + mContentWidth; return new Pair<>(leftEdge, rightEdge); } /** * Calculates and returns the position of the content's left and right edge, depending on the * sidebar's location, using the content mode <code>RESIZE</code>, while the user performs a * drag gesture. * * @param sidebarConstraints * The current position of the sidebar's left and right edge, as an instance of the * class {@link Pair} * @return The position of the content's left and right edge as an instance of the class {@link * Pair} */ private Pair<Integer, Integer> calculateResizedContentConstraintsWhileDragging( final Pair<Integer, Integer> sidebarConstraints) { int leftEdge; int rightEdge; if (getLocation() == Location.LEFT) { leftEdge = sidebarConstraints.second - sidebarView.getShadowWidth(); rightEdge = getWidth(); } else { leftEdge = 0; rightEdge = sidebarConstraints.first + sidebarView.getShadowWidth(); } return new Pair<>(leftEdge, rightEdge); } /** * Handles when a drag gesture is performed by the user. * * @param dragPosition * The current horizontal position of the drag gesture as a {@link Float} value * @return True, if the sidebar has been moved by the drag gesture, false otherwise */ private boolean handleDrag(final float dragPosition) { if (!isAnimationRunning()) { dragHelper.update(dragPosition); if (dragHelper.hasThresholdBeenReached() && isDraggingAllowed(dragHelper.getStartPosition())) { Pair<Integer, Integer> sidebarPos = calculateSidebarConstraintsWhileDragging(); Pair<Integer, Integer> contentPos = calculateContentConstraintsWhileDragging(sidebarPos); sidebarView.layout(sidebarPos.first, sidebarView.getTop(), sidebarPos.second, sidebarView.getBottom()); contentView.setOverlayTransparency(calculateContentOverlayTransparency()); contentView.getLayoutParams().width = contentPos.second - contentPos.first; contentView.layout(contentPos.first, contentView.getTop(), contentPos.second, contentView.getBottom()); if (getContentMode() == ContentMode.RESIZE) { contentView.requestLayout(); } return true; } } return false; } /** * Handles when a drag gesture has been ended by the user. */ private void handleRelease() { dragHelper.reset(); float thresholdPosition = calculatePositionWhereDragThresholdIsReached(); float speed = Math.max(dragHelper.getDragSpeed(), animationSpeed); if (getLocation() == Location.LEFT) { if (sidebarView.getRight() - sidebarView.getShadowWidth() > thresholdPosition) { animateShowSidebar(calculateAnimationDistance(true), speed, new DecelerateInterpolator()); } else { animateHideSidebar(calculateAnimationDistance(false), speed, new DecelerateInterpolator()); } } else { if (sidebarView.getLeft() + sidebarView.getShadowWidth() < thresholdPosition) { animateShowSidebar(calculateAnimationDistance(true), speed, new DecelerateInterpolator()); } else { animateHideSidebar(calculateAnimationDistance(false), speed, new DecelerateInterpolator()); } } } /** * Handles when the sidebar or content is clicked by the user. * * @param clickPosition * The horizontal position of the click as a {@link Float} value */ private void handleClick(final float clickPosition) { dragHelper.reset(); if (isSidebarClicked(clickPosition)) { if (showOnSidebarClick) { showSidebar(); } } else if (isContentClicked(clickPosition)) { if (hideOnContentClick) { hideSidebar(); } } } /** * Returns, whether a click at a specific position targets the content, or not. * * @param clickPosition * The horizontal position of the click as a {@link Float} value * @return True, if the content has been clicked, false otherwise */ private boolean isContentClicked(final float clickPosition) { return !isSidebarClicked(clickPosition); } /** * Returns, whether a click at a specific position targets the sidebar, or not. * * @param clickPosition * The horizontal position of the click as a {@link Float} value * @return True, if the sidebar has been clicked, false otherwise */ private boolean isSidebarClicked(final float clickPosition) { if (getLocation() == Location.LEFT) { if (isSidebarShown()) { return clickPosition < mSidebarWidth; } else { return clickPosition < mOffset; } } else { if (isSidebarShown()) { return clickPosition > getWidth() - mSidebarWidth; } else { return clickPosition > mContentWidth; } } } /** * Returns, whether a click at a specific position targets the edge of the sidebar's parent * view, or not. * * @param clickPosition * The horizontal position of the click as a {@link Float} value * @return True, if the edge of the sidebar's parent view has been clicked, false otherwise */ private boolean isEdgeClicked(final float clickPosition) { int tolerance = dpToPixels(getContext(), EDGE_DRAGGING_TOLERANCE); if (getLocation() == Location.LEFT) { return clickPosition <= tolerance; } else { return clickPosition >= getWidth() - tolerance; } } /** * Returns, whether a drag gesture, which has been started at a specific position, is allowed, * depending on the used drag modes, or not. * * @param dragStartPosition * The horizontal position, the drag gesture has been started at, as an {@link Integer} * value * @return True, if the drag gesture is allowed, false otherwise */ private boolean isDraggingAllowed(final int dragStartPosition) { DragMode currentDragMode = dragModeWhenHidden; if (isSidebarShown()) { currentDragMode = dragModeWhenShown; } if (currentDragMode == DragMode.DISABLED) { return false; } else if (currentDragMode == DragMode.SIDEBAR_ONLY) { return isSidebarClicked(dragStartPosition); } else if (currentDragMode == DragMode.CONTENT_ONLY) { return isContentClicked(dragStartPosition); } else if (currentDragMode == DragMode.EDGE) { return isEdgeClicked(dragStartPosition); } return true; } /** * Calculates the position, where the drag threshold is reached, depending on whether the * sidebar's location and whether it is currently shown or hidden. * * @return The horizontal position, where the drag threshold is reached, as a {@link Float} * value */ private float calculatePositionWhereDragThresholdIsReached() { float position; if (getLocation() == Location.LEFT) { if (isSidebarShown()) { position = mSidebarWidth - ((mSidebarWidth - mOffset) * dragThreshold); } else { position = mOffset + ((mSidebarWidth - mOffset) * dragThreshold); } } else { if (isSidebarShown()) { position = getWidth() - mSidebarWidth + ((mSidebarWidth - mOffset) * dragThreshold); } else { position = getWidth() - mOffset - ((mSidebarWidth - mOffset) * dragThreshold); } } return position; } /** * Calculates and returns the distance after which dragging has an effect on the sidebar in * pixels. The distance depends on the current set drag sensitivity, which corresponds to an * internal value range. * * @return The distance after which dragging has an effect on the sidebar in pixels as an {@link * Integer} value */ private int calculateDragSensitivity() { int range = MAX_DRAG_SENSITIVITY - MIN_DRAG_SENSITIVITY; return Math.round((1 - getDragSensitivity()) * range + MIN_DRAG_SENSITIVITY); } /** * Calculates and returns the transparency of the content overlay, depending on the sidebar's * current position and the transparency, which should be applied, when the sidebar is shown. * * @return The transparency of the content overlay as a {@link Float} value. If the transparency * is 0.0, the overlay will be completely transparent, if it is 1.0, the overlay will be not * transparent at all */ private float calculateContentOverlayTransparency() { float totalDistance = mSidebarWidth - mOffset; float distance = Math.abs(calculateAnimationDistance(false)); return (1 - getContentOverlayTransparency()) * (distance / totalDistance); } /** * Creates a new custom view, which allows to show a sidebar, which overlaps the view's main * content and can be shown or hidden in an animated manner. * * @param context * The context, the sidebar should belong to, as an instance of the class {@link * Context}. The context may not be null */ public Sidebar(@NonNull final Context context) { this(context, null); } /** * Creates a new custom view, which allows to show a sidebar, which overlaps the view's main * content and can be shown or hidden in an animated manner. This constructor is called when a * preference is being constructed from an XML file, supplying attributes that were specified in * the XML file. This version uses a default style of 0, so the only attribute values applied * are those in the context's theme and the given attribute set. * * @param context * The context, the sidebar should belong to, as an instance of the class {@link * Context}. The context may not be null * @param attributeSet * The attribute set, the view's attributes should be obtained from, as an instance of * the type {@link AttributeSet} or null, if no attributes should be obtained */ public Sidebar(@NonNull final Context context, @Nullable final AttributeSet attributeSet) { super(context, attributeSet); initialize(attributeSet); } /** * Creates a new custom view, which allows to show a sidebar, which overlaps the view's main * content and can be shown or hidden in an animated manner. This constructor allows subclasses * to use their own base style when they are inflating. * * @param context * The context, the sidebar should belong to, as an instance of the class {@link * Context}. The context may not be null * @param attributeSet * The attribute set, the view's attributes should be obtained from, as an instance of * the type {@link AttributeSet} or null, if no attributes should be obtained * @param defaultStyle * The default style to apply to this preference. If 0, no style will be applied (beyond * what is included in the theme). This may either be an attribute resource, whose value * will be retrieved from the current theme, or an explicit style resource */ public Sidebar(@NonNull final Context context, @Nullable final AttributeSet attributeSet, final int defaultStyle) { super(context, attributeSet, defaultStyle); initialize(attributeSet); } /** * Creates a new custom view, which allows to show a sidebar, which overlaps the view's main * content and can be shown or hidden in an animated manner. This constructor allows subclasses * to use their own base style when they are inflating. * * @param context * The context, the sidebar should belong to, as an instance of the class {@link * Context}. The context may not be null * @param attributeSet * The attribute set, the view's attributes should be obtained from, as an instance of * the type {@link AttributeSet} or null, if no attributes should be obtained * @param defaultStyle * The default style to apply to this preference. If 0, no style will be applied (beyond * what is included in the theme). This may either be an attribute resource, whose value * will be retrieved from the current theme, or an explicit style resource * @param defaultStyleResource * A resource identifier of a style resource that supplies default values for the * preference, used only if the default style is 0 or can not be found in the theme. Can * be 0 to not look for defaults */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public Sidebar(@NonNull final Context context, @Nullable final AttributeSet attributeSet, final int defaultStyle, final int defaultStyleResource) { super(context, attributeSet, defaultStyle, defaultStyleResource); initialize(attributeSet); } /** * Returns, whether the sidebar is currently shown, or not. * * @return True, if the sidebar is currently shown, false otherwise */ public final boolean isSidebarShown() { return shown; } /** * Shows the sidebar, if it is currently hidden. */ public final void showSidebar() { if (contentView == null) { throw new IllegalStateException("The content view is not set"); } else if (sidebarView == null) { throw new IllegalStateException("The sidebar view is not set"); } else if (!isSidebarShown()) { animateShowSidebar(calculateAnimationDistance(true), new AccelerateDecelerateInterpolator()); } } /** * Hides the sidebar, if it is currently shown. */ public final void hideSidebar() { if (contentView == null) { throw new IllegalStateException("The content view is not set"); } else if (sidebarView == null) { throw new IllegalStateException("The sidebar view is not set"); } else if (isSidebarShown()) { animateHideSidebar(calculateAnimationDistance(false), new AccelerateDecelerateInterpolator()); } } /** * Hides the sidebar, if it is currently shown, or shows it, if it is currently hidden. */ public final void toggleSidebar() { if (isSidebarShown()) { hideSidebar(); } else { showSidebar(); } } /** * Returns the view, which is contained by the sidebar. * * @return The view, which is contained by the sidebar, as an instance of the class {@link View} * or null, if no sidebar view is set */ public final View getSidebarView() { if (sidebarView != null) { return sidebarView.getSidebarView(); } else { return null; } } /** * Sets the view, which should be contained by the sidebar. * * @param sidebarViewId * The resource id of the view, which should be set, as an {@link Integer} value. The id * must be a valid resource id */ public final void setSidebarView(@LayoutRes final int sidebarViewId) { inflateSidebarView(InflaterFactory.createInflater(sidebarViewId)); } /** * Sets the view, which should be contained by the sidebar. * * @param sidebarView * The view, which should be set, as an instance of the class {@link View}. The view may * not be null */ public final void setSidebarView(@NonNull final View sidebarView) { inflateSidebarView(InflaterFactory.createInflater(sidebarView)); } /** * Returns the view, which is used as the main content. * * @return The view, which is used as the main content, as an instance of the class {@link View} */ public final View getContentView() { if (contentView != null) { return contentView.getContentView(); } else { return null; } } /** * Sets the view, which should be used as the main content. * * @param contentViewId * The resource id of the view, which should be set, as an {@link Integer} value. The id * must be a valid resource id */ public final void setContentView(@LayoutRes final int contentViewId) { inflateContentView(InflaterFactory.createInflater(contentViewId)); } /** * Sets the view, which should be used as the main content. * * @param contentView * The view, which should be set, as an instance of the class {@link View}. The view may * not be null */ public final void setContentView(@NonNull final View contentView) { inflateContentView(InflaterFactory.createInflater(contentView)); } /** * Returns the background of the sidebar. * * @return The background of the sidebar as an instance of the class {@link Drawable} or null, * if the default background is used */ public final Drawable getSidebarBackground() { return sidebarBackground; } /** * Sets the background of the sidebar. * * @param background * The background, which should be set, as an instance of the class {@link Drawable} or * null, if the default background should be used */ public final void setSidebarBackground(@Nullable final Drawable background) { this.sidebarBackground = background; if (sidebarView != null) { sidebarView.setSidebarBackground(sidebarBackground); } } /** * Sets the background of the sidebar. * * @param resourceId * The resource id of the background, which should be set, as an {@link Integer} value. * The id must be a valid resource id */ public final void setSidebarBackground(@DrawableRes final int resourceId) { if (resourceId == -1) { this.sidebarBackground = null; } else { this.sidebarBackground = ContextCompat.getDrawable(getContext(), resourceId); } if (sidebarView != null) { sidebarView.setSidebarBackground(sidebarBackground); } } /** * Returns the location of the sidebar. * * @return The location of the sidebar as a value of the enum {@link Location}. The location may * either be <code>LEFT</code> or <code>RIGHT</code> */ public final Location getLocation() { return location; } /** * Sets the location of the sidebar. * * @param location * The location, which should be set, as a value of the enum {@link Location}. The * location may either be <code>LEFT</code> or <code>RIGHT</code> */ public final void setLocation(@NonNull final Location location) { ensureNotNull(location, "The location may not be null"); this.location = location; if (sidebarView != null && contentView != null) { sidebarView.setLocation(location); } requestLayout(); } /** * Returns the speed of the animation, which is used to show or hide the sidebar. * * @return The speed of the animation in dp per millisecond as a {@link Float} value. The speed * must be greater than 0 */ public final float getAnimationSpeed() { return pixelsToDp(getContext(), animationSpeed); } /** * Sets the speed of the animation, which is used to show or hide the sidebar. * * @param animationSpeed * The speed, which should be set, in dp per millisecond as a {@link Float} value. The * speed must be greater than 0 */ public final void setAnimationSpeed(final float animationSpeed) { ensureGreater(animationSpeed, 0, "The animation speed must be greater than 0"); this.animationSpeed = dpToPixels(getContext(), animationSpeed); } /** * Returns the width of the sidebar in relation to the width of the parent view. * * @return The width of the sidebar in relation to the width of the parent view, as a {@link * Float} value. The width must be at least 0 and at maximum 1 */ public final float getSidebarWidth() { return sidebarWidth; } /** * Sets the width of the sidebar in relation to the width of the parent view. * * @param sidebarWidth * The width, which should be set, as a {@link Float} value. The width must be at least * 0 and at maximum 1 */ public final void setSidebarWidth(final float sidebarWidth) { ensureAtLeast(sidebarWidth, 0, "The sidebar width must be at least 0"); ensureAtMaximum(sidebarWidth, 1, "The sidebar width must be at maximum 1"); ensureGreater(sidebarWidth, sidebarOffset, "The sidebar width must be greater than the sidebar offset"); this.sidebarWidth = sidebarWidth; measureSidebarWidth(); requestLayout(); } /** * Returns the maximum width of the sidebar. * * @return The maximum width of the sidebar in dp as an {@link Integer} value or -1, if the * sidebar's width is not restricted */ public final int getMaxSidebarWidth() { if (maxSidebarWidth != -1) { return pixelsToDp(getContext(), maxSidebarWidth); } else { return -1; } } /** * Sets the maximum width of the sidebar. * * @param maxSidebarWidth * The maximum width, which should be set, in dp as an {@link Integer} value or -1, if * the sidebar's width should not be restricted */ public final void setMaxSidebarWidth(final int maxSidebarWidth) { if (maxSidebarWidth != -1) { ensureGreater(maxSidebarWidth, 0, "The maximum sidebar width must be greater than 0"); setMaxSidebarWidthInPixels(dpToPixels(getContext(), maxSidebarWidth)); } else { setMaxSidebarWidthInPixels(-1); } } /** * Returns the amount of space in relation to the width of the parent view, the sidebar is * visible, even if it is currently hidden. * * @return The offset of the sidebar as a {@link Float} value. The offset must be at least 0 and * at maximum 1 */ public final float getSidebarOffset() { return sidebarOffset; } /** * Sets the amount of space in relation to the width of the parent view, the sidebar should be * visible, even if it is currently hidden. * * @param sidebarOffset * The offset of the sidebar, which should be set, as a {@link Float} value. The offset * must be at least 0 and at maximum 1 */ public final void setSidebarOffset(final float sidebarOffset) { ensureAtLeast(sidebarOffset, 0, "The sidebar offset must be at least 0"); ensureAtMaximum(sidebarOffset, 1, "The sidebar offset must be at maximum 1"); ensureSmaller(sidebarOffset, sidebarWidth, "The sidebar offset must be less than the sidebar width"); this.sidebarOffset = sidebarOffset; measureSidebarOffset(); requestLayout(); } /** * Returns the maximum offset of the sidebar. * * @return The maximum offset of the sidebar in dp as an {@link Integer} value or -1, if the * sidebar's offset should not be restricted */ public final int getMaxSidebarOffset() { if (maxSidebarOffset != -1) { return pixelsToDp(getContext(), maxSidebarOffset); } else { return -1; } } /** * Sets the maximum offset of the sidebar. * * @param maxSidebarOffset * The maximum offset of the sidebar, which should be set, in dp as an {@link Integer} * value or -1, if the sidebar's offset should not be restricted */ public final void setMaxSidebarOffset(final int maxSidebarOffset) { if (maxSidebarOffset != -1) { ensureGreater(maxSidebarOffset, 0, "The maximum sidebar offset must be greater than 0"); setMaxSidebarOffsetInPixels(dpToPixels(getContext(), maxSidebarOffset)); } else { setMaxSidebarOffsetInPixels(-1); } } /** * Returns the content mode, which specifies how the main content is handled, when the sidebar * becomes shown or hidden. * * @return The content mode as a value of the enum {@link ContentMode}. The content mode may * either be <code>SCROLL</code> or <code>RESIZE</code> */ public final ContentMode getContentMode() { return contentMode; } /** * Sets the content mode, which specifies how the main content should be handled, when the * sidebar becomes shown or hidden. * * @param contentMode * The content mode, which should be set, as a value of the enum {@link ContentMode}. * The content mode may either be <code>SCROLL</code> or <code>RESIZE</code> */ public final void setContentMode(@NonNull final ContentMode contentMode) { ensureNotNull(contentMode, "The content mode may not be null"); this.contentMode = contentMode; requestLayout(); } /** * Returns the ratio between the distance, the sidebar is moved by, when it becomes shown or * hidden, in relation to the distance, the content is moved by. * * @return The scroll ratio as a {@link Float} value. The scroll ratio must be at least 0 and at * maximum 1. If set to 1.0, the content is moved exactly as far as the sidebar, if set to 0.0, * the content is not moved at all. The scroll ratio does only apply, if the content mode is set * to <code>SCROLL</code> */ public final float getScrollRatio() { return scrollRatio; } /** * Sets the ratio between the distance, the sidebar is moved by, when it becomes shown or * hidden, in relation to the distance, the content is moved by. * * @param scrollRatio * The scroll ratio, which should be set, as a {@link Float} value. The scroll ratio * must be at least 0 and at maximum 1. If set to 1.0, the content will be moved exactly * as far as the sidebar, if set to 0.0, the content will not be moved at all. The * scroll ratio will only apply, if the content mode is set to <code>SCROLL</code> */ public final void setScrollRatio(final float scrollRatio) { ensureAtLeast(scrollRatio, 0, "The scroll ratio must be at least 0"); ensureAtMaximum(scrollRatio, 1, "The scroll ratio must be at maximum 1"); this.scrollRatio = scrollRatio; requestLayout(); } /** * Returns the drag mode, which specifies the region, where drag gestures are recognized, when * the sidebar is currently hidden. * * @return The drag mode as a value of the enum {@link DragMode}. The drag mode may either be * <code>BOTH</code>, <code>SIDEBAR_ONLY</code>, <code>CONTENT_ONLY</code>, * <code>DISABLED</code> or <code>EDGE</code> */ public final DragMode getDragModeWhenHidden() { return dragModeWhenHidden; } /** * Sets the drag mode, which specifies the region, where drag gestures should be recognized, * when the sidebar is currently hidden. * * @param dragMode * The drag mode as a value of the enum {@link DragMode}. The drag mode may either be * <code>BOTH</code>, <code>SIDEBAR_ONLY</code>, <code>CONTENT_ONLY</code>, * <code>DISABLED</code> or <code>EDGE</code> */ public final void setDragModeWhenHidden(@NonNull final DragMode dragMode) { ensureNotNull(dragMode, "The drag mode may not be null"); this.dragModeWhenHidden = dragMode; } /** * Returns the drag mode, which specifies the region, where drag gestures are recognized, when * the sidebar is currently shown. * * @return The drag mode as a value of the enum {@link DragMode}. The drag mode may either be * <code>BOTH</code>, <code>SIDEBAR_ONLY</code>, <code>CONTENT_ONLY</code> or * <code>DISABLED</code> */ public final DragMode getDragModeWhenShown() { return dragModeWhenShown; } /** * Sets the drag mode, which specifies the region, where drag gestures should be recognized, * when the sidebar is currently shown. * * @param dragMode * The drag mode as a value of the enum {@link DragMode}. The drag mode may either be * <code>BOTH</code>, <code>SIDEBAR_ONLY</code>, <code>CONTENT_ONLY</code> or * <code>DISABLED</code> */ public final void setDragModeWhenShown(@NonNull final DragMode dragMode) { ensureNotNull(dragMode, "The drag mode may not be null"); if (dragMode == DragMode.EDGE) { throw new IllegalArgumentException("The drag mode when shown may not be " + DragMode.EDGE); } this.dragModeWhenShown = dragMode; } /** * Returns the distance, the sidebar has to be dragged until its state changes, in relation to * the whole distance. * * @return The drag threshold as a {@link Float} value. The drag threshold must be at least 0 * and at maximum 1 */ public final float getDragThreshold() { return dragThreshold; } /** * Sets the distance, the sidebar has to be dragged until its state changes, in relation to the * whole distance. * * @param dragThreshold * The drag threshold, which should be set, as a {@link Float} value. The drag threshold * must be at least 0 and at maximum 1 */ public final void setDragThreshold(final float dragThreshold) { ensureAtLeast(dragThreshold, 0, "The drag threshold must be at least 0"); ensureAtMaximum(dragThreshold, 1, "The drag threshold must be at maximum 1"); this.dragThreshold = dragThreshold; } /** * Returns the sensitivity, which specifies the distance after which dragging has an effect on * the sidebar, in relation to an internal value range. * * @return The drag sensitivity as a {@link Float} value. The drag sensitivity must be at lest 0 * and at maximum 1 */ public final float getDragSensitivity() { return dragSensitivity; } /** * Sets the sensitivity, which specifies the distance after which dragging has an effect on the * sidebar, in relation to an internal value range. * * @param dragSensitivity * The drag sensitivity, which should be set, as a {@link Float} value. The drag * sensitivity must be at lest 0 and at maximum 1 */ public final void setDragSensitivity(final float dragSensitivity) { ensureAtLeast(dragSensitivity, 0, "The drag sensitivity must be at least 0"); ensureAtMaximum(dragSensitivity, 1, "The drag sensitivity must be at maximum 1"); this.dragSensitivity = dragSensitivity; this.dragHelper = new DragHelper(calculateDragSensitivity()); } /** * Returns, whether the sidebar is hidden, when the device's back button is clicked, or not. * * @return True, if the sidebar is hidden, when the device's back button is clicked, false * otherwise */ public final boolean isHiddenOnBackButton() { return hideOnBackButton; } /** * Sets, whether the sidebar should be hidden, when the device's back button is clicked, or * not. * * @param hideOnBackButton * True, if the sidebar should be hidden, when the device's back button is clicked, * false otherwise */ public final void hideOnBackButton(final boolean hideOnBackButton) { this.hideOnBackButton = hideOnBackButton; } /** * Returns, whether the sidebar is hidden, when the content is clicked by the user, or not. * * @return True, if the sidebar is hidden, when the content is clicked by the user, false * otherwise */ public final boolean isHiddenOnContentClick() { return hideOnContentClick; } /** * Sets, whether the sidebar should be hidden, when the content is clicked by the user, or not. * * @param hideOnContentClick * True, if the sidebar should be hidden, when the content is clicked by the user, false * otherwise */ public final void hideOnContentClick(final boolean hideOnContentClick) { this.hideOnContentClick = hideOnContentClick; } /** * Returns, whether the sidebar is shown, when it is clicked by the user, or not. * * @return True, if the sidebar is shown, when it is clicked by the user, false otherwise */ public final boolean isShownOnSidebarClick() { return showOnSidebarClick; } /** * Sets, whether the sidebar should be shown, when it is clicked by the user, or not. * * @param showOnSidebarClick * True, if the sidebar should be shown, when it is clicked by the user, false * otherwise */ public final void showOnSidebarClick(final boolean showOnSidebarClick) { this.showOnSidebarClick = showOnSidebarClick; } /** * Returns the color of the overlay, which is shown in front of the main content, when the * sidebar is shown. * * @return The color of the overlay, which is shown in front of the main content, when the * sidebar is shown, as an {@link Integer} value */ public final int getContentOverlayColor() { return contentOverlayColor; } /** * Sets the color of the overlay, which is shown in front of the main content, when the sidebar * is shown. * * @param contentOverlayColor * The color, which should be set, as an {@link Integer} value */ public final void setContentOverlayColor(@ColorInt final int contentOverlayColor) { this.contentOverlayColor = contentOverlayColor; if (contentView != null) { contentView.setOverlayColor(contentOverlayColor); } } /** * Returns the transparency of the overlay, which is shown in front of the main content, when * the sidebar is shown. * * @return The transparency of the overlay, which is shown in front of the main content, when * the sidebar is shown, as a {@link Float} value. The transparency must be at least 0 and at * maximum 1. If the transparency is 1.0, the overlay is completely transparent, if it is 0.0, * the overlay is not transparent at all */ public final float getContentOverlayTransparency() { return contentOverlayTransparency; } /** * Sets the transparency of the overlay, which is shown in front of the main content, when the * sidebar is shown. * * @param contentOverlayTransparency * The transparency, which should be set, as a {@link Float} value. The transparency * must be at least 0 and at maximum 1. If the transparency is 1.0, the overlay will be * completely transparent, if it is 0.0, the overlay will not be transparent at all */ public final void setContentOverlayTransparency(final float contentOverlayTransparency) { ensureAtLeast(contentOverlayTransparency, 0, "The transparency must be at least 0"); ensureAtMaximum(contentOverlayTransparency, 1, "The transparency must be at maximum 1"); this.contentOverlayTransparency = contentOverlayTransparency; requestLayout(); } /** * Returns the elevation of the sidebar. * * @return The elevation of the sidebar in dp as an {@link Integer} value */ public final int getSidebarElevation() { return sidebarElevation; } /** * Sets the elevation of the sidebar. * * @param elevation * The elevation, which should be set, in dp as an {@link Integer} value. The elevation * must be at least 0 and at maximum 16 */ public final void setSidebarElevation(final int elevation) { ensureAtLeast(elevation, 0, "The sidebar elevation must be at least 0"); ensureAtMaximum(elevation, ElevationUtil.MAX_ELEVATION, "The sidebar elevation must be at least " + ElevationUtil.MAX_ELEVATION); this.sidebarElevation = elevation; if (sidebarView != null) { sidebarView.setSidebarElevation(elevation); } measureSidebarWidth(); requestLayout(); } /** * Adds a new listener, which should be notified, when the sidebar becomes shown or hidden. * * @param listener * The listener, which should be added, as an instance of the type {@link * SidebarListener}. The listener may not be null */ public final void addSidebarListener(@NonNull final SidebarListener listener) { ensureNotNull(listener, "The listener may not be null"); listeners.add(listener); } /** * Removes a specific listener, which should not be notified, when the sidebar becomes shown or * hidden, anymore. * * @param listener * The listener, which should be removed, as an instance of the type {@link * SidebarListener}. The listener may not be null */ public final void removeSidebarListener(@NonNull final SidebarListener listener) { ensureNotNull(listener, "The listener may not be null"); listeners.remove(listener); } /** * Returns, whether a drag gesture, which moves the sidebar, is currently performed, or not. * * @return True, if a drag gesture, which moves the sidebar, is currently performed, false * otherwise */ public final boolean isDragging() { return !dragHelper.isReseted() && dragHelper.hasThresholdBeenReached() && isDraggingAllowed(dragHelper.getStartPosition()); } /** * Returns, whether an animation, which moves the sidebar, is currently running, or not. * * @return True, if an animation, which moves the sidebar, is currently running, false otherwise */ public final boolean isAnimationRunning() { return sidebarView != null && sidebarView.getAnimation() != null; } @Override public final boolean onKeyPreIme(final int keyCode, final KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP && isSidebarShown() && hideOnBackButton) { hideSidebar(); return true; } return false; } @Override public final boolean dispatchTouchEvent(final MotionEvent event) { boolean handled = false; if (isSidebarClicked(event.getX()) && !isSidebarShown() && isShownOnSidebarClick()) { handled = true; } else if (isContentClicked(event.getX()) && isSidebarShown() && isHiddenOnContentClick()) { handled = true; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: handled = handleDrag(event.getX()); break; case MotionEvent.ACTION_UP: if (dragHelper.hasThresholdBeenReached() && isDraggingAllowed(dragHelper.getStartPosition())) { handleRelease(); } else { handleClick(event.getX()); } break; default: break; } return handled || super.dispatchTouchEvent(event); } @Override public final boolean onTouchEvent(final MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: return true; case MotionEvent.ACTION_MOVE: handleDrag(event.getX()); return true; case MotionEvent.ACTION_UP: if (dragHelper.hasThresholdBeenReached() && isDraggingAllowed(dragHelper.getStartPosition())) { handleRelease(); } else { handleClick(event.getX()); } performClick(); return true; default: break; } return super.onTouchEvent(event); } @Override public final boolean performClick() { super.performClick(); return true; } @Override protected final void onSizeChanged(final int width, final int height, final int oldWidth, final int oldHeigth) { super.onSizeChanged(width, height, oldWidth, oldHeigth); measureSidebarWidth(); measureSidebarOffset(); } @Override protected final void onLayout(final boolean changed, final int l, final int t, final int r, final int b) { if (!isDragging() && !isAnimationRunning()) { Pair<Integer, Integer> sidebarPos = calculateSidebarConstraints(); sidebarView.layout(sidebarPos.first, t, sidebarPos.second, b); Pair<Integer, Integer> contentPos = calculateContentConstraints(); contentView.setOverlayTransparency(calculateContentOverlayTransparency()); contentView.getLayoutParams().width = contentPos.second - contentPos.first; contentView.layout(contentPos.first, t, contentPos.second, b); if (getContentMode() == ContentMode.RESIZE) { contentView.requestLayout(); } } } @Override protected final void onMeasure(final int w, final int h) { super.onMeasure(w, h); super.measureChildren(w, h); } @Override protected final Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SidebarSavedState savedState = new SidebarSavedState(superState); savedState.setLocation(getLocation()); savedState.setAnimationSpeed(getAnimationSpeed()); savedState.setSidebarWidth(getSidebarWidth()); savedState.setMaxSidebarWidth(getMaxSidebarWidth()); savedState.setSidebarOffset(getSidebarOffset()); savedState.setMaxSidebarOffset(getMaxSidebarOffset()); savedState.setContentMode(getContentMode()); savedState.setScrollRatio(getScrollRatio()); savedState.setDragThreshold(getDragThreshold()); savedState.setDragSensitivity(getDragSensitivity()); savedState.setDragModeWhenHidden(getDragModeWhenHidden()); savedState.setDragModeWhenShown(getDragModeWhenShown()); savedState.setHideOnBackButton(isHiddenOnBackButton()); savedState.setHideOnContentClick(isHiddenOnContentClick()); savedState.setShowOnSidebarClick(isShownOnSidebarClick()); savedState.setContentOverlayColor(getContentOverlayColor()); savedState.setContentOverlayTransparency(getContentOverlayTransparency()); savedState.setSidebarElevation(getSidebarElevation()); savedState.setShown(isSidebarShown()); return savedState; } @Override protected final void onRestoreInstanceState(final Parcelable state) { if (state != null && state instanceof SidebarSavedState) { SidebarSavedState savedState = (SidebarSavedState) state; setLocation(savedState.getLocation()); setAnimationSpeed(savedState.getAnimationSpeed()); setSidebarWidth(savedState.getSidebarWidth()); setMaxSidebarWidth(savedState.getMaxSidebarWidth()); setSidebarOffset(savedState.getSidebarOffset()); setMaxSidebarOffset(savedState.getMaxSidebarOffset()); setContentMode(savedState.getContentMode()); setScrollRatio(savedState.getScrollRatio()); setDragThreshold(savedState.getDragThreshold()); setDragSensitivity(savedState.getDragSensitivity()); setDragModeWhenHidden(savedState.getDragModeWhenHidden()); setDragModeWhenShown(savedState.getDragModeWhenShown()); hideOnBackButton(savedState.isHideOnBackButton()); hideOnContentClick(savedState.isHideOnContentClick()); showOnSidebarClick(savedState.isShowOnSidebarClick()); setContentOverlayColor(savedState.getContentOverlayColor()); setContentOverlayTransparency(savedState.getContentOverlayTransparency()); setSidebarElevation(savedState.getSidebarElevation()); shown = savedState.isShown(); requestLayout(); super.onRestoreInstanceState(savedState.getSuperState()); } else { super.onRestoreInstanceState(state); } } }