0

I am trying to gain konwledges on View and custom views. I read the series of articles here, and there. Then I decided to take a look on how Google coded some Views.

I retrieved the code for LabelView and I created an app that consists on a RelativLayout in which I add dynamically a (modified) LabelView. I am trying to modify the position of a label overriding setX, setY. Instead of having x, y for the upper left side, I want to have x, y the center of the view. What I did does not work (on the figure below "3" and "30" should have their centers aligned on the same y)

CustomLabel views are not centered

I added some log and that where questions raised.

  • Why are measureHeight and measureWidth called several time?
  • Why are getWidht, getHeight, getRight and getBottom returning 0?

Below the log (I removed what was not relevant):

... I/MainActivity: customLabel.width = 0
... I/MainActivity: customLabel.height = 0
... I/MainActivity: customLabel.getRight = 0
... I/MainActivity: customLabel.getBottom = 0
... I/MainActivity: customLabel2.width = 0
... I/MainActivity: customLabel2.height = 0
... I/MainActivity: customLabel2.getRight = 0
... I/MainActivity: customLabel2.getBottom = 0
...
... I/CustomLabelView: (lbl2) measureWidth: result = 96
... I/CustomLabelView: (lbl2) measureHeight: result = 99
... I/CustomLabelView: (lbl1) measureWidth: result = 33
... I/CustomLabelView: (lbl1) measureHeight: result = 61
... I/CustomLabelView: (lbl2) measureWidth: result = 96
... I/CustomLabelView: (lbl2) measureHeight: result = 99
... I/CustomLabelView: (lbl1) measureWidth: result = 33
... I/CustomLabelView: (lbl1) measureHeight: result = 61
...
... I/CustomLabelView: (lbl2) measureWidth: result = 96
... I/CustomLabelView: (lbl2) measureHeight: result = 99
... I/CustomLabelView: (lbl1) measureWidth: result = 33
... I/CustomLabelView: (lbl1) measureHeight: result = 61
... I/CustomLabelView: (lbl2) measureWidth: result = 96
... I/CustomLabelView: (lbl2) measureHeight: result = 99
... I/CustomLabelView: (lbl1) measureWidth: result = 33
... I/CustomLabelView: (lbl1) measureHeight: result = 61
... I/CustomLabelView: (lbl2) measureWidth: result = 96
... I/CustomLabelView: (lbl2) measureHeight: result = 99
... I/CustomLabelView: (lbl1) measureWidth: result = 33
... I/CustomLabelView: (lbl1) measureHeight: result = 61
... I/CustomLabelView: (lbl2) measureWidth: result = 96
... I/CustomLabelView: (lbl2) measureHeight: result = 99
... I/CustomLabelView: (lbl1) measureWidth: result = 33
... I/CustomLabelView: (lbl1) measureHeight: result = 61

Below MainActivity

public class MainActivity extends AppCompatActivity {

    private final String TAG = getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RelativeLayout container = (RelativeLayout) findViewById(R.id.container);

        CustomLabelView customLabel = new CustomLabelView(this, "lbl1");
        customLabel.setText("3");
        customLabel.setX(150);
        customLabel.setY(200);
        container.addView(customLabel);

        Log.i(TAG, "customLabel.width = " + customLabel.getWidth());
        Log.i(TAG, "customLabel.height = " + customLabel.getHeight());
        Log.i(TAG, "customLabel.getRight = " + customLabel.getRight());
        Log.i(TAG, "customLabel.getBottom = " + customLabel.getBottom());

        CustomLabelView customLabel2 = new CustomLabelView(this, "lbl2");
        customLabel2.setText("66");
        customLabel2.setX(450);
        customLabel2.setY(200);
        customLabel2.setTextSize(80);
        container.addView(customLabel2);

        Log.i(TAG, "customLabel2.width = " + customLabel2.getWidth());
        Log.i(TAG, "customLabel2.height = " + customLabel2.getHeight());
        Log.i(TAG, "customLabel2.getRight = " + customLabel2.getRight());
        Log.i(TAG, "customLabel2.getBottom = " + customLabel2.getBottom());
    }
}

Below the LabelView

/*
 * based on android-21/legacy/ApiDemos/src/com/example/android/apis/view/LabelView.java
 */
public class CustomLabelView extends View {

    private final String TAG = getClass().getSimpleName();

    private Paint mTextPaint;
    private String mText;
    private int mAscent;

    //tag to identify instance in log
    private String mTag;

    /**
     * Constructor.  This version is only needed if you will be instantiating
     * the object manually (not from a layout XML file).
     *
     * @param context
     */
    public CustomLabelView(Context context, String tag) {
        super(context);
        initLabelView();
        mTag = tag;
    }


    private final void initLabelView() {
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        // Must manually scale the desired text size to match screen density
        mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
        mTextPaint.setColor(0xFF000000);
        setPadding(3, 3, 3, 3);
    }

    /**
     * Sets the text to display in this label
     *
     * @param text The text to display. This will be drawn as one line.
     */
    public void setText(String text) {
        mText = text;
        requestLayout();
        invalidate();
    }

    /**
     * Sets the text size for this label
     *
     * @param size Font size
     */
    public void setTextSize(int size) {
        // This text size has been pre-scaled by the getDimensionPixelOffset method
        mTextPaint.setTextSize(size);
        requestLayout();
        invalidate();
    }

    /**
     * Sets the text color for this label.
     *
     * @param color ARGB value for the text
     */
    public void setTextColor(int color) {
        mTextPaint.setColor(color);
        invalidate();
    }

    /**
     * @see android.view.View#measure(int, int)
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * Determines the width of this view
     *
     * @param measureSpec A measureSpec packed into an int
     * @return The width of the view, honoring constraints from measureSpec
     */
    private int measureWidth(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text
            result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
                    + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        Log.i(TAG, "(" + mTag + ") measureWidth: result = " + result);

        return result;
    }

    /**
     * Determines the height of this view
     *
     * @param measureSpec A measureSpec packed into an int
     * @return The height of the view, honoring constraints from measureSpec
     */
    private int measureHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        mAscent = (int) mTextPaint.ascent();
        if (specMode == MeasureSpec.EXACTLY) {
            // We were told how big to be
            result = specSize;
        } else {
            // Measure the text (beware: ascent is a negative number)
            result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
                    + getPaddingBottom();
            if (specMode == MeasureSpec.AT_MOST) {
                // Respect AT_MOST value if that was what is called for by measureSpec
                result = Math.min(result, specSize);
            }
        }

        Log.i(TAG, "(" + mTag + ") measureHeight: result = " + result);

        return result;
    }

    /**
     * Render the text
     *
     * @see android.view.View#onDraw(android.graphics.Canvas)
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText(mText, getPaddingLeft(), getPaddingTop() - mAscent, mTextPaint);
    }


    @Override
    public void setX(float x) {
        float x_center = x + getWidth() / 2;
        super.setX(x_center);

        super.setX(x_center);
    }

    @Override
    public void setY(float y) {
        float y_center = y - getHeight() / 2;
        super.setY(y_center);
    }
}
eqtèöck
  • 971
  • 1
  • 13
  • 27

1 Answers1

1

Why are measureHeight and measureWidth called several time?

measureHeight and measureWidth calls are a result of onMeasure callback.

This one is called "to determine the size requirements for this view and all of its children" (read more here). Which basically means that when a View or a Layout that this View is placed in, needs to know what size requirements this View has, it causes onMeasure to be called.

It's important that it causes onMeasure to be called. It doesn't call it directly. You should never call onMeasure yourself. The easiest way to explain how can you cause it to be called is this View lifecycle diagram. A call to requestLayout on a View will cause it onMeasure to be called. See that your CustomLabelView also calls its requestLayout in some methods.

There's no really a way to determine the exact number of the onMeasure calls, because it can vary depending on the type of Layout you're using, your View behavior and other Views' in the hierarchy.


Why are getWidth, getHeight, getRight and getBottom returning 0?

You're trying to call them in onCreate of your Activity. That means that your Activity is just being created and there's nothing that already has been drawn on screen. In other words a "layout pass" has not yet been performed. Which basically means that your CustomLabelView has not yet been drawn. If it hasn't been drawn it has no dimensions (thats why all parameters are 0).

If you tried putting the following code in your onCreate you would probably get non-0 values (after 2 seconds):

final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        Log.i(TAG, "customLabel.width = " + customLabel.getWidth());
        Log.i(TAG, "customLabel.height = " + customLabel.getHeight());
        Log.i(TAG, "customLabel.getRight = " + customLabel.getRight());
        Log.i(TAG, "customLabel.getBottom = " + customLabel.getBottom());
    }

}, 2000);

Remember to mark your customLabel as final.

Community
  • 1
  • 1
Bartek Lipinski
  • 30,698
  • 10
  • 94
  • 132
  • Thanks @Bartek! Your answer to the first question is confirmed [here](https://www.youtube.com/watch?v=NYtB6mlu7vA) at 12'50, it is said "Viewgroups will call Measure on eacf one of thier child views" – eqtèöck Mar 05 '16 at 18:13