Created
December 9, 2020 08:33
-
-
Save WSAyan/69901ffc3cfd07eeee070740c0c1c81d to your computer and use it in GitHub Desktop.
Custom android imageview for rounded corner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<declare-styleable name="RoundedImageView"> | |
<attr name="cornerRadius" format="dimension" /> | |
<attr name="reverseMask" format="boolean" /> | |
<attr name="roundedCorners"> | |
<flag name="topLeft" value="8" /> | |
<flag name="topRight" value="4" /> | |
<flag name="bottomLeft" value="2" /> | |
<flag name="bottomRight" value="1" /> | |
<flag name="top" value="12" /> | |
<flag name="bottom" value="3" /> | |
<flag name="all" value="15" /> | |
</attr> | |
</declare-styleable> | |
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.TargetApi | |
import android.content.Context | |
import android.graphics.* | |
import android.os.Build | |
import android.util.AttributeSet | |
import android.view.View | |
import android.view.ViewOutlineProvider | |
import androidx.appcompat.widget.AppCompatImageView | |
import androidx.core.view.ViewCompat | |
import com.wsayan.example.R | |
import java.util.* | |
import kotlin.math.roundToInt | |
class RoundedCornerImageView : AppCompatImageView { | |
private lateinit var paint: Paint | |
private lateinit var path: Path | |
private var pathWidth: Int = 0 | |
private var pathHeight: Int = 0 | |
private var cornerRadius = 0 | |
private var isCircle: Boolean = false | |
private var roundedTopLeft: Boolean = false | |
private var roundedBottomLeft: Boolean = false | |
private var roundedTopRight: Boolean = false | |
private var roundedBottomRight: Boolean = false | |
private var reverseMask: Boolean = false | |
private var _paddingTop = 0 | |
private var _paddingStart = 0 | |
private var _paddingEnd = 0 | |
private var _paddingBottom = 0 | |
constructor(context: Context) : super(context) { | |
init() | |
setupPath() | |
} | |
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { | |
val a = getContext().obtainStyledAttributes(attrs, R.styleable.RoundedImageView, 0, 0) | |
val cornerRadius = a.getDimensionPixelSize(R.styleable.RoundedImageView_cornerRadius, 0) | |
val roundedCorners = a.getInt(R.styleable.RoundedImageView_roundedCorners, ALL_ROUNDED_CORNERS_VALUE) | |
reverseMask = a.getBoolean(R.styleable.RoundedImageView_reverseMask, reverseMask) | |
a.recycle() | |
init() | |
setCornerRadiusInternal(cornerRadius) | |
setRoundedCornersInternal(roundedCorners) | |
fixPadding() | |
setupPath() | |
} | |
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { | |
val a = getContext().obtainStyledAttributes(attrs, R.styleable.RoundedImageView, defStyleAttr, 0) | |
val cornerRadius = a.getDimensionPixelSize(R.styleable.RoundedImageView_cornerRadius, 0) | |
val roundedCorners = a.getInt(R.styleable.RoundedImageView_roundedCorners, ALL_ROUNDED_CORNERS_VALUE) | |
reverseMask = a.getBoolean(R.styleable.RoundedImageView_reverseMask, reverseMask) | |
a.recycle() | |
init() | |
setCornerRadiusInternal(cornerRadius) | |
setRoundedCornersInternal(roundedCorners) | |
fixPadding() | |
setupPath() | |
} | |
private fun copyPadding() { | |
_paddingTop = paddingTop | |
_paddingStart = ViewCompat.getPaddingStart(this) | |
_paddingEnd = ViewCompat.getPaddingEnd(this) | |
_paddingBottom = paddingBottom | |
} | |
private fun fixPadding() { | |
if (reverseMask) { | |
if (ViewCompat.getPaddingStart(this) != 0 | |
|| ViewCompat.getPaddingEnd(this) != 0 | |
|| paddingTop != 0 | |
|| paddingBottom != 0) { | |
copyPadding() | |
ViewCompat.setPaddingRelative(this, 0, 0, 0, 0) | |
} | |
} else { | |
if (ViewCompat.getPaddingStart(this) != _paddingStart | |
|| ViewCompat.getPaddingEnd(this) != _paddingEnd | |
|| paddingTop != _paddingTop | |
|| paddingBottom != _paddingBottom) { | |
copyPadding() | |
ViewCompat.setPaddingRelative(this, _paddingStart, _paddingTop, _paddingEnd, _paddingBottom) | |
} | |
} | |
} | |
private fun init() { | |
paint = Paint() | |
path = Path() | |
setupPaint() | |
} | |
override fun onSizeChanged(newWidth: Int, newHeight: Int, oldWidth: Int, oldHeight: Int) { | |
super.onSizeChanged(newWidth, newHeight, oldWidth, oldHeight) | |
val _newWidth = newWidth - (_paddingStart + _paddingEnd) | |
val _newHeight = newHeight - (_paddingTop + _paddingBottom) | |
if (pathWidth != _newWidth || pathHeight != _newHeight) { | |
pathWidth = _newWidth | |
pathHeight = _newHeight | |
setupPath() | |
} | |
} | |
/** | |
* @param cornerRadius in pixels, default is 0 | |
*/ | |
fun setCornerRadius(cornerRadius: Int) { | |
if (setCornerRadiusInternal(cornerRadius)) { | |
setupPath() | |
} | |
} | |
private fun setCornerRadiusInternal(cornerRadius: Int): Boolean { | |
if (this.cornerRadius != cornerRadius) { | |
this.cornerRadius = cornerRadius | |
return true | |
} | |
return false | |
} | |
/** | |
* Clips the inside, instead of the outside of the ImageView | |
* @param reverseMask default is false | |
*/ | |
fun setReverseMask(reverseMask: Boolean) { | |
if (this.reverseMask != reverseMask) { | |
this.reverseMask = reverseMask | |
fixPadding() | |
setupPath() | |
} | |
} | |
/** | |
* @param corners, default is All rounded corners | |
*/ | |
fun setRoundCorners(corners: EnumSet<Corner>) { | |
if (roundedBottomLeft != corners.contains(Corner.BOTTOM_LEFT) | |
|| roundedBottomRight != corners.contains(Corner.BOTTOM_RIGHT) | |
|| roundedTopLeft != corners.contains(Corner.TOP_LEFT) | |
|| roundedTopRight != corners.contains(Corner.TOP_RIGHT)) { | |
roundedBottomLeft = corners.contains(Corner.BOTTOM_LEFT) | |
roundedBottomRight = corners.contains(Corner.BOTTOM_RIGHT) | |
roundedTopLeft = corners.contains(Corner.TOP_LEFT) | |
roundedTopRight = corners.contains(Corner.TOP_RIGHT) | |
setupPath() | |
} | |
} | |
private fun setRoundedCornersInternal(roundedCorners: Int) { | |
roundedTopLeft = TOP_LEFT == roundedCorners and TOP_LEFT | |
roundedTopRight = TOP_RIGHT == roundedCorners and TOP_RIGHT | |
roundedBottomLeft = BOTTOM_LEFT == roundedCorners and BOTTOM_LEFT | |
roundedBottomRight = BOTTOM_RIGHT == roundedCorners and BOTTOM_RIGHT | |
} | |
/** | |
* @param roundedCorners, where 1111 is All Corners {@see #Companion.ALL_ROUNDED_CORNERS_VALUE} | |
*/ | |
fun setRoundedCorners(roundedCorners: Int) { | |
setRoundedCornersInternal(roundedCorners) | |
setupPath() | |
} | |
private fun setupPaint(): Paint { | |
paint.style = Paint.Style.FILL | |
paint.color = Color.TRANSPARENT | |
paint.isAntiAlias = true | |
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) | |
return paint | |
} | |
private fun setupPath() { | |
if (roundedTopLeft && roundedTopRight && roundedBottomRight && roundedBottomLeft | |
&& (cornerRadius >= pathHeight / 2 && cornerRadius >= pathWidth / 2)) { | |
isCircle = true | |
path = circlePath(path, _paddingStart + (pathWidth / 2.0f), _paddingTop + (pathHeight / 2.0f), pathWidth, pathHeight, reverseMask) | |
} else { | |
isCircle = false | |
path = roundedRect(path, | |
left = _paddingStart.toFloat(), | |
top = _paddingTop.toFloat(), | |
right = _paddingStart + pathWidth.toFloat(), | |
bottom = _paddingTop + pathHeight.toFloat(), | |
rx = cornerRadius.toFloat(), | |
ry = cornerRadius.toFloat(), | |
tl = roundedTopLeft, | |
tr = roundedTopRight, | |
br = roundedBottomRight, | |
bl = roundedBottomLeft, | |
reverseMask = reverseMask) | |
} | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | |
if (outlineProvider == ViewOutlineProvider.BACKGROUND | |
|| outlineProvider is CircularOutlineProvider | |
|| outlineProvider is RoundedRectangleOutlineProvider) | |
outlineProvider = ViewOutlineProvider.BACKGROUND | |
if (isInEditMode && !reverseMask) { | |
clipToOutline = true | |
} | |
} | |
} | |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) | |
inner class CircularOutlineProvider : ViewOutlineProvider() { | |
override fun getOutline(view: View, outline: Outline) { | |
val radius = Math.min(pathWidth, pathHeight) / 2.0 | |
val left = (width / 2.0) - radius | |
val top = (height / 2.0) - radius | |
val right = (width / 2.0) + radius | |
val bottom = (height / 2.0) + radius | |
outline.setOval(Math.ceil(left).roundToInt(), | |
Math.ceil(top).roundToInt(), | |
Math.ceil(right).roundToInt(), | |
Math.ceil(bottom).roundToInt()) | |
} | |
} | |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) | |
inner class RoundedRectangleOutlineProvider : ViewOutlineProvider() { | |
override fun getOutline(view: View, outline: Outline) { | |
try { | |
outline.setConvexPath(path) | |
} catch (iae: IllegalArgumentException) { | |
if (roundedTopLeft && roundedBottomLeft && roundedBottomRight && roundedTopRight) | |
outline.setRoundRect(_paddingStart, paddingTop, pathWidth + _paddingStart, paddingTop + pathHeight, cornerRadius.toFloat()) | |
else outline.setEmpty() | |
} | |
} | |
} | |
@TargetApi(Build.VERSION_CODES.LOLLIPOP) | |
override fun setOutlineProvider(provider: ViewOutlineProvider?) { | |
if (provider == ViewOutlineProvider.BACKGROUND | |
|| provider is CircularOutlineProvider | |
|| provider is RoundedRectangleOutlineProvider) { | |
val viewOutlineProvider: ViewOutlineProvider? | |
when { | |
reverseMask -> viewOutlineProvider = null | |
isCircle -> viewOutlineProvider = CircularOutlineProvider() | |
else -> viewOutlineProvider = RoundedRectangleOutlineProvider() | |
} | |
super.setOutlineProvider(viewOutlineProvider) | |
} else | |
super.setOutlineProvider(provider) | |
} | |
override fun onDraw(canvas: Canvas) { | |
if (!isInEditMode) { | |
val saveCount = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) | |
canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null) | |
else | |
canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null, Canvas.ALL_SAVE_FLAG) | |
super.onDraw(canvas) | |
canvas.drawPath(path, paint) | |
canvas.restoreToCount(saveCount) | |
} else { | |
super.onDraw(canvas) | |
} | |
} | |
override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) { | |
super.setPaddingRelative(start, top, end, bottom) | |
fixPadding() | |
} | |
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { | |
super.setPadding(left, top, right, bottom) | |
fixPadding() | |
} | |
companion object { | |
enum class Corner { | |
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT; | |
companion object { | |
val ALL = EnumSet.allOf(Corner::class.java) | |
val TOP = EnumSet.of(TOP_LEFT, TOP_RIGHT) | |
} | |
} | |
const val TOP_LEFT = 8 //1000 base 2 | |
const val TOP_RIGHT = 4 //0100 base 2 | |
const val BOTTOM_LEFT = 2 //0010 base 2 | |
const val BOTTOM_RIGHT = 1 //0001 base 2 | |
const val ALL_ROUNDED_CORNERS_VALUE = 15 //base 2 = 1111 | |
fun circlePath(path: Path, x: Float, y: Float, viewWidth: Int, viewHeight: Int, reverseMask: Boolean): Path { | |
path.reset() | |
val radius = Math.min(viewWidth, viewHeight) / 2.0f | |
path.addCircle(x, y, radius, Path.Direction.CCW) | |
path.fillType = if (reverseMask) Path.FillType.EVEN_ODD else Path.FillType.INVERSE_EVEN_ODD | |
return path | |
} | |
fun roundedRect(path: Path?, | |
left: Float, top: Float, right: Float, bottom: Float, | |
rx: Float, ry: Float, | |
tl: Boolean, tr: Boolean, br: Boolean, bl: Boolean, | |
reverseMask: Boolean): Path { | |
var rx = rx | |
var ry = ry | |
path!!.reset() | |
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) | |
if (tr) { | |
path.rQuadTo(0f, -ry, -rx, -ry)//top-right corner | |
} else { | |
path.rLineTo(0f, -ry) | |
path.rLineTo(-rx, 0f) | |
} | |
path.rLineTo(-widthMinusCorners, 0f) | |
if (tl) { | |
path.rQuadTo(-rx, 0f, -rx, ry) //top-left corner | |
} else { | |
path.rLineTo(-rx, 0f) | |
path.rLineTo(0f, ry) | |
} | |
path.rLineTo(0f, heightMinusCorners) | |
if (bl) { | |
path.rQuadTo(0f, ry, rx, ry)//bottom-left corner | |
} else { | |
path.rLineTo(0f, ry) | |
path.rLineTo(rx, 0f) | |
} | |
path.rLineTo(widthMinusCorners, 0f) | |
if (br) { | |
path.rQuadTo(rx, 0f, rx, -ry) //bottom-right corner | |
} else { | |
path.rLineTo(rx, 0f) | |
path.rLineTo(0f, -ry) | |
} | |
path.rLineTo(0f, -heightMinusCorners) | |
path.close() //Given close, last lineto can be removed. | |
path.fillType = if (!reverseMask) Path.FillType.INVERSE_EVEN_ODD else Path.FillType.EVEN_ODD | |
return path | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment