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();
}
Related
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
I'm reading through the implementation of RippleDrawable and RippleForeground (the software rendering part), and I already knew that being bounded means that the ripple has a mask with it.
But I'm still confused on some points of the implementation:
Why did the implementation say "Bounded ripples don't have enter animations" and simply skip the enter animation for it? How can the ripple animation be started in this case (if user did not release his touch so no exit is fired)?
#Override
protected Animator createSoftwareEnter(boolean fast) {
// Bounded ripples don't have enter animations.
if (mIsBounded) {
return null;
}
...
}
Why did the implementation pick a nearly constant value (and why is that random()) for mBoundedRadius and mTargetRadius? What if the view masked with ColorDrawable is larger than that size, will it work correctly?
public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
boolean isBounded) {
...
if (isBounded) {
mBoundedRadius = MAX_BOUNDED_RADIUS * 0.9f
+ (float) (MAX_BOUNDED_RADIUS * Math.random() * 0.1);
}
...
}
...
private void computeBoundedTargetValues() {
...
mTargetRadius = mBoundedRadius;
}
For the first question, I've found the answer myself by digging into commit history and trying the new Marshmallow image. The answer is simple:
They removed the (foreground) ripple on touch for bounded RippleDrawable, but not for unbounded, leaving this inconsistency deliberately.
I just tested on the Marshmallow image from Android SDK. It is removed, and even worse, they left the exiting ripple in the place user first touched the screen instead of where their finger lifted from the screen.
I cannot understand this design decision since it seems like a regression much more than an improvement to me, but as in the commit log, they believe they did implement bounded ripple animation, instead of removing it.
But for the second question, I still haven't got an answer yet.
So I'm trying to understand how I can properly use hardware acceleration (when available) in a custom View that is persistently animating. This is the basic premise of my onDraw():
canvas.drawColor(mBackgroundColor);
for (Layer layer : mLayers) {
canvas.save();
canvas.translate(layer.x, layer.y);
//Draw that number of images in a grid, offset by -1
for (int i = -1; i < layer.xCount - 1; i++) {
for (int j = -1; j < layer.yCount - 1; j++) {
canvas.drawBitmap(layer.bitmap, layer.w * i, layer.h * j, null);
}
}
//If the layer's x has moved past its width, reset back to a seamless position
layer.x += ((difference * layer.xSpeed) / 1000f);
float xOverlap = layer.x % layer.w;
if (xOverlap > 0) {
layer.x = xOverlap;
}
//If the layer's y has moved past its height, reset back to a seamless position
layer.y += ((difference * layer.ySpeed) / 1000f);
float yOverlap = layer.y % layer.h;
if (yOverlap > 0) {
layer.y = yOverlap;
}
canvas.restore();
}
//Redraw the view
ViewCompat.postInvalidateOnAnimation(this);
I'm enabling hardware layers in onAttachedToWindow() and disabling them in onDetachedFromWindow(), but I'm trying to understand whether or not I'm actually using it. Essentially, the i/j loop that calls drawBitmap() never changes; the only thing that changes is the Canvas translation. Is the Bitmap automatically saved to the GPU as a texture behind the scenes, or is there something I need to do manually to do so?
On what view(s) are you setting View.LAYER_TYPE_HARDWARE exactly? If you are setting a hardware layer on the view that contains the drawing code shown above, you are causing the system to do a lot more work than necessary. Since you are only drawing bitmaps you don't need to do anything here. If you call Canvas.drawBitmap() the framework will cache the resulting OpenGL texture on your behalf.
You could however optimize your code a little more. Instead of calling drawBitmap(), you could use child views. If you move these children using the offset*() methods (or setX()/setY()) the framework will apply further optimizations to avoid calling the draw() methods again.
In general, hardware layers should be set on views that are expensive to draw and whose content won't change often (so pretty much the opposite of what you're doing :)
You can use Android's Tracer for OpenGL ES to see if your view issue OpenGL commands.
From developer.android.com
Tracer is a tool for analyzing OpenGL for Embedded Systems (ES) code in your Android application. The tool allows you to capture OpenGL ES commands and frame by frame images to help you understand how your graphics commands are being executed.
There is also a tutorial about Android Performance Study by Romain Guy which describes its use almost step by step.
I have a ViewPager which I need to move as a whole on button press. I use an animation for this.
When I press it, I translate the 'x' for it. I use setFillAfter(true) to keep the new position.
But when I change the page of the ViewPager, it jumps back to the original x-position!
I only saw this issue on Android 4.1, with Android 4.0 there is no problem! So it looks like some kind of regression in Android.
I attached a testproject where I could reproduce the issue without all my other stuff around it. I think it is best if you want to help me figure this out to import the project in your Eclipse and see it for yourself.
I also added to video's, one on my HTC One X where I see the issue, and the other on a tablet with Android 4.0, where the issue is not there.
I have been desperately looking to fix this ugly side effect, but no luck till now...
(Sorry for the big movie files...)
Video of Android 4.0 without the side effect
Video Android 4.1 with the side effect
the project where you can reproduce the issue with
Edit:
I added the following:
#Override
public void onAnimationEnd(Animation animation) {
RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
if (!i)
lp.setMargins(300,0,0,0);
else
lp.setMargins(0,0,0,0);
myViewPager.setLayoutParams(lp);
}
After that it stays at the correct position, but it 'flickers' quickly, like the animation is still showing at the end and when I change the margin, it still shows the offset it had after animation. Then it jumps to the correct position.
The main problem seems to be incorrect choice of animation type. You see, View Animation as a tool is not intended to be used with complex interactive objects like ViewPager. It offers only low-cost animation of the drawing place of views. The visual behaivior of the animated ViewPager in response to user-actions is undefined and should not be relied on.
Ugly flicks, when you substitute a "gost" with the real object are only natural.
The mechanism, that is intended to use in your case since API 11 is specialized property animator built in Views for optimized performance: ViewPropertyAnimator, or not specialized, but more versatile ObjectAnimator and AnimatorSet.
Property animation makes the View to really change its place and function normally there.
To make project, to use, say, ViewPropertyAnimator, change your listener setting to this:
btn.setOnClickListener(new OnClickListener() {
boolean b = false;
#Override
public void onClick(View v) {
if(b) {
myViewPager.animate().translationX(0f).setDuration(700);
}
else {
myViewPager.animate().translationX(300f).setDuration(700);
}
b=!b;
}
});
If you want to use xml configuration only, stick to |ObjectAnimator and AnimatorSet. Read through the above link for further information.
In case, you are anxious to support pre-Honeycomb devices, you can use Jake Warton's NineOldAndroids project. Hope that helps.
That's because the Animation's setFillAfter(true) doesn't actually change the position or any attributes of the View; all it does is create a Bitmap of the view's drawing cache and leaves it where the animation ends. Once the screen is invalidated again (ie. changing the page in the ViewPager), the bitmap will be removed and it will appear as if the View is returning to it's original position, when in fact it was already there.
If you want the View to retain it's position after the animation has finished, you need to actually adjust the View's LayoutParams to match your desired effect. To achieve this, you can override the onAnimationEnd method of the Animation, and adjust the LayoutParams of the View inside there.
Once you adjust the LayoutParams, you can remove your call to setFillAfter(true) and your View will actually stay where you expect it to.
Regarding the flicker issue:
I have encountered this issue before, and it stems from the possibility of the onAnimationEnd() call not syncing up with the next layout pass. Animation works by applying a transformation to a View, drawing it relative to its current position.
However, it is possible for a View to be rendered after you have moved it in your onAnimationEnd() method. In this case, the Animation's transformation is still being applied correctly, but the Animation thinks the View has not changed its original position, which means it will be drawn relative to its ENDING position instead of its STARTING position.
My solution was to create a custom subclass of Animation and add a method, changeYOffset(int change), which modifies the y translation that is applied during the Animation's applyTransformation method. I call this new method in my View's onLayout() method, and pass the new y-offset.
Here is some of my code from my Animation, MenuAnimation:
/**
* Signal to this animation that a layout pass has caused the View on which this animation is
* running to have its "top" coordinate changed.
*
* #param change
* the difference in pixels
*/
public void changeYOffset(int change) {
fromY -= change;
toY -= change;
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float reverseTime = 1f - interpolatedTime;
float dy = (interpolatedTime * toY) + (reverseTime * fromY);
float alpha = (interpolatedTime * toAlpha) + (reverseTime * fromAlpha);
if (alpha > 1f) {
alpha = 1f;
}
else if (alpha < 0f) {
alpha = 0f;
}
t.setAlpha(alpha);
t.getMatrix().setTranslate(0f, dy);
}
And from the View class:
private int lastTop;
// ...
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// the animation is expecting that its View will not be moved by the container
// during its time period. if this does happen, we need to inform it of the change.
Animation anim = getAnimation();
if (anim != null && anim instanceof MenuAnimation) {
MenuAnimation animation = (MenuAnimation) anim;
animation.changeYOffset(top - lastTop);
}
// ...
lastTop = top;
super.onLayout(changed, left, top, right, bottom);
}
Crucero has it right about setFillAfter not adjusting params post invalidation. When the view is re-layed out (which'll happen the pass after it's invalidated), its layout params will be the ones that always applied, so it should go back to the original position.
And Jschools is right about onAnimationEnd. Strongly encourage you to step through the source code with a debugger, where you'll instructively discover that an update is made that affects the drawn position of the view after onAnimationEnd is fired, at which point you've actually applied the layout params, hence the flicker caused by doubled up offset.
But this can be solved quite simply by making sure you relayout at the right time. You want to put your re-positioning logic at the end of the ui message queue at the time of animation end so that it is polled after the animation but before laying out. There's nowhere that suggests doing this, annoyingly, but I've yet find a reason in any release of the SDK reason why (when doing this just once and not incorrectly using ui thread) this shouldn't work.
Also clear the animation due to another issue we found on some older devices.
So, try:
#Override
public void onAnimationEnd(final Animation animation) {
myViewPager.post(new Runnable() {
#Override
public public void run() {
final RelativeLayout.LayoutParams lp = (android.widget.RelativeLayout.LayoutParams) myViewPager.getLayoutParams();
if (!someBooleanIPresume)
lp.setMargins(300,0,0,0);
else
lp.setMargins(0,0,0,0);
myViewPager.setLayoutParams(lp);
myViewPager.clearAnimation();
}
}
In the course of developing an Android application, I'm finding a need to draw
several unfilled concentric circles centered on an arbitrary point, enough that
some of them are only partly visible on the display. However, this does not
appear to work with hardware acceleration. My test rig is a stock Samsung Galaxy
Tab 10.1 running Android 3.2.
The following code comes from a test subclass of View I wrote to isolate the
issue:
private Paint paint = new Paint();
private int count = 0;
private static final int[] COLORS = { 0xffff0000, 0xff00ff00, 0xff0000ff, 0xffff00ff };
public TestCircles(Context context) {
super(context);
paint.setStrokeWidth(1.0f);
paint.setStyle(Paint.Style.STROKE);
}
public TestCircles(Context context, AttributeSet attributes) {
super(context, attributes);
paint.setStrokeWidth(1.0f);
paint.setStyle(Paint.Style.STROKE);
}
public boolean onTouchEvent(MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN)
invalidate();
return true;
}
protected void onDraw(Canvas canvas) {
// Pick the color to use, cycling through the colors list repeatedly, so that we can
// see the different redraws.
paint.setColor(COLORS[count++]);
count %= COLORS.length;
// Set up the parameters for the circles; they will be centered at the center of the
// canvas and have a maximum radius equal to the distance between a canvas corner
// point and its center.
final float x = canvas.getWidth() / 2f;
final float y = canvas.getHeight() / 2f;
final float maxRadius = (float) Math.sqrt((x * x) + (y * y));
// Paint the rings until the rings are too large to see.
for (float radius = 20; radius < maxRadius;
radius += 20)
canvas.drawCircle(x, y, radius, paint);
}
I am running TestCircles as the only View in an Activity, laying it out to fill
the available width and height (i.e. it is nearly full-screen). I can tap on
the display (triggering redraws) only a few times before the redraws no longer
occur (i.e. the circles' color doesn't change). Actually, the onDraw() code is
still running in response to each tap -- as proven with diagnostic messages --
but nothing changes onscreen.
When onDraw() first starts to fail to redraw, the debug log includes the
following entry, once for every call to onDraw():
E/OpenGLRenderer(21867): OpenGLRenderer is out of memory!
If I turn off hardware acceleration in the manifest, these problems go away --
not surprising since clearly OpenGL is having problems -- and actually it is
a good deal faster than the few times it actually works under hardware
acceleration.
My questions are:
Am I misusing Canvas, or is this a bug, or both? Is Android allocating large
bitmaps under the hood to draw these circles? It doesn't seem like this should be
this challenging to OpenGL, but I'm new to hardware accelerated app development.
What's a good alternative way to draw large unfilled circles that have portions
extending out of the clipping region of the Canvas? Losing hardware acceleration
is not an option.
Thanks in advance...
I've since learned from others that the problem I described here is the result of a bug in Android 3.2. The workaround for now is of course to use a software layer instead of hardware acceleration. Apparently this problem is fixed in Android 4.0 (Ice Cream Sandwich).