在 RelativeLayout 中偏移时自定义 ViewGroup 中的自定义视图不显示

Custom View in custom ViewGroup not displaying when offset in RelativeLayout

我有一个自定义视图 class (PinView),它将根据某些属性放置在自定义 ViewGroup (RightAngleTriangleView) 中。但是,当它相对于 RelativeLayout 中的另一个视图放置时,PinView 不显示。请参阅下面的代码:

RightAngleTriangleView

public class RightAngleTriangleView extends ViewGroup {
private static final String TAG = "RightAngleTriangleView";

private int pinOrientation;
private boolean isRightFilled, diagonalIsTopRightBottomLeft;
private Paint trianglePaint, pinPaint;

private Path trianglePath, pinPath;
private PointF triStart, triMiddle, triEnd;

private PinView pinView;
private float pinLengthDiff, pinThickness;

public RightAngleTriangleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    setWillNotDraw(false); // remove default (non) drawing behaviour for ViewGroup

    /*** extract XML attributes ***/
    TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.RightAngleTriangleView);
    int fillColour = a.getColor(R.styleable.RightAngleTriangleView_fillColour,
            context.getResources().getColor(android.R.color.darker_gray));
    int fillPosition = a.getInt(R.styleable.RightAngleTriangleView_fillPosition,
            context.getResources().getInteger(
                    R.integer.RightAngleTriangleView_fillPosition_left));
    int diagonal = a.getInt(R.styleable.RightAngleTriangleView_diagonal,
            context.getResources().getInteger(
                    R.integer.RightAngleTriangleView_diagonal_topLeftToBottomRight));

    isRightFilled = fillPosition == context.getResources().getInteger(
            R.integer.RightAngleTriangleView_fillPosition_right);

    diagonalIsTopRightBottomLeft = diagonal == getContext().getResources().getInteger(
            R.integer.RightAngleTriangleView_diagonal_topRightToBottomLeft);

    pinOrientation = a.getInt(R.styleable.RightAngleTriangleView_pinOrientation,
            context.getResources().getInteger(
                    R.integer.RightAngleTriangleView_pinOrientation_none));
    a.recycle();

    /*** setup drawing related variables ***/
    trianglePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    trianglePaint.setStyle(Paint.Style.FILL_AND_STROKE);
    trianglePaint.setColor(fillColour);

    trianglePath = new Path();
    trianglePath.setFillType(Path.FillType.EVEN_ODD);

    triStart = new PointF();
    triMiddle = new PointF();
    triEnd = new PointF();

    pinPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    pinPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    pinPaint.setColor(context.getResources().getColor(R.color.pin_color));

    pinPath = new Path();
    pinPath.setFillType(Path.FillType.EVEN_ODD);

    // create pinView (if present)
    if(pinOrientation != context.getResources().getInteger(
            R.integer.RightAngleTriangleView_pinOrientation_none)){
        pinView = new PinView(context, UiUtils.makeAttributeSet(context, getResourceId()));
        addView(pinView);
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    if(hasPin()){
        // measure child to obtain 'wrapped' valid dimension
        measureChild(pinView, widthMeasureSpec, heightMeasureSpec);
    }
}

@Override
protected void onLayout (boolean changed, int left, int top, int right, int bottom){
    Log.d(TAG, "onLayout() changed = " + changed);

    if(hasPin()){
        // use 'wrapped' valid dimension as pinThickness
        if(pinView.isHorizontal()) {
            pinThickness = pinView.getMeasuredHeight() / 2;
            pinLengthDiff = (pinThickness * getWidth()) / getHeight();
        }
        else{
            pinThickness = pinView.getMeasuredWidth() / 2;
            pinLengthDiff = (pinThickness * getHeight()) / getWidth();
        }
        placePinView(left, top, right, bottom);
    }
}

@Override
protected void onDraw(Canvas canvas){
    // draw pin 'edge' if applicable
    if(hasPin()){
        pinPath.reset(); // remove any previously drawn paths
        if(pinView.isHorizontal()){
            pinPath.addRect((getWidth() - pinLengthDiff) / 2, (getHeight() - pinThickness) / 2,
                    (getWidth() + pinLengthDiff) / 2, (getHeight() + pinThickness) / 2,
                    Path.Direction.CW);
        }
        else{
            pinPath.addRect((getWidth() - pinThickness) / 2, (getHeight() - pinLengthDiff) / 2,
                    (getWidth() + pinThickness) / 2, (getHeight() + pinLengthDiff) / 2,
                    Path.Direction.CW);
        }
        canvas.drawPath(pinPath, pinPaint);
    }

    // determine triangle vertices
    if(diagonalIsTopRightBottomLeft){
        // draw diagonal
        triStart.set(getWidth(), 0);
        triMiddle.set(0, getHeight());

        // determine triEnd based on fill position
        if(isRightFilled){
            triEnd.set(getWidth(), getHeight());
        }
        else{
            triEnd.set(0, 0);
        }
    }
    else{
        // draw diagonal
        triStart.set(0, 0);
        triMiddle.set(getWidth(), getHeight());

        // determine triEnd based on fill position
        if(isRightFilled){
            triEnd.set(getWidth(), 0);
        }
        else{
            triEnd.set(0, getHeight());
        }
    }

    // draw triangle
    trianglePath.reset(); // remove any previously drawn paths
    trianglePath.moveTo(triStart.x, triStart.y);
    trianglePath.lineTo(triMiddle.x, triMiddle.y);
    trianglePath.lineTo(triEnd.x, triEnd.y);
    trianglePath.close(); // automatically draw third side

    canvas.drawPath(trianglePath, trianglePaint);
}

public boolean hasPin(){
    return pinView != null;
}

private void placePinView(int left, int top, int right, int bottom){
    int l, t, r, b, pinPosition;

    int trbl = diagonalIsTopRightBottomLeft ? 1<<2 : 0;
    int rightFilled = isRightFilled ? 1<<1 : 0;
    int horizontal = pinView.isHorizontal() ? 1 : 0;
    int result = trbl + rightFilled + horizontal;
    // determine pin size and position
    switch (result){
        case 0: // diagonal = top-left to bottom-right, left-filled, pin vertical
            t = top;
            b = t + (int) (getHeight() - pinLengthDiff) / 2;
            l = left + (int) (getWidth() - pinThickness)/2;
            r = l + pinView.getMeasuredWidth();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_top);
            break;
        case 1: // diagonal = top-left to bottom-right, left-filled, pin horizontal
            l = left + (int) (getWidth() + pinLengthDiff)/2;
            r = right;
            b = top + (int) (getHeight() + pinThickness)/ 2;
            t = b - pinView.getMeasuredHeight();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_right);
            break;
        case 2: // diagonal = top-left to bottom-right, right-filled, pin vertical
            t = top + (int) (getHeight() + pinLengthDiff) / 2;
            b = bottom;
            r = left + (int) (getWidth() + pinThickness)/2;
            l = r - pinView.getMeasuredWidth();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_bottom);
            break;
        case 3: // diagonal = top-left to bottom-right, right-filled, pin horizontal
            l = left;
            t = top + (int) (getHeight() - pinThickness)/ 2;
            r = l + (int) (getWidth() - pinLengthDiff) / 2;
            b = t + pinView.getMeasuredHeight();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_left);
            break;
        case 4: // diagonal = top-right to bottom-left, left-filled, pin vertical
            t = top + (int) (getHeight() + pinLengthDiff) / 2;
            b = bottom;
            l = left + (int) (getWidth() - pinThickness)/2;
            r = l + pinView.getMeasuredWidth();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_bottom);
            break;
        case 5: // diagonal = top-right to bottom-left, left-filled, pin horizontal
            l = left + (int) (getWidth() + pinLengthDiff)/2;
            t = top + (int) (getHeight() - pinThickness)/ 2;
            r = right;
            b = t + pinView.getMeasuredHeight();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_right);
            break;
        case 6: // diagonal = top-right to bottom-left, right-filled, pin vertical
            t = top;
            b = t + (int) (getHeight() - pinLengthDiff) / 2;
            r = left + (int) (getWidth() + pinThickness)/2;
            l = r - pinView.getMeasuredWidth();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_top);
            break;
        case 7: // diagonal = top-right to bottom-left, right-filled, pin horizontal
            l = left;
            r = l + (int) (getWidth() - pinLengthDiff) / 2;
            b = top + (int) (getHeight() + pinThickness)/2;
            t = b - pinView.getMeasuredHeight();
            pinPosition = getContext().getResources().getInteger(
                    R.integer.PinView_position_left);
            break;
        default:
            l = left;
            t = top;
            r = right;
            b = bottom;
            pinPosition = -1;
            break;
    }

    // remeasure / resize pinView accounting for correct unspecified dimension
    measureChild(pinView, MeasureSpec.makeMeasureSpec(r - l, MeasureSpec.EXACTLY),
            MeasureSpec.makeMeasureSpec(b - t, MeasureSpec.EXACTLY));

    pinView.setPosition(pinPosition); // ensure that pinName is in correct position
    pinView.layout(l, t, r, b); // position pinView
}

private int getResourceId(){
    int trbl = diagonalIsTopRightBottomLeft ? 1<<2 : 0;
    int rightFilled = isRightFilled ? 1<<1 : 0;
    int horizontal = pinOrientation == getContext().getResources().getInteger(
            R.integer.RightAngleTriangleView_pinOrientation_horizontal) ? 1 : 0;

    int result = trbl + rightFilled + horizontal;
    Log.d(TAG, "getResourceId(): result = " + result);
    switch (result){
        case 0: // diagonal = top-left to bottom-right, left-filled, pin vertical
            return R.xml.pinview_vertical_namebelow;
        case 1: // diagonal = top-left to bottom-right, left-filled, pin horizontal
            return R.xml.pinview_horizontal;
        case 2: // diagonal = top-left to bottom-right, right-filled, pin vertical
            return R.xml.pinview_vertical;
        case 3: // diagonal = top-left to bottom-right, right-filled, pin horizontal
            return R.xml.pinview_horizontal_namebelow;
        case 4: // diagonal = top-right to bottom-left, left-filled, pin vertical
            return R.xml.pinview_vertical_namebelow;
        case 5: // diagonal = top-right to bottom-left, left-filled, pin horizontal
            return R.xml.pinview_horizontal_namebelow;
        case 6: // diagonal = top-right to bottom-left, right-filled, pin vertical
            return R.xml.pinview_vertical;
        case 7: // diagonal = top-right to bottom-left, right-filled, pin horizontal
            return R.xml.pinview_horizontal;
        default:
            return -1;
    }
}
}

PinView

public class PinView extends RelativeLayout {
private TextView pinNameView, signalTextView;

private boolean isHorizontal;

public PinView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    /*** extract XML attributes ***/
    TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.PinView);
    boolean nameBelow = a.getBoolean(R.styleable.PinView_nameBelow, false);
    int orientation = a.getInt(R.styleable.PinView_orientation, 0);
    int position = a.getInt(R.styleable.PinView_position, -1);
    isHorizontal = orientation == 0;
    a.recycle();

    /*** create children ***/
    LinearLayout backgroundLayout = new LinearLayout(context);
    backgroundLayout.setId(R.id.pinview_backgroundlayout);
    backgroundLayout.setBackgroundColor(context.getResources().getColor(R.color.pin_color));

    if(isHorizontal){
        pinNameView = new TextView(context);
        signalTextView = new TextView(context);
    }
    else{
        pinNameView = new VerticalTextView(context);
        signalTextView = new VerticalTextView(context);
    }

    pinNameView.setId(R.id.pinview_pinname);
    pinNameView.setTextColor(context.getResources().getColor(android.R.color.black));

    signalTextView.setId(R.id.pinview_signaltext);
    signalTextView.setTextColor(context.getResources().getColor(android.R.color.black));
    signalTextView.setBackgroundColor(context.getResources().getColor(R.color.pin_sig_color));

    /*** determine children layouts and positions ***/
    LayoutParams lpSigText = new LayoutParams(
            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    backgroundLayout.addView(signalTextView, lpSigText); // add signalView to backgroundLayout, NOT this view

    LayoutParams lpBackgroundLayout;
    if(isHorizontal){
        lpBackgroundLayout = new LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    else{
        lpBackgroundLayout = new LayoutParams(
                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    }

    LayoutParams lpPinName = new LayoutParams(
            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

    // place pin name accordingly
    if(nameBelow){
        addView(backgroundLayout, lpBackgroundLayout);

        if(isHorizontal){
            lpPinName.addRule(RelativeLayout.BELOW, backgroundLayout.getId());
        }
        else{
            lpPinName.addRule(RelativeLayout.RIGHT_OF, backgroundLayout.getId());
        }
        addView(pinNameView, lpPinName);
        setPosition(position);
    }
    else{
        addView(pinNameView, lpPinName);
        setPosition(position);

        if(isHorizontal){
            lpBackgroundLayout.addRule(RelativeLayout.BELOW, pinNameView.getId());
        }
        else{
            lpBackgroundLayout.addRule(RelativeLayout.RIGHT_OF, pinNameView.getId());
        }
        addView(backgroundLayout, lpBackgroundLayout);
    }
}

public void setPosition(int position){
    // align pin name according to pin position on device
    LayoutParams params = (RelativeLayout.LayoutParams) pinNameView.getLayoutParams();
    switch(position){ // pin's position relative to parent device
        case 2: // top
            params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
            break;
        case 3: // bottom
            params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
            break;
        case 0: // left
            params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
            break;
        case 1: // right
            params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
            break;
        default:
            break;
    }
}

public void setData(DevicePin pin){
    pinNameView.setText(pin.name);
    signalTextView.setText(pin.data);

    // always create new animation, otherwise it will need to be reset
    Animation anim = null;
    switch (pin.direction){
        case LEFT:
            anim = AnimationUtils.loadAnimation(getContext(), R.anim.pin_transition_left);
            break;
        case RIGHT:
            anim = AnimationUtils.loadAnimation(getContext(), R.anim.pin_transition_right);
            break;
        case UP:
            anim = AnimationUtils.loadAnimation(getContext(), R.anim.pin_transition_up);
            break;
        case DOWN:
            anim = AnimationUtils.loadAnimation(getContext(), R.anim.pin_transition_down);
            break;
    }

    if(pin.startBehaviour == DevicePin.AnimStartBehaviour.DELAY){
        if(pin.animationDelay == -1){
            anim.setStartOffset(anim.getDuration());
        }
        else{
            anim.setStartOffset(pin.animationDelay);
        }
    }

    if(pin.action == DevicePin.PinAction.STATIONARY){
        anim.setDuration(0);
    }

    if(pin.animListener != null){
        anim.setAnimationListener(pin.animListener);
    }

    if(anim != null){
        signalTextView.setAnimation(anim);
    }
}

public boolean isHorizontal(){
    return isHorizontal;
}
}

Activity XML

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

    <view
    class="org.ricts.abstractmachine.ui.device.RightAngleTriangleView"
    android:id="@+id/edge"
    android:layout_width="122px"
    android:layout_height="150px"
    custom:diagonal = "topLeftToBottomRight"
    custom:fillPosition = "right"
    custom:fillColour="@color/mux_fill_color"
    custom:pinOrientation = "vertical"
    />
<view
    class="org.ricts.abstractmachine.ui.device.RightAngleTriangleView"
    android:id="@+id/edge2"
    android:layout_width="122px"
    android:layout_height="150px"
    android:layout_toRightOf="@+id/edge"
    custom:diagonal = "topLeftToBottomRight"
    custom:fillPosition = "right"
    custom:fillColour="@color/mux_fill_color"
    custom:pinOrientation = "vertical"
    />
<view
    class="org.ricts.abstractmachine.ui.device.RightAngleTriangleView"
    android:id="@+id/edge3"
    android:layout_width="122px"
    android:layout_height="150px"
    android:layout_below="@+id/edge"
    custom:diagonal = "topLeftToBottomRight"
    custom:fillPosition = "right"
    custom:fillColour="@color/mux_fill_color"
    custom:pinOrientation = "vertical"
    />
<view
    class="org.ricts.abstractmachine.ui.device.PinView"
    android:id="@+id/pin"
    android:layout_width="58px"
    android:layout_height="64px"
    android:layout_below="@+id/edge"
    android:layout_toRightOf="@+id/edge3"
    custom:orientation="vertical"
    custom:position="top"
    />

</RelativeLayout>

上面的代码是 XML for an Activity 以突出问题。我还截取了问题的屏幕截图(如下)。最上角的三角形很好,显示了内部 PinView。然而,其他两个(在第一个下方和右侧)不显示 PinView,即使代码完全相同,我已经通过 Log 确认预期的 PinView 位置似乎没问题。

Problem Screenshot

我做错了什么?

事实证明,我在调用 View.layout(l,t,r,b) 时使用的是绝对值,而不是相对值。我现在使用相对值并且有效!