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?