Search code examples
androidondrawonmeasure

Custom Button background is being drawn wrong (sometimes)


I have this custom button, which does not draw himself and has no child views. Right after app launch it looks like this:

enter image description here

At this point I don't know what code and details might be relevant for posting here. The fact is after the app changes state the button checks whether to stay VISIBLE or go INVISIBLE. It remains VISIBLE. It calls setVisibility(View.VISIBLE) and after that, when the screen shows again, it looks like this:

enter image description here

If I click the button it returns to be fine with its original background dimensions.

What have I done so far
I have debugged the code down to android sources.
First onDraw(); I only call there super.onDraw(); and it seems to deal only with the text and not the background, and if so, it works OK, since the text is still positioned and dimensioned as before.
Second onMeasure(); here too I only call super.onMeasure();; it is called several (11) times before showing for the first time and it is called 5 times after setVisibility(); it is not called at all when I click the button.
Third onTouchEvent(), called when I click the button. It sets a different color background for ACTION_DOWN and restores the original color background on ACTION_UP

@Override
protected void onDraw(Canvas canvas) {
    Log.d(TAG + " " + getText(), "+ onDraw(canvas:" + canvas + ")");
    super.onDraw(canvas);
    Log.d(TAG + " " + getText(), "- onDraw()");
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    Log.d(TAG + " " + getText(), String.format("+ onMeasure(widthMeasureSpec:%x, heightMeasureSpec:%x)", widthMeasureSpec,heightMeasureSpec));
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    Log.d(TAG + " " + getText(), String.format("- onMeasure(): width=%d, hieght=%d", getMeasuredWidth(), getMeasuredHeight()));
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.d(TAG + " " + getText(), "+ onTouchEvent(event:" + event + ")");
    super.onTouchEvent(event);

    if( clickable ) {
        if( event.getAction() == MotionEvent.ACTION_UP ) {
            setBackgroundDrawable(normalBackground);
            clickUp.soundPlay();
        } else if( event.getAction() == MotionEvent.ACTION_DOWN ) {
            setBackgroundDrawable(pressedBackground);
            clickDown.soundPlay();
        }
    }

    Log.d(TAG + " " + getText(), "- onTouchEvent()");
    return true;
}

/**
 * Sets the MyButton visible if stateFlags matches.<br>
 * @param stateFlags The current app state.<br> 
 */
public void setState(int stateFlags) {
    Log.d(TAG + " " + getText(), "+ setState(stateFlags:" + stateFlags + ")");
    if( state == stateFlags || state == State.NORMAL) {
        setVisibility(View.VISIBLE);
        Log.d(TAG + " " + getText(), "state(" + state + ") VISIBLE before was " + getVisibility());
    } else {
        setVisibility(View.INVISIBLE);
        Log.d(TAG + " " + getText(), "state(" + state + ") INVISIBLE before was " + getVisibility());
    }
    requestLayout();
    Log.d(TAG + " " + getText(), "- setState()");
}

@Override
protected int[] onCreateDrawableState(int extraSpace) {
    Log.d(TAG + " " + getText(), "+ onCreateDrawableState(extraSpace:" + extraSpace + ")");
    Log.d(TAG + " " + getText(), "- onCreateDrawableState()");
    return super.onCreateDrawableState(extraSpace);
}

these are the logs for the button:

*** Beggining - first show ***
+ onMeasure(widthMeasureSpec:800002b8, heightMeasureSpec:800003aa)
- onMeasure(): width=200, hieght=125
+ onMeasure(widthMeasureSpec:40000099, heightMeasureSpec:800003aa)
- onMeasure(): width=153, hieght=125
+ onMeasure(widthMeasureSpec:800002b8, heightMeasureSpec:80000152)
- onMeasure(): width=200, hieght=125
+ onMeasure(widthMeasureSpec:40000099, heightMeasureSpec:80000152)
- onMeasure(): width=153, hieght=125
+ onMeasure(widthMeasureSpec:800002b8, heightMeasureSpec:80000035)
- onMeasure(): width=200, hieght=53
+ onMeasure(widthMeasureSpec:40000099, heightMeasureSpec:80000035)
- onMeasure(): width=153, hieght=53
+ onSizeChanged(w:153, h:53, oldw:0, oldh:0)
- onSizeChanged()
+ onLayout(changed:true, left:12, top:3, right:165, bottom:56)
- onLayout()
+ onMeasure(widthMeasureSpec:800002b8, heightMeasureSpec:80000035)
- onMeasure(): width=200, hieght=53
+ onMeasure(widthMeasureSpec:40000099, heightMeasureSpec:80000035)
- onMeasure(): width=153, hieght=53
+ onLayout(changed:false, left:12, top:3, right:165, bottom:56)
- onLayout()
+ onDraw(canvas:android.view.Surface$CompatibleCanvas@b3e434b0)
+ onCreateDrawableState(extraSpace:0)
- onCreateDrawableState()
- onDraw()

*** App changes state - button shows wrong ****
+ setState(stateFlags:2)
state(1) VISIBLE before was 0
- setState()
+ onMeasure(widthMeasureSpec:800002b8, heightMeasureSpec:80000152)
- onMeasure(): width=200, hieght=125
+ onMeasure(widthMeasureSpec:800002b8, heightMeasureSpec:80000035)
- onMeasure(): width=200, hieght=53
+ onMeasure(widthMeasureSpec:40000099, heightMeasureSpec:80000035)
- onMeasure(): width=153, hieght=53
+ onLayout(changed:false, left:12, top:3, right:165, bottom:56)
- onLayout()
+ onDraw(canvas:android.view.Surface$CompatibleCanvas@b3e434b0)
- onDraw()

*** I am about to click the button, return to show fine ***
+ onTouchEvent(event:MotionEvent { action=ACTION_DOWN, id[0]=0, x[0]=61.0, y[0]=36.0, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=2879242, downTime=2879242, deviceId=0, source=0x1002 })
- onTouchEvent()
+ onDraw(canvas:android.view.Surface$CompatibleCanvas@b3e434b0)
+ onCreateDrawableState(extraSpace:0)
- onCreateDrawableState()
- onDraw()
+ onTouchEvent(event:MotionEvent { action=ACTION_UP, id[0]=0, x[0]=61.0, y[0]=36.0, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=2879342, downTime=2879242, deviceId=0, source=0x1002 })
- onTouchEvent()
+ performClick()
- performClick()
+ onDraw(canvas:android.view.Surface$CompatibleCanvas@b3e434b0)
+ onCreateDrawableState(extraSpace:0)
- onCreateDrawableState()
- onDraw()

More details will be posted on demand, if deemed relevant.


Solution

  • I found a bypass!, though I would like very much to understand what is really the problem.

    The bypass is:

    @Override
    protected void onDraw(Canvas canvas) {
        // This is a bypass for the problem of partial background redraw.
        // The problem causes are not understood yet.
        // But setting mBackgroundSizeChanged = true; in View causes the next Draw to be OK
        onScrollChanged(0, 0, 0, 0);
        super.onDraw(canvas);
    }
    

    The ultimate bypass line is (View.class):

    mBackgroundSizeChanged = true;
    

    But mBackgroundSizeChanged is not accessible from derived classes, and it has no setter per se. So I found the closest thing to a setter there is: onScrollChanged(); it does set mBackgroundSizeChanged = true and in my case is all it does. Check the few source code lines in TextView.class and View.class to see if in your case it does other stuff.

    Old Bypass (still works but executes more unecessary lines):

    Right after I change the visibility of a button I added these lines:

    Drawable d = cb.getBackground();
    cb.setBackgroundDrawable(null);
    cb.setBackgroundDrawable(d);
    

    And it solved the problem, now the button retains its background.

    Explanation how I got there
    This forces the button background to reset itself. I did look at the source of setBackgroundDrawable() trying to understand what code in there is making the difference. I first noticed that had to set the background to null first and reset it afterwards to force a redo.
    Further debug reduced the difference to:

    mBackgroundSizeChanged = true; 
    

    In the lasts code lines of setBackgroundDrawable()