I have a custom view that extends ImageView. After setting an image I start by highlighting something on the image. When I press a button I want to make the image black except for that part of the path. Is there a way how I can achieve that?
What I have tried initially was to create a second bitmap with a second canvas but now displaying it, while drawing on the main canvas also draw with transparent paint on the black canvas and on the final step merge them together. I could not achieve drawing transparent on the black canvas.
#SuppressLint("AppCompatCustomView")
class BlackImageView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ImageView(context, attrs, defStyleAttr) {
var path: Path = Path()
var transparentPath: Path = Path()
var paint: Paint = Paint().apply {
isAntiAlias = true
isDither = true
strokeWidth = 25f
color = ResourcesCompat.getColor(resources, R.color.highlight, null)
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
var transparentPaint: Paint = Paint().apply {
isAntiAlias = true
isDither = true
strokeWidth = 25f
color = Color.TRANSPARENT
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
}
private lateinit var blackBitmap: Bitmap
private lateinit var extraCanvas: Canvas
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
blackBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(blackBitmap)
extraCanvas.drawColor(Color.BLACK)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
event?.let {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x, event.y)
transparentPath.moveTo(event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
path.lineTo(event.x, event.y)
transparentPath.lineTo(event.x, event.y)
invalidate()
}
MotionEvent.ACTION_UP -> {
}
else -> {
invalidate()
return true
}
}
}
invalidate()
return true
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// canvas?.drawBitmap(blackBitmap, 0f, 0f, null)
// canvas?.drawPath(transparentPath, transparentPaint)
canvas?.drawPath(path, paint)
}
}
Related
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:
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.
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)
}
Im trying to add a suffix to my TextInputEditText by creating a TextDrawable then setting that using compoundDrawable. Everything is going fairly well except the drawable gets clipped outside to right of the component. What could be causing this? So far ive tried changing the font size but that is not doing any difference... Is the drawable too wide or what?
The String is "kr/månad" and as you can see it is clipped..
XML
<android.support.design.widget.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/text_input_layout"
style="#style/TextInputLayoutStyle"
android:theme="#style/TextInputLayoutTheme">
<android.support.design.widget.TextInputEditText
android:id="#+id/text_input_edit_text"
style="#style/TextInputEditTextStyle" />
</android.support.design.widget.TextInputLayout>
COMPONENT CODE
textInputEditText.setCompoundDrawables(null, null, TextDrawable(unitText), null)
TEXTDRAWABLE
class TextDrawable(private val text: String?) : Drawable() {
private val paint: Paint
init {
paint = Paint()
paint.color = Color.BLACK
paint.textSize = 44f
paint.isAntiAlias = true
paint.isFakeBoldText = true
paint.typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
paint.style = Paint.Style.FILL
paint.textAlign = Paint.Align.CENTER
}
override fun draw(canvas: Canvas) {
text?.let { text ->
canvas.drawText(text, 0f, 0f, paint)
}
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun setColorFilter(cf: ColorFilter?) {
paint.colorFilter = cf
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
Try this code:
class TextDrawable(private val text: String?) : Drawable() {
private val paint = Paint().apply {
color = Color.BLACK
textSize = 44f
isAntiAlias = true
isFakeBoldText = true
typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
style = Paint.Style.FILL
setBounds(0, 0, measureText(text).toInt(), 0)
}
override fun draw(canvas: Canvas) {
text?.let { text ->
canvas.drawText(text, 0f, 0f, paint)
}
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun setColorFilter(cf: ColorFilter?) {
paint.colorFilter = cf
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
}
The difference is in two lines. I removed this
paint.textAlign = Paint.Align.CENTER
and added this:
setBounds(0, 0, measureText(text).toInt(), 0)
Can anyone help me out on how I going to draw semicircle of the picture below with canvas and also how to detect the collection of the drawing object?
I had try to draw it using XML and i dont know how to detect the collision of it. I just want to detect the collision of the black part but not the whole circle.
Thank you.
I've just done something similar, with a custom View:
class SemiCircleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint: Paint = Paint()
private var rectangle: RectF? = null
private var margin: Float
init {
paint.isAntiAlias = true
paint.color = ContextCompat.getColor(context, R.color.colorAquamarine)
paint.style = Paint.Style.STROKE
paint.strokeWidth = 5.dpToPx()
margin = 3.dpToPx() // margin should be >= strokeWidth / 2 (otherwise the arc is cut)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rectangle == null) {
rectangle = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
}
canvas.drawArc(rectangle!!, 90f, 180f, false, paint)
}
}
Add it you your XML layout like this:
<com.example.view.SemiCircleView
android:layout_width="120dp"
android:layout_height="120dp"/>
That's the result:
In my case I want to programmatically control the % of the circle being drawn, so I've added a method setArcProportion that controls that:
class SemiCircleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val mainPaint: Paint = Paint()
private val backgroundPaint: Paint = Paint()
private var rectangle: RectF? = null
private var margin: Float
private var arcProportion: Float = 0f
init {
mainPaint.isAntiAlias = true
mainPaint.color = ContextCompat.getColor(context, R.color.colorLutea)
mainPaint.style = Paint.Style.STROKE
mainPaint.strokeWidth = 5.dpToPx()
backgroundPaint.isAntiAlias = true
backgroundPaint.color = ContextCompat.getColor(context, R.color.black_08)
backgroundPaint.style = Paint.Style.STROKE
backgroundPaint.strokeWidth = 5.dpToPx()
margin = 3.dpToPx() // margin should be >= strokeWidth / 2 (otherwise the arc is cut)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (rectangle == null) {
rectangle = RectF(0f + margin, 0f + margin, width.toFloat() - margin, height.toFloat() - margin)
}
canvas.drawArc(rectangle!!, -90f, arcProportion * 360, false, mainPaint)
// This 2nd arc completes the circle. Remove it if you don't want it
canvas.drawArc(rectangle!!, -90f + arcProportion * 360, (1 - arcProportion) * 360, false, backgroundPaint)
}
/**
* #param arcProportion The proportion of the semi circle arc, from 0 to 1. Setting 0 makes the arc invisible, and 1
* makes a whole circle.
*/
fun setArcProportion(arcProportion: Float) {
this.arcProportion = arcProportion
invalidate()
}
}
So if I do semiCircleView.setArcProportion(0.62f) I have:
Bonus - To make the arc grow with an animation modify setArcProportion like this:
private const val ANIMATION_BASE_DURATION_MS: Long = 500 // milliseconds
fun setArcProportion(arcProportion: Float) {
ValueAnimator.ofFloat(0f, arcProportion).apply {
interpolator = DecelerateInterpolator()
// The animation duration is longer for a larger arc
duration = ANIMATION_BASE_DURATION_MS + (arcProportion * ANIMATION_BASE_DURATION_MS).toLong()
addUpdateListener { animator ->
this#SemiCircleView.arcProportion = animator.animatedValue as Float
this#SemiCircleView.invalidate()
}
start()
}
}
In your onDraw method draw a circle .
canvas.drawCircle(x, y, 10, paint);
Now draw a rect with your background colour
Paint fillBackgroundPaint = new Paint();
fillBackgroundPaint.setAntiAlias(true);
fillBackgroundPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(x,y+10,x+10,y-10);
This should serve the purpose.