Skip to content

Instantly share code, notes, and snippets.

@ephemient
Last active April 18, 2019 01:38
Show Gist options
  • Save ephemient/c07585572c6938d38f925e59380e00f3 to your computer and use it in GitHub Desktop.
Save ephemient/c07585572c6938d38f925e59380e00f3 to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CutoutBackgroundLayout_Layout">
<attr name="layout_cutoutBackground" format="boolean"/>
</declare-styleable>
</resources>
import android.content.Context
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Path
import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.Drawable
import android.graphics.drawable.LevelListDrawable
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.Px
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.graphics.withSave
import androidx.core.view.forEach
class CutoutBackgroundLayout : ConstraintLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
init {
viewTreeObserver.addOnGlobalLayoutListener { refreshOutlines() }
}
private val outline: Outline = Outline()
private val rect = Rect()
private fun refreshOutlines() {
val background = this.background as? CutoutDrawable ?: return
val path = background.cutoutPath
path.rewind()
forEach { view ->
if (view.visibility != View.VISIBLE || (view.layoutParams as? LayoutParams)?.isCutoutBackground != true) {
return@forEach
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
view.outlineProvider.getOutline(view, outline)
if (!outline.isEmpty) {
outline.offset(view.left, view.top)
if (outline.getRect(rect) && !rect.isEmpty) {
path.addRoundRect(
rect.left.toFloat(),
rect.top.toFloat(),
rect.right.toFloat(),
rect.bottom.toFloat(),
outline.radius,
outline.radius,
Path.Direction.CCW
)
return@forEach
}
}
// else... [Outline.mPath] is `@hide` :(
}
view.getHitRect(rect)
if (!rect.isEmpty) {
path.addRect(
rect.left.toFloat(),
rect.top.toFloat(),
rect.right.toFloat(),
rect.bottom.toFloat(),
Path.Direction.CCW
)
}
}
background.invalidateSelf()
}
override fun setBackground(background: Drawable?) {
super.setBackground(
if (background is CutoutDrawable?) background else CutoutDrawable(
background
)
)
refreshOutlines()
}
override fun generateDefaultLayoutParams(): ConstraintLayout.LayoutParams =
LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
override fun generateLayoutParams(p: ViewGroup.LayoutParams): ViewGroup.LayoutParams =
LayoutParams(p)
override fun generateLayoutParams(attrs: AttributeSet): LayoutParams =
LayoutParams(context, attrs)
override fun checkLayoutParams(p: ViewGroup.LayoutParams): Boolean = p is LayoutParams
class LayoutParams : ConstraintLayout.LayoutParams {
@JvmField
var isCutoutBackground: Boolean = false
constructor(source: LayoutParams) : super(source) {
isCutoutBackground = source.isCutoutBackground
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
val ta =
context.obtainStyledAttributes(attrs, R.styleable.CutoutBackgroundLayout_Layout)
isCutoutBackground =
ta.getBoolean(
R.styleable.CutoutBackgroundLayout_Layout_layout_cutoutBackground,
false
)
ta.recycle()
}
constructor(@Px width: Int, @Px height: Int) : super(width, height)
constructor(source: ViewGroup.LayoutParams) : super(source)
}
}
class CutoutDrawable() : LevelListDrawable() {
constructor(drawable: Drawable?) : this() {
addLevel(0, 0, drawable)
}
val cutoutPath = Path()
override fun draw(canvas: Canvas) {
canvas.withSave {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
clipOutPath(cutoutPath)
} else {
@Suppress("DEPRECATION")
clipPath(cutoutPath, Region.Op.DIFFERENCE)
}
super.draw(this)
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<CutoutBackgroundLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<aapt:attr name="android:background">
<layer-list android:paddingLeft="8dp" android:paddingTop="0dp" android:paddingRight="8dp" android:paddingBottom="0dp">
<item android:left="2dp" android:top="8sp" android:right="2dp" android:bottom="8sp">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<stroke android:width="2dp" />
</shape>
</item>
</layer-list>
</aapt:attr>
<TextView
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_cutoutBackground="true"
android:minHeight="8sp"
android:gravity="start|top"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@android:id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_cutoutBackground="true"
android:minHeight="8sp"
android:gravity="end|bottom"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@android:id/text2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@android:id/text1"
tools:text="@tools:sample/lorem/random" />
</CutoutBackgroundLayout>
@ephemient
Copy link
Author

test_render.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment