Cropping of ImageViews in Custom ViewGroup - android

I am implementing custom ViewGroup which has overlapping ImageViews.
I load images with GlideApp. Problem is that some images are cropped a little (visible on the left side) and I cannot prevent this. The drawables which get cropped are Vector drawables and their mDrawableWidth & mDrawableHeight are larger than the height of the ViewGroup. I thought that the images would be scaled to fit the view dimensions (mScaleType = FIT_CENTER).
class OverlappingImageViewGroup : ViewGroup {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSize: Int = getDefaultSize(0, widthMeasureSpec)
val heightSize: Int = getDefaultSize(0, heightMeasureSpec)
val majorDimension = Math.min(widthSize, heightSize)
//Measure all child views
val childSpec = MeasureSpec.makeMeasureSpec(majorDimension, MeasureSpec.EXACTLY)
measureChildren(childSpec, childSpec)
//MUST call this to save our own dimensions
setMeasuredDimension(majorDimension, majorDimension)
}
override fun onLayout(changed: Boolean,
leftRelativeToParent: Int,
topRelativeToParent: Int,
rightRelativeToParent: Int,
bottomRelativeToParent: Int) {
val percentOverlap = 0.8f
if (childCount > 0) {
for (i in 0 until childCount) {
val child = getChildAt(i)
val childWidth = child.measuredWidth
val topBound = 0 /* there is just one row */
if (i == 0) {
child.layout(0, topBound, childWidth, childWidth)
} else {
val leftBound = i * percentOverlap * childWidth
child.layout(
leftBound.toInt(),
topBound,
leftBound.toInt() + childWidth,
topBound + childWidth)
}
}
}
}
}
ImageViews are created programmatically:
val context = holder.rootView.context
val imageView: ImageView = ImageView(context)
GlideApp.with(context)
.load(imageUrl)
.placeholder(userPlaceholder)
.fallback(userPlaceholder)
.error(userPlaceholder)
.transform(CircleWithBorderTransform())
.into(imageView)
holder.membersIcons.addView(imageView)
Glide transformation:
override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val width = source.width
val height = source.height
val canvasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val shader = BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
val paint = Paint()
paint.isAntiAlias = true
paint.shader = shader
val canvas = Canvas(canvasBitmap)
val radius = if (width > height) height.toFloat() / 2f else width.toFloat() / 2f
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
//border around the image
paint.shader = null
paint.style = Paint.Style.STROKE
paint.strokeWidth = 2f
paint.color = Color.WHITE
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
return canvasBitmap
}

Related

What is the best way to do a similar chart

What is the best way to create a bar chart like this?
You can use libraries for this or make by yourself.
For example this is my round rectangle:
class CustomRectangle : View {
private var paint = Paint()
constructor(context: Context) : super(context) {
init(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
private fun init(context: Context, attrs: AttributeSet? = null) {
val ta = context.obtainStyledAttributes(attrs, R.styleable.CustomRectangle)
val color = ta.getColor(
R.styleable.CustomRectangle_rectangleColor,
ContextCompat.getColor(context, R.color.black_5)
)
color.let {
this.rectangleColor = it
}
changeColor(color)
ta.recycle()
}
var rectangleColor: Int = R.color.black_5
set(value) {
field = value
changeColor(value)
}
private fun changeColor(color: Int) {
paint.color = color
invalidate()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val path = getPath(0.0f, 0.0F, width.toFloat(), height.toFloat(), 20f, 20f, false)
if (path != null) {
canvas?.drawPath(path, paint)
}
}
private fun getPath(
left: Float,
top: Float,
right: Float,
bottom: Float,
rx: Float,
ry: Float,
conformToOriginalPost: Boolean
): Path? {
var rx = rx
var ry = ry
val path = Path()
if (rx < 0) rx = 0f
if (ry < 0) ry = 0f
val width = right - left
val height = bottom - top
if (rx > width / 2) rx = width / 2
if (ry > height / 2) ry = height / 2
val widthMinusCorners = width - 2 * rx
val heightMinusCorners = height - 2 * ry
path.moveTo(right, top + ry)
path.rQuadTo(0f, -ry, -rx, -ry) // top-right corner
path.rLineTo(-widthMinusCorners, 0f)
path.rQuadTo(-rx, 0f, -rx, ry) // top-left corner
path.rLineTo(0f, heightMinusCorners)
if (conformToOriginalPost) {
path.rLineTo(0f, ry)
path.rLineTo(width, 0f)
path.rLineTo(0f, -ry)
} else {
path.rQuadTo(0f, ry, rx, ry) // bottom-left corner
path.rLineTo(widthMinusCorners, 0f)
path.rQuadTo(rx, 0f, rx, -ry) // bottom-right corner
}
path.rLineTo(0f, -heightMinusCorners)
path.close() // Given close, last lineto can be removed.
return path
}
}
And than I use it like:
<customView.CustomRectangle
android:id="#+id/custom_rectangle"
android:layout_width="16dp"
android:layout_margin="16dp"
android:layout_gravity="center"
android:layout_height="200dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
If this is not all you need, please tell me more requirements so that I can help you

StaticLayout positioning center of canvas not working like canvas.drawText?

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.

How to make circle image in custom image view with center crop?

I want to create Custom Image View for avatars which can work in two modes:
If android:src is empty display circle with first letter of name
Else display circle avatar
At now if I use android:src my drawable is not in center, how can i make central crop of it?
Example
Here is code of custom view:
class AvatarImageView : AppCompatImageView{
private var letter = ""
private var avatarBackgroundColor = 0
private var avatarLetterColor = 0
private val paintBackground = Paint()
private val paintLetter = Paint(Paint.LINEAR_TEXT_FLAG)
private val paintAvatarImage = Paint()
private var circleX = 0f
private var circleY = 0f
private var circleRadius = 0f
private var letterX = 0f
private var letterY = 0f
private val avatarImageRect = RectF(0f, 0f, 0f, 0f)
constructor(context : Context)
: this(context, null)
constructor(context: Context, attrs : AttributeSet?)
: this(context, attrs, 0)
constructor(context: Context, attrs : AttributeSet?, defStyleAttr : Int)
: super(context, attrs, defStyleAttr){
attrs?.let{
val array = context.obtainStyledAttributes(it, R.styleable.AvatarImageView)
letter = array.getString(R.styleable.AvatarImageView_signature)?.trim()?.substring(0..0)?.toUpperCase(Locale.ROOT) ?: ""
avatarBackgroundColor = array.getColor(R.styleable.AvatarImageView_background_color, Color.BLUE)
avatarLetterColor = array.getColor(R.styleable.AvatarImageView_letter_color, Color.WHITE)
array.recycle()
}
init()
}
private fun init(){
paintBackground.style = Paint.Style.FILL
paintBackground.color = avatarBackgroundColor
paintLetter.color = avatarLetterColor
paintLetter.textAlign = Paint.Align.CENTER
paintLetter.isAntiAlias = true
paintLetter.style = Paint.Style.FILL
drawable?.let{
paintAvatarImage.isAntiAlias = true
val shader = BitmapShader(getBitmapFromDrawable(it), Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paintAvatarImage.shader = shader
}
}
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap{
if (drawable is BitmapDrawable) {
drawable.bitmap?.let {
return drawable.bitmap
}
}
val bitmap: Bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
return bitmap
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val availableWidth = width
val availableHeight = height
circleX = availableWidth / 2f
circleY = availableHeight / 2f
circleRadius = min(availableWidth, availableHeight) / 2f
paintLetter.textSize = availableHeight / 2f
letterX = availableWidth / 2f
letterY = availableHeight / 2f - ((paintLetter.descent() + paintLetter.ascent()) / 2f)
avatarImageRect.right = width.toFloat()
avatarImageRect.bottom = height.toFloat()
}
override fun onDraw(canvas: Canvas?) {
drawable?.let{
canvas?.drawRoundRect(avatarImageRect, width.toFloat(), height.toFloat(), paintAvatarImage)
} ?:run {
canvas?.drawCircle(circleX, circleY, circleRadius, paintBackground)
canvas?.drawText(letter, letterX, letterY, paintLetter)
}
}
}
My xml file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="16dp"
tools:context=".ui.AvatarsFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.test.test.views.AvatarImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="#drawable/avatar"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
style="#style/TextAppearance.AppCompat.Medium"
android:text="Ivan Ivanov"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="10dp">
<com.test.test.views.AvatarImageView
android:layout_width="32dp"
android:layout_height="32dp"
app:signature="Ivan Ivanov"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
style="#style/TextAppearance.AppCompat.Medium"
android:text="Ivan Ivanov"/>
</LinearLayout>
</LinearLayout>
UPDATED: I know about other libraries, but I want to do it without them.
I used a CircleImageView to display images and picasso to load images into the CircleImageView
Picasso centers and crops the image
It worked for me...
SOLVED:
I`m now using this code for crop my image from https://stackoverflow.com/a/12089127/12209565:
private fun getCircleBitmap(bitmap: Bitmap): Bitmap {
val output = Bitmap.createBitmap(
bitmap.width,
bitmap.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(output)
val color = -0xbdbdbe
val paint = Paint()
val rect = Rect(0, 0, bitmap.width, bitmap.height)
paint.isAntiAlias = true
canvas.drawARGB(0, 0, 0, 0)
paint.color = color
canvas.drawCircle(
bitmap.width / 2f, bitmap.height / 2f,
bitmap.width / 2f, paint
)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(bitmap, rect, rect, paint)
return output
}

How to create a 'wave' style layout using Android Canvas

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

How to get Canvas draw arc touch listener? (Android)

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.

Categories

Resources