From 1528d37454dde11936be37b7f908300e95c9671d Mon Sep 17 00:00:00 2001 From: SOUP Date: Fri, 25 Sep 2020 23:57:42 +0900 Subject: [PATCH] Library: Supports different radii depending on the corner (#38) --- .../NeumorphShapeAppearanceModel.kt | 123 +++++++++++++++--- .../soup/neumorphism/NeumorphShapeDrawable.kt | 14 +- .../neumorphism/internal/shape/FlatShape.kt | 9 +- .../internal/shape/PressedShape.kt | 28 +++- neumorphism/src/main/res/values/attrs.xml | 4 + sample/src/main/res/layout/activity_main.xml | 16 ++- sample/src/main/res/values/styles.xml | 4 +- 7 files changed, 160 insertions(+), 38 deletions(-) diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeAppearanceModel.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeAppearanceModel.kt index 208606d..da22e7b 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeAppearanceModel.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeAppearanceModel.kt @@ -7,6 +7,7 @@ import android.util.TypedValue import androidx.annotation.AttrRes import androidx.annotation.Dimension import androidx.annotation.StyleRes +import kotlin.math.min class NeumorphShapeAppearanceModel { @@ -14,14 +15,17 @@ class NeumorphShapeAppearanceModel { @CornerFamily var cornerFamily: Int = CornerFamily.ROUNDED - var cornerSize: Float = 0f + var topLeftCornerSize: Float = 0f + var topRightCornerSize: Float = 0f + var bottomLeftCornerSize: Float = 0f + var bottomRightCornerSize: Float = 0f fun setAllCorners( @CornerFamily cornerFamily: Int, @Dimension cornerSize: Float ): Builder { return setAllCorners(cornerFamily) - .setAllCornerSizes(cornerSize) + .setCornerRadius(cornerSize) } fun setAllCorners(@CornerFamily cornerFamily: Int): Builder { @@ -30,9 +34,34 @@ class NeumorphShapeAppearanceModel { } } - fun setAllCornerSizes(cornerSize: Float): Builder { + fun setCornerRadius(cornerRadius: Float): Builder { + return setTopLeftCornerSize(cornerRadius) + .setTopRightCornerSize(cornerRadius) + .setBottomLeftCornerSize(cornerRadius) + .setBottomRightCornerSize(cornerRadius) + } + + fun setTopLeftCornerSize(topLeftCornerSize: Float): Builder { + return apply { + this.topLeftCornerSize = topLeftCornerSize + } + } + + fun setTopRightCornerSize(topRightCornerSize: Float): Builder { + return apply { + this.topRightCornerSize = topRightCornerSize + } + } + + fun setBottomLeftCornerSize(bottomLeftCornerSize: Float): Builder { + return apply { + this.bottomLeftCornerSize = bottomLeftCornerSize + } + } + + fun setBottomRightCornerSize(bottomRightCornerSize: Float): Builder { return apply { - this.cornerSize = cornerSize + this.bottomRightCornerSize = bottomRightCornerSize } } @@ -43,16 +72,25 @@ class NeumorphShapeAppearanceModel { @CornerFamily private val cornerFamily: Int - private val cornerSize: Float + private val topLeftCornerSize: Float + private val topRightCornerSize: Float + private val bottomLeftCornerSize: Float + private val bottomRightCornerSize: Float private constructor(builder: Builder) { cornerFamily = builder.cornerFamily - cornerSize = builder.cornerSize + topLeftCornerSize = builder.topLeftCornerSize + topRightCornerSize = builder.topRightCornerSize + bottomLeftCornerSize = builder.bottomLeftCornerSize + bottomRightCornerSize = builder.bottomRightCornerSize } constructor() { cornerFamily = CornerFamily.ROUNDED - cornerSize = 0f + topLeftCornerSize = 0f + topRightCornerSize = 0f + bottomLeftCornerSize = 0f + bottomRightCornerSize = 0f } @CornerFamily @@ -60,8 +98,37 @@ class NeumorphShapeAppearanceModel { return cornerFamily } - fun getCornerSize(): Float { - return cornerSize + fun getTopLeftCornerSize(): Float { + return topLeftCornerSize + } + + fun getTopRightCornerSize(): Float { + return topRightCornerSize + } + + fun getBottomLeftCornerSize(): Float { + return bottomLeftCornerSize + } + + fun getBottomRightCornerSize(): Float { + return bottomRightCornerSize + } + + internal fun getCornerRadii(maximum: Float): FloatArray { + val topLeftCornerSize = min(maximum, getTopLeftCornerSize()) + val topRightCornerSize = min(maximum, getTopRightCornerSize()) + val bottomLeftCornerSize = min(maximum, getBottomLeftCornerSize()) + val bottomRightCornerSize = min(maximum, getBottomRightCornerSize()) + return floatArrayOf( + topLeftCornerSize, + topLeftCornerSize, + topRightCornerSize, + topRightCornerSize, + bottomLeftCornerSize, + bottomLeftCornerSize, + bottomRightCornerSize, + bottomRightCornerSize + ) } companion object { @@ -105,26 +172,40 @@ class NeumorphShapeAppearanceModel { R.styleable.NeumorphShapeAppearance_neumorph_cornerFamily, CornerFamily.ROUNDED ) - val cornerSize = - getCornerSize( - a, - R.styleable.NeumorphShapeAppearance_neumorph_cornerSize, - defaultCornerSize - ) + val cornerSize = a.getCornerSize( + R.styleable.NeumorphShapeAppearance_neumorph_cornerSize, + defaultCornerSize + ) + val cornerSizeTopLeft = a.getCornerSize( + R.styleable.NeumorphShapeAppearance_neumorph_cornerSizeTopLeft, + cornerSize + ) + val cornerSizeTopRight = a.getCornerSize( + R.styleable.NeumorphShapeAppearance_neumorph_cornerSizeTopRight, + cornerSize + ) + val cornerSizeBottomRight = a.getCornerSize( + R.styleable.NeumorphShapeAppearance_neumorph_cornerSizeBottomLeft, cornerSize + ) + val cornerSizeBottomLeft = a.getCornerSize( + R.styleable.NeumorphShapeAppearance_neumorph_cornerSizeBottomRight, cornerSize + ) return Builder() - .setAllCorners(cornerFamily, cornerSize) + .setAllCorners(cornerFamily) + .setTopLeftCornerSize(cornerSizeTopLeft) + .setTopRightCornerSize(cornerSizeTopRight) + .setBottomRightCornerSize(cornerSizeBottomRight) + .setBottomLeftCornerSize(cornerSizeBottomLeft) } finally { a.recycle() } } - private fun getCornerSize( - a: TypedArray, index: Int, defaultValue: Float - ): Float { - val value = a.peekValue(index) ?: return defaultValue + private fun TypedArray.getCornerSize(index: Int, defaultValue: Float): Float { + val value = peekValue(index) ?: return defaultValue return if (value.type == TypedValue.TYPE_DIMENSION) { TypedValue.complexToDimensionPixelSize( - value.data, a.resources.displayMetrics + value.data, resources.displayMetrics ).toFloat() } else { defaultValue diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt index ebde463..3fe7ec0 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt @@ -13,6 +13,7 @@ import soup.neumorphism.internal.shape.BasinShape import soup.neumorphism.internal.shape.FlatShape import soup.neumorphism.internal.shape.PressedShape import soup.neumorphism.internal.shape.Shape +import kotlin.math.min class NeumorphShapeDrawable : Drawable { @@ -310,20 +311,20 @@ class NeumorphShapeDrawable : Drawable { } private fun calculateOutlinePath(bounds: RectF, path: Path) { + val shapeAppearanceModel = drawableState.shapeAppearanceModel val left = drawableState.inset.left.toFloat() val top = drawableState.inset.top.toFloat() val right = left + bounds.width() val bottom = top + bounds.height() path.reset() - when (drawableState.shapeAppearanceModel.getCornerFamily()) { + when (shapeAppearanceModel.getCornerFamily()) { CornerFamily.OVAL -> { path.addOval(left, top, right, bottom, Path.Direction.CW) } CornerFamily.ROUNDED -> { - val cornerSize = drawableState.shapeAppearanceModel.getCornerSize() path.addRoundRect( left, top, right, bottom, - cornerSize, cornerSize, + shapeAppearanceModel.getCornerRadii(min(bounds.width() / 2f, bounds.height() / 2f)), Path.Direction.CW ) } @@ -332,13 +333,14 @@ class NeumorphShapeDrawable : Drawable { } override fun getOutline(outline: Outline) { - when (drawableState.shapeAppearanceModel.getCornerFamily()) { + val shapeAppearanceModel = drawableState.shapeAppearanceModel + when (shapeAppearanceModel.getCornerFamily()) { CornerFamily.OVAL -> { outline.setOval(getBoundsInternal()) } CornerFamily.ROUNDED -> { - val cornerSize = drawableState.shapeAppearanceModel.getCornerSize() - outline.setRoundRect(getBoundsInternal(), cornerSize) + //TODO: How to setRoundRect() with cornerRadii??? + outline.setRect(getBoundsInternal()) } } } diff --git a/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt b/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt index eb936b8..0540d34 100644 --- a/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt +++ b/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt @@ -13,6 +13,7 @@ import soup.neumorphism.NeumorphShapeDrawable.NeumorphShapeDrawableState import soup.neumorphism.internal.util.onCanvas import soup.neumorphism.internal.util.withClipOut import soup.neumorphism.internal.util.withTranslation +import kotlin.math.min import kotlin.math.roundToInt internal class FlatShape( @@ -39,13 +40,11 @@ internal class FlatShape( lightShadowBitmap?.let { val offsetX = if (LightSource.isLeft(lightSource)) -elevation - z else -elevation + z val offsetY = if (LightSource.isTop(lightSource)) -elevation - z else -elevation + z - val offset = -elevation - z drawBitmap(it, offsetX + left, offsetY + top, null) } darkShadowBitmap?.let { val offsetX = if (LightSource.isLeft(lightSource)) -elevation + z else -elevation - z val offsetY = if (LightSource.isTop(lightSource)) -elevation + z else -elevation - z - val offset = -elevation + z drawBitmap(it, offsetX + left, offsetY + top, null) } } @@ -59,9 +58,9 @@ internal class FlatShape( } CornerFamily.ROUNDED -> { shape = GradientDrawable.RECTANGLE - cornerRadii = shapeAppearanceModel.getCornerSize().let { - floatArrayOf(it, it, it, it, it, it, it, it) - } + cornerRadii = shapeAppearanceModel.getCornerRadii( + min(bounds.width() / 2f, bounds.height() / 2f) + ) } } } diff --git a/neumorphism/src/main/java/soup/neumorphism/internal/shape/PressedShape.kt b/neumorphism/src/main/java/soup/neumorphism/internal/shape/PressedShape.kt index 8dd4d33..3ae5699 100644 --- a/neumorphism/src/main/java/soup/neumorphism/internal/shape/PressedShape.kt +++ b/neumorphism/src/main/java/soup/neumorphism/internal/shape/PressedShape.kt @@ -56,7 +56,7 @@ internal class PressedShape( CornerFamily.ROUNDED -> { val cornerSize = min( min(w / 2f, h / 2f), - drawableState.shapeAppearanceModel.getCornerSize() + getCornerSizeForLightShadow() ) shape = GradientDrawable.RECTANGLE cornerRadii = getCornerRadiiForLightShadow(cornerSize) @@ -74,7 +74,7 @@ internal class PressedShape( CornerFamily.ROUNDED -> { val cornerSize = min( min(w / 2f, h / 2f), - drawableState.shapeAppearanceModel.getCornerSize() + getCornerSizeForDarkShadow() ) shape = GradientDrawable.RECTANGLE cornerRadii = getCornerRadiiForDarkShadow(cornerSize) @@ -89,6 +89,18 @@ internal class PressedShape( shadowBitmap = generateShadowBitmap(w, h) } + private fun getCornerSizeForLightShadow(): Float { + return drawableState.shapeAppearanceModel.run { + when (drawableState.lightSource) { + LightSource.LEFT_TOP -> getBottomLeftCornerSize() + LightSource.LEFT_BOTTOM -> getTopRightCornerSize() + LightSource.RIGHT_TOP -> getBottomRightCornerSize() + LightSource.RIGHT_BOTTOM -> getTopLeftCornerSize() + else -> throw IllegalStateException("LightSource ${drawableState.lightSource} is not supported.") + } + } + } + private fun getCornerRadiiForLightShadow(cornerSize: Float): FloatArray { return when (drawableState.lightSource) { LightSource.LEFT_TOP -> floatArrayOf(0f, 0f, 0f, 0f, cornerSize, cornerSize, 0f, 0f) @@ -99,6 +111,18 @@ internal class PressedShape( } } + private fun getCornerSizeForDarkShadow(): Float { + return drawableState.shapeAppearanceModel.run { + when (drawableState.lightSource) { + LightSource.LEFT_TOP -> getTopLeftCornerSize() + LightSource.LEFT_BOTTOM -> getBottomLeftCornerSize() + LightSource.RIGHT_TOP -> getTopRightCornerSize() + LightSource.RIGHT_BOTTOM -> getBottomRightCornerSize() + else -> throw IllegalStateException("LightSource ${drawableState.lightSource} is not supported.") + } + } + } + private fun getCornerRadiiForDarkShadow(cornerSize: Float): FloatArray { return when (drawableState.lightSource) { LightSource.LEFT_TOP -> floatArrayOf(cornerSize, cornerSize, 0f, 0f, 0f, 0f, 0f, 0f) diff --git a/neumorphism/src/main/res/values/attrs.xml b/neumorphism/src/main/res/values/attrs.xml index 36fc094..9d32907 100644 --- a/neumorphism/src/main/res/values/attrs.xml +++ b/neumorphism/src/main/res/values/attrs.xml @@ -131,6 +131,10 @@ + + + + diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 29d5685..d65a302 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -109,6 +109,17 @@ app:layout_constraintTop_toBottomOf="@id/card_barrier" app:neumorph_shapeType="basin" /> + + @@ -132,7 +143,6 @@ android:scaleType="centerInside" android:src="@drawable/ic_filter" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:neumorph_shapeAppearance="@style/CustomShapeAppearance" /> + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml index 01cbe10..cb99238 100644 --- a/sample/src/main/res/values/styles.xml +++ b/sample/src/main/res/values/styles.xml @@ -2,6 +2,8 @@