Setting the discreet seekbar attributes to android:max="1" does not show the tickmark on both ends of the seekbar even though its seekable.
But setting android:max="2" shows the three tickmarks.
How to make the tickmark appear for max value 0 and 1?
If you look at the source code for AbsSeekBar, you'll find this method:
/**
* #hide
*/
protected void drawTickMarks(Canvas canvas) {
if (mTickMark != null) {
final int count = getMax() - getMin();
if (count > 1) {
final int w = mTickMark.getIntrinsicWidth();
final int h = mTickMark.getIntrinsicHeight();
final int halfW = w >= 0 ? w / 2 : 1;
final int halfH = h >= 0 ? h / 2 : 1;
mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count;
final int saveCount = canvas.save();
canvas.translate(mPaddingLeft, getHeight() / 2);
for (int i = 0; i <= count; i++) {
mTickMark.draw(canvas);
canvas.translate(spacing, 0);
}
canvas.restoreToCount(saveCount);
}
}
}
The important part is the line if (count > 1); for whatever reason, the Google team decided that tick marks shouldn't be shown if there were only two selectable values.
There's no great way to work around this.
You'd have to override this entire method, but it's annotated with #hide which means that the outside world shouldn't know about it. This won't actually stop you from doing it, but it means that you always have to worry about it breaking whenever Android is updated.
The other problem is that this method uses a lot of fields that you won't have access to. You can use getters to work around that but it's a little gnarly.
Put together, you can make this class:
public class MySeekBar extends SeekBar {
public MySeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
protected void drawTickMarks(Canvas canvas) {
if (getTickMark() != null) {
final int count = getMax() - getMin();
// this is the important part: you change this to 0 from 1
if (count > 0) {
final int w = getTickMark().getIntrinsicWidth();
final int h = getTickMark().getIntrinsicHeight();
final int halfW = w >= 0 ? w / 2 : 1;
final int halfH = h >= 0 ? h / 2 : 1;
getTickMark().setBounds(-halfW, -halfH, halfW, halfH);
final float spacing = (getWidth() - getPaddingLeft() - getPaddingRight()) / (float) count;
final int saveCount = canvas.save();
canvas.translate(getPaddingLeft(), getHeight() / 2);
for (int i = 0; i <= count; i++) {
getTickMark().draw(canvas);
canvas.translate(spacing, 0);
}
canvas.restoreToCount(saveCount);
}
}
}
}
I'm trying to build a custom ViewGroup base on DavidPizarro/AutoLabelUI
https://github.com/DavidPizarro/AutoLabelUI
My goal is to create a page that looks like this:
but this is the closest that I got:
This is the important part of my code:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
totalWidth = sizeWidth;
int width = 0;
int height = getPaddingTop() + getPaddingBottom();
int lineWidth = 0;
int lineHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// child.setMinimumWidth(1080);
boolean lastChild = i == childCount - 1;
if (child.getVisibility() == View.GONE) {
if (lastChild) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
continue;
}
Logger.Log("");
//child.setMinimumWidth(300);
measureChildWithMargins(child, widthMeasureSpec, lineWidth, heightMeasureSpec, height);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidthMode = MeasureSpec.AT_MOST;
int childWidthSize = sizeWidth;
int childHeightMode = MeasureSpec.AT_MOST;
int childHeightSize = sizeHeight;
if (lp.width == LayoutParams.MATCH_PARENT) {
childWidthMode = MeasureSpec.EXACTLY;
childWidthSize -= lp.leftMargin + lp.rightMargin;
} else if (lp.width >= 0) {
childWidthMode = MeasureSpec.EXACTLY;
childWidthSize = lp.width;
}
if (lp.height >= 0) {
childHeightMode = MeasureSpec.EXACTLY;
childHeightSize = lp.height;
} else if (modeHeight == MeasureSpec.UNSPECIFIED) {
childHeightMode = MeasureSpec.UNSPECIFIED;
childHeightSize = 0;
}
Logger.Log("");
child.measure(
MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode),
MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode)
);
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
if (lineWidth + childWidth > sizeWidth) {
// NEW LINE!!! this code means that the width of the child is bןgger then the space left!
width = Math.max(width, lineWidth);
lineWidth = childWidth;
height += lineHeight;
lineHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
} else {
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
if (lastChild) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}
// end of loop.
width += getPaddingLeft() + getPaddingRight();
setMeasuredDimension(
(modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : width,
(modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height);
}
/**
* This method will place each label next to another. If there is not enough
* space for the next label, it will be added in a new new line.
* <p>
*/
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mLines.clear();
mLineHeights.clear();
mLineMargins.clear();
int width = getWidth();
int lineWidth = 0;
int lineHeight = 0;
lineViews.clear();
float horizontalGravityFactor = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.bottomMargin + lp.topMargin;
if (lineWidth + childWidth > width) {
mLineHeights.add(lineHeight);
mLines.add(lineViews);
mLineMargins.add((int) ((width - lineWidth) * horizontalGravityFactor) + getPaddingLeft());
lineHeight = 0;
lineWidth = 0;
lineViews = new ArrayList<>();
}
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
lineViews.add(child);
}
// END OF LOOP.
mLineHeights.add(lineHeight);
mLines.add(lineViews);
mLineMargins.add((int) ((width - lineWidth) * horizontalGravityFactor) + getPaddingLeft());
int verticalGravityMargin = 0;
int numLines = mLines.size();
int left;
int top = getPaddingTop();
// now we are looping threw the number of row in total - mLines.size(), and we want to measure each tag!
for (int i = 0; i < numLines; i++) {
lineHeight = mLineHeights.get(i);
// lineViews = number of lines for child!
lineViews = mLines.get(i);
left = mLineMargins.get(i);
int children = lineViews.size();
for (int j = 0; j < children; j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// if height is match_parent we need to remeasure child to line height
if (lp.height == LayoutParams.MATCH_PARENT) {
int childWidthMode = MeasureSpec.AT_MOST;
int childWidthSize = lineWidth;
if (lp.width == LayoutParams.MATCH_PARENT) {
childWidthMode = MeasureSpec.EXACTLY;
} else if (lp.width >= 0) {
childWidthMode = MeasureSpec.EXACTLY;
childWidthSize = lp.width;
}
child.measure(
MeasureSpec.makeMeasureSpec(childWidthSize, childWidthMode),
MeasureSpec.makeMeasureSpec(lineHeight - lp.topMargin - lp.bottomMargin,
MeasureSpec.EXACTLY)
);
}
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
int gravityMargin = 0;
if (Gravity.isVertical(lp.gravity)) {
switch (lp.gravity) {
case Gravity.TOP:
default:
break;
case Gravity.CENTER_VERTICAL:
case Gravity.CENTER:
gravityMargin = (lineHeight - childHeight - lp.topMargin - lp.bottomMargin) / 2;
break;
case Gravity.BOTTOM:
gravityMargin = lineHeight - childHeight - lp.topMargin - lp.bottomMargin;
break;
}
}
child.layout(left + lp.leftMargin,
top + lp.topMargin + gravityMargin + verticalGravityMargin,
left + childWidth + lp.leftMargin,
top + childHeight + lp.topMargin + gravityMargin + verticalGravityMargin);
left += childWidth + lp.leftMargin + lp.rightMargin;
}
// end of loop
top += lineHeight;
}
// end of loop
}
I would appreciate if you can show me what I have to do to change the size of the child to make it stretched all the way to fill the empty space.
I honestly don't know what all that code you posted above was doing, too long, too little sense made, don't even know how it worked.
Out of curiosity, I'm gonna try solve this problem directly here, I hope it works.
First the onMeasure() method:
// maximum available width for children
int maxW = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
// you want all children to be as wide as they want, but not wider than the view group
int widthMS = MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST);
// you probably want them all to have the same height.
// maybe define it in xml attrs?
int heightMS = MeasureSpec.makeMeasureSpec(rowHeight, MeasureSpec.EXACTLY);
// remaining width of current row, initially equals to w
int remW = w;
// index of first child in the current row
int rowFirstChild = 0;
// row count, used to calculate the view group's height
int rows = 0;
int i = 0;
while (i < getChildCount()) {
Child ch = getChildAt(i);
if (ch.getVisibility() != View.GONE) {
// ask how big does it want to be
child.measure(widthMS, heightMS);
int chWidth = ch.getMeasuredWidth();
// if it fits
if (remW - chWidth >= 0) {
// update remW, then go to next child
remW -= chWidth;
i++;
}
// if it does not fit
else {
// if it's not the first child in the row
if (i > rowFirstChild) {
stretchViews(rowFirstChild, i - 1, maxW, remW, heightMS);
// this will be the first child in the next row and take the space from
// that row's remW
rowFirstChild = i;
remW = maxW - chWidth;
}
// if it's the first and only child on this row
else {
// it should just take the full width and give first spot in the next
// row to the next child, and reset remW
rowFirstChild = i + 1;
remW = maxW;
}
// both of these scenarios result in the addition of a new row
rows++;
}
}
}
// finally, we want to take as much width as we can, and as minimum height as we can
int finalW = MeasureSpec.getSize(widthMeasureSpec);
int finalH = Math.min(
rows * rowHeight + getPaddingTop() + getPaddingBottom(),
MeasureSpec.getSize(heightMeasureSpec)
);
setMeasuredDimensions(finalW, finalH);
// note that this measurement ignores height measure spec and automatically takes the
// minimum height, and ignores the height measure spec of its children as well and
// just gives them height from its rowHeight attr.
// you are free to change this setting.
Now, onLayout() method. Here, since we already assigned measurements to our children, they are all set, we simply have to lay them out, not much work to do:
// maximum usable width in any row
int maxW = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
// left starting point
int left = getPaddingLeft();
// top starting point
int top = getPaddingTop();
int i = 0;
while (i < getChildCount()) {
Child ch = getChildAt(i);
if (ch.getVisibility() != GONE) {
int chWidth = ch.getMeasuredWidth();
ch.layout(
left,
top,
left + chWidth,
top + rowHeight
);
// if this child is the last in row (-1 for error tolerance when rounding ints)
if (left + chWidth >= maxW - 1) {
// reset'left' back to its starting point
left = getPaddingLeft();
// move 'top' down by 'rowHeight'
top += rowHeight;
}
// if it wasn't the last child in the row
else {
// then simply advance the 'left' position
left += chWidth;
}
}
}
// Note that this method only supports LTR layout.
// You can make minor changes to add support for RTL.
// Also, margins between children are ignored, spend some time on that if you want
Let me know if this works
Update:
Never mind, I'll just do the stretching method anyway:
/**
* Stretches children of this view group horizontally, and proportionally to their widths
* to span the entire row width
*
* #param firstIndex index of first child to stretch (inclusive)
* #param lastIndex index of last child to stretch (inclusive)
* #param fullWidth the width to cover (max available width for row children)
* #param remWidth remaining width to cover
* #param heightMS the hight measure spec to reuse on these views
*/
private void stretchViews(int firstIndex, int lastIndex, int fullWdith, int
remWidth, int heightMS) {
for (int i = firstIndex; i <= lastIndex; i++) {
Child ch = getChildAt(i);
if (ch.getVisibility() != GONE) {
int measuredW = ch.getMeasuredWidth();
int widthMS = MeasureSpec.makeMeasureSpec(
measuredW + remWidth * measuredW / fullWidth, MeasureSpec.EXACTLY);
ch.measure(widthMS, heightMS);
}
}
}
I have horizontal RecyclerView with images.
When i scroll it left, i have image order like this.
When i scroll it right, i have order like this.
I need to set central image on top, above left and right ones. How to do it?
I overrided getChildDrawingOrder method:
#Override
protected int getChildDrawingOrder(int childCount, int i) {
int centerChild;
//find center row
if ((childCount % 2) == 0) { //even childCount number
centerChild = childCount / 2; // if childCount 8 (actualy 0 - 7), then 4 and 4-1 = 3 is in centre.
int otherCenterChild = centerChild - 1;
//Which more in center?
View child = this.getChildAt(centerChild);
final int left = child.getLeft();
final int right = child.getRight();
//if this row goes through center then this
final int absParentCenterX = getLeft() + getWidth() / 2;
//Log.i("even", i + " from " + (childCount - 1) + ", while centerChild = " + centerChild);
if ((left < absParentCenterX) && (right > absParentCenterX)) {
//this child is in center line, so it is last
//centerChild is in center, no need to change
} else {
centerChild = otherCenterChild;
}
} else {//not even - done
centerChild = childCount / 2;
//Log.i("not even", i + " from " + (childCount - 1) + ", while centerChild = " + centerChild);
}
int rez;
//find drawIndex by centerChild
if (i > centerChild) {
//below center
rez = (childCount - 1) - i + centerChild;
} else if (i == centerChild) {
//center row
//draw it last
rez = childCount - 1;
} else {
//above center - draw as always
// i < centerChild
rez = i;
}
//Log.i("return", "" + rez);
return rez;
}
Also i set
setChildrenDrawingOrderEnabled(true);
Full code is here.
Fixed with adding ViewCompat.setTranslationZ(view, translationZ);
protected final void updateViews() {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//setMarginsForChild(child);
if (mAppearAnimationIsWorking)
child.setAlpha(0);
else
child.setAlpha(1);
if (mScaleUnfocusedViews && !mAppearAnimationIsWorking) {
float scale = computeScale(child);
child.setTranslationY(-getMeasuredHeight() * (1 - scaleFactor) / 2);
ViewCompat.setTranslationZ(child, -getPercentageFromCenter(child));
child.setScaleX(scale * scaleFactor * innerScale);
child.setScaleY(scale * scaleFactor * innerScale);
}
}
}
getChildDrawingOrder and setChildrenDrawingOrderEnabled can be removed.
I have created a custom view group. I implemented the onLayout and onMeasured inside that group. But both these methods are only being called once, i.e for the first time. When i remove a child view and add a new child the onLayout and onMeasured are not being invoked. Any help would be appreciated.
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// Iterate through all children, measuring them and computing our dimensions
// from their size.
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// Measure the child.
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
setMeasuredDimension(displayMetrics.widthPixels,displayMetrics.heightPixels);
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
// These are the far left and right edges in which we are performing layout.
int topPosition = 0;
int leftPosition = 0;
int rightPosition = 0;
int bottomPosition = 0;
int i;
int j;
int decrementX = 0;
int incrementX = 0;
int verticalGap = 75;
int width;
int height;
View child;
if( count > 0 ){
int medium = count/2;
int gap = getMeasuredWidth()/count;
if( count == 1 ){
child = this.getChildAt(0);
width = child.getMeasuredWidth();
height = child.getMeasuredHeight();
leftPosition = getMeasuredWidth()/2 - width/2;
rightPosition = getMeasuredWidth() - ( getMeasuredWidth() - ( leftPosition + width ));
topPosition = 10;
bottomPosition = getMeasuredHeight() - ( getMeasuredHeight() - (topPosition + height));
child.layout(leftPosition,topPosition,rightPosition,bottomPosition);
}
else if( count%2 == 0 ){
child = this.getChildAt(0);
width = child.getMeasuredWidth();
height = child.getMeasuredHeight();
gap = getMeasuredWidth()/(count+1);
decrementX = incrementX = getMeasuredWidth()/2 - width/2;
for(i=medium,j=medium-1;(i<count && j>=0);i++,j--){
child = this.getChildAt(i);
width = child.getMeasuredWidth();
height = child.getMeasuredHeight();
decrementX -= gap;
leftPosition = decrementX;
rightPosition = getMeasuredWidth() - (getMeasuredWidth() - ( leftPosition + width ));
topPosition += verticalGap;
bottomPosition = getMeasuredHeight() - ( getMeasuredHeight() - ( topPosition + height ));
child.layout(leftPosition,topPosition,rightPosition,bottomPosition);
child = this.getChildAt(j);
incrementX += gap;
leftPosition = incrementX;
rightPosition = getMeasuredWidth() - ( getMeasuredWidth() - ( leftPosition + width ));
child.layout(leftPosition,topPosition,rightPosition,bottomPosition);
}
}
else if( count%3 == 0 || count%3 == 2 ){
child = this.getChildAt(medium);
if( child.getVisibility() != GONE ){
width = child.getMeasuredWidth();
height = child.getMeasuredHeight();
topPosition = 10;
leftPosition = getMeasuredWidth()/2 - width/2;
bottomPosition = getMeasuredHeight() - ( getMeasuredHeight() - ( topPosition + height ) );
rightPosition = getMeasuredWidth() - (getMeasuredWidth() - (leftPosition + width));
child.layout(leftPosition,topPosition,rightPosition,bottomPosition);
decrementX = leftPosition;
incrementX = leftPosition;
}
for(i=medium+1,j=medium-1;(i<count && j>=0); i++,j--){
child = this.getChildAt(i);
width = child.getMeasuredWidth();
height = child.getMeasuredHeight();
decrementX -= gap;
leftPosition = decrementX;
rightPosition = getMeasuredWidth() - ( getMeasuredWidth() - ( width + leftPosition ));
topPosition += verticalGap;
bottomPosition = getMeasuredHeight() - ( getMeasuredHeight() - (height + topPosition));
child.layout(leftPosition,topPosition,rightPosition,bottomPosition);
child = this.getChildAt(j);
width = child.getMeasuredWidth();
height = child.getMeasuredHeight();
incrementX += gap;
leftPosition = incrementX;
rightPosition = getMeasuredWidth() - ( getMeasuredWidth() - (width + leftPosition));
child.layout(leftPosition,topPosition,rightPosition,bottomPosition);
// final View leftView = getChildAt(j);
}
}
}
The above are the onMeasure and onLayout methods.
As per the Google design patterns I have been implementing the dashboard layout by using the DashboardLayout.java file used by Google in there Google IO app. This has been working fine when using buttons, but as soon as I add a custom view the grid view produced by the DashboardLayout.java file falls apart:
Working without custom view:
Not working with custom view:
The code for the custom view is:
public class Countdown extends View {
int viewWidth;
int viewHeight;
Paint textPaint;
Paint titlePaint;
Paint labelPaint;
Paint rectanglePaint;
PeriodFormatter daysFormatter;
PeriodFormatter hoursFormatter;
PeriodFormatter minutesFormatter;
PeriodFormatter secondsFormatter;
DateTimeZone frenchTimeZone;
DateTime expiry;
Context ctx;
static int[] rectWidth;
static int[] rectHeight;
boolean flag = true;
public Countdown(Context context) {
super(context);
ctx = context;
init();
}
public Countdown(Context context, AttributeSet attrs) {
super(context, attrs);
ctx = context;
init();
}
public Countdown(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
ctx = context;
init();
}
private void init()
{
rectWidth = new int[]{0,0,0,0};
rectHeight = new int[]{0,0,0,0};
textPaint = new Paint();
titlePaint = new Paint();
labelPaint = new Paint();
rectanglePaint = new Paint();
frenchTimeZone = DateTimeZone.forID("Europe/Paris");
expiry = new DateTime(2012, 6, 17, 8, 30, frenchTimeZone);
//setup paints
//turn antialiasing on
textPaint.setAntiAlias(true);
int timerScaledSize = getResources().getDimensionPixelSize(R.dimen.text_size_dashboard_timer);
textPaint.setTextSize(timerScaledSize);
textPaint.setColor(Color.WHITE);
textPaint.setTextAlign(Align.CENTER);
labelPaint.setAntiAlias(true);
int labelScaledSize = getResources().getDimensionPixelSize(R.dimen.text_size_dashboard_timer_boxes_label);
labelPaint.setTextSize(labelScaledSize);
labelPaint.setColor(Color.BLACK);
labelPaint.setTextAlign(Align.CENTER);
labelPaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
titlePaint.setAntiAlias(true);
int titleScaledSize = getResources().getDimensionPixelSize(R.dimen.text_size_dashboard_title);
titlePaint.setTextSize(titleScaledSize);
titlePaint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
titlePaint.setColor(Color.WHITE);
rectanglePaint.setAntiAlias(true);
daysFormatter = new PeriodFormatterBuilder()
.printZeroIfSupported()
.minimumPrintedDigits(2)
.appendDays()
.toFormatter();
hoursFormatter = new PeriodFormatterBuilder()
.printZeroIfSupported()
.minimumPrintedDigits(2)
.appendHours()
.toFormatter();
minutesFormatter = new PeriodFormatterBuilder()
.printZeroIfSupported()
.minimumPrintedDigits(2)
.appendMinutes()
.toFormatter();
secondsFormatter = new PeriodFormatterBuilder()
.printZeroIfSupported()
.minimumPrintedDigits(2)
.appendSeconds()
.toFormatter();
}
#Override
public void onDraw(Canvas canvas)
{
DateTime now = new DateTime();
Period p = new Period(now, expiry, PeriodType.dayTime());
canvas.drawColor(Color.TRANSPARENT);
if(flag)
{
// To ensure the rectangles will be wide enough for all numbers we cheat and initially set the width based upon 00.
flag = false;
drawTextRectangle(0, textPaint, labelPaint, canvas, "00", "", scaleForDensity(20, ctx), scaleForDensity(33, ctx));
drawTextRectangle(1, textPaint, labelPaint, canvas, "00", "", scaleForDensity(53, ctx), scaleForDensity(33, ctx));
drawTextRectangle(2, textPaint, labelPaint, canvas, "00", "", scaleForDensity(87, ctx), scaleForDensity(33, ctx));
drawTextRectangle(3, textPaint, labelPaint, canvas, "00", "", scaleForDensity(120, ctx), scaleForDensity(33, ctx));
}
String title = "Countdown";
float textWidth = titlePaint.measureText(title);
float titleStartPositionX = (viewWidth - textWidth) / 2;
canvas.drawText(title, titleStartPositionX, viewHeight - scaleForDensity(5, ctx), titlePaint);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dashboard_counter);
canvas.drawBitmap(bitmap, 0, 0, null);
drawTextRectangle(0, textPaint, labelPaint, canvas, daysFormatter.print(p), "DAYS", scaleForDensity(20, ctx), scaleForDensity(33, ctx));
drawTextRectangle(1, textPaint, labelPaint, canvas, hoursFormatter.print(p), "HRS", scaleForDensity(53, ctx), scaleForDensity(33, ctx));
drawTextRectangle(2, textPaint, labelPaint, canvas, minutesFormatter.print(p), "MINS", scaleForDensity(87, ctx), scaleForDensity(33, ctx));
drawTextRectangle(3, textPaint, labelPaint, canvas, secondsFormatter.print(p), "SECS", scaleForDensity(120, ctx), scaleForDensity(33, ctx));
invalidate();
}
private void drawTextRectangle(int index, Paint paint, Paint labelPaint, Canvas canvas, String text, String label, float x, float y) {
paint.setTextAlign(Align.CENTER);
Rect bounds = new Rect();
bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
if(rectWidth[index] == 0)
{
rectWidth[index] = Math.abs(bounds.right - bounds.left);
rectWidth[index] += scaleForDensity(5, ctx);
}
if(rectHeight[index] == 0)
{
rectHeight[index] = Math.abs(bounds.bottom - bounds.top);
rectHeight[index] += scaleForDensity(5, ctx);
}
bounds.left = (int) (x - (rectWidth[index] / 2));
bounds.top = (int) (y - rectHeight[index]);
bounds.right = bounds.left + rectWidth[index];
bounds.bottom = (int) (bounds.top + rectHeight[index] + scaleForDensity(7, ctx));
Paint rectanglePaint = new Paint();
rectanglePaint.setAntiAlias(true);
rectanglePaint.setShader(new LinearGradient(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom, 0xff8ed8f8, 0xff207d94, TileMode.MIRROR));
RectF boundsF = new RectF(bounds);
canvas.drawRoundRect(boundsF, 2f, 2f, rectanglePaint);
canvas.drawText(text, x, y, paint);
canvas.drawText(label, x, y + rectHeight[index], labelPaint);
}
public float scaleForDensity(float px, Context context)
{
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return px * metrics.density + .5f;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec, widthMeasureSpec);
viewWidth = width;
viewHeight = height;
setMeasuredDimension(width, height);
}
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 = measureSpec;
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
private int measureHeight(int measureSpecHeight, int measureSpecWidth) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpecHeight);
int specSize = MeasureSpec.getSize(measureSpecHeight);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text (beware: ascent is a negative number)
result = viewWidth;
/*if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}*/
}
return result;
}
}
The DashboardLayout code that I am using:
/**
* Custom layout that arranges children in a grid-like manner, optimizing for even horizontal and
* vertical whitespace.
*/
public class DashboardLayout extends ViewGroup {
private static final int UNEVEN_GRID_PENALTY_MULTIPLIER = 10;
boolean run = true;
private int mMaxChildWidth = 0;
private int mMaxChildHeight = 0;
public DashboardLayout(Context context) {
super(context, null);
}
public DashboardLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public DashboardLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if(run)
{
run = false;
mMaxChildWidth = 0;
mMaxChildHeight = 0;
// Measure once to find the maximum child size.
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.AT_MOST);
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
mMaxChildWidth = Math.max(mMaxChildWidth, child.getMeasuredWidth());
mMaxChildHeight = Math.max(mMaxChildHeight, child.getMeasuredHeight());
}
// Measure again for each child to be exactly the same size.
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
mMaxChildWidth, MeasureSpec.EXACTLY);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
mMaxChildHeight, MeasureSpec.EXACTLY);
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
setMeasuredDimension(
resolveSize(mMaxChildWidth, widthMeasureSpec),
resolveSize(mMaxChildHeight, heightMeasureSpec));
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = r - l;
int height = b - t;
final int count = getChildCount();
// Calculate the number of visible children.
int visibleCount = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
++visibleCount;
}
if (visibleCount == 0) {
return;
}
// Calculate what number of rows and columns will optimize for even horizontal and
// vertical whitespace between items. Start with a 1 x N grid, then try 2 x N, and so on.
int bestSpaceDifference = Integer.MAX_VALUE;
int spaceDifference;
// Horizontal and vertical space between items
int hSpace = 0;
int vSpace = 0;
int cols = 1;
int rows;
while (true) {
rows = (visibleCount - 1) / cols + 1;
hSpace = ((width - mMaxChildWidth * cols) / (cols + 1));
vSpace = ((height - mMaxChildHeight * rows) / (rows + 1));
spaceDifference = Math.abs(vSpace - hSpace);
if (rows * cols != visibleCount) {
spaceDifference *= UNEVEN_GRID_PENALTY_MULTIPLIER;
}
if (spaceDifference < bestSpaceDifference) {
// Found a better whitespace squareness/ratio
bestSpaceDifference = spaceDifference;
// If we found a better whitespace squareness and there's only 1 row, this is
// the best we can do.
if (rows == 1) {
break;
}
} else {
// This is a worse whitespace ratio, use the previous value of cols and exit.
--cols;
rows = (visibleCount - 1) / cols + 1;
hSpace = ((width - mMaxChildWidth * cols) / (cols + 1));
vSpace = ((height - mMaxChildHeight * rows) / (rows + 1));
break;
}
++cols;
}
// Lay out children based on calculated best-fit number of rows and cols.
// If we chose a layout that has negative horizontal or vertical space, force it to zero.
hSpace = Math.max(0, hSpace);
vSpace = Math.max(0, vSpace);
// Re-use width/height variables to be child width/height.
width = (width - hSpace * (cols + 1)) / cols;
height = (height - vSpace * (rows + 1)) / rows;
int left, top;
int col, row;
int visibleIndex = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
row = visibleIndex / cols;
col = visibleIndex % cols;
left = hSpace * (col + 1) + width * col;
top = vSpace * (row + 1) + height * row;
child.layout(left, top,
(hSpace == 0 && col == cols - 1) ? r : (left + width),
(vSpace == 0 && row == rows - 1) ? b : (top + height));
++visibleIndex;
}
}
}
And last but not least the layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
style="#style/HeaderTextView"
android:text="#string/header_dashboard" />
<View
android:layout_width="fill_parent"
android:layout_height="#dimen/content_divider_height"
android:layout_marginLeft="#dimen/content_divider_margin"
android:layout_marginRight="#dimen/content_divider_margin"
android:background="#color/content_divider_colour" />
<com.a.b.ui.DashboardLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
style="#style/Container">
<!-- The custom view that once un-commented cause the problem -->
<!-- <com.a.b.widget.Countdown
style="#style/DashboardButton" /> -->
<Button android:id="#+id/home_btn_news"
style="#style/DashboardButton"
android:text="A"
android:drawableTop="#drawable/dashboard_counter" />
<Button android:id="#+id/home_btn_feed"
style="#style/DashboardButton"
android:text="B"
android:drawableTop="#drawable/dashboard_counter" />
<Button android:id="#+id/home_btn_guide"
style="#style/DashboardButton"
android:text="C"
android:drawableTop="#drawable/dashboard_counter" />
<Button android:id="#+id/home_btn_sessions"
style="#style/DashboardButton"
android:text="D"
android:drawableTop="#drawable/dashboard_counter" />
<Button android:id="#+id/home_btn_events"
style="#style/DashboardButton"
android:text="E"
android:drawableTop="#drawable/dashboard_counter" />
</com.a.b.ui.DashboardLayout>
</LinearLayout>
Apologies over the amount of code posted, but I hope it makes it easier to see the issue(s).
I have since discovered the bug in the my onMeasureWidth function in the custom view. Instead of:
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 = measureSpec;
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
It should be:
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 = viewWidth;
}
return result;
}