/* * Copyright 2017 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.backlayer; import android.content.Context; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.material.expandable.ExpandableWidget; import com.google.android.material.expandable.ExpandableWidgetHelper; import com.google.android.material.stateful.ExtendableSavedState; import android.support.design.widget.CoordinatorLayout; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; /** * BackLayerLayout implements the Material back layer concept, and can be used to implement * navigation drawers and other surfaces. * *
The back layer concept represents a background layer overlapped by a foreground layer. When * the background layer is expanded to show additional content (usually as a result of user * interaction), it pushes the foreground layer partially off-screen. * *
This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}. * *
Notice BackLayerLayout is a LinearLayout, so you need to make sure you're using the correct * orientation that matches the position you've chosen for the back layer (i.e. use {@code * android:orientation="vertical"} in conjunction with {@code android:gravity="top"} or {@code * android:gravity="bottom"}). * *
Usage guide: * *
{@code
*
*
*
*
*
*
*
*
*
*
* }
*
* The reason you need to specify both {@code android:gravity} and {@code android:layout_gravity}
* and they must match is that they are used for different purposes:
*
* Notice that this method does not automatically change visibility on child views of the back * layer, the developer has to prepare the contents of the back layer either before calling this * method or in {@link BackLayerCallback#onBeforeExpand()} or {@link * BackLayerCallback#onBeforeCollapse()}. */ @Override public boolean setExpanded(boolean expanded) { return expandableWidgetHelper.setExpanded(expanded); } @Override public boolean isExpanded() { return expandableWidgetHelper.isExpanded(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); childViewAccessibilityHelper = new ChildViewAccessibilityHelper(this); setOnHierarchyChangeListener(childViewAccessibilityHelper); childViewAccessibilityHelper.disableChildFocus(); } @Override public void requestLayout() { super.requestLayout(); expandedSizeMeasured = false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!originalMeasureSpecsSaved) { originalWidthMeasureSpec = widthMeasureSpec; originalHeightMeasureSpec = heightMeasureSpec; originalMeasureSpecsSaved = true; } // Measure the minimum size only if it's not previously set, for example in XML layout. if (ViewCompat.getMinimumHeight(this) == 0 && ViewCompat.getMinimumWidth(this) == 0) { // Find the CollapsedBackLayerContents boolean foundCollapsed = false; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child instanceof CollapsedBackLayerContents) { if (foundCollapsed) { throw new IllegalStateException( "More than one CollapsedBackLayerContents found inside BackLayerLayout"); } foundCollapsed = true; LinearLayout.LayoutParams childLayoutParams = (LinearLayout.LayoutParams) child.getLayoutParams(); child.measure(childLayoutParams.width, childLayoutParams.height); setMinimumHeight( child.getMeasuredHeight() + childLayoutParams.bottomMargin + childLayoutParams.topMargin); setMinimumWidth( child.getMeasuredWidth() + childLayoutParams.leftMargin + childLayoutParams.rightMargin); } } if (!foundCollapsed) { throw new IllegalStateException( "No CollapsedBackLayerContents found inside BackLayerLayout"); } } super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** * Call this to measure the BackLayerLayout's expanded size on-demand. This must be called before * {@link #calculateExpandedWidth()} and {@link #calculateExpandedHeight()} are queried. */ private void remeasureExpandedSize() { if (expandedSizeMeasured) { return; } CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) getLayoutParams(); final int absoluteGravity = Gravity.getAbsoluteGravity(layoutParams.gravity, ViewCompat.getLayoutDirection(this)); int heightMeasureSpec = originalHeightMeasureSpec; int widthMeasureSpec = originalWidthMeasureSpec; // In order to know the measurements for a expanded version of the back layer we need to // measure the back layer with one dimension set to MeasureSpec.UNSPECIFIED instead of the // setting // that came in the original MeasureSpec (MeasureSpec.EXACTLY, since the BackLayerLayout must // use match_parent for both dimensions). // // While it would seem natural to use MeasureSpec.AT_MOST, this method can be called from // onRestoreInstanceState(Parcelable) which would happen before the first measure pass, and thus // the original measure specs would be 0, causing a wrong measurement. switch (absoluteGravity) { case Gravity.LEFT: case Gravity.RIGHT: widthMeasureSpec = MeasureSpec.makeMeasureSpec( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED); break; case Gravity.TOP: case Gravity.BOTTOM: int size = MeasureSpec.getSize(heightMeasureSpec); heightMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.UNSPECIFIED); break; default: break; } measure(widthMeasureSpec, heightMeasureSpec); expandedHeight = getMeasuredHeight(); expandedWidth = getMeasuredWidth(); // Recalculate with the original measure specs, so it fits the entire coordinator layout. measure(originalWidthMeasureSpec, originalHeightMeasureSpec); expandedSizeMeasured = true; } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); ExtendableSavedState state = new ExtendableSavedState(superState); state.extendableStates.put( "expandableWidgetHelper", expandableWidgetHelper.onSaveInstanceState()); return state; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof ExtendableSavedState)) { super.onRestoreInstanceState(state); return; } ExtendableSavedState ess = (ExtendableSavedState) state; super.onRestoreInstanceState(ess.getSuperState()); expandableWidgetHelper.onRestoreInstanceState( ess.extendableStates.get("expandableWidgetHelper")); } void onExpandAnimationStart() { for (BackLayerCallback callback : callbacks) { callback.onBeforeExpand(); } } void onExpandAnimationEnd() { for (BackLayerCallback callback : callbacks) { callback.onAfterExpand(); } } void onCollapseAnimationStart() { for (BackLayerCallback callback : callbacks) { callback.onBeforeCollapse(); } } void onCollapseAnimationEnd() { childViewAccessibilityHelper.disableChildFocus(); for (BackLayerCallback callback : callbacks) { callback.onAfterCollapse(); } } /** The measured height for the expanded version of the back layer. */ int calculateExpandedHeight() { if (!expandedSizeMeasured) { remeasureExpandedSize(); } return expandedHeight; } /** The measured width for the expanded version of the back layer. */ int calculateExpandedWidth() { if (!expandedSizeMeasured) { remeasureExpandedSize(); } return expandedWidth; } }