问题
I have to implement a banner according to the following designs:
The complexity here is in the shadow of the round logo, the shadow of the logo circle is the continuation of the shadow of the rectangular card of the banner. The border of the shadow is outlined in the following image:
Of course the shadow shouldn't be casted on the surface below the top side of the card. Also the logo center has some offset from the border of the card.
How can I achieve this effect? Standard android shapes doesn't allow to form such a contour. Drawing everything manually seems to be too complex decision for the problem. We have minSdkVersion
21.
回答1:
You can achieve it using this trick
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="20dp"
app:cardCornerRadius="56dp"
app:cardElevation="16dp"
android:layout_width="56dp"
android:layout_height="56dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="match_parent"
android:src="@color/colorPrimary"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
app:cardElevation="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="48dp"
android:layout_width="350dp"
android:layout_height="500dp">
<LinearLayout
android:layout_marginTop="-28dp"
android:layout_gravity="center_horizontal"
android:layout_width="56dp"
android:layout_height="56dp">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="match_parent"
android:src="@color/colorPrimary"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
The result will be:
The main idea create two cardview with images, one under the main card and another one in the cardview and using margins make the look like one circle.
回答2:
I've ended up on using the MaterialShapeDrawable class. It allows to customize drawable edges by manually defining how the edge should de drawn. This is achieved by deriving EdgeTreatment class (similar is possible for corners with CornerTreatment).
As a result the background of the banner looks like this:
Here is the banner top edge treatment class:
private class BannerTopEdgeTreatment(private val circleRadius: Int,
private val circleCenterOffset: Int) : EdgeTreatment(), Cloneable {
init {
// do not allow to offset circle center up
if (circleCenterOffset < 0)
throw IllegalArgumentException()
}
override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) {
// use interpolated radius
val radius = circleRadius * interpolation
// if circle lays entirely inside the rectangle then just draw a line
val circleTop = circleCenterOffset - radius
if (circleTop >= 0) {
shapePath.lineTo(length, 0f)
return
}
// calc the distance from the center of the edge to the point where arc begins
// ignore the case when the radius is so big that the circle fully covers the edge
// just draw a line for now, but maybe it can be replaced by drawing the arc
val c = sqrt(radius.pow(2) - circleCenterOffset.toDouble().pow(2))
if (c > center) {
shapePath.lineTo(length, 0f)
return
}
// draw a line from the left corner to the start of the arc
val arcStart = center - c
shapePath.lineTo(arcStart.toFloat(), 0f)
// calc the start angle and the sweep angle of the arc and draw the arc
// angles are measured clockwise with 0 degrees at 3 o'clock
val alpha = Math.toDegrees(asin(circleCenterOffset / radius).toDouble())
val startAngle = 180 + alpha
val sweepAngle = 180 - 2 * alpha
shapePath.addArc(
center - radius,
circleCenterOffset - radius,
center + radius,
circleCenterOffset + radius,
startAngle.toFloat(),
sweepAngle.toFloat())
// draw the line from the end of the arc to the right corner
shapePath.lineTo(length, 0f)
}
}
The method to construct a background drawable for banner:
fun createBannerBackgroundDrawable(backgroundColor: ColorStateList,
@Px circleRadius: Int,
@Px circleCenterOffset: Int,
@Px cornersRadius: Int,
@Px elevation: Int): Drawable {
val appearanceModel = ShapeAppearanceModel.builder()
.setTopEdge(BannerTopEdgeTreatment(circleRadius, circleCenterOffset))
.setAllCorners(CornerFamily.ROUNDED, cornersRadius.toFloat())
.build()
val drawable = MaterialShapeDrawable(appearanceModel)
drawable.fillColor = backgroundColor
drawable.elevation = elevation.toFloat()
return drawable
}
Then this drawable is used as a background of the banner view:
banner.background = createVerticalBannerBackground(...)
And also it is necessary to set clipChildren
attribute of the parent view of the banner to false
:
android:clipChildren="false"
The final result:
来源:https://stackoverflow.com/questions/62090962/shadow-of-a-custom-shape