Android Text Centering - android

I'm trying to draw a number inside of a circle and having issues with the centering on the text. The following is the xml I'm using to display the TextView
<RelativeLayout
android:id="#+id/label"
android:layout_width="72dip"
android:layout_height="72dip"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_margin="8dip" >
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/hole_background"
android:layout_centerInParent="true"/>
<TextView
android:id="#+id/hole"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="#android:color/white"
android:textSize="56sp"
android:textStyle="bold"
android:gravity="center"/>
</RelativeLayout>
The problem is it is just slightly more towards the bottom (by about 10 pixels on the device I tested). This is a screenshot from the view hierarchy viewer that shows what I'm talking about...
I've tried combining the views and using the background such as here, or the removing the font padding like suggested here but neither worked, the font padding ending up as seen below...
I am currently am using a negative margin to adjust it to look right but that doesn't seem like the correct way to do it. Does anyone have any ideas on how to make text centered without using negative margins that I have to manually check based upon the text size?
Thanks in advance

So based upon the comments of LairdPleng and 323go I ended up just creating a custom view. The following view will do the centering exactly based upon the height of the number being drawn...
public class Label extends View {
private static final int TEXT_SIZE = 56;
private String mText;
private float mCenterX;
private float mCenterY;
private float mRadius;
private Paint mCirclePaint;
private Paint mTextPaint;
private Rect mTextBounds;
public HoleLabel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setupCanvasComponents(context);
}
public HoleLabel(Context context, AttributeSet attrs) {
super(context, attrs);
setupCanvasComponents(context);
}
public HoleLabel(Context context) {
super(context);
setupCanvasComponents(context);
}
public void setText(String text) {
if (!StringUtils.equals(mText, text)) {
mText = text;
invalidate();
}
}
private void setupCanvasComponents(Context context) {
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setColor(Color.BLACK);
mCirclePaint.setStyle(Paint.Style.FILL);
Typeface font = Typeface.createFromAsset(context.getAssets(), "fonts/CustomFont.otf");
DisplayMetrics displayMetrics = new DisplayMetrics();
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay().getMetrics(displayMetrics);
float scaledDensity = displayMetrics.scaledDensity;
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(TEXT_SIZE * scaledDensity);
mTextPaint.setTypeface(font);
mTextPaint.setTextAlign(Align.CENTER);
mTextBounds = new Rect();
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w / 2.0f;
mCenterY = h / 2.0f;
mRadius = w / 2.0f;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw the background
canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
// Draw the text
if (mText != null) {
mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
canvas.drawText(mText, mCenterX, (mCenterY + mTextBounds.height() / 2), mTextPaint);
}
}
}
And in xml...
<com.example.widget.Label
android:id="#+id/label"
android:layout_width="72dip"
android:layout_height="72dip"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_margin="8dip" />
The result being...
I had a very specific use case so I don't know if it'll work well if with less rigid size specifications but it was the way to go for me.

Would add this as a comment, but don't have enough reputation. Expanding on 323go's comment, you might find it easier to use images for your numbers, rather than spending a whole lot of time changing fonts and manually adjusting offsets - which may look fine on one dpi then look off again on another.

Related

Android textview text get cut off on the sides with custom font

This is what happens in the preview and on device:
TextView is nothing special, it just loads the custom font:
public class TestTextView extends AppCompatTextView {
public TestTextView(Context context) {
super(context);
init(context);
}
public TestTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public TestTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
void init(Context context) {
Typeface t = Typeface.createFromAsset(context.getAssets(), "fonts/daisy.ttf");
setTypeface(t);
}
}
Layout is also very basic, but just in case:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/material_red200"
android:orientation="vertical">
<*custompackage* .TestTextView
android:gravity="left"
android:padding="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="just some text for testing"
android:textColor="#color/material_black"
android:textSize="100dp" />
</LinearLayout>
As you can see, the left parts, like 'j' and 'f' are cut off.
Setting the padding or margin did not work.
This font fits into it's frame when using from other programs.
Thanks in advance.
Edit:
What #play_err_ mentioned is not a solution in my case.
I am using in the final version a textview that resizes automatically, so adding spaces would be terribly difficult.
I need an explanation why other programs (eg photoshop, after effects...) can calculate a proper bounding box and android cannot
I am also loading different fonts dynamically and I do not want to create an
if(badfont)
addSpaces()
This answer has led me to the right path:
https://stackoverflow.com/a/28625166/4420543
So, the solution is to create a custom Textview and override the onDraw method:
#Override
protected void onDraw(Canvas canvas) {
final Paint paint = getPaint();
final int color = paint.getColor();
// Draw what you have to in transparent
// This has to be drawn, otherwise getting values from layout throws exceptions
setTextColor(Color.TRANSPARENT);
super.onDraw(canvas);
// setTextColor invalidates the view and causes an endless cycle
paint.setColor(color);
System.out.println("Drawing text info:");
Layout layout = getLayout();
String text = getText().toString();
for (int i = 0; i < layout.getLineCount(); i++) {
final int start = layout.getLineStart(i);
final int end = layout.getLineEnd(i);
String line = text.substring(start, end);
System.out.println("Line:\t" + line);
final float left = layout.getLineLeft(i);
final int baseLine = layout.getLineBaseline(i);
canvas.drawText(line,
left + getTotalPaddingLeft(),
// The text will not be clipped anymore
// You can add a padding here too, faster than string string concatenation
baseLine + getTotalPaddingTop(),
getPaint());
}
}
I have encountered the same problem and i found a one liner solution for thouse who are not using the TextView.shadowLayer.
this is based on the source code that [Dmitry Kopytov] brought here:
editTextOrTextView.setShadowLayer(editTextOrTextView.textSize, 0f, 0f, Color.TRANSPARENT)
that's it, now the canvas.clipRect in TextView.onDraw() won't cut off the curly font sides.
Reworked #Dmitry Kopytov solution:
in Kotlin
recycle the old bitmap
added documentation
fall back on default TextView rendering if the bitmap cannot be created (not enough memory)
Code:
/**
* This TextView is able to draw text on the padding area.
* It's mainly used to support italic texts in custom fonts that can go out of bounds.
* In this case, you've to set an horizontal padding (or just end padding).
*
* This implementation is doing a render-to-texture procedure, as such it consumes more RAM than a standard TextView,
* it uses an additional bitmap of the size of the view.
*/
class TextViewNoClipping(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
private class NonClippableCanvas(#NonNull val bitmap: Bitmap) : Canvas(bitmap) {
override fun clipRect(left: Float, top: Float, right: Float, bottom: Float): Boolean {
return true
}
}
private var rttCanvas: NonClippableCanvas? = null
override fun onSizeChanged(width: Int, height: Int,
oldwidth: Int, oldheight: Int) {
if ((width != oldwidth || height != oldheight) && width > 0 && height > 0) {
rttCanvas?.bitmap?.recycle()
try {
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)?.let {
rttCanvas = NonClippableCanvas(it)
}
} catch (t: Throwable) {
// If for some reasons the bitmap cannot be created, we fall back on default rendering (potentially cropping the text).
rttCanvas?.bitmap?.recycle()
rttCanvas = null
}
}
super.onSizeChanged(width, height, oldwidth, oldheight)
}
override fun onDraw(canvas: Canvas) {
rttCanvas?.let {
// Clear the RTT canvas from the previous font.
it.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
// Draw on the RTT canvas (-> bitmap) that will use clipping on the NonClippableCanvas, resulting in no-clipping
super.onDraw(it)
// Finally draw the bitmap that contains the rendered text (no clipping used here, will display on top of padding)
canvas.drawBitmap(it.bitmap, 0f, 0f, null)
} ?: super.onDraw(canvas) // If rtt is not available, use default rendering process
}
}
I encountered the same problem when I used some fonts in EditText.
My first attempt was to use padding. Size of view increased but text is still cropped.
Then I looked at the source code TextView. In method onDraw method Canvas.clipRect is called to perform this crop.
My solution to bypass cropping when use padding :
1) Сreate custom class inherited from Canvas and override method clipRect
public class NonClippableCanvas extends Canvas {
public NonClippableCanvas(#NonNull Bitmap bitmap) {
super(bitmap);
}
#Override
public boolean clipRect(float left, float top, float right, float bottom) {
return true;
}
}
2) Create custom TextView and override methods onSizeChanged and onDraw.
In the method onSizeChanged create bitmap and canvas.
In the method onDraw draw on bitmap by passing our custom Canvas to method super.onDraw. Next, draw this bitmap on the target canvas.
public class CustomTextView extends AppCompatTextView {
private Bitmap _bitmap;
private NonClippableCanvas _canvas;
#Override
protected void onSizeChanged(final int width, final int height,
final int oldwidth, final int oldheight) {
if (width != oldwidth || height != oldheight) {
_bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
_canvas = new NonClippableCanvas(_bitmap);
}
super.onSizeChanged(width, height, oldwidth, oldheight);
}
#Override
protected void onDraw(Canvas canvas) {
_canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
super.onDraw(_canvas);
canvas.drawBitmap(_bitmap, 0, 0, null);
}
}
A workaround is to add a space before typing. It will save you a lot of coding but will result in a "padding" to the left.
android:text=" text after a space"
replace TextView.BufferType.SPANNABLE with TextView.BufferType.NORMAL
What if you wrap it in another layout and add padding to that? For example something like this:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<*custompackage* .TestTextView
android:gravity="left"
android:padding="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="just some text for testing"
android:textColor="#color/material_black"
android:textSize="100dp" />
</RelativeLayout>
Not having your font and other themes etc I've just tried it with the cursive font for example and on my machine it would look like this.
screenshot
Update:
Looks like you're not the only one to have had this issue and the other answers here and here both unfortunately relate to adding extra spaces.
I've created a bug ticket here since it looks like a bug to me.

Blurry image after canvas rotate, only in Android 6

I've got a custom view with the following code:
private final Drawable outerGauge;
private final Drawable innerGauge;
private float rotateX;
private float rotateY;
private int rotation = 0;
{
outerGauge = getContext().getDrawable(R.drawable.gauge_outer);
innerGauge = getContext().getDrawable(R.drawable.gauge_inner);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
outerGauge.draw(canvas);
canvas.rotate(rotation, rotateX, rotateY);
innerGauge.draw(canvas);
canvas.rotate(-rotation, rotateX, rotateY);
}
Most of the time this produces perfectly clear images. However, sometimes the result looks like this:
This only seems to happen on one of my two test devices. The device is a Motorola moto G, with the Android 6 upgrade. The other test device, which always seems to produce perfectly clear images, is an Oneplus X, Android 5. It's also not consistent, it happens sometimes, and then doesn't again the next moment. From what I've been able to test, it does not even depend on the amount of rotation applied. I've never seen it happen on straight angles though, (0, 90, 180 degrees,) and it does seem to be worse at angles closer to 45 or 135 degrees.
The image in question is an imported SVG, placed directly in the res/drawable folder. Therefore it can't be the resolution. (Also, gauge_outer is placed in exactly the same folder and made exactly the same way, though this one does not become blurry.)
Any ideas on how to solve this?
Edit:
Okay, never mind what I said about the complete inconsistency. It appears to be fully consistent, and be worst when the rotation comes closer and closer to 90 degrees. Also, as soon as the rotation is exactly 90 degrees, the indicator completely disappears.
Edit:
Behold: two emulators, one running Android 5 and one running Android 6:
The full source code is as follows:
package nl.dvandenberg.gauge;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
public class GaugeView extends View {
private static final int ORIGINAL_ROTATE_Y = 510;
private static final int ORIGINAL_IMAGE_HEIGHT = 613;
private static final int ORIGINAL_IMAGE_WIDTH = 1046;
private final Drawable outerGauge;
private final Drawable innerGauge;
private float rotateX;
private float rotateY;
private int rotation = 0;
{
outerGauge = getContext().getDrawable(R.drawable.gauge_outer);
innerGauge = getContext().getDrawable(R.drawable.gauge_inner);
}
public GaugeView(Context context) {
super(context);
setProgress(48);
}
public GaugeView(Context context, AttributeSet attrs) {
super(context, attrs);
setProgress(48);
}
public GaugeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setProgress(48);
}
public void setProgress(double percentage) {
this.rotation = (int) (180 * Math.min(100, Math.max(0, percentage)) / 100);
invalidate();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
double width = MeasureSpec.getSize(widthMeasureSpec);
double idealHeight = ORIGINAL_IMAGE_HEIGHT * width / ORIGINAL_IMAGE_WIDTH;
double height = Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec));
width = width * height / idealHeight;
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) height, MeasureSpec.getMode(heightMeasureSpec));
rotateX = (float) (width / 2f);
rotateY = (float) (height / ORIGINAL_IMAGE_HEIGHT * ORIGINAL_ROTATE_Y);
outerGauge.setBounds(0, 0, (int) width, (int) height);
innerGauge.setBounds(0, 0, (int) width, (int) height);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
outerGauge.draw(canvas);
canvas.rotate(rotation, rotateX, rotateY);
innerGauge.draw(canvas);
}
}
with drawable/gauge_inner.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1046dp"
android:height="613dp"
android:viewportWidth="1046"
android:viewportHeight="613">
<path
android:fillColor="#aa3939"
android:pathData="M142.541,516.071 C145.053,517.623,156.088,519.334,183.255,522.586
C203.832,525.024,251.438,530.676,289.03,535.184
C326.708,539.641,359.782,543.523,362.537,543.896
C365.292,544.268,388.127,547.018,413.445,550.067 L459.289,555.468
L462.946,560.401 C468.075,567.485,479.691,577.405,489.255,582.968
C499.701,589.062,520.069,594.737,531.817,594.883
C571.623,595.225,607.57,570.083,620.01,533.226
C624.956,518.592,626.123,507.412,624.269,492.201
C622.686,479.259,620.262,472.461,612.212,458.518
C602.012,440.852,592.681,431.69,575.424,422.602
C537.988,402.763,489.163,413.401,462.78,447.108 L458.957,452.086
L449.523,453.146 C444.316,453.727,420.115,456.614,395.829,459.552
C371.456,462.538,346.451,465.429,340.177,466.165
C333.904,466.9,293.067,471.772,249.427,476.991
C205.788,482.211,164.951,487.082,158.678,487.817
C144.122,489.408,139.036,491.998,136.796,498.719
C134.433,505.626,136.72,512.388,142.541,516.07 Z" />
</vector>
and drawable/gauge_outer.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1046dp"
android:height="613dp"
android:viewportWidth="1046"
android:viewportHeight="613">
<path
android:fillColor="#aa3939"
android:pathData="M488.981,0.56719 C465.882,2.06727,430.783,6.96753,412.984,11.0677
C392.285,15.768,387.285,17.0681,375.285,20.6683
C231.691,63.4706,113.696,164.376,49.898,299.183
C16.6993,369.187,0,444.491,0,523.495
C0,540.296,0.0999961,541.696,1.99992,543.596
C3.99984,545.596,5.29979,545.596,59.4977,545.596
C113.696,545.596,114.996,545.596,116.995,543.596
C118.895,541.696,118.995,540.296,118.995,522.595
C118.995,504.894,118.895,503.494,116.995,501.594
C115.095,499.694,113.695,499.594,85.2962,499.594 L55.6974,499.594
L56.2974,489.793 C60.0973,433.69,76.3966,372.387,101.396,320.384
C103.996,314.984,106.496,310.383,106.896,310.183
C107.396,309.883,110.796,311.483,114.596,313.683
C118.396,315.983,124.396,319.483,127.995,321.583
C131.595,323.583,139.195,328.083,144.994,331.484
C155.694,337.684,159.993,338.884,163.193,336.284
C164.893,334.984,171.293,324.483,177.992,312.083
C183.292,302.282,183.092,299.882,176.492,295.782
C173.992,294.282,162.593,287.582,151.093,281.081 L130.294,269.08 L135.294,261.58
C166.593,214.877,210.691,170.375,258.589,137.273
C268.189,130.673,269.889,129.873,270.489,131.273
C272.389,136.273,298.388,179.776,299.988,180.576
C300.988,181.176,302.788,181.576,303.888,181.576
C306.288,181.576,334.787,165.275,336.787,162.775
C339.187,159.575,337.987,155.575,330.887,143.274
C326.987,136.574,322.987,129.773,322.087,128.273
C321.187,126.673,318.087,121.273,315.287,116.372
C312.387,111.372,309.987,107.072,309.987,106.671
C309.987,105.371,342.586,90.7702,360.385,84.0698
C388.684,73.5692,427.382,63.5687,455.981,59.6685
C468.68,57.8684,490.98,55.5683,495.579,55.5683 L499.979,55.5683 L499.979,85.0699
C499.979,113.271,500.079,114.671,501.979,116.572
C503.879,118.472,505.279,118.572,522.978,118.572
C540.677,118.572,542.077,118.472,543.977,116.572
C545.877,114.672,545.977,113.272,545.977,84.8703 L545.977,55.2687
L555.977,55.9687 C581.776,57.5688,617.875,63.7691,644.874,71.0695
C670.273,77.9699,702.072,89.7705,722.771,99.871
C729.071,102.971,734.671,105.671,735.271,105.871
C735.871,106.071,730.171,117.072,722.172,131.072
C713.772,145.773,707.973,156.973,707.973,158.573
C707.973,162.273,709.373,163.573,718.973,169.274
C741.272,182.375,743.072,183.075,746.772,179.775
C748.472,178.375,765.571,149.773,773.871,134.373 L776.471,129.773
L787.471,137.373 C834.969,170.075,877.067,212.377,910.266,260.98
C912.866,264.78,914.866,268.28,914.766,268.78
C914.566,269.28,903.866,275.78,890.967,283.181
C878.068,290.581,866.668,297.582,865.768,298.782
C862.268,302.782,863.268,305.182,878.268,330.084
C884.168,339.785,886.468,339.885,900.967,331.484
C906.767,328.084,914.366,323.584,917.966,321.583
C921.566,319.483,927.566,315.983,931.365,313.683
C935.265,311.383,938.565,309.583,938.865,309.583
C939.565,309.583,946.665,324.184,952.164,337.084
C972.463,383.986,986.363,440.49,989.663,489.792 L990.263,499.592
L960.664,499.592 C932.265,499.592,930.865,499.692,928.965,501.592
C927.065,503.492,926.965,504.892,926.965,522.593
C926.965,540.294,927.065,541.694,928.965,543.594
C930.965,545.594,932.265,545.594,986.463,545.594
C1041.86,545.594,1041.96,545.594,1044.06,543.494
C1046.26,541.294,1046.26,540.994,1045.66,513.192
C1044.76,470.69,1040.36,436.088,1031.36,398.586
C1027.46,382.685,1026.86,380.485,1020.26,360.084
C1009.06,325.382,990.461,284.58,971.762,253.578
C923.864,174.276,855.866,108.873,775.07,64.3706
C712.572,29.8688,645.075,8.96764,574.477,2.06727
C555.278,0.16716,507.68,-0.63288,488.981,0.56719 Z" />
</vector>
Though not an answer, I have managed to find a workaround. This workaround relies on drawing the image onto a canvas, which is linked to a bitmap, which is then drawn onto the final, rotated canvas in the onDraw method.
It seems like this problem really only arises with nodpi-drawables, in other words, imported svg's. It is however, very consistent. Whether the shape is a multi-path vector or a simple square does not matter, the problem will always take exactly the same shape, with images disappearing entirely when the canvas is rotated 90°.
The full code I used to bypass this problem is as follows:
package nl.dvandenberg.energymonitor.customViews;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import nl.dvandenberg.energymonitor.R;
public class GaugeView extends View {
private static final int ORIGINAL_ROTATE_Y = 510;
private static final int ORIGINAL_IMAGE_HEIGHT = 613;
private static final int ORIGINAL_IMAGE_WIDTH = 1046;
private final Drawable outerGauge, innerGauge;
private float rotateX;
private float rotateY;
private int rotation = 0;
private Bitmap innerGaugeBitmap;
private final Canvas innerGaugeCanvas;
{
outerGauge = getContext().getDrawable(R.drawable.gauge_outer);
innerGauge = getContext().getDrawable(R.drawable.gauge_inner);
innerGaugeCanvas = new Canvas();
}
public GaugeView(Context context) {
super(context);
}
public GaugeView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public GaugeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setProgress(double percentage) {
this.rotation = (int) (180 * Math.min(100, Math.max(0, percentage)) / 100);
invalidate();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
double width = MeasureSpec.getSize(widthMeasureSpec);
double idealHeight = ORIGINAL_IMAGE_HEIGHT * width / ORIGINAL_IMAGE_WIDTH;
double height = Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec));
width = width * height / idealHeight;
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) height, MeasureSpec.getMode(heightMeasureSpec));
rotateX = (float) (width / 2f);
rotateY = (float) (height / ORIGINAL_IMAGE_HEIGHT * ORIGINAL_ROTATE_Y);
outerGauge.setBounds(0, 0, (int) width, (int) height);
innerGauge.setBounds(0, 0, (int) width, (int) height);
if (innerGaugeBitmap != null){
innerGaugeBitmap.recycle();
}
innerGaugeBitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888); // Gives LINT-warning draw-allocation, but no other way to upscale bitmaps exists.
innerGaugeCanvas.setBitmap(innerGaugeBitmap);
innerGaugeBitmap.eraseColor(Color.TRANSPARENT);
innerGauge.draw(innerGaugeCanvas);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
#Override
protected void onDraw(Canvas canvas) {
outerGauge.draw(canvas);
canvas.rotate(rotation, rotateX, rotateY);
canvas.drawBitmap(innerGaugeBitmap,0,0,null);
}
}
with the important part occuring in the onMeasure method:
if (innerGaugeBitmap != null){
innerGaugeBitmap.recycle();
}
innerGaugeBitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888); // Gives LINT-warning draw-allocation, but no other way to upscale bitmaps exists.
innerGaugeCanvas.setBitmap(innerGaugeBitmap);
innerGaugeBitmap.eraseColor(Color.TRANSPARENT);
innerGauge.draw(innerGaugeCanvas);
I have filed a bugreport at https://code.google.com/p/android/issues/detail?id=208453

Android : Semi Circle Progress Bar

I want semi circle progress bar in background of image. just like below image.
i have tried to draw using canvas but can't get success. i have also tired some custom progress bar library but result is same.
any suggestions.
looking for one time development and used in every screen size.
This can be implemented by clipping a canvas containing an image at an angle (By drawing an arc).
You can use an image something like this
And clip that image by drawing an arc.
Here is how you can do it.
//Convert the progress in range of 0 to 100 to angle in range of 0 180. Easy math.
float angle = (progress * 180) / 100;
mClippingPath.reset();
//Define a rectangle containing the image
RectF oval = new RectF(mPivotX, mPivotY, mPivotX + mBitmap.getWidth(), mPivotY + mBitmap.getHeight());
//Move the current position to center of rect
mClippingPath.moveTo(oval.centerX(), oval.centerY());
//Draw an arc from center to given angle
mClippingPath.addArc(oval, 180, angle);
//Draw a line from end of arc to center
mClippingPath.lineTo(oval.centerX(), oval.centerY());
And once you get the path, you can use clipPath function to clip the canvas in that path.
canvas.clipPath(mClippingPath);
Here is the Complete code
SemiCircleProgressBarView.java
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
public class SemiCircleProgressBarView extends View {
private Path mClippingPath;
private Context mContext;
private Bitmap mBitmap;
private float mPivotX;
private float mPivotY;
public SemiCircleProgressBarView(Context context) {
super(context);
mContext = context;
initilizeImage();
}
public SemiCircleProgressBarView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
initilizeImage();
}
private void initilizeImage() {
mClippingPath = new Path();
//Top left coordinates of image. Give appropriate values depending on the position you wnat image to be placed
mPivotX = getScreenGridUnit();
mPivotY = 0;
//Adjust the image size to support different screen sizes
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.circle);
int imageWidth = (int) (getScreenGridUnit() * 30);
int imageHeight = (int) (getScreenGridUnit() * 30);
mBitmap = Bitmap.createScaledBitmap(bitmap, imageWidth, imageHeight, false);
}
public void setClipping(float progress) {
//Convert the progress in range of 0 to 100 to angle in range of 0 180. Easy math.
float angle = (progress * 180) / 100;
mClippingPath.reset();
//Define a rectangle containing the image
RectF oval = new RectF(mPivotX, mPivotY, mPivotX + mBitmap.getWidth(), mPivotY + mBitmap.getHeight());
//Move the current position to center of rect
mClippingPath.moveTo(oval.centerX(), oval.centerY());
//Draw an arc from center to given angle
mClippingPath.addArc(oval, 180, angle);
//Draw a line from end of arc to center
mClippingPath.lineTo(oval.centerX(), oval.centerY());
//Redraw the canvas
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//Clip the canvas
canvas.clipPath(mClippingPath);
canvas.drawBitmap(mBitmap, mPivotX, mPivotY, null);
}
private float getScreenGridUnit() {
DisplayMetrics metrics = new DisplayMetrics();
((Activity)mContext).getWindowManager().getDefaultDisplay().getMetrics(metrics);
return metrics.widthPixels / 32;
}
}
And using it in any activity is very easy.
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"
tools:context=".MainActivity" >
<com.example.progressbardemo.SemiCircleProgressBarView
android:id="#+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
Note that clipPath function doesn't work if the hardware acceleration is turned on. You can turn off the hardware acceleration only for that view.
//Turn off hardware accleration
semiCircleProgressBarView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
MainActivity.java
public class MainActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SemiCircleProgressBarView semiCircleProgressBarView = (SemiCircleProgressBarView) findViewById(R.id.progress);
semiCircleProgressBarView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
semiCircleProgressBarView.setClipping(70);
}
}
As and when the progress changes you can set the progressbar by calling function,
semiCircleProgressBarView.setClipping(progress);
Ex: semiCircleProgressBarView.setClipping(50); //50% progress
semiCircleProgressBarView.setClipping(70); //70% progress
You can use your own Image to match the requirements. Hope it helps!!
Edit : To move the semi circle to bottom of the screen, change mPivotY value. Something like this
//In `SemiCircleProgressBarView.java`
//We don't get the canvas width and height initially, set `mPivoyY` inside `onWindowFocusChanged` since `getHeight` returns proper results by that time
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
mPivotX = getScreenGridUnit();
mPivotY = getHeight() - (mBitmap.getHeight() / 2);
}
You can try SeekArc Library. I know its a different kind of seekbar, but with some minor customization, you can use it for your app as a progressbar. I've done the same. You just need to change some properties like seekarc:touchInside="false".
Its fairly simple.
Now the custom implementation on my app looks somewhat like this:
img src: CleanMaster at Google Play
You can also use native ProgressBar to achieve semi circle.
Define ProgressBar like this:
<ProgressBar
android:id="#+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:max="200"
android:progress="0"
android:progressDrawable="#drawable/circular" />
Create drawable:
circular (API Level < 21):
<shape
android:innerRadiusRatio="2.3"
android:shape="ring"
android:thickness="5sp" >
<solid android:color="#color/someColor" />
</shape>
circular (API Level >= 21):
<shape
android:useLevel="true"
android:innerRadiusRatio="2.3"
android:shape="ring"
android:thickness="5sp" >
<solid android:color="#color/someColor" />
</shape>
useLevel is false by default in API Level 21.
Now since we have set max = 200, to achieve semi circle, range of the progress should be 0 to 100. You can play around with these values to achieve desired shape.
Thus use it like this:
ProgressBar progressBar = (Progressbar) view.findViewById(R.id.progressBar);
progressBar.setProgress(value); // 0 <= value <= 100
This is a view which has height equal to half its width.
Use the setters to adjust the behaviour as desired.
By default the progress is 0 and the width of the arc is 20.
Calling setProgress() will invalidate the view with the progress given.
Adding a background drawable is possible and the progress bar will be draw on top.
public class SemicircularProgressBar extends View {
private int mProgress;
private RectF mOval;
private RectF mOvalInner;
private Paint mPaintProgress;
private Paint mPaintClip;
private float ovalsDiff;
private Path clipPath;
public SemicircularProgressBar(Context context) {
super(context);
init();
}
public SemicircularProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public SemicircularProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mProgress = 0;
ovalsDiff = 20;
mOval = new RectF();
mOvalInner = new RectF();
clipPath = new Path();
mPaintProgress = new Paint();
mPaintProgress.setColor(Color.GREEN);
mPaintProgress.setAntiAlias(true);
mPaintClip = new Paint();
mPaintClip.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mPaintClip.setAlpha(0);
mPaintClip.setAntiAlias(true);
}
// call this from the code to change the progress displayed
public void setProgress(int progress) {
this.mProgress = progress;
invalidate();
}
// sets the width of the progress arc
public void setProgressBarWidth(float width) {
this.ovalsDiff = width;
invalidate();
}
// sets the color of the bar (#FF00FF00 - Green by default)
public void setProgressBarColor(int color){
this.mPaintProgress.setColor(color);
}
#Override
public void onDraw(Canvas c) {
super.onDraw(c);
mOval.set(0, 0, this.getWidth(), this.getHeight()*2);
mOvalInner.set(0+ovalsDiff, 0+ovalsDiff, this.getWidth()-ovalsDiff, this.getHeight()*2);
clipPath.addArc(mOvalInner, 180, 180);
c.clipPath(clipPath, Op.DIFFERENCE);
c.drawArc(mOval, 180, 180f * ((float) mProgress / 100), true, mPaintProgress);
}
// Setting the view to be always a rectangle with height equal to half of its width
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
this.setMeasuredDimension(parentWidth/2, parentHeight);
ViewGroup.LayoutParams params = this.getLayoutParams();
params.width = parentWidth;
params.height = parentWidth/2;
this.setLayoutParams(params);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
You can use this library :
compile 'com.github.lzyzsd:circleprogress:1.1.1'
for example :
<com.github.lzyzsd.circleprogress.DonutProgress
android:layout_marginLeft="50dp"
android:id="#+id/donut_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:donut_progress="30"/>
<com.github.lzyzsd.circleprogress.ArcProgress
android:id="#+id/arc_progress"
android:background="#214193"
android:layout_marginLeft="50dp"
android:layout_width="100dp"
android:layout_height="100dp"
custom:arc_progress="55"
custom:arc_bottom_text="MEMORY"/>
For more information see the following website :
https://github.com/lzyzsd/CircleProgress
You may be able to use this github library - circularseekbar. To achieve the half circle, you will need to manipulate the following attributes: "app:start_angle" & "app:end_angle"
More Options:
The Holo Seekbar library
Tutorial showing semi-circular seekbar link to tutorial

Getting square images like gmail app

I want to show images with alphabets like gmail app as shown in the below figure.
Are all those images are images to be kept in drawable folder or they are drawn as square shapes and then letters are drawn to them? Below is what I tried so far to do dynamically. I got just a square shape. Can someone suggest the way to achieve like in gmail app?
GradientDrawable gd = new GradientDrawable();
gd.mutate();
gd.setColor(getResources().getColor(gColors[i]));
button.setBackgroundDrawable(gd);
Update 2:
I have fixed some of the bugs and released the code as an open source library at: https://github.com/amulyakhare/TextDrawable. It also include some other features that you might want to check out.
Old Answer:
I recommend you to use the following class CharacterDrawable (just copy-paste this):
public class CharacterDrawable extends ColorDrawable {
private final char character;
private final Paint textPaint;
private final Paint borderPaint;
private static final int STROKE_WIDTH = 10;
private static final float SHADE_FACTOR = 0.9f;
public CharacterDrawable(char character, int color) {
super(color);
this.character = character;
this.textPaint = new Paint();
this.borderPaint = new Paint();
// text paint settings
textPaint.setColor(Color.WHITE);
textPaint.setAntiAlias(true);
textPaint.setFakeBoldText(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextAlign(Paint.Align.CENTER);
// border paint settings
borderPaint.setColor(getDarkerShade(color));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(STROKE_WIDTH);
}
private int getDarkerShade(int color) {
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
(int)(SHADE_FACTOR * Color.green(color)),
(int)(SHADE_FACTOR * Color.blue(color)));
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
// draw border
canvas.drawRect(getBounds(), borderPaint);
// draw text
int width = canvas.getWidth();
int height = canvas.getHeight();
textPaint.setTextSize(height / 2);
canvas.drawText(String.valueOf(character), width/2, height/2 - ((textPaint.descent() + textPaint.ascent()) / 2) , textPaint);
}
#Override
public void setAlpha(int alpha) {
textPaint.setAlpha(alpha);
}
#Override
public void setColorFilter(ColorFilter cf) {
textPaint.setColorFilter(cf);
}
#Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
Then using this is simple: new CharacterDrawable('A', 0xFF805781); by passing the character and the color value (example Color.RED or some other color in hex 0xFF805781):
ImageView imageView = (ImageView) findViewById(R.id.imageView);
CharacterDrawable drawable = new CharacterDrawable('A', 0xFF805781);
imageView.setImageDrawable(drawable);
or based on your question:
CharacterDrawable drawable = new CharacterDrawable('A', 0xFF805781);
button.setBackgroundDrawable(drawable);
The drawable will scale to fit the size of the ImageView. Result will be:
Update: Updated code for adding a border which is of darker shade (automatically picks a dark shade based on the fill color).
1) Change the value of STROKE_WIDTH based on your needs for the border thikness.
2) Change the value of SHADE_FACTOR for border darkness. If SHADE_FACTOR is small (eg. 0.2f), the border will be darker and vice versa.
Note: You can easily vary the size and font of the character
Simple thing is that you have use Linear Layout and set that background color and set TectView inside that root layout. Its Over.
You should use ColorCode Intesed of images that will good thing compare to use images in terms of loading on UI thread.
<LinearLayout
android:id="#+id/get_more"
android:layout_width="70dp" // this root layout will set your square
android:layout_height="70dp"
android:background="#654321" // set background color of square
android:orientation="horizontal" >
<TextView
android:id="#+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="24sp"
android:text="C"
android:background="#ffffff" // Text Color , set as White
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
I tweak the code a little bit..., and it works everytime even with different screen sizes. The trick is to obtain the ImageView canvas size in pixels (which sometimes is density dependent on various devices)
package net.mypapit.android.ui;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.ColorDrawable;
public class CharacterDrawable extends ColorDrawable {
private final char character;
private final Paint textPaint;
private final Paint borderPaint;
private static final int STROKE_WIDTH = 10;
private static final float SHADE_FACTOR = 0.9f;
private int mwidth, mheight;
public CharacterDrawable(char character, int color, int width, int height) {
super(color);
this.character = character;
this.textPaint = new Paint();
this.borderPaint = new Paint();
this.mwidth = width;
this.mheight = height;
// text paint settings
textPaint.setColor(Color.WHITE);
textPaint.setAntiAlias(true);
textPaint.setFakeBoldText(true);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextAlign(Paint.Align.CENTER);
// border paint settings
borderPaint.setColor(getDarkerShade(color));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(STROKE_WIDTH);
}
private int getDarkerShade(int color) {
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
(int)(SHADE_FACTOR * Color.green(color)),
(int)(SHADE_FACTOR * Color.blue(color)));
}
public void draw(Canvas canvas) {
super.draw(canvas);
// draw border
canvas.drawRect(getBounds(), borderPaint);
// draw text
int width = this.mwidth;
int height = this.mheight;
textPaint.setTextSize(height / 2);
canvas.drawText(String.valueOf(character), width/2, height/2 - ((textPaint.descent() + textPaint.ascent()) / 2) , textPaint);
}
public void setAlpha(int alpha) {
textPaint.setAlpha(alpha);
}
public void setColorFilter(ColorFilter cf) {
textPaint.setColorFilter(cf);
}
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
Then, refer back to the original Amulya Khare answer:
ImageView imageView = (ImageView) findViewById(R.id.imageView);
CharacterDrawable drawable = new CharacterDrawable('A', 0xFF805781,imageView.getWidth(),imageView.getHeight());
imageView.setImageDrawable(drawable);
It should work on different screen density by now =)

How to calculate line height containing TextView with specific height in Android?

I want to calculate the line (or layout) height (in DP) which contains only TextView as outcome of the TextView text size when using default line spacing ?
I.E. for this layout :
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<TextView
android:id="#+id/minRow1col1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textIsSelectable="false"
android:textSize="11dp" />
</LinearLayout>
What will be the layout/line height ? (any formula ? I don't need the value in run time)
Thanks !
I don't know if this help you guys, but my solution to get the height of a line it's independent of the height of the layout, just take the font metrics like this:
myTextView.getPaint().getFontMetrics().bottom - myTextView.getPaint().getFontMetrics().top)
With this you will get the line height and for me, it's works with all words ( there are some chars like "g" or "j" that take some bottom space, the so called "descender" and so on ).
Try using the TextPaint object of TextView.
TextView tv = useTextView;
String text = tv.getText().toString();
Paint textPaint = tv.getPaint();
Rect textRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), textRext);
int textHeight = textRect.height();
Per documentation of Paint#getTextBound:
Return in bounds (allocated by the caller) the smallest rectangle that
encloses all of the characters, with an implied origin at (0,0).
Using the Paint object that the TextView uses will ensure it has the same parameters set that will be used to draw the text.
You have to create custom Textview and use getActualHeight() method. Where the formula is: actualHeight=(int) ((getLineCount()-1)*getTextSize());
public class TextViewHeightPlus extends TextView {
private static final String TAG = "TextView";
private int actualHeight=0;
public int getActualHeight() {
return actualHeight;
}
public TextViewHeightPlus(Context context) {
super(context);
}
public TextViewHeightPlus(Context context, AttributeSet attrs) {
super(context, attrs);
setCustomFont(context, attrs);
}
public TextViewHeightPlus(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
actualHeight=0;
actualHeight=(int) ((getLineCount()-1)*getTextSize());
}
}
You can try Paint.getTextBounds():
String finalVal ="Hello";
Paint paint = new Paint();
paint.setTextSize(18);
paint.setTypeface(Typeface.SANS_SERIF);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.FILL);
Rect result = new Rect();
// Measure the text rectangle to get the height
paint.getTextBounds(finalVal, 0, finalVal.length(), result);
Log.d("WIDTH :", String.valueOf(result.width()));
Log.d("HEIGHT :", String.valueOf(result.height()));
Source.
For my usecase I started out by assuming that the line height is a linear function that depends on the textsize, something like:
line_height = text_size*some_constant
Now some_constant is probably a function as well, that depends on what font you use. But since in my requirements the font was static, I was able to calculate some_constant and use repeatedly in a safe way.
To clue you in a little a bit more on my usecase, I was scaling a piece of multiline text in order to fit it in a box of variable height.
In my usecase I wanted to go a step further and included the space multiplier. It was quite simple, as it was just another factor in the equation:
line_height = text_size*some_constant*spacing_multiplier
In summary if you stick to the same font and you calculate the value of some_constant once, you can (probably) get your function.
DISCLAIMER: I say "probably" a lot because I haven't tested a lot of my assumptions, but like I said, it worked for a decently complex usecase.

Categories

Resources