Android labels on custom seekbar - android

I have custom styles for a SeekBar, and I would like to add labels. The SeekBar has a progress drawable that looks like:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="#android:id/background"
android:drawable="#drawable/seekbar_background"/>
</layer-list>
The seekbar_background 9-patch looks like this:
I would like to have a label over each end of the bar. Preferably, the label would have the text centered over the bulb on each side. It seems like I should be able to center a TextView over the endpoints of the SeekBar, but I couldn't find a way to align one view's center with another view's edge.
Is there a way, either using the SeekBar API, or via clever use of layout alignment, to make this happen?

I think creation of custom View / ViewGroup might do the trick, below is some example of how I believe it can be done.
main.xml:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res/com.example.TestApp"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="#+id/root">
<com.example.TestApp.LabelledSeekBar
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
custom:labelLeft="#string/left_label"
custom:labelRight="#string/right_label"
custom:labelPaddingBottom="#dimen/label_padding_bottom"
custom:labelCenterSidePadding="#dimen/label_padding_side"
android:textSize="#dimen/label_text_size"
android:textColor="#android:color/white"
android:textStyle="italic"
android:progressDrawable="#drawable/seek_progress" />
</RelativeLayout>
attrs.xml with custom attributes:
<resources>
<declare-styleable name="LabelledSeekBar">
<attr name="labelLeft" format="string"/>
<attr name="labelRight" format="string"/>
<attr name="labelPaddingBottom" format="dimension"/>
<!-- This is bulb center padding -->
<attr name="labelCenterSidePadding" format="dimension"/>
</declare-styleable>
</resources>
The core idea of custom View / ViewGroup is just provide proper placing of the text and do right measurement for the view. Custom ViewGroup itself is (lot of code below):
public class LabelledSeekBar extends ViewGroup {
/** SeekBar itself */
private final SeekBar mSeekBar;
/** Label for left end */
private String mLeftLabel = null;
/** Label for right end */
private String mRightLabel = null;
/** Bottom paddings for labels */
private static final int DEFAULT_LABEL_PADDING_BOTTOM = 10; // px
private int mLabelPaddingBottom = DEFAULT_LABEL_PADDING_BOTTOM;
/** Center of 'bulbs' to draw labels above centered */
private static final int DEFAULT_LABEL_PADDING_SIDE = 10; // px
private int mLabelCenterPadding = DEFAULT_LABEL_PADDING_SIDE;
/** Here goes labels attributes, they are similar to TextViews ones */
private static final int DEFAULT_TEXT_SIZE = 10; // px
private static final int DEFAULT_TEXT_COLOR = Color.BLACK; // px
private static final Typeface DEFAULT_TEXT_STYLE = Typeface.DEFAULT; // px
private int mTextSize = DEFAULT_TEXT_SIZE;
private int mTextColor = DEFAULT_TEXT_COLOR;
private Typeface mTextStyle = DEFAULT_TEXT_STYLE;
/** Bounds for labels rects */
private Rect mLeftTextBound = null;
private Rect mRightTextBound = null;
/** Rect for SeekBar */
private Rect mSeekBarRect = null;
/** Default height for SeekBar */
private int mDefaultSeekBarHeight = 0;
/** Paint for text */
private Paint mTextPaint = null;
/** Flag to draw or not the labels */
private boolean mDrawLabels = false;
/**
* Constructor
*/
public LabelledSeekBar(final Context context) {
super(context);
mSeekBar = new SeekBar(context);
init(null);
}
/**
* Constructor
*/
public LabelledSeekBar(final Context context, final AttributeSet attrs) {
super(context, attrs);
mSeekBar = new SeekBar(context, attrs);
init(attrs);
}
/**
* Constructor
*/
public LabelledSeekBar(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
mSeekBar = new SeekBar(context, attrs, defStyle);
init(attrs);
}
#Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
mSeekBar.layout(mSeekBarRect.left, mSeekBarRect.top, mSeekBarRect.right, mSeekBarRect.bottom);
}
/**
* Initializes Seek bar extended attributes from xml
*
* #param attributeSet {#link AttributeSet}
*/
private void init(final AttributeSet attributeSet) {
final TypedArray attrsArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.LabelledSeekBar, 0, 0);
mDefaultSeekBarHeight = getResources().getDimensionPixelSize(R.dimen.default_seekbar_height);
mLeftLabel = attrsArray.getString(R.styleable.LabelledSeekBar_labelLeft);
mRightLabel = attrsArray.getString(R.styleable.LabelledSeekBar_labelRight);
mLabelPaddingBottom = attrsArray.getDimensionPixelOffset(R.styleable.LabelledSeekBar_labelPaddingBottom, DEFAULT_LABEL_PADDING_BOTTOM);
mLabelCenterPadding = attrsArray.getDimensionPixelOffset(R.styleable.LabelledSeekBar_labelCenterSidePadding, DEFAULT_LABEL_PADDING_SIDE);
// Now get needed Text attributes
final int textSizeResource = attributeSet.getAttributeResourceValue("http://schemas.android.com/apk/res/android", "textSize", 0);
if (0 != textSizeResource) {
mTextSize = getResources().getDimensionPixelSize(textSizeResource);
}
final int textColorResource = attributeSet.getAttributeResourceValue("http://schemas.android.com/apk/res/android", "textColor", 0);
if (0 != textColorResource) {
mTextColor = getResources().getColor(textColorResource);
}
final int typeface = attributeSet.getAttributeIntValue("http://schemas.android.com/apk/res/android", "textStyle", 0);
switch (typeface) {
// normale
case 0:
mTextStyle = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL);
break;
// bold
case 1:
mTextStyle = Typeface.create(Typeface.DEFAULT, Typeface.BOLD);
break;
// italic
case 2:
mTextStyle = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC);
break;
// bold | italic
case 3:
mTextStyle = Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC);
break;
}
mTextPaint = new TextPaint();
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setTypeface(mTextStyle);
mTextPaint.setTextAlign(Paint.Align.LEFT);
mTextPaint.setStyle(Paint.Style.FILL);
addView(mSeekBar);
}
/**
* Setters for labels
*
* #param leftLabel {#link String}
* #param rightLabel {#link String}
*/
public void setLabels(final String leftLabel, final String rightLabel) {
mLeftLabel = leftLabel;
mRightLabel = rightLabel;
requestLayout();
}
#Override
protected synchronized void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
// measure labels height - this logic is not very strict and can be changed
mLeftTextBound = new Rect();
mTextPaint.getTextBounds(mLeftLabel, 0, mLeftLabel.length(), mLeftTextBound);
mRightTextBound = new Rect();
mTextPaint.getTextBounds(mRightLabel, 0, mRightLabel.length(), mRightTextBound);
final int labelHeight = Math.max(mLeftTextBound.height(), mRightTextBound.height());
final int desiredMinHeight = labelHeight + mLabelPaddingBottom;
final int desiredMinWidth = mLeftTextBound.width() + mRightTextBound.width();
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
mSeekBarRect = new Rect();
// Calculate width
switch (widthMode) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
if (widthSize < desiredMinWidth) {
mDrawLabels = false;
} else {
mDrawLabels = true;
mSeekBarRect.set(mLeftTextBound.width() / 2 - mLabelCenterPadding, desiredMinHeight,
widthSize - mRightTextBound.width() / 2 + mLabelCenterPadding, heightSize);
}
measuredWidth = widthSize;
break;
case MeasureSpec.UNSPECIFIED:
mDrawLabels = true;
measuredWidth = desiredMinWidth + mLabelCenterPadding * 4;
mSeekBarRect.set(mLeftTextBound.width() / 2 - mLabelCenterPadding, desiredMinHeight,
widthSize - mRightTextBound.width() / 2 + mLabelCenterPadding, heightSize);
break;
}
if (mDrawLabels) {
// Calculate height
switch (heightMode) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
if (heightSize < desiredMinHeight) {
mDrawLabels = false;
} else {
mDrawLabels = true;
mSeekBarRect.top = desiredMinHeight;
mSeekBarRect.bottom = heightSize > mDefaultSeekBarHeight ? (desiredMinHeight + mDefaultSeekBarHeight) : heightSize;
}
measuredHeight = (heightSize > (desiredMinHeight + mDefaultSeekBarHeight)) ? (desiredMinHeight + mDefaultSeekBarHeight) : heightSize;
break;
case MeasureSpec.UNSPECIFIED:
mDrawLabels = true;
measuredHeight = desiredMinHeight + mDefaultSeekBarHeight;
mSeekBarRect.top = desiredMinHeight;
mSeekBarRect.bottom = measuredHeight;
break;
}
} else {
switch (heightMode) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
measuredHeight = heightSize;
break;
case MeasureSpec.UNSPECIFIED:
measuredHeight = mDefaultSeekBarHeight;
break;
}
}
if (!mDrawLabels) {
// define SeekBar rect
mSeekBarRect.set(0, 0, measuredWidth, measuredHeight);
}
mSeekBar.measure(MeasureSpec.makeMeasureSpec(mSeekBarRect.width(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mSeekBarRect.height(), MeasureSpec.EXACTLY));
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
* {#inheritDoc}
*/
#Override
protected void dispatchDraw(final Canvas canvas) {
if (mDrawLabels) {
final int height = Math.max(mLeftTextBound.height(), mRightTextBound.height());
canvas.drawText(mLeftLabel, 0, height, mTextPaint);
canvas.drawText(mRightLabel, getMeasuredWidth() - mRightTextBound.width(), height, mTextPaint);
}
super.dispatchDraw(canvas);
}
/**
* Any layout manager that doesn't scroll will want this.
*/
#Override
public boolean shouldDelayChildPressedState() {
return false;
}
}
Result looks like the following (notice different text, but both are almost centred above bulbs):

Related

Make custom EditText numeric-only

I have a custom EditText which I got from here.
PROBLEM: This by default, opens the normal keyboard. I want to open the numeric keyboard. I tried adding inputType="number" in the XML, but then it stops showing the placeholder lines.
How can I make this open the numeric keyboard while still showing the placeholder lines ?
Also, how is it possible to set maxLength from inside the class ?
Below is the code:
public class PinEntryEditText extends android.support.v7.widget.AppCompatEditText {
public static final String XML_NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android";
private float mSpace = 10; //24 dp by default, space between the lines
private float mCharSize;
private float mNumChars = 6;
private float mLineSpacing = 8; //8dp by default, height of the text from our lines
private int mMaxLength = 6;
private int pinLength;
private OnClickListener mClickListener;
private float mLineStroke = 1; //1dp by default
private float mLineStrokeSelected = 2; //2dp by default
private Paint mLinesPaint;
int[][] mStates = new int[][]{
new int[]{android.R.attr.state_selected}, // selected
new int[]{android.R.attr.state_focused}, // focused
new int[]{-android.R.attr.state_focused}, // unfocused
};
int[] mColors = new int[]{
Color.GREEN,
Color.BLACK,
Color.GRAY
};
ColorStateList mColorStates = new ColorStateList(mStates, mColors);
public PinEntryEditText(Context context) {
super(context);
}
public PinEntryEditText(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PinEntryEditText , 0, 0);
try {
pinLength = ta.getInteger(R.styleable.PinEntryEditText_pinLength , mMaxLength);
} finally {
ta.recycle();
}
mNumChars = pinLength;
mMaxLength = pinLength;
init(context, attrs);
}
public PinEntryEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
float multi = context.getResources().getDisplayMetrics().density;
mLineStroke = multi * mLineStroke;
mLineStrokeSelected = multi * mLineStrokeSelected;
mLinesPaint = new Paint(getPaint());
mLinesPaint.setStrokeWidth(mLineStroke);
if (!isInEditMode()) {
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.colorControlActivated,
outValue, true);
final int colorActivated = outValue.data;
mColors[0] = colorActivated;
context.getTheme().resolveAttribute(R.attr.colorPrimaryDark,
outValue, true);
final int colorDark = outValue.data;
mColors[1] = colorDark;
context.getTheme().resolveAttribute(R.attr.colorControlHighlight,
outValue, true);
final int colorHighlight = outValue.data;
mColors[2] = colorHighlight;
}
setBackgroundResource(0);
mSpace = multi * mSpace; //convert to pixels for our density
mLineSpacing = multi * mLineSpacing; //convert to pixels for our density
mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", mMaxLength);
mNumChars = mMaxLength;
//Disable copy paste
super.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
public void onDestroyActionMode(ActionMode mode) {
}
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
return false;
}
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}
});
// When tapped, move cursor to end of text.
super.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
setSelection(getText().length());
if (mClickListener != null) {
mClickListener.onClick(v);
}
}
});
}
#Override
public void setOnClickListener(OnClickListener l) {
mClickListener = l;
}
#Override
public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
throw new RuntimeException("setCustomSelectionActionModeCallback() not supported.");
}
#Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
int availableWidth = getWidth() - getPaddingRight() - getPaddingLeft();
if (mSpace < 0) {
mCharSize = (availableWidth / (mNumChars * 2 - 1));
} else {
mCharSize = (availableWidth - (mSpace * (mNumChars - 1))) / mNumChars;
}
int startX = getPaddingLeft();
int bottom = getHeight() - getPaddingBottom();
//Text Width
Editable text = getText();
int textLength = text.length();
float[] textWidths = new float[textLength];
getPaint().getTextWidths(getText(), 0, textLength, textWidths);
for (int i = 0; i < mNumChars; i++) {
updateColorForLines(i == textLength);
canvas.drawLine(startX, bottom, startX + mCharSize, bottom, mLinesPaint);
if (getText().length() > i) {
float middle = startX + mCharSize / 2;
canvas.drawText(text, i, i + 1, middle - textWidths[0] / 2, bottom - mLineSpacing, getPaint());
}
if (mSpace < 0) {
startX += mCharSize * 2;
} else {
startX += mCharSize + mSpace;
}
}
}
private int getColorForState(int... states) {
return mColorStates.getColorForState(states, Color.GRAY);
}
/**
* #param next Is the current char the next character to be input?
*/
private void updateColorForLines(boolean next) {
if (isFocused()) {
mLinesPaint.setStrokeWidth(mLineStrokeSelected);
mLinesPaint.setColor(getColorForState(android.R.attr.state_focused));
if (next) {
mLinesPaint.setColor(getColorForState(android.R.attr.state_selected));
}
} else {
mLinesPaint.setStrokeWidth(mLineStroke);
mLinesPaint.setColor(getColorForState(-android.R.attr.state_focused));
}
}
}
XML:
<com.mridulahuja.kudamm.tools.PinEntryEditText
android:id="#+id/txtToken"
android:layout_width="0dp"
android:layout_height="55dp"
android:ems="10"
pin:pinLength="6"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="8dp"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginRight="15dp"
android:layout_marginEnd="15dp"
android:layout_marginLeft="15dp"
android:layout_marginStart="15dp"
app:layout_constraintLeft_toLeftOf="parent"/>
Default length is set from code:
private int mMaxLength = 6;
But this library reads value from xml too:
mMaxLength = attrs.getAttributeIntValue(XML_NAMESPACE_ANDROID, "maxLength", mMaxLength);
so you can use both approaches.
In order to make placeholders visible you should add the _ symbol like this:
android:digits="0123456789_"
May be you'll need to add some other symbols, based on your needs.

Android edittext cursor not seen despite edittext.isCursorVisible() returning true

In my xml I have already added following attributes.
<com.project.current.widget.AutoResizeEditText
android:id="#+id/edittext_add_text_new"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="#string/text_sticker_hint"
android:inputType="textNoSuggestions|textVisiblePassword|textMultiLine"
android:isScrollContainer="false"
android:padding="#dimen/dimen_margin_small"
android:textColor="#color/black"
android:textColorHint="#color/blue_popover"
android:textSize="#dimen/text_font_edit_text_default"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:background="#drawable/bg_text_sticker"
android:layout_toRightOf="#+id/resize_icon_left"
android:cursorVisible="true"
android:textCursorDrawable="#null"
android:textIsSelectable="true" />
I have set Focusable and FocusableOnTouch to true. I also have cursorVisible set to true. And just to make sure the cursor isn't taking white color(the background is whitish), I have set the textCursorDrawable to '#null'
But the cursor still isnt visible in the view. I have a touchListener implemented to the edittext (since I also need to move it around the parent layout). So I have set the keyboard to pop up at ACTION_UP. At that point I when I check
editTextViewNew.isCursorVisible()
It returns true. But the cursor is actually not visible. How do I make it visible and the text within my edittext selectable? (The long press option to 'paste' copied text also doesn't work.)
I am using the following Custom AutoResizeEditText to resize the text within editText as per my need.
public class AutoResizeEditText extends android.support.v7.widget.AppCompatEditText {
private static final int NO_LINE_LIMIT = -1;
private final RectF _availableSpaceRect = new RectF();
private final SparseIntArray _textCachedSizes = new SparseIntArray();
private final SizeTester _sizeTester;
private float _maxTextSize;
private float _spacingMult = 1.0f;
private float _spacingAdd = 0.0f;
private float _minTextSize;
private int _widthLimit;
private int _maxLines;
private boolean _enableSizeCache = true;
private boolean _initiallized = false;
private TextPaint paint;
//set Boolean for disabling auto-resize when user is resizing using controls
public static boolean isUserTyping = false;
//disable autoresizing when user is not typing text
public static void checkIfUserTyping(boolean isActive){
isUserTyping = isActive;
}
private interface SizeTester {
/**
* AutoResizeEditText
*
* #param suggestedSize
* Size of text to be tested
* #param availableSpace
* available space in which text must fit
* #return an integer < 0 if after applying {#code suggestedSize} to
* text, it takes less space than {#code availableSpace}, > 0
* otherwise
*/
public int onTestSize(int suggestedSize, RectF availableSpace);
}
public AutoResizeEditText(final Context context) {
this(context, null, 0);
}
public AutoResizeEditText(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoResizeEditText(final Context context, final AttributeSet attrs,
final int defStyle) {
super(context, attrs, defStyle);
// using the minimal recommended font size
_minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
12, getResources().getDisplayMetrics());
_maxTextSize = getTextSize();
if (_maxLines == 0)
// no value was assigned during construction
_maxLines = NO_LINE_LIMIT;
// prepare size tester:
_sizeTester = new SizeTester() {
final RectF textRect = new RectF();
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
#Override
public int onTestSize(final int suggestedSize,
final RectF availableSPace) {
paint.setTextSize(suggestedSize);
final String text = getText().toString();
final boolean singleline = getMaxLines() == 1;
if (singleline) {
textRect.bottom = paint.getFontSpacing();
textRect.right = paint.measureText(text);
} else {
final StaticLayout layout = new StaticLayout(text, paint,
_widthLimit, Layout.Alignment.ALIGN_NORMAL, _spacingMult,
_spacingAdd, true);
// return early if we have more lines
Log.d("NLN", "Current Lines = " + Integer.toString(layout.getLineCount()));
Log.d("NLN", "Max Lines = " + Integer.toString(getMaxLines()));
if (getMaxLines() != NO_LINE_LIMIT
&& layout.getLineCount() > getMaxLines())
return 1;
textRect.bottom = layout.getHeight();
int maxWidth = -1;
for (int i = 0; i < layout.getLineCount(); i++)
if (maxWidth < layout.getLineWidth(i))
maxWidth = (int) layout.getLineWidth(i);
textRect.right = maxWidth;
}
textRect.offsetTo(0, 0);
if (availableSPace.contains(textRect))
// may be too small, don't worry we will find the best match
return -1;
// else, too big
return 1;
}
};
setEnabled(true);
setFocusableInTouchMode(true);
setFocusable(true);
setEnableSizeCache(false);
setMovementMethod(null);
_initiallized = true;
}
#Override
public void setTypeface(final Typeface tf) {
if (paint == null)
paint = new TextPaint(getPaint());
paint.setTypeface(tf);
super.setTypeface(tf);
}
#Override
public void setTextSize(final float size) {
_maxTextSize = size;
_textCachedSizes.clear();
adjustTextSize();
}
#Override
public void setMaxLines(final int maxlines) {
super.setMaxLines(maxlines);
_maxLines = maxlines;
reAdjust();
}
#Override
public int getMaxLines() {
return _maxLines;
}
#Override
public void setSingleLine() {
super.setSingleLine();
_maxLines = 1;
reAdjust();
}
#Override
public void setSingleLine(final boolean singleLine) {
super.setSingleLine(singleLine);
if (singleLine)
_maxLines = 1;
else
_maxLines = NO_LINE_LIMIT;
reAdjust();
}
#Override
public void setLines(final int lines) {
super.setLines(lines);
_maxLines = lines;
reAdjust();
}
#Override
public void setTextSize(final int unit, final float size) {
final Context c = getContext();
Resources r;
if (c == null)
r = Resources.getSystem();
else
r = c.getResources();
_maxTextSize = TypedValue.applyDimension(unit, size,
r.getDisplayMetrics());
_textCachedSizes.clear();
adjustTextSize();
}
#Override
public void setLineSpacing(final float add, final float mult) {
super.setLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
/**
* Set the lower text size limit and invalidate the view
*
* #param
*/
public void setMinTextSize(final float minTextSize) {
_minTextSize = minTextSize;
reAdjust();
}
private void reAdjust() {
adjustTextSize();
}
private void adjustTextSize() {
if (!_initiallized)
return;
Log.d("IsUserTyping"," "+isUserTyping);
if(!isUserTyping)
return;
final int startSize = (int) _minTextSize;
/*final int heightLimit = getMeasuredHeight()
- getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMeasuredWidth() - getCompoundPaddingLeft()
- getCompoundPaddingRight();*/
//User maxWidth and maxHeight the textBox can attain to calculate resize values
final int heightLimit = getMaxHeight()
- getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMaxWidth() - getCompoundPaddingLeft()
- getCompoundPaddingRight();
if (_widthLimit <= 0)
return;
_availableSpaceRect.right = _widthLimit;
_availableSpaceRect.bottom = heightLimit;
super.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
efficientTextSizeSearch(startSize, (int) _maxTextSize,
_sizeTester, _availableSpaceRect));
}
/**
* Enables or disables size caching, enabling it will improve performance
* where you are animating a value inside TextView. This stores the font
* size against getText().length() Be careful though while enabling it as 0
* takes more space than 1 on some fonts and so on.
*
* #param enable
* enable font size caching
*/
public void setEnableSizeCache(final boolean enable) {
_enableSizeCache = enable;
_textCachedSizes.clear();
adjustTextSize();
}
private int efficientTextSizeSearch(final int start, final int end,
final SizeTester sizeTester, final RectF availableSpace) {
if (!_enableSizeCache)
return binarySearch(start, end, sizeTester, availableSpace);
final String text = getText().toString();
final int key = text == null ? 0 : text.length();
int size = _textCachedSizes.get(key);
if (size != 0)
return size;
size = binarySearch(start, end, sizeTester, availableSpace);
_textCachedSizes.put(key, size);
return size;
}
private int binarySearch(final int start, final int end,
final SizeTester sizeTester, final RectF availableSpace) {
int lastBest = start;
int lo = start;
int hi = end - 1;
int mid = 0;
while (lo <= hi) {
mid = lo + hi >>> 1;
final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
if (midValCmp < 0) {
lastBest = lo;
lo = mid + 1;
} else if (midValCmp > 0) {
hi = mid - 1;
lastBest = hi;
} else
return mid;
}
// make sure to return last best
// this is what should always be returned
return lastBest;
}
#Override
protected void onTextChanged(final CharSequence text, final int start,
final int before, final int after) {
super.onTextChanged(text, start, before, after);
reAdjust();
}
#Override
protected void onSizeChanged(final int width, final int height,
final int oldwidth, final int oldheight) {
_textCachedSizes.clear();
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
reAdjust();
}
}
I found these details implemented here, which was easy to emulate on my end.
android:textIsSelectable="true"
Set it to false if you wanna see your cursor
you don't have to set textCursorDrawable to #null because the color only depends on your choice of accent color
Please remove the line that has setMovementMethod from null, this will appear the cursor to change the cursor drawable you can use textCursorDrawable

How to avoid character wrap of text in TextView, for auto-resizing text?

Background
After a lot of searching for the best solution of auto-resizing TextView (according to content, size, min&max lines, and font-size restrictions), I've made a merged solution for it all, here.
NOTE: I don't use other solutions because they don't work well, each has its own issues (something isn't supported, text goes outside of TextView, text get truncated,...) .
Demonstration of it works:
https://raw.githubusercontent.com/AndroidDeveloperLB/AutoFitTextView/master/animationPreview.gif
The problem
On some cases, the last character of one line wraps to the next line, as such:
Green is the boundaries of the TextView, red is outside of it.
The code
Basically, given the size of the TextView, its min&max font size and min&max lines, and the content (text) that's supposed to be within, it finds (using binary search) what font size should fit within the boundaries of the TextView.
The code is available in Github already, but here it is just in case :
public class AutoResizeTextView extends AppCompatTextView {
private static final int NO_LINE_LIMIT = -1;
private final RectF _availableSpaceRect = new RectF();
private final SizeTester _sizeTester;
private float _maxTextSize, _spacingMult = 1.0f, _spacingAdd = 0.0f, _minTextSize;
private int _widthLimit, _maxLines;
private boolean _initialized = false;
private TextPaint _paint;
private interface SizeTester {
/**
* #param suggestedSize Size of text to be tested
* #param availableSpace available space in which text must fit
* #return an integer < 0 if after applying {#code suggestedSize} to
* text, it takes less space than {#code availableSpace}, > 0
* otherwise
*/
int onTestSize(int suggestedSize, RectF availableSpace);
}
public AutoResizeTextView(final Context context) {
this(context, null, android.R.attr.textViewStyle);
}
public AutoResizeTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public AutoResizeTextView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
// using the minimal recommended font size
_minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics());
_maxTextSize = getTextSize();
_paint = new TextPaint(getPaint());
if (_maxLines == 0)
// no value was assigned during construction
_maxLines = NO_LINE_LIMIT;
// prepare size tester:
_sizeTester = new SizeTester() {
final RectF textRect = new RectF();
#TargetApi(Build.VERSION_CODES.JELLY_BEAN)
#Override
public int onTestSize(final int suggestedSize, final RectF availableSPace) {
_paint.setTextSize(suggestedSize);
final TransformationMethod transformationMethod = getTransformationMethod();
final String text;
if (transformationMethod != null)
text = transformationMethod.getTransformation(getText(), AutoResizeTextView.this).toString();
else
text = getText().toString();
final boolean singleLine = getMaxLines() == 1;
if (singleLine) {
textRect.bottom = _paint.getFontSpacing();
textRect.right = _paint.measureText(text);
} else {
final StaticLayout layout = new StaticLayout(text, _paint, _widthLimit, Alignment.ALIGN_NORMAL, _spacingMult, _spacingAdd, true);
// return early if we have more lines
if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines())
return 1;
textRect.bottom = layout.getHeight();
int maxWidth = -1;
for (int i = 0; i < layout.getLineCount(); i++)
if (maxWidth < layout.getLineRight(i) - layout.getLineLeft(i))
maxWidth = (int) layout.getLineRight(i) - (int) layout.getLineLeft(i);
textRect.right = maxWidth;
}
textRect.offsetTo(0, 0);
if (availableSPace.contains(textRect))
// may be too small, don't worry we will find the best match
return -1;
// else, too big
return 1;
}
};
_initialized = true;
}
#Override
public void setAllCaps(boolean allCaps) {
super.setAllCaps(allCaps);
adjustTextSize();
}
#Override
public void setTypeface(final Typeface tf) {
super.setTypeface(tf);
adjustTextSize();
}
#Override
public void setTextSize(final float size) {
_maxTextSize = size;
adjustTextSize();
}
#Override
public void setMaxLines(final int maxlines) {
super.setMaxLines(maxlines);
_maxLines = maxlines;
adjustTextSize();
}
#Override
public int getMaxLines() {
return _maxLines;
}
#Override
public void setSingleLine() {
super.setSingleLine();
_maxLines = 1;
adjustTextSize();
}
#Override
public void setSingleLine(final boolean singleLine) {
super.setSingleLine(singleLine);
if (singleLine)
_maxLines = 1;
else _maxLines = NO_LINE_LIMIT;
adjustTextSize();
}
#Override
public void setLines(final int lines) {
super.setLines(lines);
_maxLines = lines;
adjustTextSize();
}
#Override
public void setTextSize(final int unit, final float size) {
final Context c = getContext();
Resources r;
if (c == null)
r = Resources.getSystem();
else r = c.getResources();
_maxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics());
adjustTextSize();
}
#Override
public void setLineSpacing(final float add, final float mult) {
super.setLineSpacing(add, mult);
_spacingMult = mult;
_spacingAdd = add;
}
/**
* Set the lower text size limit and invalidate the view
*
* #param minTextSize
*/
public void setMinTextSize(final float minTextSize) {
_minTextSize = minTextSize;
adjustTextSize();
}
private void adjustTextSize() {
// This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14
// TODO think of a nicer, elegant solution.
// post(new Runnable()
// {
// #Override
// public void run()
// {
if (!_initialized)
return;
final int startSize = (int) _minTextSize;
final int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
_widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
if (_widthLimit <= 0)
return;
_paint = new TextPaint(getPaint());
_availableSpaceRect.right = _widthLimit;
_availableSpaceRect.bottom = heightLimit;
superSetTextSize(startSize);
// }
// });
}
private void superSetTextSize(int startSize) {
int textSize = binarySearch(startSize, (int) _maxTextSize, _sizeTester, _availableSpaceRect);
super.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
}
private int binarySearch(final int start, final int end, final SizeTester sizeTester, final RectF availableSpace) {
int lastBest = start, lo = start, hi = end - 1, mid;
while (lo <= hi) {
mid = lo + hi >>> 1;
final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
if (midValCmp < 0) {
lastBest = lo;
lo = mid + 1;
} else if (midValCmp > 0) {
hi = mid - 1;
lastBest = hi;
} else return mid;
}
// make sure to return last best
// this is what should always be returned
return lastBest;
}
#Override
protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
super.onTextChanged(text, start, before, after);
adjustTextSize();
}
#Override
protected void onSizeChanged(final int width, final int height, final int oldwidth, final int oldheight) {
super.onSizeChanged(width, height, oldwidth, oldheight);
if (width != oldwidth || height != oldheight)
adjustTextSize();
}
}
The question
Why does it occur? What can I do to fix this?
Seems it's possible using support library:
<TextView
android:layout_width="250dp" android:layout_height="wrap_content" android:background="#f00"
android:breakStrategy="balanced" android:hyphenationFrequency="none"
android:text="This is an example text" android:textSize="30dp" app:autoSizeTextType="uniform"/>
Sadly, it has 2 disadvantages:
Doesn't always spread words nicely. Reported here.
Requires Android API 23 and above (here).
More information here.
I had similar problem in my project. Long-time Googling, StackOverflow (with your question among others). Nothing.
And my final "solution" was BreakIterator for words + measure them all to check this situation.
UPDATE (2018-08-10):
static public boolean isTextFitWidth(final #Nullable String source, final #NonNull BreakIterator bi, final #NonNull TextPaint paint, final int width, final float textSize)
{
if (null == source || source.length() <= 0) {
return true;
}
TextPaint paintCopy = new TextPaint();
paintCopy.set(paint);
paintCopy.setTextSize(textSize);
bi.setText(source);
int start = bi.first();
for (int end = bi.next(); BreakIterator.DONE != end; start = end, end = bi.next()) {
int wordWidth = (int)Math.ceil(paintCopy.measureText(source, start, end));
if (wordWidth > width) {
return false;
}
}
return true;
}
static public boolean isTextFitWidth(final #NonNull TextView textView, final #NonNull BreakIterator bi, final int width, final #Nullable Float textSize)
{
final int textWidth = width - textView.getPaddingLeft() - textView.getPaddingRight();
return isTextFitWidth(textView.getText().toString(), bi, textView.getPaint(), textWidth,
null != textSize ? textSize : textView.getTextSize());
}

Encounter Range-seek-bar runtime error in android

I have used this tutorial https://code.google.com/p/range-seek-bar/#Example_usage_as_Integer_range?.
Encountered runtime error of my activity stopped. Would like to seek help from you. I attempted to correct the following compile errors as listed below.
RangeSeekBar<Integer> seekBar = new RangeSeekBar<Integer>(20, 75, context);
Context cannot be resolved to a variable
I tried adding changing context to this.
Log.i(TAG, "User selected new range values: MIN=" + minValue + ", MAX=" + maxValue);
TAG cannot be resolved to a variable.
I tried adding "protected static final String TAG = null;" to main activity.
ViewGroup layout = (ViewGroup) findViewById(<your-layout-id>)
Does the layout id refer to my main_activity.xml in my layout?
Really grateful for your feedback.
MainActivity.Java
package com.example.rangeseekbargooglecode;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.ViewGroup;
import com.example.rangeseekbargooglecode.RangeSeekBar.OnRangeSeekBarChangeListener;
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String TAG = null;
// create RangeSeekBar as Integer range between 20 and 75
RangeSeekBar<Integer> seekBar = new RangeSeekBar<Integer>(20, 75, this);
seekBar.setOnRangeSeekBarChangeListener(new OnRangeSeekBarChangeListener<Integer>() {
#Override
public void onRangeSeekBarValuesChanged(RangeSeekBar<?> bar, Integer minValue, Integer maxValue) {
// handle changed range values
Log.i(TAG, "User selected new range values: MIN=" + minValue + ", MAX=" + maxValue);
}
});
// add RangeSeekBar to pre-defined layout
ViewGroup layout = (ViewGroup) findViewById(R.layout.activity_main);
layout.addView(seekBar);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<SeekBar
android:id="#+id/seekBar1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginTop="109dp" />
</RelativeLayout>
Edited Code to allow TextView to display range.
public class MainActivity extends Activity {
private TextView textview;
protected static final String TAG = "com.example.gto_doubleseekbar";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textview = (TextView) findViewById(R.id.textView1);
// create RangeSeekBar as Integer range between 20 and 75
RangeSeekBar<Integer> seekBar = new RangeSeekBar<Integer>(20, 75, this);
seekBar.setOnRangeSeekBarChangeListener(new OnRangeSeekBarChangeListener<Integer>() {
#Override
public void onRangeSeekBarValuesChanged(RangeSeekBar<?> bar, Integer minValue, Integer maxValue) {
// handle changed range values
String powerranger = "User selected new range values: MIN=" + minValue + ", MAX=" + maxValue;
Log.i(TAG, powerranger);
textview.setText(powerranger);
}
});
// add RangeSeekBar to pre-defined layout
LayoutInflater inflater = (LayoutInflater)getApplicationContext().getSystemService
(Context.LAYOUT_INFLATER_SERVICE);
ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.activity_main,null);
layout.addView(seekBar);
setContentView(layout);
}
RangeSeekBar seekBar = new RangeSeekBar(20, 75, context); 1. Context cannot be resolved to a variable
this must work:
RangeSeekBar seekBar = new RangeSeekBar(20, 75, this);
Log.i(TAG, "User selected new range values: MIN=" + minValue + ", MAX=" + maxValue); 2. TAG cannot be resolved to a variable. I tried adding "protected static final String TAG = null;" to main activity.
Don't set it to null. Usually you use the app or component name, e.g.
protected static final String TAG = "MyApp";
ViewGroup layout = (ViewGroup) findViewById() 3. Does the layout id refer to my main_activity.xml in my layout?
Use this:
LayoutInflater inflater = (LayoutInflater)getApplicationContext().getSystemService (Context.LAYOUT_INFLATER_SERVICE);
ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.activity_main,null);
layout.addView(seekBar);
setContentView(layout);
Try to implement custom RangeSeekBar.
xml file:
<com.doondoz.utility.common_function.RangeSeekBar
android:id="#+id/seekBarPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:left_index="0"
android:layout_weight="0.90" />
java file:
RangeSeekBar.java
public class RangeSeekBar extends View {
private static final int DEFAULT_HEIGHT = 70;
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_TICK_COUNT = 100;
private Thumb leftThumb, rightThumb;
private Thumb pressedThumb = null;
private SeekBar seekBar;
private Paint thumbPaint;
private OnRangeSeekBarChangerListener mListener;
private int mTickCount;
private int mLeftIndex = 0;
private int mRightIndex;
private int mThumbColor;
private int mThumbNormalRadius;
private int mThumbPressedRadius;
public RangeSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public RangeSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public RangeSeekBar(Context context) {
super(context);
}
#SuppressWarnings("deprecation")
private void init(Context context, AttributeSet attrs) {
Resources resources = getResources();
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RangeSeekBar);
try {
mTickCount = typedArray.getInteger(R.styleable.RangeSeekBar_tick_count, DEFAULT_TICK_COUNT);
mRightIndex = mTickCount - 1;
mThumbColor = typedArray.getColor(R.styleable.RangeSeekBar_thumb_color, getResources().getColor(R.color.thumb_default));
mThumbNormalRadius = typedArray.getDimensionPixelSize(R.styleable.RangeSeekBar_thumb_normal_radius, 12);
mThumbPressedRadius = typedArray.getDimensionPixelSize(R.styleable.RangeSeekBar_thumb_pressed_radius, 16);
mLeftIndex = typedArray.getInteger(R.styleable.RangeSeekBar_left_index, 0);
mRightIndex = typedArray.getInteger(R.styleable.RangeSeekBar_right_index, mRightIndex);
if (mLeftIndex < 0)
throw new IllegalArgumentException("Left index must be >= 0");
if (mRightIndex > mTickCount)
throw new IllegalArgumentException("Right index must be <= tick count");
} finally {
typedArray.recycle();
}
setUp();
}
private void setUp() {
thumbPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
thumbPaint.setStyle(Paint.Style.FILL);
thumbPaint.setColor(mThumbColor);
leftThumb = new Thumb(0, 0, thumbPaint, mThumbNormalRadius, mThumbPressedRadius);
rightThumb = new Thumb(0, 0, thumbPaint, mThumbNormalRadius, mThumbPressedRadius);
seekBar = new SeekBar(0, 0, 0, Color.BLACK, 1, mThumbColor, 3);
seekBar.setTickNumb(mTickCount);
}
public void setOnRangeBarChangeListener(OnRangeSeekBarChangerListener onRangeBarChangeListener) {
mListener = onRangeBarChangeListener;
}
/**
* Set number of ticks
*
* #param tickCount Default is 100
*/
public void setTickCount(int tickCount) {
mTickCount = tickCount;
seekBar.setTickNumb(mTickCount);
invalidate();
}
/**
* Set thumb's color
*
* #param thumbColor Default is orange
*/
public void setThumbColor(int thumbColor) {
mThumbColor = thumbColor;
thumbPaint.setColor(mThumbColor);
invalidate();
}
/**
* Set thumb's normal radius
*
* #param thumbRadius Default is 6dp
*/
public void setThumbNormalRadius(float thumbRadius) {
mThumbNormalRadius = (int) (thumbRadius*getResources().getDisplayMetrics().density);
leftThumb.radius = mThumbNormalRadius;
rightThumb.radius = mThumbNormalRadius;
invalidate();
}
/**
* Set thumb's pressed radius
*
* #param thumbPressedRadius Default is 8dp
*/
public void setThumbPressedRadius(float thumbPressedRadius) {
mThumbPressedRadius = (int) (thumbPressedRadius*getResources().getDisplayMetrics().density);
leftThumb.pressedRadius = mThumbPressedRadius;
rightThumb.pressedRadius = mThumbPressedRadius;
invalidate();
}
/**
* Set index for the Left Thumb
*
* #param leftIndex Default is 0
*/
public void setLeftIndex(int leftIndex) {
if (leftIndex < 0) {
throw new IllegalArgumentException("Left index must be >= 0");
}
mLeftIndex = leftIndex;
leftThumb.setIndex(seekBar, mLeftIndex);
invalidate();
}
/**
* Set index for the Right thumb
*
* #param rightIndex Default is 99
*/
public void setRightIndex(int rightIndex) {
if (rightIndex > mTickCount) {
throw new IllegalArgumentException("Left index must be <= tick count");
}
mRightIndex = rightIndex;
leftThumb.setIndex(seekBar, mRightIndex);
invalidate();
}
/**
* Get left index
*
* #return int
*/
public int getLeftIndex() {
return mLeftIndex;
}
/**
* Get right index
*
* #return int
*/
public int getRightIndex() {
return mRightIndex;
}
/**
* Get number of tick
*
* #return int
*/
public int getTickCount() {
return mTickCount;
}
#Override
protected synchronized void onDraw(Canvas canvas) {
seekBar.draw(canvas, leftThumb, rightThumb);
leftThumb.draw(canvas);
rightThumb.draw(canvas);
if (pressedThumb != null && pressedThumb.isAnimating) {
invalidate();
}
}
#Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height, width;
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(heightSize, DEFAULT_HEIGHT);
} else {
height = DEFAULT_HEIGHT;
}
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, DEFAULT_WIDTH);
} else {
width = DEFAULT_WIDTH;
}
setMeasuredDimension(width, height);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
pressedThumb = checkThumbPressed(eventX, eventY);
if (pressedThumb == null) {
return super.onTouchEvent(event);
}
pressedThumb.setPressed(true);
invalidate();
setPressed(true);
return true;
case MotionEvent.ACTION_MOVE:
onActionMove(eventX);
return true;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (pressedThumb == null) {
return super.onTouchEvent(event);
}
onActionUp();
break;
}
return super.onTouchEvent(event);
}
private void onActionUp() {
pressedThumb.onActionUp(seekBar);
invalidate();
}
private void onActionMove(float eventX) {
if (eventX >= seekBar.leftX && eventX <= seekBar.rightX) {
pressedThumb.x = eventX;
invalidate();
if (leftThumb.x > rightThumb.x) {
final Thumb temp = leftThumb;
leftThumb = rightThumb;
rightThumb = temp;
}
int leftIndex = seekBar.getNearestTick(leftThumb);
int rightIndex = seekBar.getNearestTick(rightThumb);
if (mLeftIndex != leftIndex || mRightIndex != rightIndex) {
mLeftIndex = leftIndex;
mRightIndex = rightIndex;
if (mListener != null) {
mListener.onIndexChange(this, mLeftIndex, mRightIndex);
}
}
}
}
private Thumb checkThumbPressed(float eventX, float eventY) {
Thumb result = null;
boolean isLeftThumbPressed = leftThumb.isInTargetZone(eventX, eventY);
boolean isRightThumbPressed = rightThumb.isInTargetZone(eventX, eventY);
if (isLeftThumbPressed && isRightThumbPressed) {
result = (eventX / getWidth() >= 0.5f) ? leftThumb : rightThumb;
} else if (isLeftThumbPressed) {
result = leftThumb;
} else if (isRightThumbPressed) {
result = rightThumb;
}
return result;
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
leftThumb.x = (getPaddingLeft() + 20 + leftThumb.normalRadius / 2);
leftThumb.y = (h + getPaddingTop() + getPaddingBottom()) / 2;
rightThumb.y = leftThumb.y;
rightThumb.x = (w - getPaddingRight() - 20 - rightThumb.normalRadius / 2);
seekBar.leftX = leftThumb.x;
seekBar.rightX = rightThumb.x;
seekBar.y = leftThumb.y;
leftThumb.setIndex(seekBar, mLeftIndex);
rightThumb.setIndex(seekBar, mRightIndex);
}
#Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
SavedState state = new SavedState(super.onSaveInstanceState());
state.tickCount = mTickCount;
state.leftIndex = mLeftIndex;
state.rightIndex = mRightIndex;
state.thumbColor = mThumbColor;
state.thumbNormalRadius = mThumbNormalRadius;
state.thumbPressedRadius = mThumbPressedRadius;
bundle.putParcelable(SavedState.STATE,state);
return bundle;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
SavedState savedState = bundle.getParcelable(SavedState.STATE);
mTickCount = savedState.tickCount;
mThumbColor = savedState.thumbColor;
mThumbNormalRadius = savedState.thumbNormalRadius;
mThumbPressedRadius = savedState.thumbPressedRadius;
mLeftIndex = savedState.leftIndex;
mRightIndex = savedState.rightIndex;
super.onRestoreInstanceState(savedState.getSuperState());
return;
}
super.onRestoreInstanceState(SavedState.EMPTY_STATE);
}
public interface OnRangeSeekBarChangerListener {
void onIndexChange(RangeSeekBar rangeBar, int leftIndex, int rightIndex);
}
static class SavedState extends BaseSavedState {
static final String STATE = "RangeSeekBar.STATE";
int tickCount;
int leftIndex;
int rightIndex;
int thumbColor;
int thumbNormalRadius;
int thumbPressedRadius;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
}
#Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
}
public static final Creator<SavedState> CREATOR =
new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}

"Mosaic" (splitted) images - Gmail's letters style

On the new versions of gmail, there is a cool imageView that shows multiple contacts images in it (link here for example) .
for example, if someone has sent me an email, i only see his image:
#######
# #
# A #
# #
#######
if i've replied to him, i can see my image next to it, but both my image and his are halved and share the same space of the imageView (and i think both have scaleType to be center crop) :
#######
# # #
# A# B#
# # #
#######
if another person has joined the conversation, it could look like this:
#######
# # B#
# A####
# # C#
#######
and if another one has joined, it could look like this:
#######
# A# C#
#######
# B# D#
#######
i'm not sure about the order of the items (and the rules, so everything here is my guess) , and what happens when more people are joining.
the important thing is that i want to know how to achieve this .
does anyone know of a solution for this? how they did it? which view was used?
it's most certainly a custom view, but what's the best way to do it? a way that is probably most efficient and doesn't use a lot of memory ...
i might even want to make the final image to be rounded, so it might be better to handle bitmaps instead of an imageView...
i'm not even sure how to call such a view. i've thought of a "CollageView" or a "MosaicView" .
just to make it clear, i think that such a problem should be handled using the next API :
public static Bitmap createMosaicOfBitmaps(int targetWidth,int targetHeight,ArrayList<Bitmap> imagesToShow)
or, if the bitmaps might take too much memory , we could use something like:
public static Bitmap createMosaicOfBitmaps(int targetWidth,int targetHeight,ArrayList<LazyBitmap> imagesToShow)
/**interface for lazy loading of a bitmap, while downscaling the bitmap to the needed size*/
public interface LazyBitmap{
public getBitmap(int width,int height);
}
i've come up with 2 solutions, each has its own advantages and disadvantages, but i still need to perform special effects on the final result (especially rounded corners, but maybe other things too ), and this is something that i don't know how to do.
can anyone please help? what do you think google has used on their app ?
EDIT: i've come up with a few possible solutions, for each i've written an answer to this thread. i'm not sure which is the best so i've posted them all . i guess each has its own advantages and disadvantages.
none of my current solutions handles a bitmap as i've offered, but they are quite intuitive...
i would still wish for some advice as to how this should be done in your opinion.
here's a solution i call:
The XML solution
it uses XML to set how the mosaicView would look like. still not as i've planned, but it might help some people who need such a thing and be able to change it the way they want.
what i've added is the ability to add custom dividers (uses IcsLinearLayout from actionBarSherlock for this) . of course, you can add whatever you wish...
here's the code:
public class MosaicView extends FrameLayout {
public static final int SHOW_DIVIDER_NONE = 0;
public static final int SHOW_DIVIDER_OUTER = 0x01;
public static final int SHOW_DIVIDER_INNER = 0x02;
private ImageView mTopLeftImageView, mTopRightImageView, mBottomRightImageView, mBottomLeftImageView;
private IcsLinearLayout mLeftContainer, mRightContainer, mMainContainer;
private int mShowDivider;
private Drawable mHorizontalDividerDrawable;
private Drawable mVerticalDividerDrawable;
public MosaicView(final Context context) {
super(context);
init(context, null, 0);
}
public MosaicView(final Context context, final AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public MosaicView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
private void init(final Context context, final AttributeSet attrs, final int defStyle) {
removeAllViews();
final LayoutInflater inflater = LayoutInflater.from(context);
inflater.inflate(R.layout.mosaic_view, this, true);
mTopLeftImageView = (ImageView) findViewById(R.id.mosaicView__topLeftImageView);
mTopRightImageView = (ImageView) findViewById(R.id.mosaicView__topRightImageView);
mBottomLeftImageView = (ImageView) findViewById(R.id.mosaicView__bottomLeftImageView);
mBottomRightImageView = (ImageView) findViewById(R.id.mosaicView__bottomRightImageView);
mLeftContainer = (IcsLinearLayout) findViewById(R.id.mosaicView__leftContainer);
mRightContainer = (IcsLinearLayout) findViewById(R.id.mosaicView__rightContainer);
mMainContainer = (IcsLinearLayout) findViewById(R.id.mosaicView__mainContainer);
//
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MosaicView, defStyle, 0);
final int attributeCount = a.getIndexCount();
for (int i = 0; i < attributeCount; i++) {
final int curAttr = a.getIndex(i);
switch (curAttr) {
case R.styleable.MosaicView_mosaicVerticalDividerDrawable:
setVerticalDividerDrawable(a.getDrawable(curAttr));
break;
case R.styleable.MosaicView_mosaicHorizontalDividerDrawable:
setHorizontalDividerDrawable(a.getDrawable(curAttr));
break;
case R.styleable.MosaicView_mosaicShowDividers:
setShowDivider(a.getInt(curAttr, SHOW_DIVIDER_NONE));
break;
}
}
a.recycle();
//
if (!isInEditMode())
resetAllImageViews();
else {
final ArrayList<Bitmap> bitmaps = new ArrayList<Bitmap>();
for (int i = 0; i < 4; ++i)
bitmaps.add(BitmapFactory.decodeResource(getResources(), android.R.drawable.sym_def_app_icon));
setImages(bitmaps);
}
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setVerticalDividerDrawable(final Drawable drawable) {
mVerticalDividerDrawable = drawable;
mMainContainer.setDividerDrawable(drawable);
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setHorizontalDividerDrawable(final Drawable drawable) {
mHorizontalDividerDrawable = drawable;
mLeftContainer.setDividerDrawable(drawable);
mRightContainer.setDividerDrawable(drawable);
}
public Drawable getVerticalDividerDrawable() {
return this.mVerticalDividerDrawable;
}
public Drawable getHorizontalDividerDrawable() {
return this.mHorizontalDividerDrawable;
}
public int getShowDivider() {
return this.mShowDivider;
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setShowDivider(final int dividers) {
mShowDivider = dividers;
int containersDividers = IcsLinearLayout.SHOW_DIVIDER_NONE;
if ((dividers & SHOW_DIVIDER_INNER) != 0)
containersDividers |= IcsLinearLayout.SHOW_DIVIDER_MIDDLE;
if ((dividers & SHOW_DIVIDER_OUTER) != 0)
containersDividers |= IcsLinearLayout.SHOW_DIVIDER_END | IcsLinearLayout.SHOW_DIVIDER_BEGINNING;
mLeftContainer.setShowDividers(containersDividers);
mRightContainer.setShowDividers(containersDividers);
mMainContainer.setShowDividers(containersDividers);
}
private void resetAllImageViews() {
mTopLeftImageView.setImageResource(0);
mTopRightImageView.setImageResource(0);
mBottomLeftImageView.setImageResource(0);
mBottomRightImageView.setImageResource(0);
mTopLeftImageView.setVisibility(View.GONE);
mTopRightImageView.setVisibility(View.GONE);
mBottomLeftImageView.setVisibility(View.GONE);
mBottomRightImageView.setVisibility(View.GONE);
mLeftContainer.setVisibility(View.GONE);
mRightContainer.setVisibility(View.GONE);
}
public void setImages(final ArrayList<Bitmap> images) {
resetAllImageViews();
if (images == null || images.size() == 0)
return;
switch (images.size()) {
case 1:
mTopLeftImageView.setImageBitmap(images.get(0));
mTopLeftImageView.setVisibility(View.VISIBLE);
mLeftContainer.setVisibility(View.VISIBLE);
break;
case 2:
mTopLeftImageView.setImageBitmap(images.get(0));
mTopRightImageView.setImageBitmap(images.get(1));
mTopLeftImageView.setVisibility(View.VISIBLE);
mTopRightImageView.setVisibility(View.VISIBLE);
mLeftContainer.setVisibility(View.VISIBLE);
mRightContainer.setVisibility(View.VISIBLE);
break;
case 3:
mTopLeftImageView.setImageBitmap(images.get(0));
mTopRightImageView.setImageBitmap(images.get(1));
mBottomRightImageView.setImageBitmap(images.get(2));
mBottomRightImageView.setVisibility(View.VISIBLE);
mTopLeftImageView.setVisibility(View.VISIBLE);
mTopRightImageView.setVisibility(View.VISIBLE);
mLeftContainer.setVisibility(View.VISIBLE);
mRightContainer.setVisibility(View.VISIBLE);
break;
default:
// TODO handle case of more than 4 images
case 4:
mTopLeftImageView.setImageBitmap(images.get(0));
mTopRightImageView.setImageBitmap(images.get(1));
mBottomRightImageView.setImageBitmap(images.get(2));
mBottomLeftImageView.setImageBitmap(images.get(3));
mBottomLeftImageView.setVisibility(View.VISIBLE);
mBottomRightImageView.setVisibility(View.VISIBLE);
mTopLeftImageView.setVisibility(View.VISIBLE);
mTopRightImageView.setVisibility(View.VISIBLE);
mLeftContainer.setVisibility(View.VISIBLE);
mRightContainer.setVisibility(View.VISIBLE);
break;
}
}
}
mosaic_view.xml:
<com.actionbarsherlock.internal.widget.IcsLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mosaicView__mainContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".MainActivity" >
<com.actionbarsherlock.internal.widget.IcsLinearLayout
android:id="#+id/mosaicView__leftContainer"
android:layout_width="0px"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" >
<ImageView
android:id="#+id/mosaicView__topLeftImageView"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:scaleType="centerCrop"
android:src="#android:drawable/sym_def_app_icon" />
<ImageView
android:id="#+id/mosaicView__bottomLeftImageView"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:scaleType="centerCrop"
android:src="#android:drawable/sym_def_app_icon" />
</com.actionbarsherlock.internal.widget.IcsLinearLayout>
<com.actionbarsherlock.internal.widget.IcsLinearLayout
android:id="#+id/mosaicView__rightContainer"
android:layout_width="0px"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" >
<ImageView
android:id="#+id/mosaicView__topRightImageView"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:scaleType="centerCrop"
android:src="#android:drawable/sym_def_app_icon" />
<ImageView
android:id="#+id/mosaicView__bottomRightImageView"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:scaleType="centerCrop"
android:src="#android:drawable/sym_def_app_icon" />
</com.actionbarsherlock.internal.widget.IcsLinearLayout>
</com.actionbarsherlock.internal.widget.IcsLinearLayout>
attr.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<declare-styleable name="MosaicView">
<attr name="mosaicVerticalDividerDrawable" format="reference" />
<attr name="mosaicHorizontalDividerDrawable" format="reference" />
<attr name="mosaicShowDividers">
<flag name="none" value="0x00" />
<flag name="outer" value="0x01" />
<flag name="inner" value="0x02" />
</attr>
</declare-styleable>
</resources>
here's a solution i like to call
the viewGroup solution
sadly it uses multiple imageViews and it doesn't have a final bitmap to mess with.
please, if anyone knows of a good way to show the images, post it.
here's the code:
public class MosaicView extends ViewGroup {
private ArrayList<Bitmap> mImages;
private ImageView[] mImageViews;
public MosaicView(final Context context) {
super(context);
}
public MosaicView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MosaicView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
public void setImages(final ArrayList<Bitmap> images) {
this.mImages = images;
removeAllViews();
mImageViews = new ImageView[Math.min(4, mImages.size())];
for (int i = 0; i < mImageViews.length; ++i) {
ImageView imageView;
imageView = mImageViews[i] = new ImageView(getContext());
imageView.setImageBitmap(mImages.get(i));
imageView.setScaleType(ScaleType.CENTER_CROP);
addView(mImageViews[i]);
}
invalidate();
}
#Override
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
if (!changed)
return;
final int width = r - l;
final int height = b - t;
if (mImageViews != null)
switch (mImageViews.length) {
case 0:
break;
case 1:
// all area
mImageViews[0].layout(0, 0, width, height);
break;
case 2:
// left
mImageViews[0].layout(0, 0, width / 2, height);
// right
mImageViews[1].layout(width / 2, 0, width, height);
break;
case 3:
// left
mImageViews[0].layout(0, 0, width / 2, height);
// right top
mImageViews[1].layout(width / 2, 0, width, height / 2);
// right bottom
mImageViews[2].layout(width / 2, height / 2, width, height);
break;
default:
// TODO think what should be done when more than 4 items should be shown
case 4:
// left top
mImageViews[0].layout(0, 0, width / 2, height / 2);
// right top
mImageViews[1].layout(width / 2, 0, width, height / 2);
// right bottom
mImageViews[2].layout(width / 2, height / 2, width, height);
// left bottom
mImageViews[3].layout(0, height / 2, width / 2, height);
break;
}
}
}
I suggest you extend ViewGroup and lay your children out like you want them in the block. I achieved something similar by doing this. You can specify parameters that will determine your layout by the amount of images in each block. Your parent will specify your children's size and position. So for example if you have 2 items you want to display in the parent, the parent will see that and measure half of the block's width for the one child and the other half for the other child, then the parent will position the children so that they are displayed correctly.
For your children you can extend ImageView and fill it with a sampled bitmap. This will reduce memory usage and you will be able to use more than one image block in your parent. If your image is downloaded I suggest you create a AsyncTask that does all the work for you and then updates the ImageView Bitmap after sampling ect is done. You can also use this task to load your images into your ImageView when using recycling in your ListView. Your children's size will obviously be determined by the parent when the onMeasure is executed in the parent.
You can then use that custom view that you created and implement it in your ListView to get the desired effect
You can have a look at this, this and this to get you started
----- EDIT -----
Here is a screen shot of the control I implemented. This isn't exactly the same but it has the same approach and principle. In this control my Parent (full screen) is your small block that contains the images and my child is (the colored blocks) is your image. Now in your child you can do anything to achieve the desired effect. You can implement onTouch events on each child, add animations to each child ect. The possibilities are endless if you implement the parent child structure correctly.
This is how I layed out my children in the ViewGroup parent in the example screenshot above
#Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childCount = getChildCount();
final int childWidth = _viewWidth;
final int childHeight = _viewHeight;
final int hPadding = (int) _paddingW; //set horizontal padding
final int vPadding = (int) _paddingH; //set vertical padding
if (childCount > 0) {
int rowTop = 0;
int rowBottom = 1;
int columnCount = 1;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int childLeft = (columnCount != 1) ? (hPadding * columnCount) + (childWidth * (columnCount-1)) : hPadding;
int childRight = (columnCount != 1) ? (hPadding * columnCount) + childWidth * columnCount : hPadding + childWidth;
int childTop = (rowTop == 0) ? vPadding : vPadding + ((childHeight + vPadding) * rowTop);
int childBottom = (rowBottom == 1) ? vPadding + childHeight : (childHeight + vPadding) * rowBottom;
child.layout(childLeft, childTop, childRight, childBottom);
if (columnCount < BLOCK_COUNT) {
columnCount++;
} else {
rowTop++;
rowBottom++;
columnCount = 1;
}
}
}
}
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int desiredWidth = 100;
int desiredHeight = 100;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
int maxHeight = 0;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredItemDimentions(width, height);
final int childWidth = _viewWidth;
final int childHeight = _viewHeight;
final int vPadding = (int) _paddingH; //set vertical padding
final int count = getChildCount();
int columnCount = 1;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
child.measure(childWidth, childHeight);
if (columnCount < BLOCK_COUNT) {
columnCount++;
} else {
maxHeight += childHeight + vPadding;
columnCount = 1;
}
}
if (count % BLOCK_COUNT != 0) maxHeight += childHeight + vPadding;
maxHeight += vPadding;
setMeasuredDimension(width, maxHeight);
}
This layout will only display 2 columns but an infinite amount of rows, so it won't work a hundred percent like you want it to, but you can use a similar approach.
Here is an example of my child
public class Block extends ViewGroup {
private static final String TAG = Block.class.getSimpleName();
private String _text;
private State _state;
private Context _context;
private int _viewWidth;
private int _viewHeight;
private int _textSize;
public enum State {
GOOD, NEAR, PASSED;
}
public Block(Context context) {
super(context);
_context = context;
_textSize = 15;
TextView tx = new TextView(context);
tx.setTextColor(context.getResources().getColor(R.color.terminal_text_color));
tx.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
tx.setGravity(Gravity.CENTER);
tx.setTypeface(null, Typeface.BOLD);
addView(tx);
TextView stateText = new TextView(context);
stateText.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
stateText.setTextSize(18);
stateText.setGravity(Gravity.CENTER);
stateText.setTextColor(context.getResources().getColor(R.color.terminal_text_color));
stateText.setGravity(Gravity.CENTER);
addView(stateText);
}
#Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childCount = getChildCount();
final int childWidth = _viewWidth;
final int childHeight = _viewHeight;
if (childCount > 0) {
TextView child = (TextView) getChildAt(0);
int padding = (int) (childWidth * 0.05);
int childLeft = padding;
int childRight = childWidth - padding;
int childTop = padding;
int childBottom = (int) (childHeight * 0.5);
if (child != null) {
child.layout(childLeft, childTop, childRight, childBottom);
child.setText(_text);
child.setTextSize(_textSize);
}
TextView stateText = (TextView) getChildAt(1);
if (stateText != null) {
stateText.layout(padding, ((int) (childHeight * 0.75)), childWidth - padding, ((int) (childHeight * 0.95)));
if (stateText != null)
switch (_state) {
case GOOD:
stateText.setBackgroundColor(_context.getResources().getColor(R.color.google_green));
stateText.setText(_context.getResources().getString(R.string.bottom_bar_legend_good));
break;
case NEAR:
stateText.setBackgroundColor(_context.getResources().getColor(R.color.google_yellow));
stateText.setText(_context.getResources().getString(R.string.bottom_bar_legend_mild));
break;
case PASSED:
stateText.setBackgroundColor(_context.getResources().getColor(R.color.google_red));
stateText.setText(_context.getResources().getString(R.string.bottom_bar_legend_passed));
break;
}
}
}
}
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
_viewWidth = widthMeasureSpec;
_viewHeight = heightMeasureSpec;
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
int padding = (int) (widthMeasureSpec * 0.05);
TextView child = (TextView) getChildAt(0);
if (child != null) child.measure(widthMeasureSpec - ((int)(widthMeasureSpec * 0.1)), heightMeasureSpec - ((int)(widthMeasureSpec * 0.5)) - padding);
TextView childLayout = (TextView) getChildAt(1);
if (childLayout != null) childLayout.measure(widthMeasureSpec - ((int)(widthMeasureSpec * 0.1)), heightMeasureSpec);
}
}
I used a ViewGroup for my child because my requirements were different than yours but you can use a simple ImageViewbecause you only want to display a manipulated bitmap. You can give your bitmap rounded corners in the child by using this method (as you mentioned in the comments).
Hope this helps
here's a solution i call:
the imageView solution
it extends from ImageView, and override its onDraw method. it works fine, but it has some disadvantages which i would be happy if anyone could improve:
it doesn't do the operations on a bitmap.
i have no idea how to perform special operations on the imageView i've extended from, such as reflection, rounded corners, etc...
it doesn't follow the suggested API that i've written, in order to conserve memory usage.
the code is here:
public class MosaicView extends ImageView {
private ArrayList<Bitmap> mImages;
private ArrayList<Rect> mImagesRects;
private final Paint mPaint = new Paint();
private Rect mTopLeftRect, mLeftRect, mWholeRect, mRightRect, mTopRightRect, mBottomLeftRect, mBottomRightRect;
private boolean mIsDirty = false;
private final Rect mCenterCropRect = new Rect();
public MosaicView(final Context context) {
super(context);
}
public MosaicView(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public MosaicView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
}
public void setImages(final ArrayList<Bitmap> images) {
this.mImages = images;
if (mImages == null)
mImagesRects = null;
else {
mImagesRects = new ArrayList<Rect>(images.size());
for (final Bitmap bitmap : images)
mImagesRects.add(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
}
mIsDirty = true;
invalidate();
}
#Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth();
final int height = getHeight();
if (mIsDirty) {
mIsDirty = false;
mTopLeftRect = new Rect(0, 0, width / 2, height / 2);
mLeftRect = new Rect(0, 0, width / 2, height);
mWholeRect = new Rect(0, 0, width, height);
mRightRect = new Rect(width / 2, 0, width, height);
mTopRightRect = new Rect(width / 2, 0, width, height / 2);
mBottomLeftRect = new Rect(0, height / 2, width / 2, height);
mBottomRightRect = new Rect(width / 2, height / 2, width, height);
}
if (mImages == null)
return;
Bitmap b;
switch (mImages.size()) {
case 0:
break;
case 1:
b = mImages.get(0);
getCenterCropRect(mImagesRects.get(0), mWholeRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mWholeRect, mPaint);
break;
case 2:
b = mImages.get(0);
getCenterCropRect(mImagesRects.get(0), mLeftRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mLeftRect, mPaint);
b = mImages.get(1);
getCenterCropRect(mImagesRects.get(1), mRightRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mRightRect, mPaint);
break;
case 3:
b = mImages.get(0);
getCenterCropRect(mImagesRects.get(0), mLeftRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mLeftRect, mPaint);
b = mImages.get(1);
getCenterCropRect(mImagesRects.get(1), mTopRightRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mTopRightRect, mPaint);
b = mImages.get(2);
getCenterCropRect(mImagesRects.get(2), mBottomRightRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mBottomRightRect, mPaint);
break;
default:
case 4:
b = mImages.get(0);
getCenterCropRect(mImagesRects.get(0), mTopLeftRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mTopLeftRect, mPaint);
b = mImages.get(1);
getCenterCropRect(mImagesRects.get(1), mTopRightRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mTopRightRect, mPaint);
b = mImages.get(2);
getCenterCropRect(mImagesRects.get(2), mBottomRightRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mBottomRightRect, mPaint);
b = mImages.get(3);
getCenterCropRect(mImagesRects.get(3), mBottomLeftRect, mCenterCropRect);
canvas.drawBitmap(b, mCenterCropRect, mBottomLeftRect, mPaint);
break;
}
}
private void getCenterCropRect(final Rect srcRect, final Rect limitRect, final Rect dstRect) {
final float scaleX = (float) srcRect.width() / limitRect.width();
final float scaleY = (float) srcRect.height() / limitRect.height();
if (scaleX >= scaleY) {
// image will fit in height, and truncate from the width
dstRect.top = srcRect.top;
dstRect.bottom = srcRect.bottom;
final float newWidth = limitRect.width() * scaleY;
dstRect.left = (int) (srcRect.width() / 2 - newWidth / 2);
dstRect.right = (int) (srcRect.width() / 2 + newWidth / 2);
} else {
// image will fit in width, and truncate from the height
dstRect.left = srcRect.left;
dstRect.right = srcRect.right;
final float newHeight = limitRect.height() * scaleX;
dstRect.top = (int) (srcRect.height() / 2 - newHeight / 2);
dstRect.bottom = (int) (srcRect.height() / 2 + newHeight / 2);
}
}
}

Categories

Resources