Android 和Compose 仿骨架屏

213 阅读6分钟

思路以及实现

  • 插值器ValueAnimatoranimateFloatAsState实现固定频率向右平移阴影动画
  • 选择Canvas绘制单个带圆角的背景灰框,圆角大小可调节,背景框附着阴影动画
  • Canvas绘制一个组合式的背景,给背景加阴影动画
  • 继承FrameLayout,给父布局加阴影动画

选择Canvas绘制单个带圆角的背景灰框,圆角大小可调节,背景框附着阴影动画。原生代码实现如下

package com.nio.skeletondemo;  
  
import android.animation.ValueAnimator;  
import android.content.Context;  
import android.content.res.TypedArray;  
import android.graphics.Canvas;  
import android.graphics.Color;  
import android.graphics.LinearGradient;  
import android.graphics.Paint;  
import android.graphics.PixelFormat;  
import android.graphics.PorterDuff;  
import android.graphics.RectF;  
import android.graphics.Shader;  
import android.util.AttributeSet;  
import android.view.SurfaceHolder;  
import android.view.SurfaceView;  
  
import androidx.annotation.NonNull;  
  
/**  
* @author Administrator  
* @date 2024/5/31 0031 8:52  
* @description: SkeletonSquare  
*/  
public class SkeletonSquare extends SurfaceView implements SurfaceHolder.Callback{  
private int foregroundColor=Color.parseColor("#eeeeee");  
  
/**  
* #3FADA996  
*/  
private int gradientColor= Color.parseColor("#3FDCDFE6");  
private ValueAnimator value;  
private float offsetX;  
private int duration=1000;  
private Paint mPaint;  
private Paint mBgPaint;  
private float cornerRadius = 20.0f;  
public SkeletonSquare(Context context) {  
super(context);  
init(context, null);  
}  
  
public SkeletonSquare(Context context, AttributeSet attrs) {  
super(context, attrs);  
  
init(context, attrs);  
}  
  
public SkeletonSquare(Context context, AttributeSet attrs, int defStyleAttr) {  
super(context, attrs, defStyleAttr);  
init(context, attrs);  
}  
  
public SkeletonSquare(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {  
super(context, attrs, defStyleAttr, defStyleRes);  
init(context, attrs);  
}  
  
private void init(Context context, AttributeSet attrs) {  
if (attrs!=null) {  
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.SkeletonSquare);  
cornerRadius = typedArray.getDimension(R.styleable.SkeletonSquare_cornerRadius,cornerRadius);  
foregroundColor=typedArray.getColor(R.styleable.SkeletonSquare_foregroundColor,foregroundColor);  
duration=typedArray.getInteger(R.styleable.SkeletonSquare_duration, duration);  
gradientColor=typedArray.getInteger(R.styleable.SkeletonSquare_gradientColor, gradientColor);  
typedArray.recycle();  
}  
setZOrderOnTop(true);  
getHolder().setFormat(PixelFormat.TRANSPARENT);  
getHolder().addCallback(this);  
mPaint=new Paint();  
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);  
mPaint.setAntiAlias(true);  
mBgPaint=new Paint();  
mBgPaint.setStyle(Paint.Style.FILL);  
mBgPaint.setAntiAlias(true);  
mBgPaint.setColor(foregroundColor);  
}  
  
@Override  
public void surfaceCreated(@NonNull SurfaceHolder holder) {  
  
}  
  
@Override  
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {  
if (value!=null&&value.isRunning()) {  
value.end();  
value.cancel();  
}  
value = ValueAnimator.ofFloat(0.0f,1.0f);  
value.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
@Override  
public void onAnimationUpdate(@NonNull ValueAnimator animation) {  
offsetX= (float) animation.getAnimatedValue();  
draw(width,height);  
}  
});  
value.setDuration(duration);  
value.setRepeatCount(ValueAnimator.INFINITE);  
value.setRepeatMode(ValueAnimator.RESTART);  
value.start();  
}  
@Override  
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {  
getHolder().removeCallback(this);  
if (value!=null&&value.isRunning()) {  
value.end();  
value.cancel();  
}  
}  
private void draw(int width, int height) {  
Canvas canvas = getHolder().lockCanvas();  
if (canvas != null) {  
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);  
float strokeWidth=width/4f;  
RectF rect=new RectF(offsetX*width-strokeWidth, 0f,offsetX*width+strokeWidth, height);  
LinearGradient linearGradient = new LinearGradient(rect.left, 0f, rect.right,0f,new int[]{Color.TRANSPARENT, gradientColor,Color.TRANSPARENT }, new float[]{0f ,0.5f,1f}, Shader.TileMode.MIRROR);  
mPaint.setShader(linearGradient);  
mPaint.setAlpha(Float.valueOf((1.0f-offsetX)*255).intValue());  
RectF rectBg=new RectF(0f, 0f,width, height);  
canvas.drawRoundRect(rectBg,cornerRadius,cornerRadius,mBgPaint);  
canvas.drawRect(rect,mPaint);  
// canvas.drawRoundRect(rect,cornerRadius,cornerRadius,mPaint);  
  
getHolder().unlockCanvasAndPost(canvas);  
}  
}  
}

attrs.xml

<declare-styleable name="SkeletonSquare">  
<attr name="duration" format="integer"/>  
<attr name="cornerRadius" format="dimension"/>  
<attr name="foregroundColor" format="color"/>  
<attr name="gradientColor" format="color"/>  
</declare-styleable>

Canvas绘制一个组合式的背景,给背景加阴影动画。参数不是动态调整参数,仅作为实现思路需进一步完善

package com.nio.skeletondemo;  
  
import android.animation.ValueAnimator;  
import android.content.Context;  
import android.graphics.Bitmap;  
import android.graphics.BitmapFactory;  
import android.graphics.BitmapShader;  
import android.graphics.Canvas;  
import android.graphics.Color;  
import android.graphics.LinearGradient;  
import android.graphics.Matrix;  
import android.graphics.Paint;  
import android.graphics.Rect;  
import android.graphics.RectF;  
import android.graphics.RuntimeShader;  
import android.graphics.Shader;  
import android.os.Handler;  
import android.os.Looper;  
import android.os.Message;  
import android.util.AttributeSet;  
import android.util.Log;  
import android.view.SurfaceHolder;  
import android.view.SurfaceView;  
import android.view.animation.LinearInterpolator;  
  
import androidx.annotation.ColorInt;  
import androidx.annotation.NonNull;  
  
/**  
* @author Administrator  
* @date 2024/5/30 0030 10:27  
* @description: SkeletonHoriztalView  
*/  
public class SkeletonHorizontalView extends SurfaceView implements SurfaceHolder.Callback {  
  
  
private SurfaceHolder surfaceHolder;  
private int strokeWidth=200;  
private int barWidth=50;  
private int barPadding=10;  
private int barStep=15;  
private float offsetX=0;  
private Paint mPaint;  
private ValueAnimator value;  
private int duration=1000;  
private int BG_COLOR=Color.parseColor("#F6F8FA");  
private int colors[]={ Color.TRANSPARENT, Color.parseColor("#50F8F8FF"), Color.parseColor("#50F8F8FF"), Color.TRANSPARENT};  
private Paint mBgPaint;  
  
public SkeletonHorizontalView(Context context) {  
super(context);  
init();  
}  
  
public SkeletonHorizontalView(Context context, AttributeSet attrs) {  
super(context, attrs);  
init();  
}  
  
public SkeletonHorizontalView(Context context, AttributeSet attrs, int defStyleAttr) {  
super(context, attrs, defStyleAttr);  
init();  
}  
  
public SkeletonHorizontalView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {  
super(context, attrs, defStyleAttr, defStyleRes);  
init();  
}  
  
  
@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
final int minimumWidth = getSuggestedMinimumWidth();  
final int minimumHeight = getSuggestedMinimumHeight();  
int width = measureWidth(minimumWidth, widthMeasureSpec);  
int height = measureHeight(minimumHeight, heightMeasureSpec);  
setMeasuredDimension(width, height);  
}  
private int measureWidth(int defaultWidth, int measureSpec) {  
int specMode = MeasureSpec.getMode(measureSpec);  
int specSize = MeasureSpec.getSize(measureSpec);  
switch (specMode) {  
case MeasureSpec.AT_MOST:  
defaultWidth = (int) getPaddingLeft() + getPaddingRight();  
break;  
case MeasureSpec.EXACTLY:  
defaultWidth = specSize;  
break;  
case MeasureSpec.UNSPECIFIED:  
defaultWidth = Math.max(defaultWidth, specSize);  
}  
return defaultWidth;  
}  
  
  
private int measureHeight(int defaultHeight, int measureSpec) {  
int specMode = MeasureSpec.getMode(measureSpec);  
int specSize = MeasureSpec.getSize(measureSpec);  
switch (specMode) {  
case MeasureSpec.AT_MOST:  
defaultHeight = barStep*5+barWidth*3;  
break;  
case MeasureSpec.EXACTLY:  
defaultHeight = specSize;  
break;  
case MeasureSpec.UNSPECIFIED:  
defaultHeight = Math.max(defaultHeight, specSize);  
break;  
}  
return defaultHeight;  
}  
  
private void init() {  
  
surfaceHolder=getHolder();  
surfaceHolder.addCallback(this);  
mPaint=new Paint();  
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);  
mPaint.setAntiAlias(true);  
mBgPaint=new Paint();  
mBgPaint.setColor(Color.parseColor("#F1F1F1"));//"#F1F1F1"  
mBgPaint.setStyle(Paint.Style.FILL_AND_STROKE);  
mBgPaint.setStrokeWidth(barWidth);  
mBgPaint.setStrokeCap(Paint.Cap.ROUND);  
// Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_shader_layer);  
// strokeWidth=bitmap.getWidth();  
// Bitmap bitmapScale = Bitmap.createScaledBitmap(bitmap, strokeWidth, bitmap.getHeight(), true);  
//// bitmap.recycle();  
// BitmapShader bitmapShader=new BitmapShader(bitmapScale,Shader.TileMode.CLAMP,Shader.TileMode.CLAMP);  
// mPaint.setShader(bitmapShader);  
}  
  
@Override  
public void surfaceCreated(@NonNull SurfaceHolder holder) {  
draw();  
}  
  
@Override  
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {  
if (value!=null&&value.isRunning()) {  
value.end();  
value.cancel();  
}  
value = ValueAnimator.ofFloat(0, getWidth());  
value.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
@Override  
public void onAnimationUpdate(@NonNull ValueAnimator animation) {  
offsetX= (float) animation.getAnimatedValue();  
// Log.e("TAG","offsetX:"+offsetX);  
draw();  
}  
});  
value.setDuration(duration);  
value.setRepeatCount(ValueAnimator.INFINITE);  
value.setRepeatMode(ValueAnimator.RESTART);  
value.start();  
}  
  
  
@Override  
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {  
holder.removeCallback(this);  
if (value!=null&&value.isRunning()) {  
value.end();  
value.cancel();  
}  
}  
private void draw() {  
Canvas canvas = surfaceHolder.lockCanvas();  
if (canvas != null) {  
canvas.drawColor(BG_COLOR);  
for(int i=0;i<3;i++){  
canvas.drawLine(strokeWidth*1.5f+barPadding,barWidth+barStep*i+barWidth*i,getWidth()-barPadding-strokeWidth,barWidth+barStep*i+barWidth*i,mBgPaint);  
}  
canvas.drawRect(barWidth,barWidth,barStep*2+barWidth*3,barWidth+barStep*2+barWidth*2, mBgPaint);  
RectF rect=new RectF(offsetX-strokeWidth, 0f,offsetX+strokeWidth, barStep*5+barWidth*3);  
LinearGradient linearGradient = new LinearGradient(offsetX-strokeWidth, 0f, offsetX+strokeWidth, 0,colors, new float[]{0.0f ,0.4f,0.6f,1f}, Shader.TileMode.CLAMP);  
mPaint.setShader(linearGradient);  
Log.e("TAG","rect:"+rect.toString()+",width:"+(rect.right-rect.left));  
canvas.drawRect(rect, mPaint);  
surfaceHolder.unlockCanvasAndPost(canvas);  
}  
}  
  
}

选择Canvas绘制单个带圆角的背景灰框,圆角大小可调节,背景框附着阴影动画。Compose实现,这里compose作为第三方库形式引入,测试过程不能直接引用,存放到自定义‘KtSkeletonFragment : Fragment()’的layout布局中。代码如下

package com.nio.skeleton  
  
import android.content.Context  
  
import android.util.AttributeSet  
import android.widget.FrameLayout  
import androidx.compose.animation.core.EaseIn  
import androidx.compose.animation.core.LinearEasing  
import androidx.compose.animation.core.RepeatMode  
import androidx.compose.animation.core.animateFloatAsState  
import androidx.compose.animation.core.infiniteRepeatable  
import androidx.compose.animation.core.tween  
import androidx.compose.foundation.Canvas  
import androidx.compose.foundation.layout.fillMaxSize  
import androidx.compose.material.Text  
import androidx.compose.runtime.LaunchedEffect  
import androidx.compose.runtime.getValue  
import androidx.compose.runtime.mutableStateOf  
import androidx.compose.runtime.remember  
import androidx.compose.runtime.setValue  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.geometry.CornerRadius  
import androidx.compose.ui.geometry.Offset  
import androidx.compose.ui.geometry.Size  
import androidx.compose.ui.graphics.BlendMode  
import androidx.compose.ui.graphics.Brush  
import androidx.compose.ui.graphics.Color  
import androidx.compose.ui.graphics.PaintingStyle.Companion.Fill  
import androidx.compose.ui.graphics.drawscope.DrawStyle  
import androidx.compose.ui.platform.ComposeView  
import androidx.compose.ui.unit.dp  
  
/**  
* @author Administrator  
* @date 2024/6/1 0001 14:19  
* @description: SkeletonSquareKtView  
*/  
class SkeletonSquareKtView:FrameLayout {  
constructor(context: Context) : super(context){init()}  
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs){init()}  
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
context,  
attrs,  
defStyleAttr  
){init()}  
  
constructor(  
context: Context,  
attrs: AttributeSet?,  
defStyleAttr: Int,  
defStyleRes: Int  
) : super(context, attrs, defStyleAttr, defStyleRes){init()}  
  
private fun init() {  
val compose=ComposeView(this.context)  
addView(compose,LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT))  
compose.setContent {  
var isStart by remember{  
mutableStateOf(false)  
}  
var offset = animateFloatAsState(targetValue = if(isStart)1f else 0f, animationSpec = infiniteRepeatable(  
animation = tween(1000, easing = LinearEasing) , repeatMode = RepeatMode.Restart  
), label = "shader")  
Canvas(modifier = Modifier.fillMaxSize()){  
drawRoundRect(Color(0xFFeeeeee), cornerRadius = CornerRadius(4.dp.value,4.dp.value), topLeft = Offset.Zero,size= Size(size.width,size.height))  
drawRoundRect(Brush.horizontalGradient(listOf(Color(0x1AC0C0C0),Color(0x3FC0C0C0),Color(0x1AFFFFFF)),size.width.times(offset.value)-size.width/2,size.width.times(offset.value)), cornerRadius = CornerRadius(4.dp.value,4.dp.value), topLeft = Offset.Zero,size= Size(size.width,size.height))  
}  
LaunchedEffect(Unit){  
isStart=true  
}  
}  
}  
}

引用

<LinearLayout  
android:layout_width="match_parent"  
android:layout_height="wrap_content"  
android:orientation="horizontal"  
android:gravity="center_vertical">  
<com.nio.skeleton.SkeletonSquareKtView  
android:layout_width="69dp"  
android:layout_height="69dp"  
android:layout_margin="8dp"/>  
<LinearLayout  
android:layout_width="match_parent"  
android:layout_height="wrap_content"  
android:orientation="vertical"  
android:layout_marginTop="10dp"  
android:layout_marginBottom="10dp">  
<com.nio.skeleton.SkeletonSquareKtView  
android:layout_width="100dp"  
android:layout_height="14dp"  
android:layout_marginBottom="10dp"/>  
<com.nio.skeleton.SkeletonSquareKtView  
android:layout_width="196dp"  
android:layout_height="14dp"  
android:layout_marginEnd="12dp"  
android:layout_marginBottom="10dp"/>  
<com.nio.skeleton.SkeletonSquareKtView  
android:layout_width="match_parent"  
android:layout_height="14dp"  
android:layout_marginEnd="12dp" />  
</LinearLayout>  
</LinearLayout>
<fragment  
android:name="com.nio.skeletondemo.KtSkeletonFragment"  
android:layout_width="match_parent"  
android:layout_height="0dp"  
android:id="@+id/fbl"  
android:tag="@string/hello_blank_fragment"  
app:layout_constraintTop_toBottomOf="@id/hv"  
app:layout_constraintStart_toStartOf="parent"  
app:layout_constraintBottom_toBottomOf="parent"/>

继承FrameLayout,给父布局加阴影动画,Compose实现

package com.nio.skeleton  
  
import android.content.Context  
import android.util.AttributeSet  
import android.view.ViewGroup  
import android.widget.FrameLayout  
import androidx.compose.animation.core.LinearEasing  
import androidx.compose.animation.core.RepeatMode  
import androidx.compose.animation.core.animateFloatAsState  
import androidx.compose.animation.core.infiniteRepeatable  
import androidx.compose.animation.core.tween  
import androidx.compose.foundation.Canvas  
import androidx.compose.foundation.layout.fillMaxSize  
import androidx.compose.foundation.layout.padding  
import androidx.compose.runtime.LaunchedEffect  
import androidx.compose.runtime.getValue  
import androidx.compose.runtime.mutableStateOf  
import androidx.compose.runtime.remember  
import androidx.compose.runtime.setValue  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.geometry.CornerRadius  
import androidx.compose.ui.geometry.Offset  
import androidx.compose.ui.geometry.Size  
import androidx.compose.ui.graphics.Brush  
import androidx.compose.ui.graphics.Color  
import androidx.compose.ui.platform.ComposeView  
import androidx.compose.ui.unit.dp  
  
  
/**  
* @author Administrator  
* @date 2024/6/1 0001 16:50  
* @description: SkeletonSquareKtParent  
*/  
class SkeletonSquareKtParent : FrameLayout {  
constructor(context: Context) : super(context) {  
init()  
}  
  
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {  
init()  
}  
  
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(  
context,  
attrs,  
defStyleAttr  
) {  
init()  
}  
  
constructor(  
context: Context,  
attrs: AttributeSet?,  
defStyleAttr: Int,  
defStyleRes: Int  
) : super(context, attrs, defStyleAttr, defStyleRes) {  
init()  
}  
  
  
  
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {  
super.onMeasure(widthMeasureSpec, heightMeasureSpec)  
val minimumWidth = suggestedMinimumWidth  
val minimumHeight = suggestedMinimumHeight  
val width = measureWidth(minimumWidth, widthMeasureSpec)  
val height = measureHeight(minimumHeight, heightMeasureSpec)  
// 初始化高度变量,将加上所有子view的高度  
var totalHeight = height  
// var maxLength = 0  
val count = childCount  
for (i in 1 until count) {  
val child = getChildAt(i)  
if (child.visibility != GONE) {  
measureChild(  
child,  
widthMeasureSpec,  
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)  
)  
  
// 更新子view的总高度  
val lp = child.layoutParams as LayoutParams  
val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin  
totalHeight += childHeight  
// // 更新最长子view的宽度  
// val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin  
// if (i == 0 || childWidth > maxLength) {  
// maxLength = childWidth  
// }  
}  
}  
setMeasuredDimension(width, totalHeight)  
  
  
}  
  
private fun measureWidth(defaultWidth: Int, measureSpec: Int): Int {  
var defaultWidth = defaultWidth  
val specMode = MeasureSpec.getMode(measureSpec)  
val specSize = MeasureSpec.getSize(measureSpec)  
when (specMode) {  
MeasureSpec.AT_MOST -> defaultWidth = paddingLeft + paddingRight  
MeasureSpec.EXACTLY -> defaultWidth = specSize  
MeasureSpec.UNSPECIFIED -> defaultWidth = Math.max(defaultWidth, specSize)  
}  
return defaultWidth  
}  
  
  
private fun measureHeight(defaultHeight: Int, measureSpec: Int): Int {  
var defaultHeight = defaultHeight  
val specMode = MeasureSpec.getMode(measureSpec)  
val specSize = MeasureSpec.getSize(measureSpec)  
when (specMode) {  
MeasureSpec.AT_MOST -> defaultHeight = paddingTop+paddingBottom  
MeasureSpec.EXACTLY -> defaultHeight = specSize  
MeasureSpec.UNSPECIFIED -> defaultHeight = Math.max(defaultHeight, specSize)  
}  
return defaultHeight  
}  
private fun init() {  
val compose = ComposeView(this.context)  
addView(compose, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))  
compose.setContent {  
var isStart by remember {  
mutableStateOf(false)  
}  
var offset = animateFloatAsState(  
targetValue = if (isStart) 1f else 0f, animationSpec = infiniteRepeatable(  
animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Restart  
), label = "shader"  
)  
Canvas(modifier = Modifier.fillMaxSize()) {  
drawRoundRect(  
Brush.horizontalGradient(  
listOf(  
Color.Transparent,  
Color(0x3Fc0c0c0),  
Color.Transparent  
),  
size.width.times(offset.value) - size.width / 4,  
size.width.times(offset.value)  
), alpha = 1.0f-offset.value,  
cornerRadius = CornerRadius(4.dp.value, 4.dp.value),  
topLeft = Offset.Zero,  
size = Size(size.width, size.height)  
)  
}  
LaunchedEffect(Unit) {  
isStart = true  
}  
}  
}  
  
}

注意compose作为第三方库时引入。我是创建了java项目,compose作为module引入,直接布局引用报错,换成kotlin Fragment存放自定义view

build.gradle.kts

id("com.android.application") version "8.2.2" apply false  
id("com.android.library") version "8.2.2" apply false  
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
plugins {  
id("com.android.library")  
id("org.jetbrains.kotlin.android")  
}  
  
android {  
namespace = "com.nio.skeleton"  
compileSdk = 34  
  
defaultConfig {  
minSdk = 24  
  
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"  
consumerProguardFiles("consumer-rules.pro")  
}  
  
buildTypes {  
release {  
isMinifyEnabled = false  
proguardFiles(  
getDefaultProguardFile("proguard-android-optimize.txt"),  
"proguard-rules.pro"  
)  
}  
}  
buildFeatures {  
compose= true  
}  
  
composeOptions {  
kotlinCompilerExtensionVersion = "1.5.10"  
}  
compileOptions {  
sourceCompatibility = JavaVersion.VERSION_1_8  
targetCompatibility = JavaVersion.VERSION_1_8  
}  
kotlinOptions {  
jvmTarget = "1.8"  
}  
}  
  
dependencies {  
  
implementation("androidx.core:core-ktx:1.12.0")  
implementation("androidx.appcompat:appcompat:1.6.1")  
implementation("com.google.android.material:material:1.9.0")  
testImplementation("junit:junit:4.13.2")  
androidTestImplementation("androidx.test.ext:junit:1.1.5")  
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")  
  
  
// implementation("androidx.core:core-ktx:1.10.1")  
// implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")  
// implementation("androidx.activity:activity-compose:1.7.0")  
// implementation(platform("androidx.compose:compose-bom:2023.08.00"))  
implementation("androidx.compose.material:material:1.6.7")  
}

最终实现效果,渐变阴影勉强仿照到位,是动画存在合理的释放销毁过程和刷新优化

Screen_recording_20240603_091931 (1).gif