diff --git a/README.md b/README.md index ebc3b53..e798a5e 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ There is a [sample](https://github.com/fornewid/neumorphism/tree/master/sample) app:neumorph_shadowColorLight="@color/solid_light_color" app:neumorph_shadowColorDark="@color/solid_dark_color" + // Set light source + app:neumorph_lightSource="leftTop|leftBottom|rightTop|rightBottom" + // Set shape type and corner size app:neumorph_shapeType="{flat|pressed|basin}" app:neumorph_shapeAppearance="@style/CustomShapeAppearance" @@ -94,6 +97,11 @@ There is a [sample](https://github.com/fornewid/neumorphism/tree/master/sample) ``` +- #### LightSource +| LEFT_TOP | LEFT_BOTTOM | RIGHT_TOP | RIGHT_BOTTOM | +| :--: | :-----: | :---: | :---: | +| | | | | + - #### ShapeType | FLAT | PRESSED | BASIN | | :--: | :-----: | :---: | diff --git a/art/lightSource_leftBottom.png b/art/lightSource_leftBottom.png new file mode 100644 index 0000000..620ceaf Binary files /dev/null and b/art/lightSource_leftBottom.png differ diff --git a/art/lightSource_leftTop.png b/art/lightSource_leftTop.png new file mode 100644 index 0000000..0442687 Binary files /dev/null and b/art/lightSource_leftTop.png differ diff --git a/art/lightSource_rightBottom.png b/art/lightSource_rightBottom.png new file mode 100644 index 0000000..4f877b0 Binary files /dev/null and b/art/lightSource_rightBottom.png differ diff --git a/art/lightSource_rightTop.png b/art/lightSource_rightTop.png new file mode 100644 index 0000000..1b970b6 Binary files /dev/null and b/art/lightSource_rightTop.png differ diff --git a/neumorphism/src/main/java/soup/neumorphism/LightSource.kt b/neumorphism/src/main/java/soup/neumorphism/LightSource.kt new file mode 100644 index 0000000..17e09fc --- /dev/null +++ b/neumorphism/src/main/java/soup/neumorphism/LightSource.kt @@ -0,0 +1,39 @@ +package soup.neumorphism + +import androidx.annotation.IntDef +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@IntDef( + LightSource.LEFT_TOP, + LightSource.LEFT_BOTTOM, + LightSource.RIGHT_TOP, + LightSource.RIGHT_BOTTOM +) +@Retention(AnnotationRetention.SOURCE) +annotation class LightSource { + companion object { + const val LEFT_TOP = 0 + const val LEFT_BOTTOM = 1 + const val RIGHT_TOP = 2 + const val RIGHT_BOTTOM = 3 + + const val DEFAULT = LEFT_TOP + + fun isLeft(@LightSource lightSource: Int): Boolean { + return lightSource == LEFT_TOP || lightSource == LEFT_BOTTOM + } + + fun isTop(@LightSource lightSource: Int): Boolean { + return lightSource == LEFT_TOP || lightSource == RIGHT_TOP + } + + fun isRight(@LightSource lightSource: Int): Boolean { + return lightSource == RIGHT_TOP || lightSource == RIGHT_BOTTOM + } + + fun isBottom(@LightSource lightSource: Int): Boolean { + return lightSource == LEFT_BOTTOM || lightSource == RIGHT_BOTTOM + } + } +} diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphButton.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphButton.kt index 4da8272..fbe0ec5 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphButton.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphButton.kt @@ -31,6 +31,7 @@ class NeumorphButton @JvmOverloads constructor( val fillColor = a.getColorStateList(R.styleable.NeumorphButton_neumorph_backgroundColor) val strokeColor = a.getColorStateList(R.styleable.NeumorphButton_neumorph_strokeColor) val strokeWidth = a.getDimension(R.styleable.NeumorphButton_neumorph_strokeWidth, 0f) + val lightSource = a.getInt(R.styleable.NeumorphButton_neumorph_lightSource, LightSource.DEFAULT) val shapeType = a.getInt(R.styleable.NeumorphButton_neumorph_shapeType, ShapeType.DEFAULT) val inset = a.getDimensionPixelSize( R.styleable.NeumorphButton_neumorph_inset, 0 @@ -64,6 +65,7 @@ class NeumorphButton @JvmOverloads constructor( shapeDrawable = NeumorphShapeDrawable(context, attrs, defStyleAttr, defStyleRes).apply { setInEditMode(isInEditMode) + setLightSource(lightSource) setShapeType(shapeType) setShadowElevation(shadowElevation) setShadowColorLight(shadowColorLight) @@ -130,6 +132,15 @@ class NeumorphButton @JvmOverloads constructor( return shapeDrawable.getStrokeWidth() } + fun setLightSource(@LightSource lightSource: Int) { + shapeDrawable.setLightSource(lightSource) + } + + @LightSource + fun getLightSource(): Int { + return shapeDrawable.getLightSource() + } + fun setShapeType(@ShapeType shapeType: Int) { shapeDrawable.setShapeType(shapeType) } diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphCardView.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphCardView.kt index e5d1203..bff7a3f 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphCardView.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphCardView.kt @@ -33,6 +33,7 @@ class NeumorphCardView @JvmOverloads constructor( val fillColor = a.getColorStateList(R.styleable.NeumorphCardView_neumorph_backgroundColor) val strokeColor = a.getColorStateList(R.styleable.NeumorphCardView_neumorph_strokeColor) val strokeWidth = a.getDimension(R.styleable.NeumorphCardView_neumorph_strokeWidth, 0f) + val lightSource = a.getInt(R.styleable.NeumorphCardView_neumorph_lightSource, LightSource.DEFAULT) val shapeType = a.getInt(R.styleable.NeumorphCardView_neumorph_shapeType, ShapeType.DEFAULT) val inset = a.getDimensionPixelSize( R.styleable.NeumorphCardView_neumorph_inset, 0 @@ -66,6 +67,7 @@ class NeumorphCardView @JvmOverloads constructor( shapeDrawable = NeumorphShapeDrawable(context, attrs, defStyleAttr, defStyleRes).apply { setInEditMode(isInEditMode) + setLightSource(lightSource) setShapeType(shapeType) setShadowElevation(shadowElevation) setShadowColorLight(shadowColorLight) @@ -143,6 +145,15 @@ class NeumorphCardView @JvmOverloads constructor( return shapeDrawable.getStrokeWidth() } + fun setLightSource(@LightSource lightSource: Int) { + shapeDrawable.setLightSource(lightSource) + } + + @LightSource + fun getLightSource(): Int { + return shapeDrawable.getLightSource() + } + fun setShapeType(@ShapeType shapeType: Int) { shapeDrawable.setShapeType(shapeType) } diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphFloatingActionButton.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphFloatingActionButton.kt index 94d154b..3e10793 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphFloatingActionButton.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphFloatingActionButton.kt @@ -31,8 +31,8 @@ class NeumorphFloatingActionButton @JvmOverloads constructor( val fillColor = a.getColorStateList(R.styleable.NeumorphFloatingActionButton_neumorph_backgroundColor) val strokeColor = a.getColorStateList(R.styleable.NeumorphFloatingActionButton_neumorph_strokeColor) val strokeWidth = a.getDimension(R.styleable.NeumorphFloatingActionButton_neumorph_strokeWidth, 0f) - val shapeType = - a.getInt(R.styleable.NeumorphFloatingActionButton_neumorph_shapeType, ShapeType.DEFAULT) + val lightSource = a.getInt(R.styleable.NeumorphFloatingActionButton_neumorph_lightSource, LightSource.DEFAULT) + val shapeType = a.getInt(R.styleable.NeumorphFloatingActionButton_neumorph_shapeType, ShapeType.DEFAULT) val inset = a.getDimensionPixelSize( R.styleable.NeumorphFloatingActionButton_neumorph_inset, 0 ) @@ -65,6 +65,7 @@ class NeumorphFloatingActionButton @JvmOverloads constructor( shapeDrawable = NeumorphShapeDrawable(context, attrs, defStyleAttr, defStyleRes).apply { setInEditMode(isInEditMode) + setLightSource(lightSource) setShapeType(shapeType) setShadowElevation(shadowElevation) setShadowColorLight(shadowColorLight) @@ -131,6 +132,15 @@ class NeumorphFloatingActionButton @JvmOverloads constructor( return shapeDrawable.getStrokeWidth() } + fun setLightSource(@LightSource lightSource: Int) { + shapeDrawable.setLightSource(lightSource) + } + + @LightSource + fun getLightSource(): Int { + return shapeDrawable.getLightSource() + } + fun setShapeType(@ShapeType shapeType: Int) { shapeDrawable.setShapeType(shapeType) } diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphImageButton.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphImageButton.kt index 1bcb4a3..48a4f74 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphImageButton.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphImageButton.kt @@ -31,6 +31,7 @@ class NeumorphImageButton @JvmOverloads constructor( val fillColor = a.getColorStateList(R.styleable.NeumorphImageButton_neumorph_backgroundColor) val strokeColor = a.getColorStateList(R.styleable.NeumorphImageButton_neumorph_strokeColor) val strokeWidth = a.getDimension(R.styleable.NeumorphImageButton_neumorph_strokeWidth, 0f) + val lightSource = a.getInt(R.styleable.NeumorphImageButton_neumorph_lightSource, LightSource.DEFAULT) val shapeType = a.getInt(R.styleable.NeumorphImageButton_neumorph_shapeType, ShapeType.DEFAULT) val inset = a.getDimensionPixelSize( R.styleable.NeumorphImageButton_neumorph_inset, 0 @@ -64,6 +65,7 @@ class NeumorphImageButton @JvmOverloads constructor( shapeDrawable = NeumorphShapeDrawable(context, attrs, defStyleAttr, defStyleRes).apply { setInEditMode(isInEditMode) + setLightSource(lightSource) setShapeType(shapeType) setShadowElevation(shadowElevation) setShadowColorLight(shadowColorLight) @@ -130,6 +132,15 @@ class NeumorphImageButton @JvmOverloads constructor( return shapeDrawable.getStrokeWidth() } + fun setLightSource(@LightSource lightSource: Int) { + shapeDrawable.setLightSource(lightSource) + } + + @LightSource + fun getLightSource(): Int { + return shapeDrawable.getLightSource() + } + fun setShapeType(@ShapeType shapeType: Int) { shapeDrawable.setShapeType(shapeType) } diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphImageView.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphImageView.kt index a180fe1..dbdd6b0 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphImageView.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphImageView.kt @@ -31,6 +31,7 @@ class NeumorphImageView @JvmOverloads constructor( val fillColor = a.getColorStateList(R.styleable.NeumorphImageView_neumorph_backgroundColor) val strokeColor = a.getColorStateList(R.styleable.NeumorphImageView_neumorph_strokeColor) val strokeWidth = a.getDimension(R.styleable.NeumorphImageView_neumorph_strokeWidth, 0f) + val lightSource = a.getInt(R.styleable.NeumorphImageView_neumorph_lightSource, LightSource.DEFAULT) val shapeType = a.getInt(R.styleable.NeumorphImageView_neumorph_shapeType, ShapeType.DEFAULT) val inset = a.getDimensionPixelSize( R.styleable.NeumorphImageView_neumorph_inset, 0 @@ -64,6 +65,7 @@ class NeumorphImageView @JvmOverloads constructor( shapeDrawable = NeumorphShapeDrawable(context, attrs, defStyleAttr, defStyleRes).apply { setInEditMode(isInEditMode) + setLightSource(lightSource) setShapeType(shapeType) setShadowElevation(shadowElevation) setShadowColorLight(shadowColorLight) @@ -130,6 +132,15 @@ class NeumorphImageView @JvmOverloads constructor( return shapeDrawable.getStrokeWidth() } + fun setLightSource(@LightSource lightSource: Int) { + shapeDrawable.setLightSource(lightSource) + } + + @LightSource + fun getLightSource(): Int { + return shapeDrawable.getLightSource() + } + fun setShapeType(@ShapeType shapeType: Int) { shapeDrawable.setShapeType(shapeType) } diff --git a/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt b/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt index 8080a2d..ebde463 100644 --- a/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt +++ b/neumorphism/src/main/java/soup/neumorphism/NeumorphShapeDrawable.kt @@ -166,6 +166,18 @@ class NeumorphShapeDrawable : Drawable { invalidateSelf() } + fun setLightSource(@LightSource lightSource: Int) { + if (drawableState.lightSource != lightSource) { + drawableState.lightSource = lightSource + invalidateSelf() + } + } + + @LightSource + fun getLightSource(): Int { + return drawableState.lightSource + } + fun setShapeType(@ShapeType shapeType: Int) { if (drawableState.shapeType != shapeType) { drawableState.shapeType = shapeType @@ -382,6 +394,8 @@ class NeumorphShapeDrawable : Drawable { var alpha = 255 + @LightSource + var lightSource: Int = LightSource.DEFAULT @ShapeType var shapeType: Int = ShapeType.DEFAULT var shadowElevation: Float = 0f @@ -408,6 +422,7 @@ class NeumorphShapeDrawable : Drawable { strokeColor = orig.strokeColor strokeWidth = orig.strokeWidth alpha = orig.alpha + lightSource = orig.lightSource shapeType = orig.shapeType shadowElevation = orig.shadowElevation shadowColorLight = orig.shadowColorLight 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 0287ed3..eb936b8 100644 --- a/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt +++ b/neumorphism/src/main/java/soup/neumorphism/internal/shape/FlatShape.kt @@ -7,6 +7,7 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import soup.neumorphism.CornerFamily +import soup.neumorphism.LightSource import soup.neumorphism.NeumorphShapeAppearanceModel import soup.neumorphism.NeumorphShapeDrawable.NeumorphShapeDrawableState import soup.neumorphism.internal.util.onCanvas @@ -29,20 +30,23 @@ internal class FlatShape( override fun draw(canvas: Canvas, outlinePath: Path) { canvas.withClipOut(outlinePath) { + val lightSource = drawableState.lightSource val elevation = drawableState.shadowElevation val z = drawableState.shadowElevation + drawableState.translationZ - val left: Float - val top: Float val inset = drawableState.inset - left = inset.left.toFloat() - top = inset.top.toFloat() + val left = inset.left.toFloat() + val top = inset.top.toFloat() 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, offset + left, offset + top, null) + 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, offset + left, offset + top, null) + drawBitmap(it, offsetX + left, offsetY + top, null) } } } 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 c279331..8dd4d33 100644 --- a/neumorphism/src/main/java/soup/neumorphism/internal/shape/PressedShape.kt +++ b/neumorphism/src/main/java/soup/neumorphism/internal/shape/PressedShape.kt @@ -6,6 +6,7 @@ import android.graphics.Path import android.graphics.Rect import android.graphics.drawable.GradientDrawable import soup.neumorphism.CornerFamily +import soup.neumorphism.LightSource import soup.neumorphism.NeumorphShapeDrawable.NeumorphShapeDrawableState import soup.neumorphism.internal.util.onCanvas import soup.neumorphism.internal.util.withClip @@ -58,7 +59,7 @@ internal class PressedShape( drawableState.shapeAppearanceModel.getCornerSize() ) shape = GradientDrawable.RECTANGLE - cornerRadii = floatArrayOf(0f, 0f, 0f, 0f, cornerSize, cornerSize, 0f, 0f) + cornerRadii = getCornerRadiiForLightShadow(cornerSize) } } } @@ -76,7 +77,7 @@ internal class PressedShape( drawableState.shapeAppearanceModel.getCornerSize() ) shape = GradientDrawable.RECTANGLE - cornerRadii = floatArrayOf(cornerSize, cornerSize, 0f, 0f, 0f, 0f, 0f, 0f) + cornerRadii = getCornerRadiiForDarkShadow(cornerSize) } } } @@ -88,6 +89,26 @@ internal class PressedShape( shadowBitmap = generateShadowBitmap(w, h) } + private fun getCornerRadiiForLightShadow(cornerSize: Float): FloatArray { + return when (drawableState.lightSource) { + LightSource.LEFT_TOP -> floatArrayOf(0f, 0f, 0f, 0f, cornerSize, cornerSize, 0f, 0f) + LightSource.LEFT_BOTTOM -> floatArrayOf(0f, 0f, cornerSize, cornerSize, 0f, 0f, 0f, 0f) + LightSource.RIGHT_TOP -> floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, cornerSize, cornerSize) + LightSource.RIGHT_BOTTOM -> floatArrayOf(cornerSize, cornerSize, 0f, 0f, 0f, 0f, 0f, 0f) + 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) + LightSource.LEFT_BOTTOM -> floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, cornerSize, cornerSize) + LightSource.RIGHT_TOP -> floatArrayOf(0f, 0f, cornerSize, cornerSize, 0f, 0f, 0f, 0f) + LightSource.RIGHT_BOTTOM -> floatArrayOf(0f, 0f, 0f, 0f, cornerSize, cornerSize, 0f, 0f) + else -> throw IllegalStateException("LightSource ${drawableState.lightSource} is not supported.") + } + } + private fun generateShadowBitmap(w: Int, h: Int): Bitmap? { fun Bitmap.blurred(): Bitmap? { if (drawableState.inEditMode) { @@ -97,12 +118,21 @@ internal class PressedShape( } val shadowElevation = drawableState.shadowElevation + val lightSource = drawableState.lightSource return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) .onCanvas { - withTranslation(-shadowElevation, -shadowElevation) { + withTranslation( + x = if (LightSource.isLeft(lightSource)) -shadowElevation else 0f, + y = if (LightSource.isTop(lightSource)) -shadowElevation else 0f + ) { lightShadowDrawable.draw(this) } - darkShadowDrawable.draw(this) + withTranslation( + x = if (LightSource.isRight(lightSource)) -shadowElevation else 0f, + y = if (LightSource.isBottom(lightSource)) -shadowElevation else 0f + ) { + darkShadowDrawable.draw(this) + } } .blurred() } diff --git a/neumorphism/src/main/res/values/attrs.xml b/neumorphism/src/main/res/values/attrs.xml index 53b1d69..36fc094 100644 --- a/neumorphism/src/main/res/values/attrs.xml +++ b/neumorphism/src/main/res/values/attrs.xml @@ -1,12 +1,18 @@ - - + + + + + + + + @@ -27,6 +33,7 @@ + @@ -44,6 +51,7 @@ + @@ -61,6 +69,7 @@ + @@ -78,6 +87,7 @@ + @@ -95,6 +105,7 @@ +