Android 视图内部 - 自定义视图定位

Android View internal - CustomView positionning

我正在尝试获得有关视图和自定义视图的知识。我阅读了系列文章here, and there。然后我决定看看 Google 如何编码一些视图。

我检索了 LabelView 的代码,并创建了一个包含 RelativLayout 的应用程序,我在其中动态添加了一个(修改后的)LabelView。我正在尝试修改覆盖 setXsetY 的标签的位置。而不是 x, y 作为左上角,我想要 x, y 视图的中心。我所做的不起作用(在下图中,“3”和“30”应该使它们的中心对齐在同一 y

我添加了一些日志以及提出问题的地方。

日志下方(我去掉了不相关的):

... 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

低于 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());
    }
}

在 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);
    }
}

Why are measureHeight and measureWidth called several time?

measureHeightmeasureWidth 调用是 onMeasure 回调的结果。

此视图称为“以确定此视图及其所有子视图的大小要求”(阅读更多内容 here)。这基本上意味着当放置此 ViewViewLayout 时,需要知道此 View 的大小要求,这会导致 onMeasure被调用。

重要的是 它会导致 onMeasure 被调用。它不会直接调用它。你永远不应该自己打电话给 onMeasure。解释如何调用它的最简单方法是 this View lifecycle diagram。在 View 上调用 requestLayout 将导致它被调用 onMeasure。看到你的 CustomLabelView 也在某些方法中调用了它的 requestLayout

没有真正的方法来确定 onMeasure 调用的确切数量,因为它会根据您使用的 Layout 类型、您的 View 行为而有所不同和层次结构中的其他 Views'


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

您正在尝试在 ActivityonCreate 中调用它们。这意味着您的 Activity 刚刚被创建,屏幕上还没有绘制任何内容。换句话说,"layout pass" 尚未执行。这基本上意味着您的 CustomLabelView 尚未绘制。如果尚未绘制,则它没有尺寸(这就是为什么所有参数均为 0)。

如果您尝试将以下代码放入 onCreate 中,您可能会得到非 0 值(2 秒后):

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);

记得将您的 customLabel 标记为 final