I'm trying to build an Android custom view that consists of multiple FloatingActionButton
(namespace android.support.design.widget
) instances that are arranged in a circle.
To do so I create a new view that inherits from ViewGroup
. The code looks as follows:
package myapp;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ImageView;
import myapp.R;
public class ButtonOverlayView extends ViewGroup
{
private final float _radius = 200.0f;
private int _desiredSize;
public ButtonOverlayView(Context context)
{
super(context);
initializeViewGroup(context);
}
public ButtonOverlayView(Context context, AttributeSet attrs)
{
super(context, attrs);
initializeViewGroup(context);
}
public ButtonOverlayView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
initializeViewGroup(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
_desiredSize = 600;
measureChildren(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(_desiredSize, _desiredSize);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3)
{
layoutChildren();
}
private void initializeViewGroup(Context context)
{
createChildren(context, getIconIdentifiers(), getColorIdentifiers());
}
private void createChildren(Context context, int[] iconIdentifiers, int[] colorIdentifiers)
{
for(int i = 0; i < iconIdentifiers.length; i++)
{
final FloatingActionButton button = new FloatingActionButton(context);
button.setImageResource(iconIdentifiers[i]);
button.setSize(FloatingActionButton.SIZE_NORMAL);
button.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
button.setBackgroundTintList(ColorStateList.valueOf(colorIdentifiers[i]));
button.setClickable(true);
addView(button);
}
}
private void layoutChildren()
{
int buttonCount = getChildCount();
int center = _desiredSize / 2;
float angle = 0.0f;
float angleIncrement = 360.0f / (buttonCount - 1);
FloatingActionButton button = (FloatingActionButton)getChildAt(0);
int halfWidth = button.getMeasuredWidth() / 2;
int halfHeight = button.getMeasuredHeight() / 2;
button.layout(center - halfWidth, center - halfHeight, center + halfWidth, center + halfHeight);
for(int i = 1; i < buttonCount; i++)
{
button = (FloatingActionButton)getChildAt(i);
halfWidth = button.getMeasuredWidth() / 2;
halfHeight = button.getMeasuredHeight() / 2;
double radians = Math.toRadians(angle);
int x = (int)(Math.cos(radians) * _radius) + center;
int y = (int)(Math.sin(radians) * _radius) + center;
button.layout(x - halfWidth, y - halfHeight, x + halfWidth, y + halfHeight);
angle += angleIncrement;
}
}
private int[] getIconIdentifiers()
{
final TypedArray icons = getResources().obtainTypedArray(R.array.icons);
int[] iconIdentifiers = new int[icons.length()];
try
{
for(int i = 0; i < icons.length(); i++)
{
iconIdentifiers[i] = icons.getResourceId(i, -1);
}
}
finally
{
icons.recycle();
}
return iconIdentifiers;
}
private int[] getColorIdentifiers()
{
final TypedArray colors = getResources().obtainTypedArray(R.array.colors);
int[] colorIdentifiers = new int[colors.length()];
try
{
for(int i = 0; i < colors.length(); i++)
{
colorIdentifiers[i] = colors.getResourceId(i, -1);
}
}
finally
{
colors.recycle();
}
return colorIdentifiers;
}
}
Icons and colors for the FloatingActionButton
are provided in a dedicated xml file with the following content:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<array name="icons">
<item>@android:drawable/ic_delete</item>
<item>@android:drawable/ic_input_add</item>
<item>@android:drawable/ic_menu_call</item>
<item>@android:drawable/ic_delete</item>
<item>@android:drawable/ic_input_add</item>
<item>@android:drawable/ic_menu_call</item>
<item>@android:drawable/ic_delete</item>
</array>
<array name="colors">
<item>@color/colorPrimary</item>
<item>@color/colorPrimary</item>
<item>@color/colorPrimary</item>
<item>@color/colorPrimary</item>
<item>@color/colorPrimary</item>
<item>@color/colorPrimary</item>
<item>@color/colorAccent2</item>
</array>
</resources>
Technically it all works, i.e. it compiles and shows up when integrated in the Android app. However, when rendered the FloatingActionButton
instances look "strange".
The following screenshots illustrate what I mean with "strange":
Phone 1 (Android 8.1):
The left screenshot shows all buttons in the unclicked state while on the right screenshot the lower right button is clicked.
Phone 2 (Android 9):
Same as above, left is the unclicked state, right shows the lower right button clicked.
Does anyone have an explanation why those buttons look "strange"? And how would I correct that issue?
Edit
Inspired by this SO question I took a closer look at the dependencies that I'm using which are as follows:
com.android.support:appcompat-v7:25.3.1
com.android.support:support-v4:25.3.1
com.android.support:support-annotations:+
com.android.support:design:25.3.1
com.android.support:support-vector-drawable:25.3.1
Because it could potentially be a bug in those libraries I upgraded them to 28.0.0
but the optical result is still the same as in the screenshots above.
Alright, I found the issue. What isn't quite obvious in the screenshots is that the buttons are actually transparent. This is happening because getColorIdentifiers
does not properly retrieve the colors from the xml file.
As a result, the method setBackgroundTintList
causes the buttons to become transparent. The solution is to correct the method as follows:
private int[] getColorIdentifiers()
{
return getContext().getResources().getIntArray(R.array.colors);
}