TLDR: I need a way to disable Android 10 gesture navigation programmatically so that they don't accidentally go back when they swipe from the sides
The backstory: Android 10 introduced gesture navigation as opposed to the buttons at the bottom. So now on Android 10 devices that have it enabled, they can swipe from either side of the screen to go back and swipe from the bottom to navigate home or between apps. However, I am working on an implementation in AR and want to lock the screen to portrait but allow users to go landscape.
If a user turns their phone to landscape but the activity is locked to portrait, the back gesture navigation is now a swipe from the top which is a common way to access the status bar in a full screen app (which this one is) so users will inadvertently go back and leave the experience if they are used to android navigations.
Does anybody know how to either a) disable the gesture navigation (but then how does the user go back/to home?) for Android 10 programmatically or b) know how to just change the orientation for the gestures without needing your activity to support landscape?
It's very easy to block the gestures programmatically, but you can't do that for entire edges on both side.
SO you have to decide on how much portion of the screen you want to disable the gestures?
Here is the code :
Define this code in your Utils class.
static List<Rect> exclusionRects = new ArrayList<>();
public static void updateGestureExclusion(AppCompatActivity activity) {
if (Build.VERSION.SDK_INT < 29) return;
exclusionRects.clear();
Rect rect = new Rect(0, 0, SystemUtil.dpToPx(activity, 16), getScreenHeight(activity));
exclusionRects.add(rect);
activity.findViewById(android.R.id.content).setSystemGestureExclusionRects(exclusionRects);
}
public static int getScreenHeight(AppCompatActivity activity) {
DisplayMetrics displayMetrics = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
return height;
}
public static int dpToPx(Context context, int i) {
return (int) (((float) i) * context.getResources().getDisplayMetrics().density);
}
Check if your layout is set in that activity where you want to exclude the edge getures and then apply this code.
// 'content' is the root view of your layout xml.
ViewTreeObserver treeObserver = content.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new
ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
content.getViewTreeObserver().removeOnGlobalLayoutListener(this);
SystemUtil.updateGestureExclusion(MainHomeActivity.this);
}
});
We are adding the exclusion rectangle width to 16dp to fetch the back gesture which you can change according to your preferrences.
Here are some things to note :-
You must not block both side gestures. If you do so, it'll be the worst user experience.
"getScreenHeight(activity)" is the height of the rectangle, So if you want to block the gesture in left side & top half of the screen then simply replace it with getScreenHeight(activity)/2
1st argument in new Rect() is 0 because we want the gestures on left sdie, If you want it right side then simply put - "getScreenWidth(activity) - SystemUtil.dpToPx(activity, 16)"
Hope this will solve your problem permanently. :)
Remember:
setSystemGestureExclusionRects() must be called in doOnLayout() for your view
My implementation:
binding.root.apply { // changing gesture rects for root view
doOnLayout {
// updating exclusion rect
val rects = mutableListOf<Rect>()
rects.add(Rect(0,0,width,(150 * resources.displayMetrics.density).toInt()))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
systemGestureExclusionRects = rects
}
}
}
I excluded gestures for 150dp from the top, and for the entire width (just to test)
While #Dev4Life's answer was helpful, I had no success until visiting the documentation:
Android Docs: setSystemGestureExclusionRects
Related
I've seen countless topics about this, but as for now I cannot find anything working. I have portrait app with a form containing mostly input fields.
I've created a ScrollView and inside content I've added all necessary fields. I've added Vertical Layout Group and Content Size Fitter. Now, when I run the app, content moves nicely, but when I open keyboard it overlaps the content and I cannot see lower input fields when editing them. It doesn't look well. So, I'm looking for a script/plugin to enable this feature for both android and iOS. I've only seen iOS-specific solutions and universal, but none worked for me. I can link all the codes I've found, but I think it'll just create an unnecessary mess. Most of these topics are old and may have worked before, but doesn't work now.
Ideally, I'd prefer a global solution, which just shrinks whole app, when keyboard is opened and expands it back, when keyboard is closed.
Btw. I'm using Unity 2019.2.15f1 and running on Pixel 3XL with Android 10, if that matters.
edit:
I've created small demo project, where you can test Keyboard size script:
https://drive.google.com/file/d/1vj2WG2JA1OHPc3uI4PNyAeYHtuHTLUXh/view?usp=sharing
It contains 3 scripts:
ScrollContent.cs - it attaches InputH.cs script to every input field programatically.
InputH.cs - it handles starting (OnPointerClick) and ending (onEndEdit) of edit single input field by calling OpenKeyboard/CloseKeyboard from KeyboardSize script.
KeyboardSize.cs - script from #Remy_rm, slightly modified (added some logs, IsKeyboardOpened method and my trial of adjust the keyboard scroll position).
The idea looks fine, but there are few issues:
1) "added" height seems to be working, however scrolled content should also be moved (my attempt to fix this is in the last line in GetKeyboard height method, but it doesn't work). It you scroll to the bottom Input Field and tap it, after keyboard is opened this field should be just above it.
2) when I tap second time on another input field, while first one is edited, it causes onEndEdit to be called and keyboard is closed.
Layout hierarchy looks like this:
This answer is made assuming you are using the native TouchScreenKeyboard.
What you can do is add an image as the last entry of your Vertical Layout Group (let's call it the "buffer"), that has its alpha set to 0 (making it invisible) and its height set to 0. This will make your Layout Group look unchanged.
Then when you open the keyboard you set the height of this "buffer" image to the height of the keyboard. Due to the content size fitter and the Vertical Layout Group the input fields of your form will be pushed to above your keyboard, and "behind" your keyboard will be the empty image.
Getting the height of the keyboard is easy on iOS TouchScreenKeyboard.area.height should do the trick. On Android however this returns an empty rect. This answer answer explains how to get the height of an Android keyboard.
Fully implemented this would look something like this (Only tested this on Android, but should work for iOS as well). Note i'm using the method from the before linked answer to get the Android keyboard height.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class KeyboardSize : MonoBehaviour
{
[SerializeField] private RectTransform bufferImage;
private float height = -1;
/// <summary>
/// Open the keyboard and start a coroutine that gets the height of the keyboard
/// </summary>
public void OpenKeyboard()
{
TouchScreenKeyboard.Open("");
StartCoroutine(GetKeyboardHeight());
}
/// <summary>
/// Set the height of the "buffer" image back to zero when the keyboard closes so that the content size fitter shrinks to its original size
/// </summary>
public void CloseKeyboard()
{
bufferImage.GetComponent<RectTransform>().sizeDelta = Vector2.zero;
}
/// <summary>
/// Get the height of the keyboarding depending on the platform
/// </summary>
public IEnumerator GetKeyboardHeight()
{
//Wait half a second to ensure the keyboard is fully opened
yield return new WaitForSeconds(0.5f);
#if UNITY_IOS
//On iOS we can use the native TouchScreenKeyboard.area.height
height = TouchScreenKeyboard.area.height;
#elif UNITY_ANDROID
//On Android TouchScreenKeyboard.area.height returns 0, so we get it from an AndroidJavaObject instead.
using (AndroidJavaClass UnityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
AndroidJavaObject View = UnityClass.GetStatic<AndroidJavaObject>("currentActivity").Get<AndroidJavaObject>("mUnityPlayer").Call<AndroidJavaObject>("getView");
using (AndroidJavaObject Rct = new AndroidJavaObject("android.graphics.Rect"))
{
View.Call("getWindowVisibleDisplayFrame", Rct);
height = Screen.height - Rct.Call<int>("height");
}
}
#endif
//Set the height of our "buffer" image to the height of the keyboard, pushing it up.
bufferImage.sizeDelta = new Vector2(1, height);
}
StartCoroutine(CalculateNormalizedPosition());
}
private IEnumerator CalculateNormalizedPosition()
{
yield return new WaitForEndOfFrame();
//Get the new total height of the content gameobject
var newContentHeight = contentParent.sizeDelta.y;
//Get the local y position of the selected input
var selectedInputHeight = InputH.lastSelectedInput.transform.localPosition.y;
//Get the normalized position of the selected input
var selectedInputfieldHeightNormalized = 1 - selectedInputHeight / -newContentHeight;
//Assign the button's normalized position to the scroll rect's normalized position
scrollRect.verticalNormalizedPosition = selectedInputfieldHeightNormalized;
}
I've edited your InputH to keep track of the last selected input by adding a static GameObject that is assigned to when an inputfield is clicked like so:
public class InputH : MonoBehaviour, IPointerClickHandler
{
private GameObject canvas;
//We can use a static GameObject to track the last selected input
public static GameObject lastSelectedInput;
// Start is called before the first frame update
void Start()
{
// Your start is unaltered
}
public void OnPointerClick(PointerEventData eventData)
{
KeyboardSize ks = canvas.GetComponent<KeyboardSize>();
if (!ks.IsKeyboardOpened())
{
ks.OpenKeyboard();
//Assign the clicked button to the lastSelectedInput to be used inside KeyboardSize.cs
lastSelectedInput = gameObject;
}
else
{
print("Keyboard is already opened");
}
}
One more thing that had to be done for (atleast my solution) to work is that I had to change the anchor on your "Content" GameObject to top center, instead of the stretch you had it on.
This is a very simple example to achieve the android multi screen wallpaper effect on iOS, written in Swift. Sorry for the poor and redundant English as I am a non-native speaker and new to coding.
Many android phones have a multi screen desktop with the feature that when you swipe between screens, the wallpaper moves horizontally according to your gesture. Usually the wallpaper will move in a smaller scale comparing to the icons or search boxes to convey a sense of depth.
To do this, I use a UIScrollView instead of a UIPageViewController.I tried the latter at first, which makes the viewController hierarchy become very complex, and I couldn't use the touchesMove method correctly, as when you pan around pages, a new child viewController will come in and interrupt the method.
Instead, I tried UIScrollView and here is what I have reached.
drag a wallpaper imageView into viewController, set the image, set the frame to (0,0,width of image, height of screen)
set the viewController size to freedom and set the width to theNumberOfPagesYouWantToDisplay * screenWidth (only to make the next steps easier, the width of the viewController will not affect the final result.)
drag a scrollView that fits in the viewController
add the "content UIs" into the scrollView to become a childView of it. Or you can do it using codes.
add a pageControl if you would like.
in your viewController.swift add the following code
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate{
let pagesToDisplay = 10 // You can modify this to test the result.
let scrollViewWidth = CGFloat(320)
let scrollViewHeight = CGFloat(568) // You can also modify these based on the screen size of your device, this is an example for iPhone 5/5s
#IBOutlet weak var backgroundView: UIImageView!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
scrollView.pagingEnabled = true
// Makes the scrollView feel like pageViewController.
scrollView.frame = CGRectMake(0,0,scrollViewWidth,scrollViewHeight)
//Remember to set the frame back to the screen size.
scrollView.contentSize = CGSizeMake(CGFloat(pagesToDisplay) * scrollViewWidth, scrollViewHeight)
pageControl.numberOfPages = pagesToDisplay
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageIndex = Int(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = pageIndex
}
func scrollViewDidScroll(scrollView: UIScrollView) {
let bgvMaxOffset = scrollView.frame.width - backgroundView.frame.width
let svMaxOffset = scrollView.frame.width * CGFloat(pagesToDisplay - 1)
if scrollView.contentOffset.x >= 0 &&
scrollView.contentOffset.x <= svMaxOffset {
backgroundView.frame.origin.x = scrollView.contentOffset.x * (bgvMaxOffset / svMaxOffset)
}
}
}
Feel free to try this on your device, and it will be really appreciated if someone could give some advice on this. Thank you.
I have an implementation of the Sliding Fragments DevByte. In addition to sliding the fragment into view, I want to draw a shadow over the content that it's occluding. I have modified the FractionalLinearLayout from the video to measure itself at twice the screen width and layout its children in the right half. During the animation loop, I draw a black rectangle of increasing alpha in the left half and set my X-coordinate to a negative value to bring the right half into view.
My problem is that this works fine when I'm sliding the content into view, but fails when I'm sliding the content back out of view. On the way in, I get exactly the behaviour I want: a translation and a darkening shadow. On the way out, I get only the translation, and the shadow remains just its first frame color all the way through the animation.
What I can see in my logging is that on the way in, the setPercentOnScreen() and onDraw() methods are called alternatingly, just as expected. On the way out however, I get one call to setPercentageOnScreen(), followed by one call to onDraw(), followed by only calls to setPercentOnScreen(). Android is optimizing away the drawing, but I can't figure out why.
updated: What's interesting is that I only see this behaviour on an emulator running Android 4.4. Emulators running 4.0.3 and 4.3 run the animation as intended in both directions. An old Nexus 7 exhibits the problem, a different emulator 4.4 does not. It appears to be consistent on a device, but vary between devices.
updated again: I've extracted a sample project and put it on GitHub: barend/android-slidingfragment. The readme file on GitHub contains test results on a dozen devices. For emulators, the problem correlates to the "Enable Host GPU" feature, but only on Jelly Bean and KitKat; not on ICS.
updated yet again: further testing shows that the problem occurs on physical devices running Jelly Bean and higher, as well as on ARM emulators with "Use Host GPU" enabled running Jelly Bean or higher. It does not occur on x86 emulators and on ARM emulators without "Use Host GPU", regardless of Android version. The exact table of my tests can be found in the github project linked above.
// Imports left out
public class HorizontalSlidingLayout extends FrameLayout {
/**
* The fraction by which the content has slid into view. Legal range: from 0.0 (all content
* off-screen) to 1.0 (all content visible).
*/
private float percentOnScreen;
private int screenWidth, screenHeight, shift;
private Paint shadowPaint;
// Constructors left out, all three call super, then init().
private void init() {
if (isInEditMode()) {
// Ensure content is visible in edit mode.
percentOnScreen = 1.0f;
} else {
setWillNotDraw(false);
percentOnScreen = 0.0f;
shadowPaint = new Paint();
shadowPaint.setAlpha(0x00);
shadowPaint.setColor(0x000000);
shadowPaint.setStyle(Paint.Style.FILL);
}
}
/** Reports our own size as (2w, h) and measures all children at (w, h). */
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
screenWidth = MeasureSpec.getSize(widthMeasureSpec);
screenHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(2 * screenWidth, screenHeight);
for (int i = 0, max = getChildCount(); i < max; i++) {
View child = getChildAt(i);
child.measure(widthMeasureSpec, heightMeasureSpec);
}
}
/** Lays out the children in the right half of the view. */
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
for (int i = 0, max = getChildCount(); i < max; i++) {
View child = getChildAt(i);
child.layout(screenWidth, top, right, bottom);
}
}
/**
* Draws a translucent shadow in the left half of the view, darkening by
* {#code percentOnScreen}, then lets superclass draw children in the right half.
*/
#Override
protected void onDraw(Canvas canvas) {
// Maintain 30% translucency
if (percentOnScreen < 0.7f) {
shadowPaint.setAlpha((int) (percentOnScreen * 0xFF));
}
android.util.Log.i("Slider", "onDraw(" + percentOnScreen + ") -> alpha(" + shadowPaint.getAlpha() + ')');
canvas.drawRect(shift, 0, screenWidth, screenHeight, shadowPaint);
super.onDraw(canvas);
}
#SuppressWarnings("unused")
public float getPercentOnScreen() {
return percentOnScreen;
}
/** Repeatedly invoked by an Animator. */
#SuppressWarnings("unused")
public void setPercentOnScreen(float fraction) {
this.percentOnScreen = fraction;
shift = (int)(fraction < 1.0 ? fraction * screenWidth : screenWidth);
setX(-shift);
android.util.Log.i("Slider", "setPOS(" + fraction + ") -> invalidate(" + shift + ',' + screenWidth + ')');
invalidate(shift, 0, screenWidth, screenHeight);
//invalidate() // Makes no difference
}
}
What's weird is that this violates symmetry. I'm doing the exact same thing in reverse when sliding out as I do when sliding in, but the behaviour is different. I'm probably overlooking something stupid. Any ideas?
It's a bug in the invalidation/redrawing logic for hardware-accelerated views in Android. I would expect to see the same bug in JB by default, but only on ICS if you opt into hardware acceleration (hw accel is enabled by default as of JB).
The problem is that when views are removed from the hierarchy and then animated out (such as what happens in the fragment transaction in your app, or in LayoutTransition when a removed view is faded out, or in an AlphaAnimation that fades out a removed view), they do not participate in the same invalidation/redrawing logic that normal/parented child views do. They are redisplayed correctly (thus we see the fragment slide out), but they are not redrawn, so that if their contents actually change during this period, they will not be redisplayed with those changes. The effect of the bug in your app is that the fragment slides out correctly, but the shadow is not drawn because that requires the view to be redrawn to pick up those changes.
The way that you are invalidating the view is correct, but the bug means that the invalidation has no effect.
The bug hasn't appeared before because, I believe, it's not common for disappearing views to change their appearance as they are being animated out (they usually just slide or fade out, which works fine).
The bug should be fixed in a future release (I have a fix for it already). Meanwhile, a workaround for your particular situation is to add an invalidation of the parent container as well; this will force the view to be redrawn and to correctly display the shadow:
if (getParent() instanceof ViewGroup) {
((ViewGroup) getParent()).invalidate();
}
I would like to know if there is some sort of way using maybe views or something to have a background service detect if the foreground app is in full screen or not. meaning the status bar is hidden or not hidden.
I was thinking maybe using constant strings to detect if a view is shown or not? But im not sure exactly. Root is an option if needed.
Thanks guys!!
How about using a broadcast receiver or something like that. That would be ideal, however there is no such broadcast for a full screen request.
Here is what I did do however.
I created an invisible overlay, this invisible overlay is 0 x 0 in size :)
I then use View.getLocationOnScreen(int[]);
This returns the int array with the values of the coordinates in x and y. I then test these coordinates (only really focusing on the y value) if it equals 0, then the current viewable activity is in full screen, (because it is at the highest most area on the screen) if the status bar is showing, then the view will return with (50 pixels on my device) meaning the view is 50 pixels from the top of the screens edge.
I place all this in a service which has a timer and when the timer expires, it gets the location, runs the tests, and does what I need to do. The timer is then cancelled when the screen shuts off. Upon screen on, timer is restarted.
I checked how much CPU it uses, and it says 0.0% in System Tuner.
I like the idea of Seth, so I made the code snippet:
/**
* Check if fullscreen is activated by a position of a top left View
* #param topLeftView View which position will be compared with 0,0
* #return
*/
public static boolean isFullscreen(View topLeftView) {
int location[] = new int[2];
topLeftView.getLocationOnScreen(location);
return location[0] == 0 && location[1] == 0;
}
Have you tried getWindow().getAttributes().flags?
public static boolean fullScreen = (getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0;
public static boolean forceNotFullScreen = (getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) != 0;
public static boolean actionbarVisible = getActionBar().isShowing();
Reference:
if( MyActivity.fullScreen){
// full screen
}
else{
// not full screen
}
I'm a little bit stuck with this one - first and foremost, the following link has been useful however I've come up with a bit of an issue with visibility:
The link: Check view visibility
I have a scroll view (parent) and a number of sub-views (LinearLayout -> TableLayout) etc. There are a number of items I set to View.GONE within the XML (android:visibility="gone").
I have some simple code to determine whether it is visible or not using getVisibility() however when I set the item to View.VISIBLE and try to immediately getDrawingRect() I get a Rect with zeros across the board. Any further click gets the correct coordinates.
Now this could be because the view has never been drawn (as defined in the XML) causing it to return no coordinates however I do set View.VISIBLE before trying to determine screen visibility. Could it be that I need to get some kind of callback from say the onDraw()? or perhaps set the view visibility of hidden items within code. A bit annoying ;(
Some code:
Rect scrollBounds = new Rect();
scroll.getHitRect(scrollBounds);
Rect viewBounds = new Rect();
if (view.getVisibility() == View.GONE) {
view.setVisibility(View.VISBLE)
viewBounds.getDrawingRect(viewBounds);
if (!Rect.intersects(scrollBounds, viewBounds) {
// do somthing
}
}
Layouts area as follows:
ScrollView
LinearLayout
TableLayout
Button
HiddenView
Of course, it's highly likely I'm going about this the wrong way altogether - basically I just want to make sure that the scrollview positions itself so the view that has become visible can be seen in it's entirety.
If any other information is required, let me know!
Ok so thanks to OceanLife for pointing me in the right direction! There was indeed a callback required and ViewTreeObserver.OnGlobalLayoutListener() did the trick. I ended up implementing the listener against my fragment class and picked it up where I needed it. Thanks for the warning too regarding the multiple calls, I resolved this using the removeOnGlobalLayoutListener() method - works a charm.
Code:
...
// vto initialised in my onCreateView() method
vto = getView().getViewTreeObserver();
vto.addOnGlobalLayoutListener(this);
...
#Override
public void onGlobalLayout() {
final int i[] = new int[2];
final Rect scrollBounds = new Rect();
sView.getHitRect(scrollBounds);
tempView.getLocationOnScreen(i);
if (i[1] >= scrollBounds.bottom) {
sView.post(new Runnable() {
#Override
public void run() {
sView.smoothScrollTo(0, sView.getScrollY() + (i[1] - scrollBounds.bottom));
}
});
}
vto.removeOnGlobalLayoutListener(this);
}
Just got to do some cleaning up now ...
So, if I am reading this right the issue you are having is that you want to find out the dimensions of some view in your layout to find out whether you have an intersection with the parent ScrollView.
What I think you are seeing (as you alluded to) is the instruction to draw the view being dispatched, and then in some sort of race-condition, the renderer resolving the measurements for the layout and the actual render where view objects get real sizes. One way to find out what sort of dimensions a view has on screen is to use the layout tree-listener. We use this observer to resolve a screen's dimensions when leveraging the Google Charts API, which requires a pixel width and height to be defined... which of course on Android is probably the biggest problem facing developers. So observe (pun intended);
final ViewTreeObserver vto = chart.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
/**
* #see android.view.ViewTreeObserver.OnGlobalLayoutListener#onGlobalLayout()
*/
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
if (view.getVisibility() == View.GONE) {
view.setVisibility(View.VISBLE)
viewBounds.getDrawingRect(viewBounds);
if (!Rect.intersects(scrollBounds, viewBounds) {
// do something
}
}
}
});
Word of warning the onGlobalLayout will get called multiple times as the renderer closes in on the final solution. The last call is the one we take.
This will provide you with a mechanism for performing actions on a drawn view like getting a view component's width and height. Hope that helps you out.
Aside: The clever chaps over at Square have designed a FEST hamcrest library that will allow you to write a test for this alongside Robotium. Check it out.