Search code examples
android-custom-viewandroid-relativelayoutandroid-viewgroup

Custom View in custom ViewGroup not displaying when offset in RelativeLayout


I have a custom view class (PinView) which is to be positioned within a custom ViewGroup (RightAngleTriangleView) based on some attributes. However, when it is placed relative to another view in a RelativeLayout, the PinView does not display. See code below:

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>

The code above is the XML for an Activity to highlight the problem. I have also taken a screenshot of the problem (below). The triangle in the top-most corner is fine and displays the inner PinView. However, the other two (below and at the right of the first one) do not display the PinView even though the code is exactly the same and I have confirmed by Log that the intended PinView positions seem ok.

Problem Screenshot

What am I doing wrong?


Solution

  • It turns out that I was using absolute values when calling View.layout(l,t,r,b) instead of relative ones. I now use the relative values and it works!