In Android, can a StaticLayout with Spans be Align.CENTER'ed? - android

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()

Related

What is the API 29 (Q) version of this line of windowManager.currentWindowMetrics.bounds.height()?

I'm trying to create a Bitmap for a paint Canvas that uses the entire screen. Currently I have the following code that kind of works:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val dw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowManager.currentWindowMetrics.bounds.width()
} else {
displayMetrics.widthPixels
}
val dh = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowManager.currentWindowMetrics.bounds.height()
} else {
displayMetrics.heightPixels
}
bitmap = Bitmap.createBitmap(dw, dh, Bitmap.Config.ARGB_8888)
canvas = Canvas(bitmap)
paint = Paint()
paint.color = Color.YELLOW
paint.strokeWidth = 15f
drawWireCanvas.setImageBitmap(bitmap)
}
Think connect-the-dots, I have the following code in a MotionEvent.ACTION_MOVE listener to create a line when a point is touched and follow the touch:
if (point1Touched == 1) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
canvas.drawLine(point1.x, point1.y, motionEvent.rawX, motionEvent.rawY, paint)
drawWireCanvas.invalidate()
}
Everything works perfectly for the >R API targets, but when I test on an Android 10 device (Q API) the start point of drawLine is off on the y axis by a bit. This problem replicates across all the different points. This issue also replicates if I use displayMetics.widthPixels on >= R API targets.
I'm pretty sure the issue lies in that displayMetics line of code but I can't find any sources online as to what the fix would be.
Thanks in advance!

Drawing a Box Around Face To Existed Photos with Google Face Detection ML Kit

We implemented Android ML Kit for face detection in Android. It works like charm, detect faces.
The problem: We want to draw rectangles around detected faces when multiple faces detected
What we have done:
Implemented
implementation 'com.google.android.gms:play-services-mlkit-face-detection:16.1.5'
Created a custom View :
class FaceView(val theContext : Context, val bounds : Rect) : View(theContext) {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val myPaint = Paint()
myPaint.color = Color.BLACK
myPaint.style = Paint.Style.STROKE
myPaint.strokeWidth = 10f
canvas?.drawRect(bounds, myPaint)
}
}
Tried to draw a rectangle to the bound we got from the face object ML kit created
val result = detector.process(image).addOnSuccessListener { faces ->
for (face in faces) {
val bounds = face.boundingBox
val view = FaceView(requireContext(), bounds)
binding.actionRoot.addView(view)
val lp : ConstraintLayout.LayoutParams =
ConstraintLayout.LayoutParams(bounds.width(),bounds.height())
lp.startToStart = binding.actionPhoto.id
lp.topToTop = binding.actionPhoto.id
lp.marginStart = bounds.right
lp.topMargin = bounds.bottom
view.layoutParams = lp
}}
Result :
How can we draw a rectangle for each face that we produced from URI(not from CameraX) and make them clickable?
you can reference the project here, but it is the java code
https://github.com/kkdroidgit/FaceDetect
-- While ML kit gives Rect of detected Faces. I think Google Developers make it more easier for us and draw rectangle automatically and provide faces as standalone bitmap objects.
For my solution :
I used the example project that #anonymous suggested to draw lines around the face.
First I got start, end, bottom and top points from provided face rect.
( This rect is created from original bitmap, not from the imageview. So the rect points are belong to the original bitmap I was using).
As Rect points are not belong to ImageView but the bitmap itself, We need to find related rect points on ImageView (or create a scaled bitmap and detect faces on it). We calculated this points as percentages in original Bitmap.
While we use original points as in the example project to draw lines. We implemented a view with Transparent Background with using calculated points to make face rectangles clickable.
-As the last thing we created bitmaps for each face.
detector.process(theImage)
.addOnSuccessListener { faces ->
val bounds = face.boundingBox
val screenWidth = GetScreenWidth().execute()
// val theImage = InputImage.fromBitmap(tempBitmap,0)
val paint = Paint()
paint.strokeWidth = 1f
paint.color = Color.RED
paint.style = Paint.Style.STROKE
val theStartPoint = if(bounds.left < 0) 0 else{ bounds.left}
val theEndPoint = if(bounds.right > tempBitmap.width) tempBitmap.width else { bounds.right}
val theTopPoint = if(bounds.top < 0) 0 else { bounds.top }
val theBottomPoint = if(bounds.bottom > tempBitmap.height) tempBitmap.height else { bounds.bottom }
val faceWidth = theEndPoint - theStartPoint
val faceHeight = theBottomPoint - theTopPoint
Log.d(Statics.LOG_TAG, "Face width : ${faceWidth} Face Height $faceHeight")
val startPointPercent = theStartPoint.toFloat() / tempBitmap.width.toFloat()
val topPointPercent = theTopPoint.toFloat() / tempBitmap.height.toFloat()
Log.d(Statics.LOG_TAG, "Face start point percent : ${startPointPercent} Face top percent $topPointPercent")
val faceWidthPercent = faceWidth / tempBitmap.width.toFloat()
val faceHeightPercent = faceHeight / tempBitmap.height.toFloat()
Log.d(Statics.LOG_TAG, "Face width percent: ${faceWidthPercent} Face Height Percent $faceHeightPercent")
val faceImage = ConstraintLayout(requireContext())
faceImage.setBackgroundColor(Color.TRANSPARENT)
binding.actionRoot.addView(faceImage)
val boxWidth = screenWidth.toFloat()*faceWidthPercent
val boxHeight = screenWidth.toFloat()*faceHeightPercent
Log.d(Statics.LOG_TAG, "Box width : ${boxWidth} Box Height $boxHeight")
val lp : ConstraintLayout.LayoutParams =
ConstraintLayout.LayoutParams(
boxWidth.toInt(),
boxHeight.toInt()
)
lp.startToStart = binding.actionPhoto.id
lp.topToTop = binding.actionPhoto.id
lp.marginStart = (screenWidth * startPointPercent).toInt()
lp.topMargin = (screenWidth * topPointPercent).toInt()
faceImage.layoutParams = lp
val theFaceBitmap = Bitmap.createBitmap(
tempBitmap,
theStartPoint,
theTopPoint,
faceWidth,
faceHeight)
if (face.trackingId != null) {
val id = face.trackingId
faceImage.setOnClickListener {
binding.actionPhoto.setImageBitmap(theFaceBitmap)
}
}
}
Result :

Android Path Not Closing, I can't fill with color

I have a custom squircle android view. I am drawing a Path but and I cannot fill the path with color.
StackOverflow is asking me to provide more detail, but I don't think I can explain this better. All I need is to fill the path with code. If you need to see the whole class let me know.
init {
val typedValue = TypedValue()
val theme = context!!.theme
theme.resolveAttribute(R.attr.colorAccentTheme, typedValue, true)
paint.color = typedValue.data
paint.strokeWidth = 6f
paint.style = Paint.Style.FILL_AND_STROKE
paint.isAntiAlias = true
shapePadding = 10f
}
open fun onLayoutInit() {
val hW = (this.measuredW / 2) - shapePadding
val hH = (this.measuredH / 2) - shapePadding
/*
Returns a series of Vectors along the path
of the squircle
*/
points = Array(360) { i ->
val angle = toRadians(i.toDouble())
val x = pow(abs(cos(angle)), corners) * hW * sgn(cos(angle))
val y = pow(abs(sin(angle)), corners) * hH * sgn(sin(angle))
Pair(x.toFloat(), y.toFloat())
}
/*
Match the path to the points
*/
for (i in 0..points.size - 2) {
val p1 = points[i]
val p2 = points[i + 1]
path.moveTo(p1.first, p1.second)
path.lineTo(p2.first, p2.second)
}
/*
Finish closing the path's points
*/
val fst = points[0]
val lst = points[points.size - 1]
path.moveTo(lst.first, lst.second)
path.lineTo(fst.first, fst.second)
path.fillType = Path.FillType.EVEN_ODD
path.close()
postInvalidate()
}
override fun onDraw(canvas: Canvas?) {
canvas?.save()
canvas?.translate(measuredW / 2, measuredH / 2)
canvas?.drawPath(path, paint)
canvas?.restore()
super.onDraw(canvas)
}
}
The path stroke works fine, but I can't fill it with color.

How to fit content of GIF animation in View and in live wallpaper?

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

Call getMeasuredWidth() or getWidth() for RecyclerView return 0 on data binding

I'm using data binding to setup a RecyclerView. Here is the binding adapter:
fun setRecyclerDevices(recyclerView: RecyclerView, items: List<Device>, itemBinder: MultipleTypeItemBinder,
listener: BindableListAdapter.OnClickListener<Device>?) {
var adapter = recyclerView.adapter as? DevicesBindableAdapter
if (adapter == null) {
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
recyclerView.addItemDecoration(SpaceItemDecorator(left = 15, top = 15, right = 15, bottom = 15))
adapter = DevicesBindableAdapter(items, itemBinder)
adapter.setOnClickListener(listener)
recyclerView.adapter = adapter
} else {
adapter.setOnClickListener(listener)
adapter.setItemBinder(itemBinder)
adapter.setItems(items)
}
}
getSpanSizeFromScreenWidth needs the recycler's width to do some calculation. But it always returns 0.
I tried to apply a ViewTreeObserver like this:
recyclerView.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
recyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
}
})
Or use post like this:
recyclerView.post({
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
})
Code of getSpanSizeFormScreenWidth:
private fun getSpanSizeFromScreenWidth(context: Context, recyclerView: RecyclerView): Int {
val availableWidth = recyclerView.width.toFloat()
val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300f, context.resources.displayMetrics)
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 15f, context.resources.displayMetrics)
return Math.max(1, Math.floor((availableWidth / (px + margin)).toDouble()).toInt()) * DevicesBindableAdapter.WIDTH_UNIT_VALUE
}
But it still returns 0 despite my RecyclerView being displayed on the screen (not 0).
Any ideas?
In inspecting the code, it appears that your RecyclerView may actually be fine, but your logic in getSpanSizeFromScreenWidth may not be.
It looks like this: Math.floor((availableWidth / (px + margin)).toDouble()).toInt() will always be 0 when availableWidth is less than (px + margin). This will then cause getSpanSizeFromScreenWidth to return 0.
Breaking it down:
Math.floor - rounds a double down to a whole number
availableWidth / (px + margin) - will be a low number (a fraction of availableWidth)
Therefore, you're going to get 0 at times especially on smaller screens and/or smaller density screens.
Does that make sense? May not be this issue, but I'd start there. It's hard to tell you exactly the issue without knowing the whole context, but that's likely your issue.
If that is not your issue, could you say what your value is for availableWidth, px, and margin during execution?

Categories

Resources