I am struggling with a custom view, I want it to have this effect.
But the problem is that the RadialGradient its very soft.
This is my class:
class CustomView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var radius = 0f
private var centerX = 0f
private var centerY = 0f
private val mainPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.RED
}
private val gradientPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
radius = w.coerceAtMost(h) * 0.50f
if (radius < 0) return
centerX = w * 0.5f
centerY = h * 0.5f
gradientPaint.shader = RadialGradient(
centerX,
centerY,
radius,
Color.BLACK,
Color.TRANSPARENT,
Shader.TileMode.REPEAT
)
}
override fun onDraw(canvas: Canvas) {
canvas.drawCircle(centerX, centerY, radius, mainPaint)
canvas.drawCircle(centerX, centerY, radius, gradientPaint)
}
}
The only way I achieved this effect is by drawing a lot of circles (like 10 or more) in the canvas, but this way doesn't feel like the right approach and since I will be doing more stuff in this view, I don't want to have performance issues.
Hope you can help me to find a right way to achieve this. Thank you.
Related
While drawing with usual drawText the text is positioning to the center of canvas. But my requirment is to place multiline text, so I've to use StaticLayout, but StaticLayout is not getting placed like drawText
Here's what I've tried so far.
class TestView : View {
private lateinit var staticLayout: StaticLayout
private lateinit var ststicTextPaint: TextPaint
private lateinit var textPaint: TextPaint
private val helloworld = "Hello world!"
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(
context,
attrs,
defStyleAttr,
defStyleRes
) {
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
setUp(w, h)
}
private fun setUp(w: Int, h: Int) {
textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
textPaint.color = Color.BLUE
textPaint.textSize = 40f
textPaint.textAlign = Paint.Align.CENTER
ststicTextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
ststicTextPaint.color = Color.GREEN
ststicTextPaint.textSize = 40f
// textPaint.textAlign = Paint.Align.CENTER
staticLayout = StaticLayout(
helloworld,
ststicTextPaint,
w,
Layout.Alignment.ALIGN_CENTER,
0f,
0f,
false
)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//Just drawn a rect with cross-hair to know the relative position
val rect = Rect(0, 0, width, height)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.style = Paint.Style.STROKE
paint.color = Color.RED
paint.strokeWidth = 5f
canvas?.drawRect(rect, paint)
canvas?.drawLine((width/2).toFloat(), 0F, (width/2).toFloat(), height.toFloat(),paint)
canvas?.drawLine(0F, (height/2).toFloat(),width.toFloat(), (height/2).toFloat(),paint)
canvas?.drawText(helloworld, (width / 2).toFloat(), (height / 2).toFloat(), textPaint)
// canvas?.drawText(helloworld, (width / 2).toFloat(), (height / 2).toFloat(), textPaint)
staticLayout.draw(canvas, (width / 2).toFloat(), (height / 2).toFloat())
}
fun StaticLayout.draw(canvas: Canvas?, x: Float, y: Float) {
canvas?.withTranslation(x, y) {
draw(canvas)
}
}
}
Here's what I'm getting with both
The blue one is done using normal drawText and the Green one is using StaticLayout
I figured out the issue guys. It was because i was ignoring the width of the StaticLayout text's width while placing it to the centre.
Here I'm giving the StaticLayout the entire width of the View.
staticLayout = StaticLayout(
"Hello world is helloworld",
ststicTextPaint,
w,
Layout.Alignment.ALIGN_CENTER,
1f,
0f,
false
)
While drawing the view
staticLayout.draw(canvas, ((width / 2)-staticLayout.width/2).toFloat(), (height / 2).toFloat())
I substracted the half of the width of the `StaticLayout width so that it'll place in the exact centre.
I have been trying to create a custom view similar to that shown below in which the white 'wave' view contains an extended 'inverse rounded corner' in the top right and a rounded corner in the bottom left.
I had attempted to achieve this using the Material Shape themeing but this doesn't seem to support the 'inverse' rounded corner.
To achieve this, I have been using a View and custom drawing within its Canvas, but have not been able to get it working, as I am unsure how to achieve the inverse rounded corner effect.
Any help or guidance would be greatly appreciated
class TestView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var mPath = Path()
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val h = height.toFloat()
val h2 = height.toFloat() / 2f
val w = width.toFloat()
val w2 = width.toFloat() / 2f
mPath.reset()
mPath.addArc(w2, 0f, w, h2, 0f, 90f)
mPath.addArc(0f, h2, w2, h, 180f, 90f)
mPath.lineTo(w, h2)
mPath.lineTo(w, h)
mPath.lineTo(0f, h)
mPath.close()
mPath.fillType = Path.FillType.WINDING
canvas?.clipPath(mPath)
canvas?.drawColor(Color.DKGRAY)
}
}
I solved this by using Bezier curves and adapting the answer found in this SO question
The code I used to achieve this was as follows:
class SweepView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint().apply {
color = Color.RED
isAntiAlias = true
style = Paint.Style.FILL
}
private val path = Path()
override fun onDraw(canvas: Canvas?) {
val h = height.toFloat()
val halfH = h / 2f
val w = width.toFloat()
val delta = width / 3f
path.reset()
path.moveTo(0f, h)
path.cubicTo(0f, halfH, 0f, halfH, delta, halfH)
path.lineTo(w - delta, halfH)
path.cubicTo(w, halfH, w, halfH, w, 0f)
path.lineTo(w, h)
path.close()
canvas?.drawPath(path, paint)
}
}
I want to have an ImageView containing an Image with rounded curves, but not circular (which is way easier of course)..
The X marked areas should be black, the rest should stay blue. It should look (kind of) like this
What I tried:
I spent hours fighting with Path.quadTo and Path.cubicTo with the help of some tools but I didn't have any success yet. I just don't really get the usage to be honest.
What my code currently looks like:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val paint = Paint()
paint.color = Color.BLACK
paint.strokeWidth = 1f
paint.strokeCap = Paint.Cap.ROUND
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)
paint.style = Paint.Style.FILL
val fHeight = canvas.height.toFloat()
val startEndHeight = canvas.height / 1.18f
val fWidth = canvas.width.toFloat()
val halfWidth = (fWidth / 2)
val path = Path()
//X = Left side, Y = close to bottom
val ptStart = PointF(0f, startEndHeight)
//X = Middle, Y = Bottom
val ptMiddle = PointF(halfWidth, fHeight + 95)
// X = Right Side, Y = close to bottom
val ptEnd = PointF(fWidth, startEndHeight)
path.moveTo(ptStart.x, ptStart.y)
path.quadTo(ptMiddle.x, ptMiddle.y, ptEnd.x, ptEnd.y)
path.close()
canvas.drawPath(path, paint)
}
It can't be that hard, right?
Is it possible to colorize the red marked areas and leave everything else untouched?
Fixed! This is my final solution which applies a quad bezier to the ImageView. I had to add 2 lines to the path to get the result I wanted.
class HeaderImageView : AppCompatImageView {
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
lateinit var paint: Paint
private fun init() {
paint = Paint()
paint.color = Color.WHITE
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)
paint.style = Paint.Style.FILL
}
#SuppressLint("CanvasSize")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val fHeight = canvas.height.toFloat()
val startEndHeight = canvas.height / 1.18f
val fWidth = canvas.width.toFloat()
val halfWidth = (fWidth / 2)
val path = Path()
//X = Left side, Y = close to bottom
val ptStart = PointF(0f, startEndHeight)
//X = Middle, Y = Bottom
val ptMiddle = PointF(halfWidth, fHeight + 95)
// X = Right Side, Y = close to bottom
val ptEnd = PointF(fWidth, startEndHeight)
path.moveTo(ptStart.x, ptStart.y)
path.quadTo(ptMiddle.x, ptMiddle.y, ptEnd.x, ptEnd.y)
path.lineTo(fWidth, fHeight)
path.lineTo(0f, fHeight)
path.close()
canvas.drawPath(path, paint)
}
}
How to get canvas arc touch listener.
I am creating a pie chart with dynamic arcs. I need to perform some task when the arc is clicked (for that I need to know which arch was clicked).
onTouchEvent of View just gives event from which we can get the x & y coordinated but here the arc has thickness.
How can I get the click listener for each arc?
NOTE - Please don't suggest any library
Need to create this kind of pie chart
https://camo.githubusercontent.com/7e8a4a3c938c21d032d44d999edd781b6e146f2a/68747470733a2f2f7261772e6769746875622e636f6d2f5068696c4a61792f4d50416e64726f696443686172742f6d61737465722f73637265656e73686f74732f73696d706c6564657369676e5f7069656368617274312e706e67
My current implementation
private lateinit var mRectF: RectF
private lateinit var mRectFInner: RectF
private lateinit var mPaint: Paint
private lateinit var mCanvas: Canvas
private var isTouched = false
private lateinit var mBitmap:Bitmap
private var pieChartItemList = arrayListOf<PieChartItem>()
private var innerOuterCircleGap: Float = 0F
constructor(context: Context) : super(context) {
init(null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init(attrs)
}
private fun init(#Nullable set: AttributeSet?) {
mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
var typedArray = context.obtainStyledAttributes(set, R.styleable.MyCustomView)
innerOuterCircleGap = typedArray.getFloat(R.styleable.MyCustomView_innerOuterCircleGap, 0F)
}
//onDraw is called several times - so don't
override fun onDraw(canvas: Canvas?) {
drawPieChart(canvas)
}
private fun drawPieChart(canvas: Canvas?) {
var unselectedConstant = 10
mPaint.color = Color.BLACK
canvas?.drawRect(0F, 0F, width.toFloat() - unselectedConstant, width.toFloat() - unselectedConstant, mPaint)
mRectF = RectF(0F, 0F, width.toFloat() - unselectedConstant, height.toFloat() - unselectedConstant)
mRectFInner = RectF(innerOuterCircleGap, innerOuterCircleGap, width.toFloat() - innerOuterCircleGap - unselectedConstant,
height.toFloat() - innerOuterCircleGap - unselectedConstant)
var startAngle = 0F
var sweepAngle: Float
var radius: Float = width.toFloat() / 2
Log.e("ANKUSH", "width = $width height = $height radius = $radius")
for (i in 0 until pieChartItemList.size) {
sweepAngle = (pieChartItemList[i].percent * 3.6).toFloat()
Log.e("ANKUSH - SweepAngle $i", sweepAngle.toString())
mPaint.color = pieChartItemList[i].color
canvas?.drawArc(mRectF, startAngle, sweepAngle, true, mPaint)
startAngle += sweepAngle
}
if (isTouched) {
mRectF = RectF(0F, 0F, width.toFloat() - unselectedConstant, height.toFloat() - unselectedConstant)
mPaint.color = Color.RED
canvas?.drawArc(mRectF, 0F, 45F, true, mPaint)
}
mPaint.color = Color.WHITE
canvas?.drawArc(mRectFInner, 0F, 360F, true, mPaint)
}
fun setPieChartItems(itemList: ArrayList<PieChartItem>) {
pieChartItemList = itemList
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
mCanvas = Canvas()
mCanvas.setBitmap(mBitmap)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
isTouched = true
Log.e("ANKUSH", mBitmap?.getPixel(event.x.toInt(), event.y.toInt()).toString())
invalidate()
}
return super.onTouchEvent(event)
}
Face same problem. Finally found a solution using this function for convert click coordinates to angle.
private fun convertTouchEventPointToAngle(xPos: Float, yPos: Float): Double {
var x = xPos - (width * 0.5f)
val y = yPos - (height * 0.5f)
var angle = Math.toDegrees(atan2(y.toDouble(), x.toDouble()) + Math.PI / 2)
angle = if (angle < 0) angle + 360 else angle
return angle
}
Found on article https://enginebai.com/2018/05/07/android-custom-view/ so might be helpful.
You need to store the event and draw it accordling something like:
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_DOWN) {
isTouched = true
touchedAtX = event.x.toInt()
touchedAtY = event.x.toInt()
Log.e("ANKUSH", mBitmap?.getPixel(event.x.toInt(), event.y.toInt()).toString())
invalidate()
}
return super.onTouchEvent(event)
}
And modify the onDraw
for (i in 0 until pieChartItemList.size) {
sweepAngle = (pieChartItemList[i].percent * 3.6).toFloat()
Log.e("ANKUSH - SweepAngle $i", sweepAngle.toString())
mPaint.color = pieChartItemList[i].color
canvas?.drawArc(mRectF, startAngle, sweepAngle, true, mPaint)
startAngle += sweepAngle
if (isTouched && mRectF.contains(touchedAtX, touchedAtY) {
canvas?.drawArc(mRectF, startAngle - 15, sweepAngle + 15, true, mPaint)
}
}
Note that my code doesn't account for the startAngle and sweepAngle while doing collision detection, but you will need to know based on those which specific arch is touched by its coordinates.
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.