mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(android): revert #8295 keyboard tracking that mispositioned add-task bar
The store v18.10.0 build positions the global add-task bar correctly above the soft keyboard and resizes normally. #8295 (merged after v18.10.0) is the only keyboard-logic change since, and it regressed both: on this branch the bar sits behind the IME and the view no longer appears to resize. #8295 stacked two extra keyboard-height signals (a native physical-px height via keyboardHeightPx$ and a baseInnerHeight-tracking VisualViewport path) on top of the single VisualViewport `obscured` signal that shipped in v18.10.0, combined as max(obscured, nativeKeyboardHeight - layoutShrink). The sources fire on separate async events and race; scheduleCommit() resets baseInnerHeight to the already-shrunk innerHeight mid-IME-animation, which zeroes layoutShrink and defeats the double-count guard, so --keyboard-height lands on a wrong value on devices that already handle the IME. Revert to the known-good single-signal logic. The narrow Android 10 case #8295 targeted will be re-addressed via a single WindowInsets IME listener that is verified on a real device matrix. Reverts #8295. Drops the orphaned calc-keyboard-height spec (its unrelated #8421 backup-ring/state changes are kept).
This commit is contained in:
parent
4fc106890a
commit
43444e6b53
6 changed files with 8 additions and 228 deletions
|
|
@ -185,11 +185,9 @@ class CapacitorMainActivity : BridgeActivity() {
|
|||
if (keypadHeight > screenHeight * 0.15) {
|
||||
// keyboard is opened
|
||||
callJSInterfaceFunctionIfExists("next", "isKeyboardShown$", "true")
|
||||
callJSInterfaceFunctionIfExists("next", "keyboardHeightPx$", keypadHeight.toString())
|
||||
} else {
|
||||
// keyboard is closed
|
||||
callJSInterfaceFunctionIfExists("next", "isKeyboardShown$", "false")
|
||||
callJSInterfaceFunctionIfExists("next", "keyboardHeightPx$", "0")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -153,11 +153,9 @@ class FullscreenActivity : AppCompatActivity() {
|
|||
if (keypadHeight > screenHeight * 0.15) {
|
||||
// keyboard is opened
|
||||
callJSInterfaceFunctionIfExists("next", "isKeyboardShown$", "true")
|
||||
callJSInterfaceFunctionIfExists("next", "keyboardHeightPx$", keypadHeight.toString())
|
||||
} else {
|
||||
// keyboard is closed
|
||||
callJSInterfaceFunctionIfExists("next", "isKeyboardShown$", "false")
|
||||
callJSInterfaceFunctionIfExists("next", "keyboardHeightPx$", "0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
import { calcKeyboardHeight } from './calc-keyboard-height';
|
||||
|
||||
describe('calcKeyboardHeight', () => {
|
||||
describe('visualViewport reports the IME (modern WebView, e.g. Android 14)', () => {
|
||||
it('returns the obscured area', () => {
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 800,
|
||||
visualViewportHeight: 500,
|
||||
nativeKeyboardHeight: 0,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('visualViewport does NOT shrink for the IME (e.g. Android 10)', () => {
|
||||
it('falls back to the native height when the viewport did not shrink', () => {
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 800,
|
||||
visualViewportHeight: 800, // no shrink → obscured 0
|
||||
nativeKeyboardHeight: 300,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(300);
|
||||
});
|
||||
|
||||
it('returns 0 while the keyboard is closed', () => {
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 800,
|
||||
visualViewportHeight: 800,
|
||||
nativeKeyboardHeight: 0,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('layout viewport shrinks for the IME (adjustResize)', () => {
|
||||
it('cancels the native height against the shrink to avoid a double offset', () => {
|
||||
// innerHeight already shrank by 300 → bar is already above the IME → 0
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 500,
|
||||
visualViewportHeight: 500,
|
||||
nativeKeyboardHeight: 300,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('reports only the residual when the shrink is partial', () => {
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 700, // shrank 100 of a 300px keyboard
|
||||
visualViewportHeight: 700,
|
||||
nativeKeyboardHeight: 300,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rotation while the keyboard is closed (regression guard)', () => {
|
||||
it('does not report a phantom keyboard from a stale portrait baseline once the baseline is refreshed', () => {
|
||||
// Portrait baseline 800, rotated to landscape innerHeight 400, keyboard
|
||||
// closed. With a fresh baseline (= current innerHeight) there is no
|
||||
// phantom layoutShrink, so the height stays 0.
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 400,
|
||||
visualViewportHeight: 400,
|
||||
nativeKeyboardHeight: 0,
|
||||
baseInnerHeight: 400,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('reports the keyboard correctly in landscape after the baseline is refreshed', () => {
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 400,
|
||||
visualViewportHeight: 400, // no viewport shrink for IME
|
||||
nativeKeyboardHeight: 250,
|
||||
baseInnerHeight: 400, // refreshed on the close→landscape transition
|
||||
}),
|
||||
).toBe(250);
|
||||
});
|
||||
|
||||
it('a STALE portrait baseline would have hidden the keyboard (demonstrates the bug the refresh fixes)', () => {
|
||||
// baseInnerHeight still 800 (portrait) but we are in landscape (400) with
|
||||
// the keyboard open → layoutShrink 400 wipes out the 250px keyboard.
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 400,
|
||||
visualViewportHeight: 400,
|
||||
nativeKeyboardHeight: 250,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('visualViewport API absent', () => {
|
||||
it('relies solely on the native height', () => {
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 800,
|
||||
visualViewportHeight: null,
|
||||
nativeKeyboardHeight: 300,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defensive guards', () => {
|
||||
it('clamps a negative obscured area to 0 (visual viewport taller than the layout viewport, e.g. pinch-zoom)', () => {
|
||||
// visualViewportHeight > innerHeight → obscured is negative; must not
|
||||
// report a negative keyboard height.
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 800,
|
||||
visualViewportHeight: 820,
|
||||
nativeKeyboardHeight: 0,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('passes through a native height that exceeds the base inner height without flipping negative', () => {
|
||||
// nativeKeyboardHeight > baseInnerHeight (e.g. native measurement carries
|
||||
// an extra inset): nativeCovered must stay positive, not clamp to 0.
|
||||
expect(
|
||||
calcKeyboardHeight({
|
||||
innerHeight: 800,
|
||||
visualViewportHeight: 800,
|
||||
nativeKeyboardHeight: 900,
|
||||
baseInnerHeight: 800,
|
||||
}),
|
||||
).toBe(900);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
export interface KeyboardHeightInput {
|
||||
/** Current layout viewport height (window.innerHeight), CSS px. */
|
||||
innerHeight: number;
|
||||
/** window.visualViewport.height, CSS px, or null when the API is absent. */
|
||||
visualViewportHeight: number | null;
|
||||
/** Native-measured keyboard height (CSS px), 0 when closed. */
|
||||
nativeKeyboardHeight: number;
|
||||
/** window.innerHeight captured while the keyboard was last closed, CSS px. */
|
||||
baseInnerHeight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the on-screen keyboard height (CSS px) from two independent signals,
|
||||
* whichever reports coverage:
|
||||
*
|
||||
* - `obscured` — the part of the layout viewport hidden by the IME according to
|
||||
* the Visual Viewport API. Correct on modern WebViews; reads 0 on e.g.
|
||||
* Android 10 where the IME does not shrink the visual viewport.
|
||||
* - `nativeCovered` — the natively measured keyboard height, minus however much
|
||||
* the layout viewport already shrank (`layoutShrink`). The subtraction keeps
|
||||
* devices where `adjustResize` genuinely shrinks the viewport from being
|
||||
* pushed up by a double offset (the fixed bar already sits above the IME).
|
||||
*
|
||||
* NOTE: `nativeKeyboardHeight` (screenHeight − visibleFrame.bottom on the native
|
||||
* side) includes the navigation-bar inset, not just the IME. The caller's
|
||||
* threshold (~100px, larger than a typical 24–48px navbar) is what discards that
|
||||
* phantom offset when the keyboard is closed — do not lower it without
|
||||
* subtracting the navbar inset here.
|
||||
*/
|
||||
export const calcKeyboardHeight = ({
|
||||
innerHeight,
|
||||
visualViewportHeight,
|
||||
nativeKeyboardHeight,
|
||||
baseInnerHeight,
|
||||
}: KeyboardHeightInput): number => {
|
||||
const obscured = visualViewportHeight !== null ? innerHeight - visualViewportHeight : 0;
|
||||
const layoutShrink = Math.max(0, baseInnerHeight - innerHeight);
|
||||
const nativeCovered = Math.max(0, nativeKeyboardHeight - layoutShrink);
|
||||
return Math.max(obscured, nativeCovered);
|
||||
};
|
||||
|
|
@ -47,7 +47,6 @@ import { FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
|||
import { LS } from '../persistence/storage-keys.const';
|
||||
import { Log } from '../log';
|
||||
import { LayoutService } from '../../core-ui/layout/layout.service';
|
||||
import { calcKeyboardHeight } from './calc-keyboard-height';
|
||||
|
||||
interface NavigationBarPlugin {
|
||||
setColor(options: { color: string; style: 'LIGHT' | 'DARK' }): Promise<void>;
|
||||
|
|
@ -650,6 +649,7 @@ export class GlobalThemeService {
|
|||
*/
|
||||
private _initVisualViewportKeyboardTracking(): void {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
const root = this.document.documentElement;
|
||||
// Filter out small differences from URL bar / overlay UI rather than the
|
||||
// IME — keeps us from setting a phantom keyboard offset.
|
||||
|
|
@ -665,28 +665,15 @@ export class GlobalThemeService {
|
|||
const KEYBOARD_RESIZE_DEBOUNCE_MS = 200;
|
||||
let resizeTimer: number | null = null;
|
||||
|
||||
let nativeKeyboardHeight = 0;
|
||||
let baseInnerHeight = window.innerHeight;
|
||||
|
||||
const measureKeyboardHeight = (): number =>
|
||||
calcKeyboardHeight({
|
||||
innerHeight: window.innerHeight,
|
||||
visualViewportHeight: vv ? vv.height : null,
|
||||
nativeKeyboardHeight,
|
||||
baseInnerHeight,
|
||||
});
|
||||
|
||||
const commit = (): void => {
|
||||
const raw = measureKeyboardHeight();
|
||||
const keyboardHeight = raw > KEYBOARD_THRESHOLD_PX ? raw : 0;
|
||||
const obscured = window.innerHeight - vv.height;
|
||||
const keyboardHeight = obscured > KEYBOARD_THRESHOLD_PX ? obscured : 0;
|
||||
root.style.setProperty(CSS_VAR_KEYBOARD_HEIGHT, `${keyboardHeight}px`);
|
||||
};
|
||||
|
||||
const scheduleCommit = (): void => {
|
||||
if (measureKeyboardHeight() <= KEYBOARD_THRESHOLD_PX) {
|
||||
// Keyboard is (or just became) closed — keep the baseline fresh so a
|
||||
// later rotation/resize doesn't leave a stale value behind.
|
||||
baseInnerHeight = window.innerHeight;
|
||||
const onViewportResize = (): void => {
|
||||
const obscured = window.innerHeight - vv.height;
|
||||
if (obscured <= KEYBOARD_THRESHOLD_PX) {
|
||||
if (resizeTimer !== null) {
|
||||
window.clearTimeout(resizeTimer);
|
||||
resizeTimer = null;
|
||||
|
|
@ -704,24 +691,9 @@ export class GlobalThemeService {
|
|||
};
|
||||
|
||||
commit();
|
||||
|
||||
if (vv) {
|
||||
vv.addEventListener('resize', scheduleCommit, { passive: true });
|
||||
}
|
||||
|
||||
if (IS_ANDROID_WEB_VIEW && androidInterface.keyboardHeightPx$) {
|
||||
androidInterface.keyboardHeightPx$
|
||||
.pipe(distinctUntilChanged(), takeUntilDestroyed(this._destroyRef))
|
||||
.subscribe((physicalPx) => {
|
||||
nativeKeyboardHeight = Math.max(0, physicalPx / (window.devicePixelRatio || 1));
|
||||
scheduleCommit();
|
||||
});
|
||||
}
|
||||
|
||||
vv.addEventListener('resize', onViewportResize, { passive: true });
|
||||
this._destroyRef.onDestroy(() => {
|
||||
if (vv) {
|
||||
vv.removeEventListener('resize', scheduleCommit);
|
||||
}
|
||||
vv.removeEventListener('resize', onViewportResize);
|
||||
if (resizeTimer !== null) {
|
||||
window.clearTimeout(resizeTimer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ export interface AndroidInterface {
|
|||
onPause$: Subject<void>;
|
||||
isInBackground$: Observable<boolean>;
|
||||
isKeyboardShown$: Subject<boolean>;
|
||||
keyboardHeightPx$: Subject<number>;
|
||||
|
||||
onShareWithAttachment$: Subject<AndroidShareData>;
|
||||
|
||||
|
|
@ -169,7 +168,6 @@ if (IS_ANDROID_WEB_VIEW) {
|
|||
androidInterface.onReminderSnooze$ = new ReplaySubject(20);
|
||||
androidInterface.onShareWithAttachment$ = new ReplaySubject(1);
|
||||
androidInterface.isKeyboardShown$ = new BehaviorSubject(false);
|
||||
androidInterface.keyboardHeightPx$ = new BehaviorSubject(0);
|
||||
|
||||
androidInterface.isInBackground$ = merge(
|
||||
androidInterface.onResume$.pipe(mapTo(false)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue