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.
What am I doing wrong?
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!