Ian Lake 501ef8ea2a [NavigationRailView] Update inset handling for Navigation Rail & Bottom Nav
Resolves https://github.com/material-components/material-components-android/pull/2253

GIT_ORIGIN_REV_ID=b6c2270b8855e38231c29cbfd48949afd785258c
PiperOrigin-RevId: 383632982
2021-07-08 16:01:01 +00:00

310 lines
11 KiB
Java

/*
* Copyright (C) 2021 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.navigationrail;
import com.google.android.material.R;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static java.lang.Math.min;
import android.content.Context;
import android.content.res.Resources;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.appcompat.widget.TintTypedArray;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.ViewUtils.RelativePadding;
import com.google.android.material.navigation.NavigationBarView;
/**
* Represents a standard navigation rail view for application. It is an implementation of <a
* href="https://material.io/components/navigation-rail">Material Design navigation rail.</a>.
*
* <p>Navigation rails make it easy for users to explore and switch between top-level views in a
* single tap. They should be placed at the side edge of large screen devices such as tablets, when
* an application has three to seven top-level destinations.
*
* <p>The bar contents can be populated by specifying a menu resource file. Each menu item title,
* icon and enabled state will be used for displaying navigation rail bar items. Menu items can also
* be used for programmatically selecting which destination is currently active. It can be done
* using {@code MenuItem#setChecked(true)}.
*
* <p>A header view (such as a {@link
* com.google.android.material.floatingactionbutton.FloatingActionButton}, logo, etc.) can be added
* with the {@code app:headerLayout} attribute or by using {@link #addHeaderView}.
*
* <pre>
* layout resource file:
* &lt;com.google.android.material.navigationrail.NavigationRailView
* xmlns:android="http://schemas.android.com/apk/res/android"
* xmlns:app="http://schema.android.com/apk/res/res-auto"
* android:id="@+id/navigation"
* android:layout_width="wrap_content"
* android:layout_height="match_parent"
* app:menu="@menu/my_navigation_items"
* app:headerLayout="@layout/my_navigation_rail_fab" /&gt;
*
* res/menu/my_navigation_items.xml:
* &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;item android:id="@+id/action_search"
* android:title="@string/menu_search"
* android:icon="@drawable/ic_search" /&gt;
* &lt;item android:id="@+id/action_settings"
* android:title="@string/menu_settings"
* android:icon="@drawable/ic_add" /&gt;
* &lt;item android:id="@+id/action_navigation"
* android:title="@string/menu_navigation"
* android:icon="@drawable/ic_action_navigation_menu" /&gt;
* &lt;/menu&gt;
*
* res/layout/my_navigation_rail_fab.xml:
* &lt;com.google.android.material.floatingactionbutton.FloatingActionButton
* xmlns:android="http://schemas.android.com/apk/res/android"
* xmlns:app="http://schemas.android.com/apk/res-auto"
* android:id="@+id/my_navigation_rail_fab"
* android:layout_width="wrap_content"
* android:layout_height="wrap_content"
* android:contentDescription="@string/my_navigation_rail_fab_content_desc"
* app:srcCompat="@drawable/ic_add" /&gt;
* </pre>
*/
public class NavigationRailView extends NavigationBarView {
static final int DEFAULT_MENU_GRAVITY = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
static final int MAX_ITEM_COUNT = 7;
private static final int DEFAULT_HEADER_GRAVITY = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
private final int topMargin;
@Nullable private View headerView;
public NavigationRailView(@NonNull Context context) {
this(context, null);
}
public NavigationRailView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.navigationRailStyle);
}
public NavigationRailView(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, R.style.Widget_MaterialComponents_NavigationRailView);
}
public NavigationRailView(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final Resources res = getResources();
topMargin = res.getDimensionPixelSize(R.dimen.mtrl_navigation_rail_margin);
// Ensure we are using the correctly themed context rather than the context that was passed in.
context = getContext();
/* Custom attributes */
TintTypedArray attributes =
ThemeEnforcement.obtainTintedStyledAttributes(
context, attrs, R.styleable.NavigationRailView, defStyleAttr, defStyleRes);
int headerLayoutRes = attributes.getResourceId(R.styleable.NavigationRailView_headerLayout, 0);
if (headerLayoutRes != 0) {
addHeaderView(headerLayoutRes);
}
setMenuGravity(
attributes.getInt(R.styleable.NavigationRailView_menuGravity, DEFAULT_MENU_GRAVITY));
attributes.recycle();
applyWindowInsets();
}
private void applyWindowInsets() {
ViewUtils.doOnApplyWindowInsets(
this,
new ViewUtils.OnApplyWindowInsetsListener() {
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(
View view,
@NonNull WindowInsetsCompat insets,
@NonNull RelativePadding initialPadding) {
// Apply the top, bottom, and start padding for a start edge aligned
// NavigationRailView to dodge the system status and navigation bars
initialPadding.top += insets.getSystemWindowInsetTop();
initialPadding.bottom += insets.getSystemWindowInsetBottom();
boolean isRtl = ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL;
int systemWindowInsetLeft = insets.getSystemWindowInsetLeft();
int systemWindowInsetRight = insets.getSystemWindowInsetRight();
initialPadding.start += isRtl ? systemWindowInsetRight : systemWindowInsetLeft;
initialPadding.applyToView(view);
return insets;
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int minWidthSpec = makeMinWidthSpec(widthMeasureSpec);
super.onMeasure(minWidthSpec, heightMeasureSpec);
if (isHeaderViewVisible()) {
int maxMenuHeight = getMeasuredHeight() - headerView.getMeasuredHeight() - topMargin;
int menuHeightSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST);
measureChild(getNavigationRailMenuView(), minWidthSpec, menuHeightSpec);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
NavigationRailMenuView menuView = getNavigationRailMenuView();
int offsetY = 0;
if (isHeaderViewVisible()) {
int usedTop = headerView.getBottom() + topMargin;
int menuTop = menuView.getTop();
if (menuTop < usedTop) {
offsetY = usedTop - menuTop;
}
} else if (menuView.isTopGravity()) {
offsetY = topMargin;
}
if (offsetY > 0) {
menuView.layout(
menuView.getLeft(),
menuView.getTop() + offsetY,
menuView.getRight(),
menuView.getBottom() + offsetY);
}
}
/**
* Adds the specified {@link View} layout resource, to appear at the top of the {@link
* NavigationRailView}. If the view already has a header view attached to it, it will be removed
* first.
*
* @param layoutRes the unique resource identifier to the layout that should be attached.
* @see #addHeaderView(View)
* @see #removeHeaderView()
* @see #getHeaderView()
*/
public void addHeaderView(@LayoutRes int layoutRes) {
addHeaderView(LayoutInflater.from(getContext()).inflate(layoutRes, this, false));
}
/**
* Adds the specified {@link View} if any, to appear at the top of the {@link NavigationRailView}.
* If the view already has a header view attached to it, it will be removed first.
*
* @param headerView reference to the {@link View} that should be attached.
* @see #addHeaderView(int)
* @see #removeHeaderView()
* @see #getHeaderView()
*/
public void addHeaderView(@NonNull View headerView) {
removeHeaderView();
this.headerView = headerView;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
params.gravity = DEFAULT_HEADER_GRAVITY;
params.topMargin = topMargin;
addView(headerView, /* index= */ 0, params);
}
/**
* Returns reference to the header view if any, that is currently attached the {@link
* NavigationRailView}.
*
* @see #addHeaderView(int)
* @see #addHeaderView(View)
* @see #removeHeaderView()
*/
@Nullable
public View getHeaderView() {
return headerView;
}
/**
* Removes the current header view if any, from the {@link NavigationRailView}.
*
* @see #addHeaderView(int)
* @see #addHeaderView(View)
* @see #getHeaderView()
*/
public void removeHeaderView() {
if (headerView != null) {
removeView(headerView);
headerView = null;
}
}
/** Sets how destinations in the menu view will be grouped. */
public void setMenuGravity(int gravity) {
getNavigationRailMenuView().setMenuGravity(gravity);
}
/** Gets the current gravity setting for how destinations in the menu view will be grouped. */
public int getMenuGravity() {
return getNavigationRailMenuView().getMenuGravity();
}
@Override
public int getMaxItemCount() {
return MAX_ITEM_COUNT;
}
private NavigationRailMenuView getNavigationRailMenuView() {
return (NavigationRailMenuView) getMenuView();
}
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@Override
@NonNull
protected NavigationRailMenuView createNavigationBarMenuView(@NonNull Context context) {
return new NavigationRailMenuView(context);
}
private int makeMinWidthSpec(int measureSpec) {
int minWidth = getSuggestedMinimumWidth();
if (MeasureSpec.getMode(measureSpec) != MeasureSpec.EXACTLY && minWidth > 0) {
minWidth += getPaddingLeft() + getPaddingRight();
return MeasureSpec.makeMeasureSpec(
min(MeasureSpec.getSize(measureSpec), minWidth), MeasureSpec.EXACTLY);
}
return measureSpec;
}
private boolean isHeaderViewVisible() {
return headerView != null && headerView.getVisibility() != View.GONE;
}
}