A bit difficult to explain, hope everyone get my point.
At onMeasure() I done this to make wrap_content work as expect.
// Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(desiredWidth, widthSize);
} else {
//Be whatever you want
width = desiredWidth;
}
but deiredWidth required getHeight() method to calculate
which getHight() not ready so, I can't calculate correct value from here.
So, is there a way to get given width from layout_width attribute at onMeasure()
or do I misunderstand somethings.
All Relevant Code
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width;
int height;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int desiredWidth = getTotalIndicatorWidth(getMeasuredHeight());
int desiredHeight = DEFAULT_HEIGHT;
// Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(desiredWidth, widthSize);
} else {
//Be whatever you want
width = desiredWidth;
}
//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
//Must be this size
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
height = Math.min(desiredHeight, heightSize);
} else {
//Be whatever you want
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
private int getTotalIndicatorWidth(int h) {
int width = 0;
int indicatorWidth = (h == 0) ? DEFAULT_HEIGHT : h;
for (int i = 0; i < indicatorSize; i++) {
if (i != 0) {
width = width + indicatorSpacing + indicatorWidth;
} else {
width = indicatorWidth;
}
}
return width;
}
#Override
protected void onDraw(Canvas canvas) {
pen.setStyle(Paint.Style.STROKE);
pen.setColor(indicatorColor);
pen.setStrokeWidth(indicatorStokeWidth);
pen.setAntiAlias(true);
drawIndicator(canvas, indicatorSize, 1);
}
private void drawIndicator(Canvas paper, int indicatorSize, int position) {
int indent = 0;
int cr = (getHeight() / 2) - getPaddingTop() - getPaddingBottom() - indicatorStokeWidth;
int cx = cr + getPaddingLeft() + indicatorStokeWidth;
int cy = (getHeight() / 2) + getPaddingTop() - getPaddingBottom();
for (int i = 0; i < indicatorSize; i++) {
paper.drawCircle(cx + indent, cy, cr, pen);
indent = (cr * 2) + (indicatorStokeWidth) + indent;
if ((i+1) < indicatorSize) {
indent += indicatorSpacing;
}
}
}
As you can see, the below image shown the area of custom view even it layout_width was set as wrap_content but it doesn't work for me now.
You say that you need to access getHeight() to calculate desiredWidth inside the onMeasure() method and also that getHeight() is not ready.
Yes, getHeight() or getWidth() can't be accessed from onMeasure() because these height and width are set in onMeasure() method. In your onMeause() method, you need to call setMeasuredDimension(). You have done that in your code.
setMeasuredDimension(width, height);
This width is what you will get when you call getWidth() and this height is what you will get when you call getHeight().
So if you want to access getHeight(), then use height instead.
The following code solve my problem of how can I get my layout_width or layout_height at onMesaure() so I can calculate my view correctly when user request for wrap_content
public PageIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
int[] attrsArray = new int[] {
android.R.attr.id, // 0
android.R.attr.background, // 1
android.R.attr.layout_width, // 2
android.R.attr.layout_height, // 3
};
TypedArray taa = context.obtainStyledAttributes(attrs, attrsArray);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PageIndicator, 0, 0);
try {
indicatorColor = ta.getColor(R.styleable.PageIndicator_indicator_color, Color.WHITE);
indicatorStokeWidth = (int) ta.getDimension(R.styleable.PageIndicator_indicator_stokeWidth, 4);
indicatorSpacing = (int) ta.getDimension(R.styleable.PageIndicator_indicator_spacing, 5);
indicatorSize = ta.getInteger(R.styleable.PageIndicator_indicator_size, 3);
indicatorPosition = ta.getInteger(R.styleable.PageIndicator_indicator_startPosition, 0);
mLayoutWidth = taa.getLayoutDimension(2, ViewGroup.LayoutParams.MATCH_PARENT);
mLayoutHeight = taa.getLayoutDimension(3, ViewGroup.LayoutParams.MATCH_PARENT);
} finally {
ta.recycle();
taa.recycle();
}
}
Related
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 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.
I'm working on a custom view that's giving me unexpected results. What I'm trying to make happen is to fill a picture the width of the view and scale it keeping its aspect ratio, but it's doing something weird...
Here's what my generated log is spitting out.
View size: 768x942
Pic is portrait
Original Size: 960x1280 Scaled: 768x768
Scaled bitmap: 768x768
Now let me explain whats the log is saying according to the code. First we let the view onMeasure itself and once it's done that we're allowed to grab the width and height of the view. Next we check if the picture is landscape or portrait. Then we just do the math to find the size we need to scale to. After the math is done we create a new scaled bitmap with the results. The width is right but the height should be 1024 not 768. I can't see where its messing up.
public void setBitmap(Bitmap bmp) {
this.mOriginalBitmap = bmp;
if(mHasMeasured) {
//Make sure the view has measured itself so we can grab the width and height
Log.d("", "View size: " + String.valueOf(this.mViewWidth)
+ "x" + String.valueOf(this.mViewHeight));
int reqWidth, reqHeight; //The required sizes we need
//Get the new sizes for the pic to fit in the view and keep aspect ratio
if(this.mOriginalBitmap.getWidth() > this.mOriginalBitmap.getHeight()) {
//Landscape :/
Log.d("", "Pic is Landscape");
reqHeight = this.mViewHeight;
reqWidth = (this.mOriginalBitmap.getWidth()
/ this.mOriginalBitmap.getHeight()) * reqHeight;
} else {
//Portrait :)
Log.d("", "Pic is portrait");
reqWidth = this.mViewWidth;
reqHeight = (this.mOriginalBitmap.getHeight()
/ this.mOriginalBitmap.getWidth()) * reqWidth;
}
Log.d("", "Original Size: "
+ String.valueOf(mOriginalBitmap.getWidth()) + "x"
+ String.valueOf(mOriginalBitmap.getHeight())
+ " Scaled: " + String.valueOf(reqWidth)
+ "x" + String.valueOf(reqHeight) );
this.mBmpScaledForView = Bitmap.createScaledBitmap(mOriginalBitmap,
reqWidth, reqHeight, false);
this.mSrc.top = 0;
this.mSrc.left = 0;
this.mSrc.right = this.mBmpScaledForView.getWidth();
this.mSrc.bottom = this.mBmpScaledForView.getHeight();
this.mDst = this.mSrc;
Log.d("", "Scaled bitmap : "
+ String.valueOf(this.mBmpScaledForView.getWidth())
+ "x" + String.valueOf(this.mBmpScaledForView.getHeight()));
}
}
The issue here is with the image ratio computation. Bitmap.getHeight() and getWidth() return ints, which makes the result of
(this.mOriginalBitmap.getHeight()
/ this.mOriginalBitmap.getWidth())
be 1 for an image that is 960x1280.
There are several options around this. You can make a float division by converting the return value of getHeight() and getWidth():
((float) this.mOriginalBitmap.getHeight()
/ (float) this.mOriginalBitmap.getWidth())
Or you can simply start by the multiplication :
reqHeight = (reqWidth * this.mOriginalBitmap.getHeight()) / this.mOriginalBitmap.getWidth();
package com.riktamtech.app.utils;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* ImageView that keeps aspect ratio when scaled
*/
public class CustomImageView extends ImageView {
public CustomImageView(Context context) {
super(context);
}
public CustomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
Drawable drawable = getDrawable();
if (drawable == null) {
setMeasuredDimension(0, 0);
} else {
int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
if (measuredHeight == 0 && measuredWidth == 0) { // Height and
// width set
// to
// wrap_content
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (measuredHeight == 0) { // Height set to wrap_content
int width = measuredWidth;
int height = width * drawable.getIntrinsicHeight()
/ drawable.getIntrinsicWidth();
setMeasuredDimension(width, height);
} else if (measuredWidth == 0) { // Width set to wrap_content
int height = measuredHeight;
int width = height * drawable.getIntrinsicWidth()
/ drawable.getIntrinsicHeight();
setMeasuredDimension(width, height);
} else { // Width and height are explicitly set (either to
// match_parent or to exact value)
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
} catch (Exception e) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
I have a custom view and I'm trying to dynamically make it bigger in the Y direction. That is the only direction that doesn't work. If I adjust any of the other variables in my custom view the rectangle gets affected in the appropriate ways. If I try adding to bottomY then the bottom border disappears and nothing draws below that. Here is the code for the view:
private class RectView extends View{
float leftX, rightX, topY, bottomY;
boolean isAppt;
boolean isBeforeTime;
boolean isSelected;
public Paint rectPaint;
private RectF rectangle;
String time;
public RectView(Context context, float _leftX, float _rightX, float _topY, float _bottomY,
boolean _isAppt, boolean _isBeforeTime, String _time){
super(context);
leftX = _leftX;
rightX = _rightX;
topY = _topY;
bottomY = _bottomY;
isAppt = _isAppt;
isBeforeTime = _isBeforeTime;
time = _time;
init();
}
private void init(){
rectPaint = new Paint();
if(leftX > rightX || topY > bottomY)
Toast.makeText(context, "Incorrect", Toast.LENGTH_SHORT).show();
MyUtility.LogD_Common("Left = " + leftX + ", Top = " + topY + ", Right = " + rightX +
", Bottom = " + bottomY);
rectangle = new RectF(leftX, topY, rightX, bottomY);
float height = bottomY;
float width = rightX - leftX;
MyUtility.LogD_Common("Height = " + height + ", Width = " + width);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.FILL_PARENT, (int) height);
//params.leftMargin = (int) leftX;
params.bottomMargin = 10;
//params.rightMargin = 10;
setLayoutParams(params);
}
protected void onDraw(Canvas canvas){
MyUtility.LogD_Common("Right = " + rightX);
rectangle.left = leftX;
rectangle.right = rightX;
rectangle.top = topY;
rectangle.bottom = bottomY;
if(!isSelected){
if(isAppt){
if(isBeforeTime)
rectPaint.setARGB(144, 119, 98, 95);
else
rectPaint.setARGB(144, 217, 131, 121);
//119,98,95
rectPaint.setStyle(Style.FILL);
}
else{
rectPaint.setARGB(0, 0, 0, 0);
rectPaint.setStyle(Style.FILL);
}
canvas.drawRect(rectangle, rectPaint);
if(isAppt){
rectPaint.setColor(Color.RED);
rectPaint.setStrokeWidth(2);
rectPaint.setStyle(Style.STROKE);
canvas.drawRect(rectangle, rectPaint);
}
}
else{
rectPaint.setARGB(144, 197, 227, 191);
rectPaint.setStyle(Style.FILL);
canvas.drawRect(rectangle, rectPaint);
rectPaint.setColor(Color.GREEN);
rectPaint.setStrokeWidth(2);
rectPaint.setStyle(Style.STROKE);
canvas.drawRect(rectangle, rectPaint);
}
}
}
Why is this happening and why only in the positive Y direction?
When you create this object you call init(), where you set the height spec for your RelativeLayout params to the local variable height, which you define with:
float height = bottomY;
By doing this, you are telling the parent RelativeLayout that this view want's to be exactly that height that bottomY was when you created the object.
If you then increase the value of bottomY for your already created object, it can no longer fit inside the height you defined in your RelativeLayout params when the object was first created.
Firstly, Changing the LayoutParams from inside the class in this way is not recommended. This makes your custom View inflexible. If you set a bunch of RelativeLayout.LayoutParams inside the class, then your custom View can only ever be used in a RelativeLayout.
You should instead set the LayoutParams in your code before you add the view. For example:
RectView rectView = new RectView(...);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
addView(rectView, params);
Secondly, when the system draws the layout that your custom view has been added to, it goes through a series of steps. In the layout process it will call getMeasuredWidth() and getMeasuredHeight() on all it's child views (including your custom view). You should override your custom views onMeasure() method and get it to report the correct size of your view. Have a look at onMeasure() in the example in the reference documentation for ViewGroup. It's a more complex onMeasure() than you need for your case, but it gives you the basic idea.
In your particular case something like the following should do the trick:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
// This method calculates the Width of your view based on the dimensions of
// your rectangle and the current widthMeasureSpec.
private int measureWidth(int widthMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Return the width of the rectangle
result = rightX - leftX;
}
return result;
}
// This method calculates the Height of your view based on the dimensions of
// your rectangle and the current widthMeasureSpec.
private int measureHeight(int heightMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Return the height of the rectangle
result = bottomY - topY;
}
return result;
}
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;
}