311 lines
9.8 KiB
Java

/*
* Copyright (C) 2022 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 com.google.android.material.sidesheet;
import com.google.android.material.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatDialog;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import android.widget.FrameLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.google.android.material.sidesheet.Sheet.StableSheetState;
/**
* Base class for {@link android.app.Dialog}s styled as a sheet, to be used by sheet dialog
* implementations such as side sheets and bottom sheets.
*/
abstract class SheetDialog extends AppCompatDialog {
private static final int COORDINATOR_LAYOUT_ID = R.id.coordinator;
private static final int TOUCH_OUTSIDE_ID = R.id.touch_outside;
@Nullable private Sheet behavior;
@Nullable private FrameLayout container;
@Nullable private FrameLayout sheet;
boolean dismissWithAnimation;
boolean cancelable = true;
private boolean canceledOnTouchOutside = true;
private boolean canceledOnTouchOutsideSet;
SheetDialog(@NonNull Context context) {
this(context, 0, 0, 0);
}
SheetDialog(
@NonNull Context context,
@StyleRes int theme,
@AttrRes int themeAttr,
@StyleRes int defaultThemeAttr) {
super(context, getThemeResId(context, theme, themeAttr, defaultThemeAttr));
// We hide the title bar for any style configuration. Otherwise, there will be a gap
// above the sheet when it is expanded.
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
}
@Override
public void setContentView(@LayoutRes int layoutResId) {
super.setContentView(wrapInSheet(layoutResId, null, null));
}
@Override
public void setContentView(@Nullable View view) {
super.setContentView(wrapInSheet(0, view, null));
}
@Override
public void setContentView(@Nullable View view, @Nullable ViewGroup.LayoutParams params) {
super.setContentView(wrapInSheet(0, view, params));
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window window = getWindow();
if (window != null) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
// The status bar should always be transparent because of the window animation.
window.setStatusBarColor(0);
window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
if (VERSION.SDK_INT < VERSION_CODES.M) {
// It can be transparent for API 23 and above because we will handle switching the status
// bar icons to light or dark as appropriate. For API 21 and API 22 we just set the
// translucent status bar.
window.addFlags(LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
}
@Override
public void setCancelable(boolean cancelable) {
super.setCancelable(cancelable);
if (this.cancelable != cancelable) {
this.cancelable = cancelable;
}
}
@Override
protected void onStart() {
super.onStart();
if (behavior != null && behavior.getState() == Sheet.STATE_HIDDEN) {
behavior.setState(getStateOnStart());
}
}
/**
* This function can be called from a few different use cases, including swiping the dialog away
* or calling `dismiss()` from a `SideSheetDialogFragment`, tapping outside a dialog, etc...
*
* <p>The default animation to dismiss this dialog is a fade-out transition through a
* windowAnimation. Set {@link #setDismissWithAnimation(boolean)} to true if you want to utilize
* the sheet animation instead.
*
* <p>If this function is called from a swipe interaction, or dismissWithAnimation is false, then
* keep the default behavior.
*/
@Override
public void cancel() {
Sheet behavior = getBehavior();
if (!dismissWithAnimation || behavior.getState() == Sheet.STATE_HIDDEN) {
super.cancel();
} else {
behavior.setState(Sheet.STATE_HIDDEN);
}
}
@Override
public void setCanceledOnTouchOutside(boolean cancel) {
super.setCanceledOnTouchOutside(cancel);
if (cancel && !cancelable) {
cancelable = true;
}
canceledOnTouchOutside = cancel;
canceledOnTouchOutsideSet = true;
}
/**
* Set to perform the swipe away animation when dismissing instead of the window animation for the
* dialog.
*
* @param dismissWithAnimation True if swipe away animation should be used when dismissing.
*/
public void setDismissWithAnimation(boolean dismissWithAnimation) {
this.dismissWithAnimation = dismissWithAnimation;
}
/**
* Returns if dismissing will perform the swipe away animation on the sheet, rather than the
* window animation for the dialog.
*/
public boolean getDismissWithAnimation() {
return dismissWithAnimation;
}
/** Creates the container layout which must exist to find the behavior */
private void ensureContainerAndBehavior() {
if (container == null) {
container = (FrameLayout) View.inflate(getContext(), getLayoutResId(), null);
sheet = container.findViewById(getDialogId());
behavior = getBehaviorFromSheet(sheet);
}
}
@NonNull
private FrameLayout getContainer() {
if (this.container == null) {
ensureContainerAndBehavior();
}
return container;
}
@NonNull
private FrameLayout getSheet() {
if (this.sheet == null) {
ensureContainerAndBehavior();
}
return sheet;
}
@NonNull
Sheet getBehavior() {
if (this.behavior == null) {
// The content hasn't been set, so the behavior doesn't exist yet. Let's create it.
ensureContainerAndBehavior();
}
return behavior;
}
private View wrapInSheet(
int layoutResId, @Nullable View view, @Nullable ViewGroup.LayoutParams params) {
ensureContainerAndBehavior();
CoordinatorLayout coordinator = getContainer().findViewById(COORDINATOR_LAYOUT_ID);
if (layoutResId != 0 && view == null) {
view = getLayoutInflater().inflate(layoutResId, coordinator, false);
}
FrameLayout sheet = getSheet();
sheet.removeAllViews();
if (params == null) {
sheet.addView(view);
} else {
sheet.addView(view, params);
}
// We treat the CoordinatorLayout as outside the dialog though it is technically inside.
coordinator
.findViewById(TOUCH_OUTSIDE_ID)
.setOnClickListener(
v -> {
if (cancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
cancel();
}
});
// Handle accessibility events.
ViewCompat.setAccessibilityDelegate(
getSheet(),
new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(
View host, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (cancelable) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS);
info.setDismissable(true);
} else {
info.setDismissable(false);
}
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS && cancelable) {
cancel();
return true;
}
return super.performAccessibilityAction(host, action, args);
}
});
return container;
}
boolean shouldWindowCloseOnTouchOutside() {
if (!canceledOnTouchOutsideSet) {
TypedArray a =
getContext().obtainStyledAttributes(new int[] {android.R.attr.windowCloseOnTouchOutside});
canceledOnTouchOutside = a.getBoolean(0, true);
a.recycle();
canceledOnTouchOutsideSet = true;
}
return canceledOnTouchOutside;
}
private static int getThemeResId(
@NonNull Context context,
@StyleRes int themeId,
@AttrRes int themeAttr,
@StyleRes int defaultTheme) {
if (themeId == 0) {
// If the provided theme is 0, retrieve the dialog theme from our theme.
TypedValue outValue = new TypedValue();
if (context.getTheme().resolveAttribute(themeAttr, outValue, true)) {
themeId = outValue.resourceId;
} else {
// Dialog theme is not provided; we default to our light theme.
themeId = defaultTheme;
}
}
return themeId;
}
@LayoutRes
protected abstract int getLayoutResId();
@IdRes
protected abstract int getDialogId();
@NonNull
protected abstract Sheet getBehaviorFromSheet(@NonNull FrameLayout sheet);
@StableSheetState
protected abstract int getStateOnStart();
}