You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义LoadingButton阴影被裁剪 求批量设置clip参数方案

自定义LoadingButton阴影裁剪问题的批量解决方案

我基于FrameLayout实现了自定义LoadingButton组件,可直接在XML中使用,本质是带独立布局的自定义Button。当前为按钮添加elevation、translationZ阴影时,阴影始终出现被裁剪的情况。需求是让按钮能动态调整边距、在不同屏幕保持固定宽高且阴影不被扭曲裁剪,但部分页面仅设置了marginTop未设置marginBottom,即便在按钮内部设置clipToPadding和clipChildren为false,仍存在裁剪问题。现寻求无需修改80余个XML文件,即可批量为所有使用该按钮的页面设置clipToPadding=false和clipChildren=false的方案。


相关代码

自定义按钮XML布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="CustomRes" type="com.project.utils.CustomResources"/>
    </data>

    <merge
        android:duplicateParentState="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <LinearLayout
            android:id="@+id/buttonParent"
            android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:duplicateParentState="true"
            android:clipToPadding="false"
            android:clipChildren="false"
            android:gravity="center">

            <ImageView
                android:id="@+id/buttonIcon"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:adjustViewBounds="true"
                android:layout_gravity="center"
                android:scaleType="fitCenter"/>

            <TextView
                android:id="@+id/buttonText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:duplicateParentState="true"
                android:textSize="@dimen/text_medium"
                android:textStyle="bold"
                android:lines="1"
                android:layout_gravity="center"/>

        </LinearLayout>

        <com.project.components.loading_indicator.LoadingIndicator
            android:id="@+id/progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:gravity="center" />

    </merge>
</layout>

自定义按钮实现类

@Suppress("DEPRECATION")
class LoadingButton : FrameLayout {

    @StyleableRes
    internal val btnIcoIndex = 0
    @SuppressLint("ResourceType")
    @StyleableRes
    internal val btnTextResIndex = 1
    @StyleableRes
    @SuppressLint("ResourceType")
    internal val btnTextIndex = 2
    @StyleableRes
    @SuppressLint("ResourceType")
    internal val btnTextSizeIndex = 3

    private val buttonParent: LinearLayout
    private val progressBar: LoadingIndicator
    private val buttonIcon: ImageView
    private val buttonText: TextView
    private var buttonTextVal: String? = null
    private var textViewInitWidth = 0

    init {
        CustomResources.inflateLayout(LayoutInflater.from(context), R.layout.loading_button, this)
        buttonParent = findViewById(R.id.buttonParent)
        buttonIcon = findViewById(R.id.buttonIcon)
        buttonText = findViewById(R.id.buttonText)
        progressBar = findViewById(R.id.progress)

        buttonText.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
            textViewInitWidth = maxOf(v.measuredWidth, textViewInitWidth)
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    constructor(context: Context) : super(context) {
        parseAttrs(context)
    }

    @SuppressLint("ClickableViewAccessibility")
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
        parseAttrs(context, attrs)
    }

    @SuppressLint("ClickableViewAccessibility")
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        parseAttrs(context, attrs, defStyleAttr)
    }

    @SuppressLint("ResourceType")
    private fun parseAttrs(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int? = null) {
        setLoading(false)
        val sets = intArrayOf(R.attr.l_buttonIcon, R.attr.l_buttonTextId, R.attr.l_buttonText, R.attr.l_buttonTextSize)
        if (attrs != null){
            val typedArray = context.obtainStyledAttributes(attrs, sets)
            val buttonIco = typedArray.getResourceId(btnIcoIndex, 0)
            val buttonTxtRes = typedArray.getText(btnTextResIndex)?.let { res ->
                App.getString(res as String).toLowerCase(Locale.getDefault()).replaceFirstChar { it.toUpperCase() }
            }
            val buttonTxtRaw = typedArray.getText(btnTextIndex)?.let { it as String }?: "null"
            val buttonTxtSize = typedArray.getDimension(btnTextSizeIndex, resources.getDimension(R.dimen.text_medium))
            val buttonStyle = attrs.styleAttribute

            App.log("RawTextButton: $buttonTxtRaw, hasValue: ${typedArray.getString(btnTextIndex)}")
            setButtonTextSize(buttonTxtSize)
            buttonTxtRes?.let { setButtonText(buttonTxtRes) }?:kotlin.run{ setButtonText(buttonTxtRaw) }
            setButtonIcon(buttonIco)
            buttonParent.gravity = Gravity.CENTER
            buttonTextVal = buttonTxtRes?.let { buttonTxtRes.toString() }?:buttonTxtRaw
            typedArray.recycle()

            clipToPadding = false
            clipChildren = false

            when(buttonStyle){
                R.style.button_primary -> {
                    App.log("BtnStyleId - primary")
                    setupTextStyle(buttonStyle)
                    setProgBarColor(R.color.button_light)
                    setIconTint(ContextCompat.getColor(context, R.color.button_light))
                    maybeSetAmbientShadow(R.color.button_primary)
                }
                R.style.button_secondary -> {
                    App.log("BtnStyleId - secondary")
                    setupTextStyle(buttonStyle)
                    setProgBarColor(R.color.button_primary)
                    setIconTint(ContextCompat.getColor(context, R.color.button_light))
                    maybeSetAmbientShadow(R.color.button_primary)
                }
                R.style.button_secondary_alert -> {
                    App.log("BtnStyleId - secondary alert")
                    setupTextStyle(buttonStyle)
                    setProgBarColor(R.color.button_invalid)
                    setIconTint(ContextCompat.getColor(context, R.color.button_invalid))
                    maybeSetAmbientShadow(R.color.button_invalid)
                }
                else -> {
                    App.log("BtnStyleId -> $buttonStyle")
                }
            }
        } else {
            App.log("BtnStyleId -> attrs==null")
        }
    }

    @SuppressLint("ResourceType")
    private fun setupTextStyle(buttonStyle: Int){
        val attrs = intArrayOf(android.R.attr.textAppearance)
        val typedAttrs = context.obtainStyledAttributes(buttonStyle, attrs)
        val textAppearance = typedAttrs.getResourceId(0, 0)
        TextViewCompat.setTextAppearance(buttonText, textAppearance)
        typedAttrs.recycle()
    }

    private fun setProgBarColor(color: Int){
        progressBar.setColorTint(color)
    }

    fun setIconTint(color: Int){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q){
            buttonIcon.colorFilter = BlendModeColorFilter(color, BlendMode.SRC_IN)
        } else {
            App.log("setting color filter")
            buttonIcon.setColorFilter(color, PorterDuff.Mode.SRC_IN)
        }
    }

    private fun maybeSetAmbientShadow(color: Int){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
            outlineAmbientShadowColor = ContextCompat.getColor(this.context, color)
            outlineSpotShadowColor = ContextCompat.getColor(this.context, color)
        }
    }

    private fun setLoading(isLoading: Boolean) {
        buttonParent.visibility = if (isLoading) View.GONE else View.VISIBLE
        progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
    }

    private fun setButtonText(text: String) {
        buttonText.text = text
    }

    private fun setButtonIcon(resId: Int) {
        if (resId != 0) {
            buttonIcon.setImageResource(resId)
        } else {
            buttonIcon.visibility = View.GONE
        }
    }

    private fun setButtonTextSize(size: Float) {
        buttonText.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, size, resources.displayMetrics)
    }
}

按钮基础样式

<style name="button" parent="@android:style/Widget.Material.Button">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:minHeight">0dp</item>
        <item name="android:minWidth">0dp</item>
        <item name="android:paddingStart">@dimen/button_padding_start</item>
        <item name="android:paddingEnd">@dimen/button_padding_end</item>
        <item name="android:paddingTop">@dimen/button_padding_top</item>
        <item name="android:paddingBottom">@dimen/button_padding_bottom</item>
        <item name="android:elevation">4dp</item>
        <item name="android:translationZ">4dp</item>
        <item name="android:stateListAnimator">@null</item>
        <item name="android:clipToPadding">false</item>
</style>

组件使用示例

<com.project.components.loading_button.LoadingButton
     android:id="@+id/loginButton"
     android:layout_gravity="center"
     android:layout_weight="0.5"
     android:layout_marginStart="8dp"
     app:l_buttonTextId="button_login"
     style="@style/button.primary" />

批量解决方案

方案1:在自定义View中强制修改父容器属性

阴影被裁剪的核心原因通常是按钮的直接父容器开启了裁剪,而非按钮自身。因此可以在LoadingButton中添加逻辑,当按钮附加到窗口时,自动修改父容器的裁剪属性:

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    // 获取直接父容器
    val parentView = parent as? ViewGroup
    parentView?.apply {
        clipToPadding = false
        clipChildren = false
        // 若父容器的父容器也可能裁剪,可按需递归处理
        val grandParent = parent as? ViewGroup
        grandParent?.apply {
            clipToPadding = false
            clipChildren = false
        }
    }
}

这种方式无需修改任何XML,所有使用LoadingButton的页面都会自动生效,彻底解决批量修改的问题。

方案2:通过主题全局设置布局默认属性

如果项目中按钮的父容器多为固定类型(如LinearLayout、ConstraintLayout),可以在App主题中为这些布局设置默认的裁剪属性:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <!-- 给LinearLayout设置默认属性 -->
    <item name="android:LinearLayoutStyle">@style/ClippingDisabledLinearLayout</item>
    <!-- 给ConstraintLayout设置默认属性 -->
    <item name="android:ConstraintLayoutStyle">@style/ClippingDisabledConstraintLayout</item>
</style>

<style name="ClippingDisabledLinearLayout" parent="Widget.MaterialComponents.LinearLayout">
    <item name="android:clipToPadding">false</item>
    <item name="android:clipChildren">false</item>
</style>

<style name="ClippingDisabledConstraintLayout" parent="Widget.MaterialComponents.ConstraintLayout">
    <item name="android:clipToPadding">false</item>
    <item name="android:clipChildren">false</item>
</style>

注意:该方案会影响所有对应类型的布局,若有部分布局需要保留裁剪功能,需单独设置属性覆盖全局配置。

方案3:添加安全边距避免阴影被裁剪

除了关闭裁剪,还可以在LoadingButton中自动添加与阴影大小匹配的边距,确保阴影不会超出父容器范围:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    // 获取阴影大小(elevation值),转换为实际像素
    val shadowSize = elevation.toInt()
    // 给按钮添加额外边距,避免阴影被裁剪
    setPadding(
        paddingLeft + shadowSize,
        paddingTop + shadowSize,
        paddingRight + shadowSize,
        paddingBottom + shadowSize
    )
}

该方案可与方案1结合使用,双重保障阴影不被裁剪。


内容的提问来源于stack exchange,提问作者Martin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 08:35:22