Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ Native navbar state.
| **`hidden`** | <code>boolean</code> | Hide the native navbar. |
| **`title`** | <code>string</code> | Main title. |
| **`subtitle`** | <code>string</code> | Secondary title where supported by the platform. |
| **`large`** | <code>boolean</code> | Prefer a large iOS title style. |
| **`large`** | <code>boolean</code> | Show a large title under the bar, like iOS large titles. On Android the bar grows by 52dp and the title moves into its own row. Send `large: false` with the same title to collapse it to the inline title (animated on Android), e.g. once the page scrolls. |
| **`transparent`** | <code>boolean</code> | Prefer transparent/scroll-edge style. |
| **`blurEffect`** | <code><a href="#nativenavigationblureffect">NativeNavigationBlurEffect</a></code> | iOS blur/material effect for the navbar background when glass is not available. Defaults to `systemChromeMaterial` for transparent bars. |
| **`glass`** | <code><a href="#nativenavigationglassoptions">NativeNavigationGlassOptions</a></code> | Optional glass background behavior. Overrides `configure({ glass })` for this navbar update. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
Expand Down Expand Up @@ -97,6 +98,12 @@ public class NativeNavigationPlugin extends Plugin {
private NavbarScrollSupport.ScrollBehavior navbarScrollBehavior = NavbarScrollSupport.ScrollBehavior.NONE;
private float navbarScrollThreshold = 8f;
private boolean navbarScrollCollapsed = false;
private TextView largeTitleView;
private boolean navbarLarge = false;
private float navbarLargeProgress = 0f;
private ValueAnimator navbarLargeAnimator;
private String navbarTitle;
private int navbarForegroundColor = Color.rgb(20, 24, 32);
private boolean tabbarVisible = false;
private String contentInsetMode = "css";
private GlassOptions defaultGlassOptions = GlassOptions.defaults();
Expand Down Expand Up @@ -212,7 +219,19 @@ public void setNavbar(PluginCall call) {
}

Toolbar nativeToolbar = ensureToolbar();
nativeToolbar.setTitle(call.getString("title", ""));
String title = call.getString("title", "");
boolean large = Boolean.TRUE.equals(call.getBoolean("large", false));
boolean animateLarge = NavbarLargeTitleSupport.shouldAnimate(
navbarContainer.getVisibility() == View.VISIBLE,
navbarTitle,
title,
navbarLarge,
large
);
navbarTitle = title;
navbarLarge = large;
nativeToolbar.setTitle(title);
largeTitleView.setText(title);
nativeToolbar.setSubtitle(call.getString("subtitle", null));
nativeToolbar.getMenu().clear();
menuActionIds.clear();
Expand All @@ -235,6 +254,7 @@ public void setNavbar(PluginCall call) {
navbarGlassConfig = call.getObject("glass", null);
navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
applyToolbarColors(nativeToolbar, colors);
setNavbarLargeProgress(large ? 1f : 0f, animateLarge);
navbarContainer.setVisibility(View.VISIBLE);
configureNavbarScrollBehavior(call);
layoutChrome();
Expand Down Expand Up @@ -775,8 +795,20 @@ private Toolbar ensureToolbar() {
return true;
});

largeTitleView = new TextView(getContext());
largeTitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, NavbarLargeTitleSupport.LARGE_TITLE_TEXT_SP);
largeTitleView.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
largeTitleView.setSingleLine(true);
largeTitleView.setEllipsize(TextUtils.TruncateAt.END);
largeTitleView.setGravity(Gravity.BOTTOM | Gravity.START);
largeTitleView.setPaddingRelative(dp(16), 0, dp(16), dp(NavbarLargeTitleSupport.LARGE_TITLE_BOTTOM_PADDING_DP));
largeTitleView.setAlpha(0f);
// The toolbar keeps the title for screen readers; the large copy is visual only.
largeTitleView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);

navbarContainer.addView(navbarGlassSurface);
navbarContainer.addView(toolbar);
navbarContainer.addView(largeTitleView);
if (root != null) {
root.addView(navbarContainer);
} else {
Expand Down Expand Up @@ -2219,7 +2251,11 @@ private void applyToolbarColors(Toolbar nativeToolbar, JSObject colors) {
navbarBackgroundColor = background;
int foreground = parseColor(colors.getString("foreground", null), foregroundFallback);
tintColor = tint;
navbarForegroundColor = foreground;
nativeToolbar.setTitleTextColor(foreground);
if (largeTitleView != null) {
largeTitleView.setTextColor(foreground);
}
nativeToolbar.setSubtitleTextColor(withAlpha(foreground, 190));
Drawable navigationIcon = nativeToolbar.getNavigationIcon();
if (navigationIcon != null) {
Expand Down Expand Up @@ -2499,7 +2535,7 @@ private void layoutChrome() {
}
int status = statusBarInset();
int bottom = navigationBarInset();
int navbarToolbar = navbarScrollCollapsed ? 0 : dp(DEFAULT_NAVBAR_DP);
int navbarToolbar = navbarScrollCollapsed ? 0 : navbarBarHeight(navbarLargeProgress);
int navbarHeight = navbarVisible ? status + navbarToolbar : 0;
boolean tabbarLayoutVisible = tabbarVisible || tabbarChromeHideAnimating;

Expand All @@ -2517,6 +2553,13 @@ private void layoutChrome() {
);
toolbarParams.topMargin = status;
toolbar.setLayoutParams(toolbarParams);
FrameLayout.LayoutParams largeTitleParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(NavbarLargeTitleSupport.LARGE_TITLE_ROW_DP),
Gravity.TOP
);
largeTitleParams.topMargin = status + dp(DEFAULT_NAVBAR_DP);
largeTitleView.setLayoutParams(largeTitleParams);
fillContainer(navbarGlassSurface);
}

Expand Down Expand Up @@ -2847,11 +2890,63 @@ private void setNavbarScrollCollapsed(boolean collapsed, boolean animated) {
}
}

/** Navbar height under the status bar for a large title progress, in device pixels. */
private int navbarBarHeight(float largeProgress) {
return Math.round(dp((double) NavbarLargeTitleSupport.barHeightDp(largeProgress)));
}

private void setNavbarLargeProgress(float target, boolean animated) {
if (navbarLargeAnimator != null) {
navbarLargeAnimator.cancel();
navbarLargeAnimator = null;
}
if (!animated || navbarLargeProgress == target) {
navbarLargeProgress = target;
applyNavbarLargeProgress();
return;
}
ValueAnimator animator = ValueAnimator.ofFloat(navbarLargeProgress, target);
animator.setDuration(NavbarLargeTitleSupport.ANIMATION_MS);
animator.setInterpolator(new PathInterpolator(0.2f, 0f, 0f, 1f));
animator.addUpdateListener((animation) -> {
navbarLargeProgress = (float) animation.getAnimatedValue();
applyNavbarLargeProgress();
resizeNavbarContainer();
});
navbarLargeAnimator = animator;
animator.start();
}

/** Animation frames only resize the navbar; the tabbar and insets keep their layout. */
private void resizeNavbarContainer() {
if (navbarContainer == null || !navbarVisible) {
return;
}
ViewGroup.LayoutParams params = navbarContainer.getLayoutParams();
if (params == null) {
return;
}
params.height = statusBarInset() + (navbarScrollCollapsed ? 0 : navbarBarHeight(navbarLargeProgress));
navbarContainer.setLayoutParams(params);
}

private void applyNavbarLargeProgress() {
if (toolbar == null || largeTitleView == null) {
return;
}
float progress = navbarLargeProgress;
largeTitleView.setAlpha(NavbarLargeTitleSupport.largeTitleAlpha(progress));
largeTitleView.setTranslationY(-(1f - progress) * dp(NavbarLargeTitleSupport.LARGE_TITLE_ROW_DP) * 0.5f);
largeTitleView.setVisibility(progress > 0f ? View.VISIBLE : View.INVISIBLE);
float inlineAlpha = NavbarLargeTitleSupport.inlineTitleAlpha(progress);
toolbar.setTitleTextColor(withAlpha(navbarForegroundColor, Math.round(Color.alpha(navbarForegroundColor) * inlineAlpha)));
}

private void applyNavbarScrollCollapsedAppearance(boolean animated) {
if (navbarContainer == null) {
return;
}
float hideDistance = dp(DEFAULT_NAVBAR_DP);
float hideDistance = navbarBarHeight(navbarLargeProgress);
if (animated) {
navbarContainer
.animate()
Expand All @@ -2868,7 +2963,8 @@ private void applyNavbarScrollCollapsedAppearance(boolean animated) {
private JSObject currentInsets() {
// View layout uses device pixels; CSS variables must be density-independent
// or a 3x screen writes `192px` which CSS treats as 192 CSS px.
int navbarToolbar = navbarScrollCollapsed ? 0 : dp(DEFAULT_NAVBAR_DP);
// Insets follow the target title state so the page moves once, not on every animation frame.
int navbarToolbar = navbarScrollCollapsed ? 0 : navbarBarHeight(navbarLarge ? 1f : 0f);
int top = navbarVisible ? statusBarInset() + navbarToolbar : 0;
int bottom = tabbarVisible ? navigationBarInset() + dp(tabbarStyle.totalHeight()) + dp(tabbarStyle.bottomGap) : 0;
JSObject insets = new JSObject();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package app.capgo.nativenavigation;

/**
* Large title geometry for the Android navbar. Progress runs from 0 (inline title in the
* toolbar row) to 1 (large title in its own row under the toolbar), like an iOS large title.
*/
public final class NavbarLargeTitleSupport {

/** Toolbar row height, matching the default Android navbar. */
public static final int TOOLBAR_DP = 56;
/** Extra row for the large title. iOS adds 52pt (44 to 96); Android uses the same row. */
public static final int LARGE_TITLE_ROW_DP = 52;
public static final int LARGE_TITLE_TEXT_SP = 32;
public static final int LARGE_TITLE_BOTTOM_PADDING_DP = 6;
public static final int ANIMATION_MS = 220;

private NavbarLargeTitleSupport() {}

/** Navbar height below the status bar, in dp, for a large title progress. */
public static float barHeightDp(float progress) {
return TOOLBAR_DP + LARGE_TITLE_ROW_DP * clamp(progress);
}

/** The large title fades out over the first half of the collapse so both titles never overlap. */
public static float largeTitleAlpha(float progress) {
return clamp(progress * 2f - 1f);
}

/** The inline toolbar title fades in over the second half of the collapse. */
public static float inlineTitleAlpha(float progress) {
return clamp(1f - progress * 2f);
}

/**
* Animate only a collapse or expand of the same visible header. A new title is a new page;
* the page transition already animates, so the header snaps to its new state.
*/
public static boolean shouldAnimate(boolean wasVisible, String previousTitle, String title, boolean wasLarge, boolean large) {
if (!wasVisible || wasLarge == large) {
return false;
}
return previousTitle == null ? title == null : previousTitle.equals(title);
}

private static float clamp(float value) {
return Math.max(0f, Math.min(1f, value));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package app.capgo.nativenavigation;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class NavbarLargeTitleSupportTest {

@Test
public void barHeightGrowsWithTheLargeTitleRow() {
assertEquals(56f, NavbarLargeTitleSupport.barHeightDp(0f), 0.001f);
assertEquals(108f, NavbarLargeTitleSupport.barHeightDp(1f), 0.001f);
assertEquals(82f, NavbarLargeTitleSupport.barHeightDp(0.5f), 0.001f);
assertEquals(108f, NavbarLargeTitleSupport.barHeightDp(3f), 0.001f);
}

@Test
public void titlesNeverShowTogether() {
assertEquals(1f, NavbarLargeTitleSupport.largeTitleAlpha(1f), 0.001f);
assertEquals(0f, NavbarLargeTitleSupport.inlineTitleAlpha(1f), 0.001f);
assertEquals(0f, NavbarLargeTitleSupport.largeTitleAlpha(0.5f), 0.001f);
assertEquals(0f, NavbarLargeTitleSupport.inlineTitleAlpha(0.5f), 0.001f);
assertEquals(0f, NavbarLargeTitleSupport.largeTitleAlpha(0f), 0.001f);
assertEquals(1f, NavbarLargeTitleSupport.inlineTitleAlpha(0f), 0.001f);
}

@Test
public void animatesOnlyCollapseOfTheSamePage() {
assertTrue(NavbarLargeTitleSupport.shouldAnimate(true, "Teams", "Teams", true, false));
assertTrue(NavbarLargeTitleSupport.shouldAnimate(true, "Teams", "Teams", false, true));
assertFalse(NavbarLargeTitleSupport.shouldAnimate(true, "Teams", "Rules", true, false));
assertFalse(NavbarLargeTitleSupport.shouldAnimate(false, "Teams", "Teams", true, false));
assertFalse(NavbarLargeTitleSupport.shouldAnimate(true, "Teams", "Teams", true, true));
}
}
4 changes: 3 additions & 1 deletion src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,9 @@ export interface NativeNavigationNavbarOptions {
subtitle?: string;

/**
* Prefer a large iOS title style.
* Show a large title under the bar, like iOS large titles. On Android the bar grows by 52dp and
* the title moves into its own row. Send `large: false` with the same title to collapse it to the
* inline title (animated on Android), e.g. once the page scrolls.
*/
large?: boolean;

Expand Down
Loading