/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 android.car.ui.provider;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.car.ui.CarUiResourceLoader;
import android.support.car.ui.QuantumInterpolator;
import android.support.car.ui.R;
import android.support.car.ui.ReversibleInterpolator;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewGroupCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* Acts as a top-level container for window content that allows for
* interactive "drawer" views to be pulled out from the edge of the window.
*
*
Drawer positioning and layout is controlled using the android:layout_gravity
* attribute on child views corresponding to which side of the view you want the drawer
* to emerge from: left or right. (Or start/end on platform versions that support layout direction.)
*
*
* To use CarDrawerLayout, add your drawer view as the first view in the CarDrawerLayout
* element and set the layout_gravity
appropriately. Drawers commonly use
* match_parent
for height with a fixed width. Add the content views as sibling views
* after the drawer view.
*
* {@link DrawerListener} can be used to monitor the state and motion of drawer views.
* Avoid performing expensive operations such as layout during animation as it can cause
* stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state.
* {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.
*/
public class CarDrawerLayout extends ViewGroup {
/**
* Indicates that any drawers are in an idle, settled state. No animation is in progress.
*/
public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
/**
* The drawer is unlocked.
*/
public static final int LOCK_MODE_UNLOCKED = 0;
/**
* The drawer is locked closed. The user may not open it, though
* the app may open it programmatically.
*/
public static final int LOCK_MODE_LOCKED_CLOSED = 1;
/**
* The drawer is locked open. The user may not close it, though the app
* may close it programmatically.
*/
public static final int LOCK_MODE_LOCKED_OPEN = 2;
private static final float MAX_SCRIM_ALPHA = 0.8f;
private static final boolean SCRIM_ENABLED = true;
private static final boolean SHADOW_ENABLED = true;
/**
* Minimum velocity that will be detected as a fling
*/
private static final int MIN_FLING_VELOCITY = 400; // dips per second
/**
* Experimental feature.
*/
private static final boolean ALLOW_EDGE_LOCK = false;
private static final boolean EDGE_DRAG_ENABLED = false;
private static final boolean CHILDREN_DISALLOW_INTERCEPT = true;
private static final float TOUCH_SLOP_SENSITIVITY = 1.f;
private static final int[] LAYOUT_ATTRS = new int[] {
android.R.attr.layout_gravity
};
public static final int DEFAULT_SCRIM_COLOR = 0xff262626;
private int mScrimColor = DEFAULT_SCRIM_COLOR;
private final Paint mScrimPaint = new Paint();
private final Paint mEdgeHighlightPaint = new Paint();
private final ViewDragHelper mDragger;
private final Runnable mInvalidateRunnable = new Runnable() {
@Override
public void run() {
requestLayout();
invalidate();
}
};
// view faders who will be given different colors as the drawer opens
private final Set mViewFaders;
private final ReversibleInterpolator mViewFaderInterpolator;
private final ReversibleInterpolator mDrawerFadeInterpolator;
private final Handler mHandler = new Handler();
private int mEndingViewColor;
private int mStartingViewColor;
private int mDrawerState;
private boolean mInLayout;
/** Whether we have done a layout yet. Used to initialize some view-related state. */
private boolean mFirstLayout = true;
private boolean mHasInflated;
private int mLockModeLeft;
private int mLockModeRight;
private boolean mChildrenCanceledTouch;
private DrawerListener mDrawerListener;
private DrawerControllerListener mDrawerControllerListener;
private Drawable mShadow;
private View mDrawerView;
private View mContentView;
private boolean mNeedsFocus;
/** Whether or not the drawer started open for the current gesture */
private boolean mStartedOpen;
private boolean mHasWheel;
/**
* Listener for monitoring events about drawers.
*/
public interface DrawerListener {
/**
* Called when a drawer's position changes.
* @param drawerView The child view that was moved
* @param slideOffset The new offset of this drawer within its range, from 0-1
*/
void onDrawerSlide(View drawerView, float slideOffset);
/**
* Called when a drawer has settled in a completely open state.
* The drawer is interactive at this point.
*
* @param drawerView Drawer view that is now open
*/
void onDrawerOpened(View drawerView);
/**
* Called when a drawer has settled in a completely closed state.
*
* @param drawerView Drawer view that is now closed
*/
void onDrawerClosed(View drawerView);
/**
* Called when a drawer is starting to open.
*
* @param drawerView Drawer view that is opening
*/
void onDrawerOpening(View drawerView);
/**
* Called when a drawer is starting to close.
*
* @param drawerView Drawer view that is closing
*/
void onDrawerClosing(View drawerView);
/**
* Called when the drawer motion state changes. The new state will
* be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
*
* @param newState The new drawer motion state
*/
void onDrawerStateChanged(int newState);
}
/**
* Used to execute when the drawer needs to handle state that the underlying views would like
* to handle in a specific way.
*/
public interface DrawerControllerListener {
void onBack();
boolean onScroll();
}
/**
* Stub/no-op implementations of all methods of {@link DrawerListener}.
* Override this if you only care about a few of the available callback methods.
*/
public static abstract class SimpleDrawerListener implements DrawerListener {
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
}
@Override
public void onDrawerOpened(View drawerView) {
}
@Override
public void onDrawerClosed(View drawerView) {
}
@Override
public void onDrawerOpening(View drawerView) {
}
@Override
public void onDrawerClosing(View drawerView) {
}
@Override
public void onDrawerStateChanged(int newState) {
}
}
/**
* Sets the color of (or tints) a view (or views).
*/
public interface ViewFader {
void setColor(int color);
}
public CarDrawerLayout(Context context) {
this(context, null);
}
public CarDrawerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CarDrawerLayout(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mViewFaders = new HashSet<>();
mEndingViewColor = getResources().getColor(R.color.car_tint);
mEdgeHighlightPaint.setColor(getResources().getColor(android.R.color.black));
final float density = getResources().getDisplayMetrics().density;
final float minVel = MIN_FLING_VELOCITY * density;
ViewDragCallback viewDragCallback = new ViewDragCallback();
mDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, viewDragCallback);
mDragger.setMinVelocity(minVel);
viewDragCallback.setDragger(mDragger);
ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
if (SHADOW_ENABLED) {
setDrawerShadow(CarUiResourceLoader.getDrawable(context, "drawer_shadow"));
}
Resources.Theme theme = context.getTheme();
TypedArray ta = theme.obtainStyledAttributes(new int[] {
android.R.attr.colorPrimaryDark
});
setScrimColor(ta.getColor(0, context.getResources().getColor(R.color.car_grey_900)));
mViewFaderInterpolator = new ReversibleInterpolator(
new QuantumInterpolator(QuantumInterpolator.FAST_OUT_SLOW_IN, 0.25f, 0.25f, 0.5f),
new QuantumInterpolator(QuantumInterpolator.FAST_OUT_SLOW_IN, 0.43f, 0.14f, 0.43f)
);
mDrawerFadeInterpolator = new ReversibleInterpolator(
new QuantumInterpolator(QuantumInterpolator.FAST_OUT_SLOW_IN, 0.625f, 0.25f, 0.125f),
new QuantumInterpolator(QuantumInterpolator.FAST_OUT_LINEAR_IN, 0.58f, 0.14f, 0.28f)
);
mHasWheel = CarUiResourceLoader.getBoolean(context, "has_wheel", false);
}
@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
int action = keyEvent.getAction();
int keyCode = keyEvent.getKeyCode();
final View drawerView = findDrawerView();
if (drawerView != null && getDrawerLockMode(drawerView) == LOCK_MODE_UNLOCKED) {
if (isDrawerOpen()) {
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|| keyCode == KeyEvent.KEYCODE_SOFT_RIGHT) {
closeDrawer();
return true;
} else if (keyCode == KeyEvent.KEYCODE_BACK
&& action == KeyEvent.ACTION_UP
&& mDrawerControllerListener != null) {
mDrawerControllerListener.onBack();
return true;
} else {
return drawerView.dispatchKeyEvent(keyEvent);
}
}
}
return mContentView.dispatchKeyEvent(keyEvent);
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
final View drawerView = findDrawerView();
if (drawerView != null
&& ev.getAction() == MotionEvent.ACTION_SCROLL
&& mDrawerControllerListener != null
&& mDrawerControllerListener.onScroll()) {
return true;
}
return super.dispatchGenericMotionEvent(ev);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mHasInflated = true;
setAutoDayNightMode();
setOnGenericMotionListener(new OnGenericMotionListener() {
@Override
public boolean onGenericMotion(View view, MotionEvent event) {
if (getChildCount() == 0) {
return false;
}
if (isDrawerOpen()) {
View drawerView = findDrawerView();
ViewGroup viewGroup = (ViewGroup) ((FrameLayout) drawerView).getChildAt(0);
return viewGroup.getChildAt(0).onGenericMotionEvent(event);
}
View contentView = findContentView();
ViewGroup viewGroup = (ViewGroup) ((FrameLayout) contentView).getChildAt(0);
return viewGroup.getChildAt(0).onGenericMotionEvent(event);
}
});
}
/**
* Set a simple drawable used for the left or right shadow.
* The drawable provided must have a nonzero intrinsic width.
*
* @param shadowDrawable Shadow drawable to use at the edge of a drawer
*/
public void setDrawerShadow(Drawable shadowDrawable) {
mShadow = shadowDrawable;
invalidate();
}
/**
* Set a color to use for the scrim that obscures primary content while a drawer is open.
*
* @param color Color to use in 0xAARRGGBB format.
*/
public void setScrimColor(int color) {
mScrimColor = color;
invalidate();
}
/**
* Set a listener to be notified of drawer events.
*
* @param listener Listener to notify when drawer events occur
* @see DrawerListener
*/
public void setDrawerListener(DrawerListener listener) {
mDrawerListener = listener;
}
public void setDrawerControllerListener(DrawerControllerListener listener) {
mDrawerControllerListener = listener;
}
/**
* Enable or disable interaction with all drawers.
*
* This allows the application to restrict the user's ability to open or close
* any drawer within this layout. DrawerLayout will still respond to calls to
* {@link #openDrawer()}, {@link #closeDrawer()} and friends if a drawer is locked.
*
* Locking drawers open or closed will implicitly open or close
* any drawers as appropriate.
*
* @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED},
* {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
*/
public void setDrawerLockMode(int lockMode) {
LayoutParams lp = (LayoutParams) findDrawerView().getLayoutParams();
setDrawerLockMode(lockMode, lp.gravity);
}
/**
* Enable or disable interaction with the given drawer.
*
* This allows the application to restrict the user's ability to open or close
* the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer()},
* {@link #closeDrawer()} and friends if a drawer is locked.
*
* Locking a drawer open or closed will implicitly open or close
* that drawer as appropriate.
*
* @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED},
* {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
* @param edgeGravity Gravity.LEFT, RIGHT, START or END.
* Expresses which drawer to change the mode for.
*
* @see #LOCK_MODE_UNLOCKED
* @see #LOCK_MODE_LOCKED_CLOSED
* @see #LOCK_MODE_LOCKED_OPEN
*/
public void setDrawerLockMode(int lockMode, int edgeGravity) {
final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity,
ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
mLockModeLeft = lockMode;
} else if (absGravity == Gravity.RIGHT) {
mLockModeRight = lockMode;
}
if (lockMode != LOCK_MODE_UNLOCKED) {
// Cancel interaction in progress
mDragger.cancel();
}
switch (lockMode) {
case LOCK_MODE_LOCKED_OPEN:
openDrawer();
break;
case LOCK_MODE_LOCKED_CLOSED:
closeDrawer();
break;
// default: do nothing
}
}
/**
* All view faders will be light when the drawer is open and fade to dark and it closes.
* NOTE: this will clear any existing view faders.
*/
public void setLightMode() {
mStartingViewColor = getResources().getColor(R.color.car_title_light);
mEndingViewColor = getResources().getColor(R.color.car_tint);
updateViewFaders();
}
/**
* All view faders will be dark when the drawer is open and stay that way when it closes.
* NOTE: this will clear any existing view faders.
*/
public void setDarkMode() {
mStartingViewColor = getResources().getColor(R.color.car_title_dark);
mEndingViewColor = getResources().getColor(R.color.car_tint);
updateViewFaders();
}
/**
* All view faders will be dark during the day and light at night.
* NOTE: this will clear any existing view faders.
*/
public void setAutoDayNightMode() {
mStartingViewColor = getResources().getColor(R.color.car_title);
mEndingViewColor = getResources().getColor(R.color.car_tint);
updateViewFaders();
}
private void resetViewFaders() {
mViewFaders.clear();
}
/**
* Check the lock mode of the given drawer view.
*
* @param drawerView Drawer view to check lock mode
* @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or
* {@link #LOCK_MODE_LOCKED_OPEN}.
*/
public int getDrawerLockMode(View drawerView) {
final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
if (absGravity == Gravity.LEFT) {
return mLockModeLeft;
} else if (absGravity == Gravity.RIGHT) {
return mLockModeRight;
}
return LOCK_MODE_UNLOCKED;
}
/**
* Resolve the shared state of all drawers from the component ViewDragHelpers.
* Should be called whenever a ViewDragHelper's state changes.
*/
private void updateDrawerState(int activeState) {
View drawerView = findDrawerView();
if (drawerView != null && activeState == STATE_IDLE) {
if (onScreen() == 0) {
dispatchOnDrawerClosed(drawerView);
} else if (onScreen() == 1) {
dispatchOnDrawerOpened(drawerView);
}
}
if (mDragger.getViewDragState() != mDrawerState) {
mDrawerState = mDragger.getViewDragState();
if (mDrawerListener != null) {
mDrawerListener.onDrawerStateChanged(mDragger.getViewDragState());
}
}
}
private void dispatchOnDrawerClosed(View drawerView) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if (lp.knownOpen) {
lp.knownOpen = false;
if (mDrawerListener != null) {
mDrawerListener.onDrawerClosed(drawerView);
}
}
}
private void dispatchOnDrawerOpened(View drawerView) {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
if (!lp.knownOpen) {
lp.knownOpen = true;
if (mDrawerListener != null) {
mDrawerListener.onDrawerOpened(drawerView);
}
}
}
private void dispatchOnDrawerSlide(View drawerView, float slideOffset) {
if (mDrawerListener != null) {
mDrawerListener.onDrawerSlide(drawerView, slideOffset);
}
}
private void dispatchOnDrawerOpening(View drawerView) {
if (mDrawerListener != null) {
mDrawerListener.onDrawerOpening(drawerView);
}
}
private void dispatchOnDrawerClosing(View drawerView) {
if (mDrawerListener != null) {
mDrawerListener.onDrawerClosing(drawerView);
}
}
private void setDrawerViewOffset(View drawerView, float slideOffset) {
if (slideOffset == onScreen()) {
return;
}
LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = slideOffset;
dispatchOnDrawerSlide(drawerView, slideOffset);
}
private float onScreen() {
return ((LayoutParams) findDrawerView().getLayoutParams()).onScreen;
}
/**
* @return the absolute gravity of the child drawerView, resolved according
* to the current layout direction
*/
private int getDrawerViewAbsoluteGravity(View drawerView) {
final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this));
}
private boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) {
final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
return (absGravity & checkFor) == checkFor;
}
/**
* @return the drawer view
*/
private View findDrawerView() {
if (mDrawerView != null) {
return mDrawerView;
}
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childAbsGravity = getDrawerViewAbsoluteGravity(child);
if (childAbsGravity != Gravity.NO_GRAVITY) {
mDrawerView = child;
return child;
}
}
throw new IllegalStateException("No drawer view found.");
}
/**
* @return the content. NOTE: this is the view with no gravity.
*/
private View findContentView() {
if (mContentView != null) {
return mContentView;
}
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; --i) {
final View child = getChildAt(i);
if (isDrawerView(child)) {
continue;
}
mContentView = child;
return child;
}
throw new IllegalStateException("No content view found.");
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
@Override
public boolean requestFocus(int direction, Rect rect) {
// Optimally we want to check isInTouchMode(), but that value isn't always correct.
if (mHasWheel) {
mNeedsFocus = true;
}
return super.requestFocus(direction, rect);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
// There needs to be a layout pending if we're not going to animate the drawer until the
// next layout, so make it so.
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
if (isInEditMode()) {
// Don't crash the layout editor. Consume all of the space if specified
// or pick a magic number from thin air otherwise.
// TODO Better communication with tools of this bogus state.
// It will crash on a real device.
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = 300;
}
else if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = 300;
}
} else {
throw new IllegalArgumentException(
"DrawerLayout must be measured with MeasureSpec.EXACTLY.");
}
}
setMeasuredDimension(widthSize, heightSize);
View view = findContentView();
LayoutParams lp = ((LayoutParams) view.getLayoutParams());
// Content views get measured at exactly the layout's size.
final int contentWidthSpec = MeasureSpec.makeMeasureSpec(
widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
final int contentHeightSpec = MeasureSpec.makeMeasureSpec(
heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
view.measure(contentWidthSpec, contentHeightSpec);
view = findDrawerView();
lp = ((LayoutParams) view.getLayoutParams());
final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec,
lp.leftMargin + lp.rightMargin,
lp.width);
final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec,
lp.topMargin + lp.bottomMargin,
lp.height);
view.measure(drawerWidthSpec, drawerHeightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
final int width = r - l;
View contentView = findContentView();
View drawerView = findDrawerView();
LayoutParams drawerLp = (LayoutParams) drawerView.getLayoutParams();
LayoutParams contentLp = (LayoutParams) contentView.getLayoutParams();
int contentRight = contentLp.getMarginStart() + getWidth();
contentView.layout(contentRight - contentView.getMeasuredWidth(),
contentLp.topMargin, contentRight,
contentLp.topMargin + contentView.getMeasuredHeight());
final int childHeight = drawerView.getMeasuredHeight();
int onScreen = (int) (drawerView.getWidth() * drawerLp.onScreen);
int offset;
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
offset = onScreen - drawerView.getWidth();
} else {
offset = width - onScreen;
}
drawerView.layout(drawerLp.getMarginStart() + offset, drawerLp.topMargin,
width - drawerLp.getMarginEnd() + offset,
childHeight + drawerLp.topMargin);
updateDrawerAlpha();
updateViewFaders();
if (mFirstLayout) {
// TODO(b/15394507): Normally, onMeasure()/onLayout() are called three times when
// you create CarDrawerLayout, but when you pop it back it's only called once which
// leaves us in a weird state. This is a pretty ugly hack to fix that.
mHandler.post(mInvalidateRunnable);
mFirstLayout = false;
}
if (mNeedsFocus) {
if (initializeFocus()) {
mNeedsFocus = false;
}
}
mInLayout = false;
}
private boolean initializeFocus() {
// Only request focus if the current view that needs focus doesn't already have it. This
// prevents some nasty bugs where focus ends up snapping to random elements and also saves
// a bunch of cycles in the average case.
mDrawerView.setFocusable(false);
mContentView.setFocusable(false);
boolean needFocus = !mDrawerView.hasFocus() && !mContentView.hasFocus();
if (!needFocus) {
return true;
}
// Find something in the hierarchy to give focus to.
List focusables;
boolean drawerOpen = isDrawerOpen();
if (drawerOpen) {
focusables = mDrawerView.getFocusables(FOCUS_DOWN);
} else {
focusables = mContentView.getFocusables(FOCUS_DOWN);
}
// The 2 else cases here are a catch all for when nothing is focusable in view hierarchy.
// If you don't have anything focusable on screen, key events will not be delivered to
// the view hierarchy and you end up getting stuck without being able to open / close the
// drawer or launch gsa.
if (!focusables.isEmpty()) {
focusables.get(0).requestFocus();
return true;
} else if (drawerOpen) {
mDrawerView.setFocusable(true);
} else {
mContentView.setFocusable(true);
}
return false;
}
@Override
public void requestLayout() {
if (!mInLayout) {
super.requestLayout();
}
}
@Override
public void computeScroll() {
if (mDragger.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private static boolean hasOpaqueBackground(View v) {
final Drawable bg = v.getBackground();
return bg != null && bg.getOpacity() == PixelFormat.OPAQUE;
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
final int height = getHeight();
final boolean drawingContent = isContentView(child);
int clipLeft = findContentView().getLeft();
int clipRight = findContentView().getRight();
final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
final int restoreCount = canvas.save();
if (drawingContent) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
if (v == child || v.getVisibility() != VISIBLE ||
!hasOpaqueBackground(v) || !isDrawerView(v) ||
v.getHeight() < height) {
continue;
}
if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
final int vright = v.getRight();
if (vright > clipLeft) {
clipLeft = vright;
}
} else {
final int vleft = v.getLeft();
if (vleft < clipRight) {
clipRight = vleft;
}
}
}
canvas.clipRect(clipLeft, 0, clipRight, getHeight());
}
final boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(restoreCount);
if (drawingContent) {
int scrimAlpha = SCRIM_ENABLED ?
(int) (baseAlpha * Math.max(0, Math.min(1, onScreen())) * MAX_SCRIM_ALPHA) : 0;
if (scrimAlpha > 0) {
int color = scrimAlpha << 24 | (mScrimColor & 0xffffff);
mScrimPaint.setColor(color);
canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint);
canvas.drawRect(clipLeft - 1, 0, clipLeft, getHeight(), mEdgeHighlightPaint);
}
LayoutParams drawerLp = (LayoutParams) findDrawerView().getLayoutParams();
if (mShadow != null
&& checkDrawerViewAbsoluteGravity(findDrawerView(), Gravity.LEFT)) {
final int offScreen = (int) ((1 - drawerLp.onScreen) * findDrawerView().getWidth());
final int drawerRight = getWidth() - drawerLp.getMarginEnd() - offScreen;
final int shadowWidth = mShadow.getIntrinsicWidth();
final float alpha =
Math.max(0, Math.min((float) drawerRight / mDragger.getEdgeSize(), 1.f));
mShadow.setBounds(drawerRight, child.getTop(),
drawerRight + shadowWidth, child.getBottom());
mShadow.setAlpha((int) (255 * alpha * alpha * alpha));
mShadow.draw(canvas);
} else if (mShadow != null
&& checkDrawerViewAbsoluteGravity(findDrawerView(),Gravity.RIGHT)) {
final int onScreen = (int) (findDrawerView().getWidth() * drawerLp.onScreen);
final int drawerLeft = drawerLp.getMarginStart() + getWidth() - onScreen;
final int shadowWidth = mShadow.getIntrinsicWidth();
final float alpha =
Math.max(0, Math.min((float) onScreen / mDragger.getEdgeSize(), 1.f));
canvas.save();
canvas.translate(2 * drawerLeft - shadowWidth, 0);
canvas.scale(-1.0f, 1.0f);
mShadow.setBounds(drawerLeft - shadowWidth, child.getTop(),
drawerLeft, child.getBottom());
mShadow.setAlpha((int) (255 * alpha * alpha * alpha * alpha));
mShadow.draw(canvas);
canvas.restore();
}
}
return result;
}
private boolean isContentView(View child) {
return child == findContentView();
}
private boolean isDrawerView(View child) {
return child == findDrawerView();
}
private void updateDrawerAlpha() {
float alpha;
if (mStartedOpen) {
alpha = mDrawerFadeInterpolator.getReverseInterpolation(onScreen());
} else {
alpha = mDrawerFadeInterpolator.getForwardInterpolation(onScreen());
}
ViewGroup drawerView = (ViewGroup) findDrawerView();
int drawerChildCount = drawerView.getChildCount();
for (int i = 0; i < drawerChildCount; i++) {
drawerView.getChildAt(i).setAlpha(alpha);
}
}
/**
* Add a view fader whose color will be set as the drawer opens and closes.
*/
public void addViewFader(ViewFader viewFader) {
addViewFader(viewFader, mStartingViewColor, mEndingViewColor);
}
public void addViewFader(ViewFader viewFader, int startingColor, int endingColor) {
mViewFaders.add(new ViewFaderHolder(viewFader, startingColor, endingColor));
updateViewFaders();
}
public void removeViewFader(ViewFader viewFader) {
for (Iterator it = mViewFaders.iterator(); it.hasNext(); ) {
ViewFaderHolder viewFaderHolder = it.next();
if (viewFaderHolder.viewFader.equals(viewFader)) {
it.remove();
}
}
}
private void updateViewFaders() {
if (!mHasInflated) {
return;
}
float fadeProgress;
if (mStartedOpen) {
fadeProgress = mViewFaderInterpolator.getReverseInterpolation(onScreen());
} else {
fadeProgress = mViewFaderInterpolator.getForwardInterpolation(onScreen());
}
for (Iterator it = mViewFaders.iterator(); it.hasNext(); ) {
ViewFaderHolder viewFaderHolder = it.next();
int startingColor = viewFaderHolder.startingColor;
int endingColor = viewFaderHolder.endingColor;
int alpha = weightedAverage(Color.alpha(startingColor),
Color.alpha(endingColor), fadeProgress);
int red = weightedAverage(Color.red(startingColor),
Color.red(endingColor), fadeProgress);
int green = weightedAverage(Color.green(startingColor),
Color.green(endingColor), fadeProgress);
int blue = weightedAverage(Color.blue(startingColor),
Color.blue(endingColor), fadeProgress);
viewFaderHolder.viewFader.setColor(alpha << 24 | red << 16 | green << 8 | blue);
}
}
private int weightedAverage(int starting, int ending, float weight) {
return (int) ((1f - weight) * starting + weight * ending);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
// "|" used deliberately here; both methods should be invoked.
final boolean interceptForDrag = mDragger.shouldInterceptTouchEvent(ev);
boolean interceptForTap = false;
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
if (onScreen() > 0 && isContentView(mDragger.findTopChildUnder((int) x, (int) y))) {
interceptForTap = true;
}
mChildrenCanceledTouch = false;
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
mChildrenCanceledTouch = false;
}
}
return interceptForDrag || interceptForTap || mChildrenCanceledTouch;
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
mDragger.processTouchEvent(ev);
final int absGravity = getDrawerViewAbsoluteGravity(findDrawerView());
final int edge;
if (absGravity == Gravity.LEFT) {
edge = ViewDragHelper.EDGE_LEFT;
} else {
edge = ViewDragHelper.EDGE_RIGHT;
}
// don't allow views behind the drawer to be touched
boolean drawerPartiallyOpen = onScreen() > 0;
return mDragger.isEdgeTouched(edge) ||
mDragger.getCapturedView() != null ||
drawerPartiallyOpen;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (CHILDREN_DISALLOW_INTERCEPT) {
// If we have an edge touch we want to skip this and track it for later instead.
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
View drawerView = findDrawerView();
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.RIGHT)) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
/**
* Open the drawer view by animating it into view.
*/
public void openDrawer() {
ViewGroup drawerView = (ViewGroup) findDrawerView();
mStartedOpen = false;
if (hasWindowFocus()) {
int left;
LayoutParams drawerLp = (LayoutParams) drawerView.getLayoutParams();
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
left = drawerLp.getMarginStart();
} else {
left = drawerLp.getMarginStart() + getWidth() - drawerView.getWidth();
}
mDragger.smoothSlideViewTo(drawerView, left, drawerView.getTop());
dispatchOnDrawerOpening(drawerView);
} else {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 1.f;
dispatchOnDrawerOpened(drawerView);
}
ViewGroup contentView = (ViewGroup) findContentView();
contentView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
drawerView.setDescendantFocusability(ViewGroup. FOCUS_AFTER_DESCENDANTS);
View focusable = drawerView.getChildAt(0);
if (focusable != null) {
focusable.requestFocus();
}
invalidate();
}
/**
* Close the specified drawer view by animating it into view.
*/
public void closeDrawer() {
ViewGroup drawerView = (ViewGroup) findDrawerView();
if (!isDrawerView(drawerView)) {
throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
mStartedOpen = true;
// Don't trigger the close drawer animation if drawer is not open.
if (hasWindowFocus() && isDrawerOpen()) {
int left;
LayoutParams drawerLp = (LayoutParams) drawerView.getLayoutParams();
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
left = drawerLp.getMarginStart() - drawerView.getWidth();
} else {
left = drawerLp.getMarginStart() + getWidth();
}
mDragger.smoothSlideViewTo(drawerView, left, drawerView.getTop());
dispatchOnDrawerClosing(drawerView);
} else {
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
dispatchOnDrawerClosed(drawerView);
}
ViewGroup contentView = (ViewGroup) findContentView();
drawerView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
contentView.setDescendantFocusability(ViewGroup. FOCUS_AFTER_DESCENDANTS);
if (!isInTouchMode()) {
List focusables = contentView.getFocusables(FOCUS_DOWN);
if (focusables.size() > 0) {
View candidate = focusables.get(0);
candidate.requestFocus();
}
}
invalidate();
}
@Override
public void addFocusables(@NonNull ArrayList views, int direction, int focusableMode) {
boolean drawerOpen = isDrawerOpen();
if (drawerOpen) {
findDrawerView().addFocusables(views, direction, focusableMode);
} else {
findContentView().addFocusables(views, direction, focusableMode);
}
}
/**
* Check if the given drawer view is currently in an open state.
* To be considered "open" the drawer must have settled into its fully
* visible state. To check for partial visibility use
* {@link #isDrawerVisible(android.view.View)}.
*
* @return true if the given drawer view is in an open state
* @see #isDrawerVisible(android.view.View)
*/
public boolean isDrawerOpen() {
return ((LayoutParams) findDrawerView().getLayoutParams()).knownOpen;
}
/**
* Check if a given drawer view is currently visible on-screen. The drawer
* may be fully extended or anywhere in between.
*
* @param drawer Drawer view to check
* @return true if the given drawer is visible on-screen
* @see #isDrawerOpen()
*/
public boolean isDrawerVisible(View drawer) {
if (!isDrawerView(drawer)) {
throw new IllegalArgumentException("View " + drawer + " is not a drawer");
}
return onScreen() > 0;
}
/**
* Check if a given drawer view is currently visible on-screen. The drawer
* may be fully extended or anywhere in between.
* If there is no drawer with the given gravity this method will return false.
*
* @return true if the given drawer is visible on-screen
*/
public boolean isDrawerVisible() {
final View drawerView = findDrawerView();
return drawerView != null && isDrawerVisible(drawerView);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams
? new LayoutParams((LayoutParams) p)
: p instanceof MarginLayoutParams
? new LayoutParams((MarginLayoutParams) p)
: new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams && super.checkLayoutParams(p);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
private boolean hasVisibleDrawer() {
return findVisibleDrawer() != null;
}
private View findVisibleDrawer() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (isDrawerView(child) && isDrawerVisible(child)) {
return child;
}
}
return null;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = null;
if (state.getClass().getClassLoader() != getClass().getClassLoader()) {
// Class loader mismatch, recreate from parcel.
Parcel stateParcel = Parcel.obtain();
state.writeToParcel(stateParcel, 0);
ss = SavedState.CREATOR.createFromParcel(stateParcel);
} else {
ss = (SavedState) state;
}
super.onRestoreInstanceState(ss.getSuperState());
if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
openDrawer();
}
setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT);
setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT);
}
@Override
protected Parcelable onSaveInstanceState() {
final Parcelable superState = super.onSaveInstanceState();
final SavedState ss = new SavedState(superState);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (!isDrawerView(child)) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.knownOpen) {
ss.openDrawerGravity = lp.gravity;
// Only one drawer can be open at a time.
break;
}
}
ss.lockModeLeft = mLockModeLeft;
ss.lockModeRight = mLockModeRight;
return ss;
}
/**
* State persisted across instances
*/
protected static class SavedState extends BaseSavedState {
int openDrawerGravity = Gravity.NO_GRAVITY;
int lockModeLeft = LOCK_MODE_UNLOCKED;
int lockModeRight = LOCK_MODE_UNLOCKED;
public SavedState(Parcel in) {
super(in);
openDrawerGravity = in.readInt();
lockModeLeft = in.readInt();
lockModeRight = in.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(openDrawerGravity);
dest.writeInt(lockModeLeft);
dest.writeInt(lockModeRight);
}
@SuppressWarnings("hiding")
public static final Creator CREATOR =
new Creator() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
private class ViewDragCallback extends ViewDragHelper.Callback {
@SuppressWarnings("hiding")
private ViewDragHelper mDragger;
public void setDragger(ViewDragHelper dragger) {
mDragger = dragger;
}
@Override
public boolean tryCaptureView(View child, int pointerId) {
CarDrawerLayout.LayoutParams lp = (LayoutParams) findDrawerView().getLayoutParams();
int edges = EDGE_DRAG_ENABLED ? ViewDragHelper.EDGE_ALL : 0;
boolean captured = isContentView(child) &&
getDrawerLockMode(child) == LOCK_MODE_UNLOCKED &&
(lp.knownOpen || mDragger.isEdgeTouched(edges));
if (captured && lp.knownOpen) {
mStartedOpen = true;
} else if (captured && !lp.knownOpen) {
mStartedOpen = false;
}
// We want dragging starting on the content view to drag the drawer. Therefore when
// touch events try to capture the content view, we force capture of the drawer view.
if (captured) {
mDragger.captureChildView(findDrawerView(), pointerId);
}
return false;
}
@Override
public void onViewDragStateChanged(int state) {
updateDrawerState(state);
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
float offset;
View drawerView = findDrawerView();
final int drawerWidth = drawerView.getWidth();
// This reverses the positioning shown in onLayout.
if (checkDrawerViewAbsoluteGravity(findDrawerView(), Gravity.LEFT)) {
offset = (float) (left + drawerWidth) / drawerWidth;
} else {
offset = (float) (getWidth() - left) / drawerWidth;
}
setDrawerViewOffset(findDrawerView(), offset);
updateDrawerAlpha();
updateViewFaders();
invalidate();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final View drawerView = findDrawerView();
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
int left;
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
// Open the drawer if they are swiping right or if they are not currently moving but
// have moved the drawer in the current gesture and released the drawer when it was
// fully open.
// Close otherwise.
left = xvel > 0 ? lp.getMarginStart() : lp.getMarginStart() - drawerView.getWidth();
} else {
// See comment for left drawer.
left = xvel < 0 ? lp.getMarginStart() + getWidth() - drawerView.getWidth()
: lp.getMarginStart() + getWidth();
}
mDragger.settleCapturedViewAt(left, releasedChild.getTop());
invalidate();
}
@Override
public boolean onEdgeLock(int edgeFlags) {
if (ALLOW_EDGE_LOCK) {
if (!isDrawerOpen()) {
closeDrawer();
}
return true;
}
return false;
}
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
View drawerView = findDrawerView();
if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) {
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.RIGHT)) {
drawerView = null;
}
} else {
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
drawerView = null;
}
}
if (drawerView != null && getDrawerLockMode(drawerView) == LOCK_MODE_UNLOCKED) {
mDragger.captureChildView(drawerView, pointerId);
}
}
@Override
public int getViewHorizontalDragRange(View child) {
return child.getWidth();
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final View drawerView = findDrawerView();
LayoutParams drawerLp = (LayoutParams) drawerView.getLayoutParams();
if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
return Math.max(drawerLp.getMarginStart() - drawerView.getWidth(),
Math.min(left, drawerLp.getMarginStart()));
} else {
return Math.max(drawerLp.getMarginStart() + getWidth() - drawerView.getWidth(),
Math.min(left, drawerLp.getMarginStart() + getWidth()));
}
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return child.getTop();
}
}
public static class LayoutParams extends MarginLayoutParams {
public int gravity = Gravity.NO_GRAVITY;
float onScreen;
boolean knownOpen;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
gravity = a.getInt(0, Gravity.NO_GRAVITY);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(int width, int height, int gravity) {
this(width, height);
this.gravity = gravity;
}
public LayoutParams(LayoutParams source) {
super(source);
gravity = source.gravity;
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
}
private static final class ViewFaderHolder {
public final ViewFader viewFader;
public final int startingColor;
public final int endingColor;
public ViewFaderHolder(ViewFader viewFader, int startingColor, int endingColor) {
this.viewFader = viewFader;
this.startingColor = startingColor;
this.endingColor = endingColor;
}
}
}