I have rectf which is drawn in canvas, based on this rectf I have drawn simple bubble alike. I want my text should be inside this bubble.
Here is what I have tried.
I have a textPaint which is initialized with textSize 30, and I'm building static layout like this.
staticLayout = StaticLayout.Builder.obtain(text, start, end, textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setTextDirection(TextDirectionHeuristics.LTR)
.setLineSpacing(0, 1.0f)
.setIncludePad(false)
.setEllipsizedWidth(10)
.setEllipsize(TextUtils.TruncateAt.START)
.setMaxLines(Integer.MAX_VALUE)
.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
.setJustificationMode(Layout.JUSTIFICATION_MODE_NONE)
.build()
From this static layout I'm getting my TextLayout's height as staticLayout.height
I want my text layout height should not exceed by 70% of my bounds height, so that my text will be always inside my bounds.
float maxTextLayoutHeight = bounds.height() * 0.70f;
And my actual static layout's height
height = staticLayout.height;
From these two values I recalculate text size and applied it to my text paint with following code.
annotationTextSize = (maxTextLayoutHeight / height) * 13;
annotationTextSize = annotationTextSize * 0.80f;
if (annotationTextSize < 25) annotationTextSize = 25; //Limited to min 25
if (annotationTextSize > 99) annotationTextSize = 99; //Limited to max 99
textPaint.setTextSize(annotationTextSize);
Here is some images
With small text size:
Text size looks perfect:
Text size looks bigger:
Any improved calculation will be much helpful.
Unfortunately, I don't think that you will be able to determine the size of the laid out text until it is laid out. Internally, autosized TextViews do measurements of the text to find the best fit through a binary search that lays out the text for each candidate text size. (See AppCompatTextViewAutoSizeHelper here.
You can borrow some code from the AppCompatTextViewAutoSizeHelper or use something simpler as the following although it will not be as quick:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
var textSize = 99
while (mStaticLayout == null) {
var layout: StaticLayout
textPaint.textSize = textSize.toFloat()
layout = makeStaticLayout(mText, 0, mText.length, textPaint, width)
if (layout.height < height || textSize == 25) {
mStaticLayout = layout
} else {
textSize--
}
}
mStaticLayout?.draw(canvas)
}
You could also use the internal workings of TextView to get a StaticLayout that can be drawn to a canvas.
private fun makeAutoSizeView(
#StringRes textId: Int, width: Int, height: Int
): AppCompatTextView {
return AppCompatTextView(this).apply {
layoutParams = ConstraintLayout.LayoutParams(width, height)
TextViewCompat.setAutoSizeTextTypeWithDefaults(
this,
TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM
)
text = resources.getString(textId)
}
}
private fun getAutoSizeLayout(width: Int, height: Int): StaticLayout {
val autoSizeTextView = makeAutoSizeView(R.string.good_to_see_you, width, height)
val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
autoSizeTextView.measure(widthSpec, heightSpec)
autoSizeTextView.layout(0, 0, width, height)
return autoSizeTextView.layout as StaticLayout
}
I calculated the height of text to be rendered using the following method. Might be helpful
private fun getMultilineTextHeight(text: CharSequence, textPaint: TextPaint, width: Int): Int {
val alignment = Layout.Alignment.ALIGN_NORMAL
val spacingMultiplier = 1f
val spacingAddition = 0f
val includePadding = false
val staticLayout =
StaticLayout(text, textPaint, width, alignment, spacingMultiplier, spacingAddition, includePadding)
return staticLayout.height
}
Related
I am trying to center CharSequences with Spans using StaticLayout. Everything works fine when textPaint.textAlign = Paint.Align.LEFT.
However, if I set textPaint.textAlign = Paint.Align.CENTER, everything gets wonky.
It looks as if the spanned portions are stripped out, then the "centering calc" is done, then the text is rendered.
In my code, the alignment is changed via doCenter.
override fun onDraw(canvas: Canvas) {
//:
val doCenter = true
val textWidPct = 0.90F
dpToUse = 10
val cs = clueDisplayText
val xPos: Float
if (doCenter) {
xPos = clueTextRect.exactCenterX()
textPaint.textAlign = Paint.Align.CENTER
} else {
xPos = clueTextRect.width() * ((1 - textWidPct) / 2)
textPaint.textAlign = Paint.Align.LEFT
}
textPaint.typeface = k.typefaceNormal
textPaint.textSize = j.dpToPx(dpToUse).toFloat()
textPaint.color = cc.Black
val wid = (width * textWidPct).round()
val staticLayout = StaticLayout.Builder
.obtain(cs, 0, cs.length, textPaint, wid)
.build()
val yPos = clueTextY + j.dpToPx(dpToUse)
canvas.withTranslation(xPos, yPos) {
staticLayout.draw(canvas)
}
}
One final point:
Changing
canvas.withTranslation(xPos, yPos) {
staticLayout.draw(canvas)
}
to simply
staticLayout.draw(canvas)
moves the output to the upper left, but it's just as wonky.
I've researched this and cannot find anything on point. Not here at SO, nor elsewhere on the web. I did find a couple of things talking about this same type of issue with CSS where the conclusion seemed to be "can't be done".
Am I missing something simple here? Or is there a more complex approach I need to take? Or is this not possible?
note: minSdk is 23 (M / Marshmallow)
I figured out my mistake. Decided to post for anyone who may encounter this issue.
Don't use
textPaint.textAlign = Paint.Align.CENTER
set the alignment in the StaticLayout.Builder
val staticLayout = StaticLayout.Builder
.obtain(cs, 0, cs.length, textPaint, wid)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build()
Suppose I have a single TextView like this
As you can see, the text is broken into three lines.
Is there any way that I can get the text areas in Rects? As this text is broken into three lines, I would need three Rects.
It is important to highlight that the left of a Rect is the left of the first character of the line, and the right is the right of the last character of the line.
Onik has the right idea, but the results will all be relative to zero. You will have to do a little more computation if you want to know where in the canvas the text of your TextView lies.
Here is a custom TextView that will outline the text on the screen.
class CustomTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.textViewStyle
) : AppCompatTextView(context, attrs, defStyleAttr) {
private val mPaint = Paint().apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
private val mLineOutline = Rect()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save()
// This is the view's padding which we want to ignore.
canvas.translate(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
for (line in 0 until lineCount) {
// This gets the outline of the text on a line but it is all relative to zero.
paint.getTextBounds(
text.toString(), layout.getLineStart(line), layout.getLineEnd(line), mLineOutline
)
canvas.save()
// We have the outline relative to zero, shift it so it outlines the text.
canvas.translate(layout.getLineLeft(line), layout.getLineBaseline(line).toFloat())
canvas.drawRect(mLineOutline, mPaint)
canvas.restore()
}
canvas.restore()
}
}
This is what is displayed:
You might not need this TextView, but you can grab its computations.
I find this posting very helpful when thinking about Android typography.
I'd do it as follows (in Kotlin):
var lineStart = 0
var lineEnd = 0
var lineText = ""
val paint = textView.paint
val rectList = arrayListOf<Rect>()
for (i in 0 until textView.lineCount) {
lineStart = textView.layout.getLineStart(i)
lineEnd = textView.layout.getLineEnd(i)
lineText = textView.text.substring(lineStart, lineEnd)
val rect = Rect()
paint.getTextBounds(lineText, 0, lineText.length - 1, rect)
rectList.add(rect)
}
I'm making half circle custom view on Android. However, I'm struggling to remove the un-needed bottom white space on wrap content. I think because it is drawing based on 'a full circle' calculation.
I'm sharing my Custom View implementation, as well as how I call it from my application.
See also this image:
Click here for the screenshot
Note: If I change the onMeasure size, then it will cut the upper circle:
Click here for the screenshot
class CircularProgressView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var circle = RectF()
private val paint = Paint()
private var size = 0
init {
paint.isAntiAlias = true
paint.style = Paint.Style.STROKE
paint.strokeWidth = strokeWidth.toFloat()
paint.strokeCap = Paint.Cap.BUTT
setupAttributes(attrs)
}
private fun setupAttributes(attrs: AttributeSet?) {
// TypedArray objects are shared and must be recycled.
typedArray.recycle()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawBackground(canvas)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
size = Math.min(measuredWidth, measuredHeight)
setMeasuredDimension(size, size)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val centerX = w / 2
val centerY = h / 2
// Pick the minimum value so that it can fit the container. Radius is half size
val radius = size / 2f
// Create the background and progress circle, adding dialStrokeWidth in equation so that make sure the dial can fit container
circle.top = (centerY - radius)
circle.bottom = (centerY + radius)
circle.left = (centerX - radius)
circle.right = (centerX + radius)
}
private fun drawBackground(canvas: Canvas) {
paint.shader = null
paint.color = backGroundColor
canvas.drawArc(circle, startAngle, sweepAngle, false, paint)
}
}
This is how I use it:
<com.enova.circular_progress.CircularProgressView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="#color/white"
app:backgroundColor="#color/colorHint"
app:dialColor="#color/colorPrimary"
app:foregroundColorEnd="#color/colorPrimary"
app:foregroundColorStart="#color/colorPrimaryDark"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:percent="80">
</com.enova.circular_progress.CircularProgressView>
Change your onMeasure. YOu're setting your measured width and height to the same value. If you only want to display a top half circle, then you want to set the height to half the width. Otherwise, it will think the view is the full circle in height.
Background
I have a small live wallpaper app, that I want to add support for it to show GIF animations.
For this, I've found various solutions. There is the solution of showing a GIF animation in a view (here), and there is even a solution for showing it in a live wallpaper (here).
However, for both of them, I can't find how to fit the content of the GIF animation nicely in the space it has, meaning any of the following:
center-crop - fits to 100% of the container (the screen in this case), cropping on sides (top&bottom or left&right) when needed. Doesn't stretch anything. This means the content seems fine, but not all of it might be shown.
fit-center - stretch to fit width/height
center-inside - set as original size, centered, and stretch to fit width/height only if too large.
The problem
None of those is actually about ImageView, so I can't just use the scaleType attribute.
What I've found
There is a solution that gives you a GifDrawable (here), which you can use in ImageView, but it seems it's pretty slow in some cases, and I can't figure out how to use it in LiveWallpaper and then fit it.
The main code of the LiveWallpaper GIF handling is as such (here) :
class GIFWallpaperService : WallpaperService() {
override fun onCreateEngine(): WallpaperService.Engine {
val movie = Movie.decodeStream(resources.openRawResource(R.raw.cinemagraphs))
return GIFWallpaperEngine(movie)
}
private inner class GIFWallpaperEngine(private val movie: Movie) : WallpaperService.Engine() {
private val frameDuration = 20
private var holder: SurfaceHolder? = null
private var visible: Boolean = false
private val handler: Handler = Handler()
private val drawGIF = Runnable { draw() }
private fun draw() {
if (visible) {
val canvas = holder!!.lockCanvas()
canvas.save()
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
}
override fun onVisibilityChanged(visible: Boolean) {
this.visible = visible
if (visible)
handler.post(drawGIF)
else
handler.removeCallbacks(drawGIF)
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(drawGIF)
}
override fun onCreate(surfaceHolder: SurfaceHolder) {
super.onCreate(surfaceHolder)
this.holder = surfaceHolder
}
}
}
The main code for handling GIF animation in a view is as such:
class CustomGifView #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private var gifMovie: Movie? = null
var movieWidth: Int = 0
var movieHeight: Int = 0
var movieDuration: Long = 0
var mMovieStart: Long = 0
init {
isFocusable = true
val gifInputStream = context.resources.openRawResource(R.raw.test)
gifMovie = Movie.decodeStream(gifInputStream)
movieWidth = gifMovie!!.width()
movieHeight = gifMovie!!.height()
movieDuration = gifMovie!!.duration().toLong()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(movieWidth, movieHeight)
}
override fun onDraw(canvas: Canvas) {
val now = android.os.SystemClock.uptimeMillis()
if (mMovieStart == 0L) { // first time
mMovieStart = now
}
if (gifMovie != null) {
var dur = gifMovie!!.duration()
if (dur == 0) {
dur = 1000
}
val relTime = ((now - mMovieStart) % dur).toInt()
gifMovie!!.setTime(relTime)
gifMovie!!.draw(canvas, 0f, 0f)
invalidate()
}
}
}
The questions
Given a GIF animation, how can I scale it in each of the above ways?
Is it possible to have a single solution for both cases?
Is it possible to use GifDrawable library (or any other drawable for the matter) for the live wallpaper, instead of the Movie class? If so, how?
EDIT: after finding how to scale for 2 kinds, I still need to know how to scale according to the third type, and also want to know why it keeps crashing after orientation changes, and why it doesn't always show the preview right away.
I'd also like to know what's the best way to show the GIF animation here, because currently I just refresh the canvas ~60fps (1000/60 waiting between each 2 frames), without consideration of what's in the file.
Project is available here.
If you have Glide in your project, You can easily load Gifs, as it provides drawing GIFs to your ImageViews and does support many scaling options (like center or a given width and ...).
Glide.with(context)
.load(imageUrl or resourceId)
.asGif()
.fitCenter() //or other scaling options as you like
.into(imageView);
OK I think I got how to scale the content. Not sure though why the app still crashes upon orientation change sometimes, and why the app doesn't show the preview right away sometimes.
Project is available here.
For center-inside, the code is:
private fun draw() {
if (!isVisible)
return
val canvas = holder!!.lockCanvas() ?: return
canvas.save()
//center-inside
val scale = Math.min(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
canvas.translate(x, y)
canvas.scale(scale, scale)
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
For center-crop, the code is:
private fun draw() {
if (!isVisible)
return
val canvas = holder!!.lockCanvas() ?: return
canvas.save()
//center crop
val scale = Math.max(canvas.width.toFloat() / movie.width().toFloat(), canvas.height.toFloat() / movie.height().toFloat());
val x = (canvas.width.toFloat() / 2f) - (movie.width().toFloat() / 2f) * scale;
val y = (canvas.height.toFloat() / 2f) - (movie.height().toFloat() / 2f) * scale;
canvas.translate(x, y)
canvas.scale(scale, scale)
movie.draw(canvas, 0f, 0f)
canvas.restore()
holder!!.unlockCanvasAndPost(canvas)
movie.setTime((System.currentTimeMillis() % movie.duration()).toInt())
handler.removeCallbacks(drawGIF)
handler.postDelayed(drawGIF, frameDuration.toLong())
}
for fit-center, I can use this:
val canvasWidth = canvas.width.toFloat()
val canvasHeight = canvas.height.toFloat()
val bitmapWidth = curBitmap.width.toFloat()
val bitmapHeight = curBitmap.height.toFloat()
val scaleX = canvasWidth / bitmapWidth
val scaleY = canvasHeight / bitmapHeight
scale = if (scaleX * curBitmap.height > canvas.height) scaleY else scaleX
x = (canvasWidth / 2f) - (bitmapWidth / 2f) * scale
y = (canvasHeight / 2f) - (bitmapHeight / 2f) * scale
...
Change the width and the height of the movie:
Add this code in onDraw method before movie.draw
canvas.scale((float)this.getWidth() / (float)movie.width(),(float)this.getHeight() / (float)movie.height());
or
canvas.scale(1.9f, 1.21f); //this changes according to screen size
Scale to fill and scale to fit:
There's already a good answer on that:
https://stackoverflow.com/a/38898699/5675325
I'm trying to replace menu icon with cross icon, but I don't know better solution than replacing source in ImageView and futher more I cannot find done libraries which converts images.
Any help will be appreciateg.
Android has a drawable for animating between hamburger and arrow: android.support.v7.graphics.drawable.DrawerArrowDrawable
This drawable uses very generic approach with canvas drawing. If you have some spare time and ready for some tedious work, you can animate pretty much anything by looking at this example.
For instance, here is "hamburger" to cross drawable:
/**
* Simple animating drawable between the "hamburger" icon and cross icon
*
* Based on [android.support.v7.graphics.drawable.DrawerArrowDrawable]
*/
class HamburgerCrossDrawable(
/** Width and height of the drawable (the drawable is always square) */
private val size: Int,
/** Thickness of each individual line */
private val barThickness: Float,
/** The space between bars when they are parallel */
private val barGap: Float
) : Drawable() {
private val paint = Paint()
private val thick2 = barThickness / 2.0f
init {
paint.style = Paint.Style.STROKE
paint.strokeJoin = Paint.Join.MITER
paint.strokeCap = Paint.Cap.BUTT
paint.isAntiAlias = true
paint.strokeWidth = barThickness
}
override fun draw(canvas: Canvas) {
if (progress < 0.5) {
drawHamburger(canvas)
} else {
drawCross(canvas)
}
}
private fun drawHamburger(canvas: Canvas) {
val bounds = bounds
val centerY = bounds.exactCenterY()
val left = bounds.left.toFloat() + thick2
val right = bounds.right.toFloat() - thick2
// Draw middle line
canvas.drawLine(
left, centerY,
right, centerY,
paint)
// Calculate Y offset to top and bottom lines
val offsetY = barGap * (2 * (0.5f - progress))
// Draw top & bottom lines
canvas.drawLine(
left, centerY - offsetY,
right, centerY - offsetY,
paint)
canvas.drawLine(
left, centerY + offsetY,
right, centerY + offsetY,
paint)
}
private fun drawCross(canvas: Canvas) {
val bounds = bounds
val centerX = bounds.exactCenterX()
val centerY = bounds.exactCenterY()
val crossHeight = barGap * 2 + barThickness * 3
val crossHeight2 = crossHeight / 2
// Calculate current cross position
val distanceY = crossHeight2 * (2 * (progress - 0.5f))
val top = centerY - distanceY
val bottom = centerY + distanceY
val left = centerX - crossHeight2
val right = centerX + crossHeight2
// Draw cross
canvas.drawLine(
left, top,
right, bottom,
paint)
canvas.drawLine(
left, bottom,
right, top,
paint)
}
override fun setAlpha(alpha: Int) {
if (alpha != paint.alpha) {
paint.alpha = alpha
invalidateSelf()
}
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
invalidateSelf()
}
override fun getIntrinsicWidth(): Int {
return size
}
override fun getIntrinsicHeight(): Int {
return size
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
/**
* Drawable color
* Can be animated
*/
var color: Int = 0xFFFFFFFF.toInt()
set(value) {
field = value
paint.color = value
invalidateSelf()
}
/**
* Animate this property to transition from hamburger to cross
* 0 = hamburger
* 1 = cross
*/
var progress: Float = 0.0f
set(value) {
field = value.coerceIn(0.0f, 1.0f)
invalidateSelf()
}
}
You can use this drawable like any other drawable, for example by setting ImageView src to this drawable:
imageView = AppCompatImageView(context)
addView(imageView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
hamburgerDrawable = HamburgerCrossDrawable(
size = dpToPx(20).toInt(),
barThickness = dpToPx(2),
barGap = dpToPx(5)
)
hamburgerDrawable.color = hamburgerColor
imageView.setImageDrawable(hamburgerDrawable)
To make the drawable actually change from hamburger to cross and back you'll need to change HamburgerCrossDrawable.progress (0 stands for hamburger and 1 stands for cross):
val animator = ValueAnimator.ofFloat(0.0f, 1.0f)
animator.interpolator = AccelerateDecelerateInterpolator()
animator.duration = 300
animator.addUpdateListener {
val progress = it.animatedValue as Float
val color = interpolateColor(hamburgerColor, crossColor, progress)
hamburgerDrawable.color = color
hamburgerDrawable.progress = progress
}
animator.start()
I seem to be a little late to the show, but I prefer a declarative approach with an animated selector for icon animations. It seems a lot clearer and the only thing you need to care about are the View or Button's states.
TL;DR: I've created a gist with all of the classes needed to achieve the animation.
Here's an example of the selector which you use as a drawable:
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/open"
android:drawable="#drawable/ic_drawer_closed"
android:state_selected="true"/>
<item
android:id="#+id/closed"
android:drawable="#drawable/ic_drawer"/>
<transition
android:drawable="#drawable/avd_drawer_close"
android:fromId="#id/open"
android:toId="#id/closed"/>
<transition
android:drawable="#drawable/avd_drawer_open"
android:fromId="#id/closed"
android:toId="#id/open"/>
</animated-selector>
And here's the animation itself:
You shoud use existing AnimatedVectorDrawable on the internet. Or you should create it with your icons on Shape Shifter web site. I recommend you to watch a tutrial on youtube for this process: ShapeShifter Tutorial.
Handle the state change of the button by setting a state value in the tag of the button and change the Animatable vector drawable resource according to the value of the current solution may also work.
btnMenuView.setOnClickListener {
val state = it.getTag(R.string.meta_tag_menu_button_state).toString().trim()
context?.let {
var animDrawable =
AnimatedVectorDrawableCompat.create(it, R.drawable.avd_drawer_open)
if (state != "open") {
animDrawable =
AnimatedVectorDrawableCompat.create(it, R.drawable.avd_drawer_close)
btnMenuView.setTag(R.string.meta_tag_menu_button_state, "open")
} else {
btnMenuView.setTag(R.string.meta_tag_menu_button_state, "close")
}
btnMenuView.setImageDrawable(animDrawable)
val animatable: Drawable? = btnMenuView.drawable
if (animatable is Animatable) {
animatable.start()
}
}
---Rest of your code
}