Align overlay to bottom or top of anchor view depending on available space (bottom by default on initial focus). Establishing visible app height now works much better on Android 5.0+. (#718)

This commit is contained in:
Matt Portune 2020-02-05 19:40:44 -05:00 committed by GitHub
parent 93132f5d7b
commit bbd8615cda
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 107 additions and 52 deletions

View file

@ -360,36 +360,31 @@ namespace Bit.Droid.Accessibility
windowManagerType, windowManagerType,
WindowManagerFlags.NotFocusable | WindowManagerFlags.NotTouchModal, WindowManagerFlags.NotFocusable | WindowManagerFlags.NotTouchModal,
Format.Transparent); Format.Transparent);
layoutParams.Gravity = GravityFlags.Bottom | GravityFlags.Left; layoutParams.Gravity = GravityFlags.Top | GravityFlags.Left;
return layoutParams; return layoutParams;
} }
public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo root, AccessibilityNodeInfo anchorView, public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo anchorView, int overlayViewHeight,
int rootRectHeight = 0) bool isOverlayAboveAnchor)
{ {
if(rootRectHeight == 0)
{
rootRectHeight = GetNodeHeight(root);
}
var anchorViewRect = new Rect(); var anchorViewRect = new Rect();
anchorView.GetBoundsInScreen(anchorViewRect); anchorView.GetBoundsInScreen(anchorViewRect);
var anchorViewRectLeft = anchorViewRect.Left; var anchorViewX = anchorViewRect.Left;
var anchorViewRectTop = anchorViewRect.Top; var anchorViewY = isOverlayAboveAnchor ? anchorViewRect.Top : anchorViewRect.Bottom;
anchorViewRect.Dispose(); anchorViewRect.Dispose();
var calculatedTop = rootRectHeight - anchorViewRectTop; if(isOverlayAboveAnchor)
if((int)Build.VERSION.SdkInt >= 24)
{ {
calculatedTop -= GetNavigationBarHeight(); anchorViewY -= overlayViewHeight;
} }
anchorViewY -= GetStatusBarHeight();
return new Point(anchorViewRectLeft, calculatedTop); return new Point(anchorViewX, anchorViewY);
} }
public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo anchorNode, AccessibilityNodeInfo root, public static Point GetOverlayAnchorPosition(AccessibilityNodeInfo anchorNode, AccessibilityNodeInfo root,
IEnumerable<AccessibilityWindowInfo> windows) IEnumerable<AccessibilityWindowInfo> windows, int overlayViewHeight, bool isOverlayAboveAnchor)
{ {
Point point = null; Point point = null;
if(anchorNode != null) if(anchorNode != null)
@ -407,34 +402,66 @@ namespace Bit.Droid.Accessibility
// node.VisibleToUser doesn't always give us exactly what we want, so attempt to tighten up the range // node.VisibleToUser doesn't always give us exactly what we want, so attempt to tighten up the range
// of visibility // of visibility
var rootNodeHeight = GetNodeHeight(root); var minY = 0;
var limitLowY = 0; int maxY;
var limitHighY = rootNodeHeight - GetNodeHeight(anchorNode);
if(windows != null) if(windows != null)
{ {
if(IsStatusBarExpanded(windows)) if(IsStatusBarExpanded(windows))
{ {
return new Point(-1, -1); return new Point(-1, -1);
} }
var inputWindowRect = GetInputMethodWindowRect(windows); maxY = GetApplicationVisibleHeight(windows);
if(inputWindowRect != null)
{
limitLowY += inputWindowRect.Height();
if(Build.VERSION.SdkInt >= BuildVersionCodes.Q)
{
limitLowY += GetNavigationBarHeight() + GetStatusBarHeight();
} }
inputWindowRect.Dispose(); else
{
var rootNodeHeight = GetNodeHeight(root);
if(rootNodeHeight == -1)
{
return null;
} }
maxY = rootNodeHeight - GetNavigationBarHeight();
} }
point = GetOverlayAnchorPosition(root, anchorNode, rootNodeHeight); point = GetOverlayAnchorPosition(anchorNode, overlayViewHeight, isOverlayAboveAnchor);
if(point.Y < limitLowY || point.Y > limitHighY) if(point.Y < minY)
{ {
if(isOverlayAboveAnchor)
{
// view nearing bounds, anchor to bottom
point.X = -1;
point.Y = 0;
}
else
{
// view out of bounds, hide overlay
point.X = -1; point.X = -1;
point.Y = -1; point.Y = -1;
} }
} }
else if(point.Y > maxY)
{
if(isOverlayAboveAnchor)
{
// view out of bounds, hide overlay
point.X = -1;
point.Y = -1;
}
else
{
// view nearing bounds, anchor to top
point.X = 0;
point.Y = -1;
}
}
else if(isOverlayAboveAnchor && point.Y < (maxY - overlayViewHeight - GetNodeHeight(anchorNode)))
{
// This else block forces the overlay to return to bottom alignment as soon as space is available
// below the anchor view. Removing this will change the behavior to wait until there isn't enough
// space above the anchor view before returning to bottom alignment.
point.X = -1;
point.Y = 0;
}
}
return point; return point;
} }
@ -456,28 +483,35 @@ namespace Bit.Droid.Accessibility
return false; return false;
} }
public static Rect GetInputMethodWindowRect(IEnumerable<AccessibilityWindowInfo> windows) public static int GetApplicationVisibleHeight(IEnumerable<AccessibilityWindowInfo> windows)
{ {
Rect windowRect = null; var appWindowHeight = 0;
var nonAppWindowHeight = 0;
if(windows != null) if(windows != null)
{ {
foreach(var window in windows) foreach(var window in windows)
{ {
if(window.Type == AccessibilityWindowType.InputMethod) var windowRect = new Rect();
{
windowRect = new Rect();
window.GetBoundsInScreen(windowRect); window.GetBoundsInScreen(windowRect);
window.Recycle(); if(window.Type == AccessibilityWindowType.Application)
break; {
appWindowHeight += windowRect.Height();
} }
window.Recycle(); else
{
nonAppWindowHeight += windowRect.Height();
} }
} }
return windowRect; }
return appWindowHeight - nonAppWindowHeight;
} }
public static int GetNodeHeight(AccessibilityNodeInfo node) public static int GetNodeHeight(AccessibilityNodeInfo node)
{ {
if(node == null)
{
return -1;
}
var nodeRect = new Rect(); var nodeRect = new Rect();
node.GetBoundsInScreen(nodeRect); node.GetBoundsInScreen(nodeRect);
var nodeRectHeight = nodeRect.Height(); var nodeRectHeight = nodeRect.Height();

View file

@ -31,9 +31,11 @@ namespace Bit.Droid.Accessibility
private AccessibilityNodeInfo _anchorNode = null; private AccessibilityNodeInfo _anchorNode = null;
private int _lastAnchorX = 0; private int _lastAnchorX = 0;
private int _lastAnchorY = 0; private int _lastAnchorY = 0;
private bool _isOverlayAboveAnchor = false;
private static bool _overlayAnchorObserverRunning = false; private static bool _overlayAnchorObserverRunning = false;
private IWindowManager _windowManager = null; private IWindowManager _windowManager = null;
private LinearLayout _overlayView = null; private LinearLayout _overlayView = null;
private int _overlayViewHeight = 0;
private long _lastAutoFillTime = 0; private long _lastAutoFillTime = 0;
private Java.Lang.Runnable _overlayAnchorObserverRunnable = null; private Java.Lang.Runnable _overlayAnchorObserverRunnable = null;
private Handler _handler = new Handler(Looper.MainLooper); private Handler _handler = new Handler(Looper.MainLooper);
@ -180,6 +182,7 @@ namespace Bit.Droid.Accessibility
_overlayView = null; _overlayView = null;
_lastAnchorX = 0; _lastAnchorX = 0;
_lastAnchorY = 0; _lastAnchorY = 0;
_isOverlayAboveAnchor = false;
if(_anchorNode != null) if(_anchorNode != null)
{ {
@ -213,8 +216,23 @@ namespace Bit.Droid.Accessibility
return; return;
} }
var intent = new Intent(this, typeof(AccessibilityActivity));
intent.PutExtra("uri", uri);
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
_overlayView = AccessibilityHelpers.GetOverlayView(this);
_overlayView.Measure(View.MeasureSpec.MakeMeasureSpec(0, 0),
View.MeasureSpec.MakeMeasureSpec(0, 0));
_overlayViewHeight = _overlayView.MeasuredHeight;
_overlayView.Click += (sender, eventArgs) =>
{
CancelOverlayPrompt();
StartActivity(intent);
};
var layoutParams = AccessibilityHelpers.GetOverlayLayoutParams(); var layoutParams = AccessibilityHelpers.GetOverlayLayoutParams();
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(root, e.Source); var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(e.Source, _overlayViewHeight,
_isOverlayAboveAnchor);
layoutParams.X = anchorPosition.X; layoutParams.X = anchorPosition.X;
layoutParams.Y = anchorPosition.Y; layoutParams.Y = anchorPosition.Y;
@ -223,17 +241,6 @@ namespace Bit.Droid.Accessibility
_windowManager = GetSystemService(WindowService).JavaCast<IWindowManager>(); _windowManager = GetSystemService(WindowService).JavaCast<IWindowManager>();
} }
var intent = new Intent(this, typeof(AccessibilityActivity));
intent.PutExtra("uri", uri);
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop);
_overlayView = AccessibilityHelpers.GetOverlayView(this);
_overlayView.Click += (sender, eventArgs) =>
{
CancelOverlayPrompt();
StartActivity(intent);
};
_anchorNode = e.Source; _anchorNode = e.Source;
_lastAnchorX = anchorPosition.X; _lastAnchorX = anchorPosition.X;
_lastAnchorY = anchorPosition.Y; _lastAnchorY = anchorPosition.Y;
@ -281,7 +288,8 @@ namespace Bit.Droid.Accessibility
windows = Windows; windows = Windows;
} }
var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(_anchorNode, root, windows); var anchorPosition = AccessibilityHelpers.GetOverlayAnchorPosition(_anchorNode, root, windows,
_overlayViewHeight, _isOverlayAboveAnchor);
if(anchorPosition == null) if(anchorPosition == null)
{ {
CancelOverlayPrompt(); CancelOverlayPrompt();
@ -292,9 +300,22 @@ namespace Bit.Droid.Accessibility
if(_overlayView.Visibility != ViewStates.Gone) if(_overlayView.Visibility != ViewStates.Gone)
{ {
_overlayView.Visibility = ViewStates.Gone; _overlayView.Visibility = ViewStates.Gone;
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Hidden");
} }
return; return;
} }
else if(anchorPosition.X == -1)
{
_isOverlayAboveAnchor = false;
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Below Anchor");
return;
}
else if(anchorPosition.Y == -1)
{
_isOverlayAboveAnchor = true;
System.Diagnostics.Debug.WriteLine(">>> Accessibility Overlay View Above Anchor");
return;
}
else if(anchorPosition.X == _lastAnchorX && anchorPosition.Y == _lastAnchorY) else if(anchorPosition.X == _lastAnchorX && anchorPosition.Y == _lastAnchorY)
{ {
if(_overlayView.Visibility != ViewStates.Visible) if(_overlayView.Visibility != ViewStates.Visible)