How to draw border on BottomNavigation MenuItem Icon? - android

I am developing an android app using:
Jetpack Lifecycle (ViewModel)
Jetpack Navigation
Coil (Image Loader)
I am trying to customize the BottomNavigationMenu.
But one thing is very hard...
The last tab is the User's profile Image with Border.
If the user's profile image background color is white, then the ui is weird.
So I should show the border.
class MainActivity {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initBottomNav(binding.bottomNav)
vm.initProfileBottomIcon()
}
private fun initBottomNav(bottomNav: BottomNavigationView) {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
bottomNav.setupWithNavController(navHostFragment.navController)
bottomNav.itemIconTintList = null
vm.profileImgUrl.observe(this) { url ->
bottomNav.menu.findItem(R.id.profileFragment).load(this, url) {
transformations(CircleCropTransformation())
}
}
}
}
This code draws the profile image on the BottomNavigationMenu.
But not draw the border.
When I googling, there is no support for CircleCrop with Border on Coil (even Glide).
So I tried the below code, But it doesn't work well..
vm.profileImg.observe(this) { imgBitmap ->
val layerBorder = ResourcesCompat.getDrawable(resources, R.drawable.oval_trans_border1_red, null)
val layerIcon = BitmapDrawable(Resources.getSystem(), imgBitmap)
val layerDrawable = LayerDrawable(arrayOf(layerIcon, layerBorder))
val bottomNavProfile = bottomNav.menu.findItem(R.id.profileFragment)
val request = ImageRequest.Builder(this)
.data(layerDrawable)
.target {
bottomNavProfile.icon = it
}
.apply {
transformations(CircleCropTransformation())
}
.build()
imageLoader.enqueue(request)
}
Somebody help me please?

You can write your own Transformation with border like this:
class BorderedCircleCropTransformation(
private val borderSize: Float = 0f,
#ColorInt private val borderColor: Int = Color.BLUE
) : Transformation {
override fun key(): String = BorderedCircleCropTransformation::class.java.name
override suspend fun transform(pool: BitmapPool, input: Bitmap, size: Size): Bitmap {
val borderOffset = (borderSize * 2).toInt()
val halfWidth = input.width / 2
val halfHeight = input.height / 2
val circleRadius = Math.min(halfWidth, halfHeight).toFloat()
val newBitmap = Bitmap.createBitmap(
input.width + borderOffset,
input.height + borderOffset,
Bitmap.Config.ARGB_8888
)
// Center coordinates of the image
val centerX = halfWidth + borderSize
val centerY = halfHeight + borderSize
val paint = Paint()
val canvas = Canvas(newBitmap).apply {
// Set transparent initial area
drawARGB(0, 0, 0, 0)
}
// Draw the transparent initial area
paint.isAntiAlias = true
paint.style = Paint.Style.FILL
canvas.drawCircle(centerX, centerY, circleRadius, paint)
// Draw the image
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(input, borderSize, borderSize, paint)
// Draw the createBitmapWithBorder
paint.xfermode = null
paint.style = Paint.Style.STROKE
paint.color = borderColor
paint.strokeWidth = borderSize
canvas.drawCircle(centerX, centerY, circleRadius, paint)
return newBitmap
}
override fun equals(other: Any?) = other is BorderedCircleCropTransformation
override fun hashCode() = javaClass.hashCode()
override fun toString() = "BorderedCircleCropTransformation()"
private companion object {
val XFERMODE = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
}
}
internal val Bitmap.safeConfig: Bitmap.Config
get() = config ?: Bitmap.Config.ARGB_8888
then pass it as a transformation to coil and it will draw what you want.
I use this code for drawing border.

Related

How can I create a shape and change its alpha in a specific place on a surface?

Let's say i have a surface and my background color is Red;
Surface(modifier = Modifier.fillMaxSize().alpha(0.1f)){}
How can i create a shape (example: Rectangle) on surface like hole so i can see the background color not with alpha 0.1f, with alpha 1.0f from inside this rectangle shape?
i want it for tutorial screen in my app, i am open to any idea except my example.
Example of my goal;
https://id.pinterest.com/pin/353814114449134862/
I implemented a sample which can give you a kick-off in your implementation:
#Composable
fun CanvasWithHole(
holeXPosition: Float,
holeYPosition: Float,
holeRadius: Float
) {
androidx.compose.foundation.Canvas(
modifier = Modifier.fillMaxSize(),
onDraw = {
drawIntoCanvas { canvas ->
val w = drawContext.size.width
val h = drawContext.size.height
drawImageWithHole(
canvas.nativeCanvas,
w, h, holeXPosition, holeYPosition, holeRadius,
)
}
}
)
}
fun drawImageWithHole(
canvas: Canvas,
w: Float,
h: Float,
holeXPosition: Float,
holeYPosition: Float,
holeRadius: Float
) {
val bitmap = Bitmap.createBitmap(
w.toInt(), h.toInt(), Bitmap.Config.ARGB_8888
).apply {
eraseColor(Color.TRANSPARENT)
}
val canvasBitmap = Canvas(bitmap)
val eraser = Paint().apply {
color = Color.TRANSPARENT
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
canvasBitmap.drawColor(Color.parseColor("#cc328fa8"))
canvasBitmap.drawCircle(holeXPosition, holeYPosition, holeRadius, eraser)
canvas.drawBitmap(bitmap, 0f, 0f, null)
}
And here is how you can use it:
#Composable
fun MyScreen() {
Box {
ContentScreen()
CanvasWithHole(
100f,
100f,
400f,
)
}
}
Here's the result:

How to draw rounded corner polygons in Jetpack Compose Canvas?

I'm trying to create a rounded triangle using Canvas in Jetpack Compose.
I try this code for drawing triangle:
#Composable
fun RoundedTriangle() {
Canvas(modifier = Modifier.size(500.dp)) {
val trianglePath = Path().apply {
val height = size.height
val width = size.width
moveTo(width / 2.0f, 0f)
lineTo(width, height)
lineTo(0f, height)
}
drawPath(trianglePath, color = Color.Blue)
}
}
But I don't know how to round the triangle corners. I also tried to use arcTo, but I was unable to get a suitable result.
How can I draw something like the figure below?
For Stroke you can specify rounding like this:
drawPath(
...
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.cornerPathEffect(4.dp.toPx())
)
)
Yet Fill seems lack of support rounding. I've created a feature request, please star it.
But Canvas has drawOutline function, which accepts both Outline, which can wrap a Path, and Paint, for which you can specify pathEffect:
Canvas(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) {
val rect = Rect(Offset.Zero, size)
val trianglePath = Path().apply {
moveTo(rect.topCenter)
lineTo(rect.bottomRight)
lineTo(rect.bottomLeft)
close()
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(trianglePath),
paint = Paint().apply {
color = Color.Black
pathEffect = PathEffect.cornerPathEffect(rect.maxDimension / 3)
}
)
}
}
Path helpers:
fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y)
fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)
Result:
Based on #philip-dukhov answer, if anyone is interested in appliying this to a square
#Composable
fun SquirclePath(
modifier: Modifier,
smoothingFactor: Int = 60,
color: Color,
strokeWidth: Float,
) {
Canvas(
modifier = modifier
) {
val rect = Rect(Offset.Zero, size)
val percent = smoothingFactor.percentOf(rect.minDimension)
val squirclePath = Path().apply {
with(rect) {
lineTo(topRight)
lineTo(bottomRight)
lineTo(bottomLeft)
lineTo(topLeft)
// this is where the path is finally linked together
close()
}
}
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(squirclePath),
paint = Paint().apply {
this.color = color
this.style = PaintingStyle.Fill
this.strokeWidth = strokeWidth
pathEffect = PathEffect.cornerPathEffect(percent)
}
)
}
}
}
fun Int.percentOf(target:Float) = (this.toFloat() / 100) * target

Android Compose - How to tile/repeat a bitmap/vector?

What is the Android Compose approach to tile an image to fill my background with a small pattern?
A naive approach for Bitmaps without rotation could be like this:
#Composable
fun TileImage() {
val pattern = ImageBitmap.imageResource(R.drawable.pattern_bitmap)
Canvas(modifier = Modifier.fillMaxSize()) {
// rotate(degrees = -15f) { // The rotation does not produce the desired effect
val totalWidth = size.width / pattern.width
val totalHeight = size.height / pattern.height
var x = 0f
var y = 0f
for (i in 0..totalHeight.toInt()) {
y = (i * pattern.height).toFloat()
for (j in 0..totalWidth.toInt()) {
x = (j * pattern.width).toFloat()
drawImage(
pattern,
colorFilter = giftColorFilter,
topLeft = Offset(x, y)
)
}
}
// }
}
}
In Android XML you can easily create XML to repeat a bitmap
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="#drawable/pattern_bitmap"
android:tileMode="repeat" />
Or if you need to tile a vector you can use a custom Drawable class to achieve your goal
TileDrawable(AppCompatResources.getDrawable(context, R.drawable.pattern_vector), Shader.TileMode.REPEAT)
class TileDrawable(drawable: Drawable, tileMode: Shader.TileMode, private val angle: Float? = null) : Drawable() {
private val paint: Paint = Paint().apply {
shader = BitmapShader(getBitmap(drawable), tileMode, tileMode)
}
override fun draw(canvas: Canvas) {
angle?.let {
canvas.rotate(it)
}
canvas.drawPaint(paint)
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun getOpacity() = PixelFormat.TRANSLUCENT
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
private fun getBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bmp = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val c = Canvas(bmp)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(c)
return bmp
}
}
If you want to use native canvas you can do something like this in jetpack compose.
Canvas(
modifier = Modifier
.fillMaxSize()
) {
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
shader = ImageShader(pattern, TileMode.Repeated, TileMode.Repeated)
}
drawIntoCanvas {
it.nativeCanvas.drawPaint(paint)
}
paint.reset()
}
And If you want to limit your repetition to a certain height and width you can use the clip modifier in canvas like below otherwise it will fill the entire screen.
Canvas(
modifier = Modifier
.width(300.dp)
.height(200.dp)
.clip(RectangleShape)
) {
----
}
Based on Rafiul's answer, I was able to come up with something a bit more succinct. Here's hoping Compose comes up with something built-in to make this simpler in the future.
val image = ImageBitmap.imageResource(R.drawable.my_image)
val brush = remember(image) { ShaderBrush(ImageShader(image, TileMode.Repeated, TileMode.Repeated)) }
Box(Modifier
.fillMaxSize()
.background(brush)) {
}

How to draw a Button like rectangle with text in Android Canvas?

In Android app , I want to draw a button in canvas , like the image exactly the below image
I tried some stack overflow answers but can't get the exact output , particularly the light shadow below . My worst code is below
val corners = floatArrayOf(
80f, 80f, // Top left radius in px
80f, 80f, // Top right radius in px
0f, 0f, // Bottom right radius in px
0f, 0f // Bottom left radius in px
)
val path = Path()
val rect = RectF(550f, 500f, 100f, 300f)
paint.style = Paint.Style.FILL;
paint.color = Color.WHITE;
path.addRoundRect(rect, corners, Path.Direction.CW)
canvas?.drawPath(path, paint)
paint.style = Paint.Style.STROKE;
paint.color = Color.BLACK;
path.addRoundRect(rect, corners, Path.Direction.CW)
canvas?.drawPath(path, paint)
paint.setColor(Color.RED)
paint.setStyle(Paint.Style.FILL)
val paint2 = Paint()
paint2.setColor(Color.GREEN)
paint2.setTextSize(50f) //set text size
val w: Float = paint2.measureText("VIEW CAMPSITE MAP") / 2
val textSize: Float = paint2.textSize
canvas?.drawText("VIEW CAMPSITE MAP", 300f, 300f ,paint2);
Also I can't set text in correct position
I also need a gradient to show bulging state like a real material button
And also need an click listener in the canvas / paint
I need it in pure canvas and paint not in any views
please help me
Here is one of the ways. Dont forget to move all variables like text, textSize, etc in custom attributes. Also I didnt get what do you mean by "click listener in the canvas"
class CustomView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
//constants. move them all to custom attributes
private val boxRadius = convertDpToPixel(10f)
private val boxColor = Color.parseColor("#52618e")
private val boxBackgroundColor = Color.WHITE
private val boxShadowSize = convertDpToPixel(2f)
private val boxStrokeWidth = convertDpToPixel(1f)
private val textColor = Color.parseColor("#21a207")
private val fontSize = convertDpToPixel(30f)
private val text = "View Campsite Plan"
private lateinit var boxShadow: RectF
private lateinit var boxBackground: RectF
private lateinit var boxShadowPaint: Paint
private lateinit var boxBackgroundPaint: Paint
private var textWidth = 0f
private var textSmallGlyphHeight = 0f
private lateinit var textPaint: Paint
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
boxShadow = RectF(0f, 0f, w.toFloat(), h.toFloat())
boxBackground = RectF(boxStrokeWidth, boxStrokeWidth,
w.toFloat()-boxStrokeWidth, h.toFloat()-boxStrokeWidth-boxShadowSize)
boxShadowPaint = Paint().apply { color = boxColor }
boxBackgroundPaint = Paint().apply { color = boxBackgroundColor }
textPaint = Paint().apply {
color = textColor
textSize = fontSize
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
textWidth = measureText(text)
textSmallGlyphHeight = fontMetrics.run { ascent + descent }
}
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawRoundRect(boxShadow, boxRadius, boxRadius, boxShadowPaint)
canvas?.drawRoundRect(boxBackground, boxRadius, boxRadius, boxBackgroundPaint)
val textStartPadding = (width - textWidth)/2f
val textTopPadding = (height - textSmallGlyphHeight)/2f
canvas?.drawText(text, textStartPadding, textTopPadding, textPaint)
}
private fun convertDpToPixel(dp: Float) =
dp*(resources.displayMetrics.densityDpi/DisplayMetrics.DENSITY_DEFAULT)
}

Kotlin - How to use a new line within a string?

So I have a dumb question, how can you make a new line within the string, instead of using Java, I would want to use Kotlin instead, the purpose of this is I want to make the TextView to display a certain string, but I also want to make a new line within the same string, but don't know the command as I search in the net, it doesn't show. Such as:
Java:
System.out.println("Hello")
System.out.println("World!")
System.out.print("Hello\n World!")
C#:
Console.WriteLine("Hello")
Console.WriteLine("World!")
Console.Write("Hello\n World!")
Here is the code of what I am planning to do:
The method
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mImageView = findViewById(R.id.image_view)
val longitudeText = mLocation.longitude
val latitudeText = mLocation.latitude
val altitudeText = mLocation.altitude
val originalBitmap = BitmapFactory.decodeResource(resources, R.drawable.gefvqkbbslamdpkxf1zv_bigstock_aerial_view_of_blue_lakes_and__227291596)?.let { it ->
firstAddWatermark(
it,
"Longitude: ${resources.getString(R.string.value_of_7, longitudeText)}м, Latitude: ${resources.getString(R.string.value_of_7, latitudeText)}м" + "\n" +
"Altitude: ${{resources.getString(R.string.value_of_2, altitudeText)}}",
WatermarkOptions(
Corner.TOP_LEFT,
textSizeToWidthRatio = 0.015f,
paddingToWidthRatio = 0.03f,
Color.RED,
Color.BLACK,
typeface = null
)
)
}
mImageView.setImageBitmap(originalBitmap)
}
The functions:
private fun firstAddWatermark(bitmap: Bitmap, firstWatermarkText: String, options: WatermarkOptions = WatermarkOptions()): Bitmap {
val result = bitmap.copy(bitmap.config, true)
val canvas = Canvas(result)
val paint = Paint(ANTI_ALIAS_FLAG or DITHER_FLAG)
paint.textAlign = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> Paint.Align.LEFT
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> Paint.Align.RIGHT
}
val textSize = result.width * options.textSizeToWidthRatio
paint.textSize = textSize
paint.color = options.textColor
if (options.shadowColor != null) {
paint.setShadowLayer(textSize / 2, 0f, 0f, options.shadowColor)
}
if (options.typeface != null) {
paint.typeface = options.typeface
}
val padding = result.width * options.paddingToWidthRatio
val coordinates = calculateCoordinates(firstWatermarkText, paint, options, canvas.width, canvas.height, padding)
canvas.drawText(firstWatermarkText, coordinates.x, coordinates.y, paint)
return result
}
private fun calculateCoordinates(watermarkText: String, paint: Paint, options: WatermarkOptions, width: Int, height: Int, padding: Float): PointF {
val x = when (options.corner) {
Corner.TOP_LEFT,
Corner.BOTTOM_LEFT -> {
padding
}
Corner.TOP_RIGHT,
Corner.BOTTOM_RIGHT -> {
width - padding
}
}
val y = when (options.corner) {
Corner.BOTTOM_LEFT,
Corner.BOTTOM_RIGHT -> {
height - padding
}
Corner.TOP_LEFT,
Corner.TOP_RIGHT -> {
val bounds = Rect()
paint.getTextBounds(watermarkText, 0, watermarkText.length, bounds)
val textHeight = bounds.height()
textHeight + padding
}
}
return PointF(x, y)
}
enum class Corner {
TOP_LEFT,
TOP_RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT
}
data class WatermarkOptions(
val corner: Corner = Corner.BOTTOM_RIGHT,
val textSizeToWidthRatio: Float = 0.04f,
val paddingToWidthRatio: Float = 0.03f,
#ColorInt val textColor: Int = Color.WHITE,
#ColorInt val shadowColor: Int? = Color.BLACK,
val typeface: Typeface? = null
)
The result of that:
postimg.cc/75SF4v3F
You can use \n in the kotlin as well.

Categories

Resources