1. 程式人生 > >layout_marginEnd 導致動態設定setLayoutParams失效的巨坑

layout_marginEnd 導致動態設定setLayoutParams失效的巨坑

targetSdkVersion >= JELLY_BEAN_MR1(17)時,在xml佈局中我們設定marginLeft或marginRight會看到這種提示

“Consider adding android:layout_marginEnd="@dimen/xx" to better support right-to-left layouts less... ”

意思是說讓我們加上layout_marginEnd來更好的支援佈局方向的改變,一般情況下我們會加上。不過加上後會導致動態設定setLayoutParams失效,而且只是marginLeft、marginRight,加上後在targetSdkVersion<17 時也沒問題,但在targetSdkVersion>=17時就坑了。

MarginLayoutParams是由父佈局的generateLayoutParams產生的,以FrameLayout作為父佈局為例。

@Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new FrameLayout.LayoutParams(getContext(), attrs);
    }

LayoutParams 繼承自 MarginLayoutParams,在建立MarginLayoutParams時有以下幾個特殊的地方。

//首先解析startMargin和endMargin
startMargin = a.getDimensionPixelSize
( R.styleable.ViewGroup_MarginLayout_layout_marginStart, DEFAULT_MARGIN_RELATIVE); endMargin = a.getDimensionPixelSize( R.styleable.ViewGroup_MarginLayout_layout_marginEnd, DEFAULT_MARGIN_RELATIVE); //判斷是否設定了layout_marginEnd或layout_marginStart,如果設定了會做一個標誌 if (isMarginRelative()) { mMarginFlags |= NEED_RESOLUTION_MASK;
} //當targetSdkVersion<17或不支援right-to-left時走相容模式 final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport(); final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion; if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) { mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK; }

建立時根據設定和targetSdkVersion設定了兩處flag,這有什麼用呢

/**
         * This will be called by {@link android.view.View#requestLayout()}. Left and Right margins
         * may be overridden depending on layout direction.
         */
        @Override
        public void resolveLayoutDirection(int layoutDirection) {
            setLayoutDirection(layoutDirection);

            // No relative margin or pre JB-MR1 case or no need to resolve, just dont do anything
            // Will use the left and right margins if no relative margin is defined.
            if (!isMarginRelative() ||
                    (mMarginFlags & NEED_RESOLUTION_MASK) != NEED_RESOLUTION_MASK) return;

            // Proceed with resolution
            doResolveMargins();
        }

和註釋說的一樣,view的requestLayout後會呼叫LayoutParams的resolveLayoutDirection方法(view的measure裡最終會調到),方法裡先做了一個判斷:如果沒有設定layout_marginEnd或layout_marginStart,或者沒有重新設定margin的標誌位則直接返回,因為我們設定了layout_marginEnd,所以會呼叫doResolveMargins方法。

private void doResolveMargins() {
            //如果是相容模式:targetSdkVersion<17或不支援right-to-left時走相容模式
            if ((mMarginFlags & RTL_COMPATIBILITY_MODE_MASK) == RTL_COMPATIBILITY_MODE_MASK) {
                // 如果marginLeft和marginRight沒設定,則用marginStart和marginEnd給他們賦值
                // defined then use those start and end margins.
                if ((mMarginFlags & LEFT_MARGIN_UNDEFINED_MASK) == LEFT_MARGIN_UNDEFINED_MASK
                        && startMargin > DEFAULT_MARGIN_RELATIVE) {
                    leftMargin = startMargin;
                }
                if ((mMarginFlags & RIGHT_MARGIN_UNDEFINED_MASK) == RIGHT_MARGIN_UNDEFINED_MASK
                        && endMargin > DEFAULT_MARGIN_RELATIVE) {
                    rightMargin = endMargin;
                }
            } else {
            //如果不是相容模式,無論是設定marginStart、marginEnd或都設定了,都要覆蓋已經設定的marginLeft和marginRight。marginStart或marginEnd沒設定的話賦值為零。
                switch(mMarginFlags & LAYOUT_DIRECTION_MASK) {
                    case View.LAYOUT_DIRECTION_RTL:
                        leftMargin = (endMargin > DEFAULT_MARGIN_RELATIVE) ?
                                endMargin : DEFAULT_MARGIN_RESOLVED;
                        rightMargin = (startMargin > DEFAULT_MARGIN_RELATIVE) ?
                                startMargin : DEFAULT_MARGIN_RESOLVED;
                        break;
                    case View.LAYOUT_DIRECTION_LTR:
                    default:
                        leftMargin = (startMargin > DEFAULT_MARGIN_RELATIVE) ?
                                startMargin : DEFAULT_MARGIN_RESOLVED;
                        rightMargin = (endMargin > DEFAULT_MARGIN_RELATIVE) ?
                                endMargin : DEFAULT_MARGIN_RESOLVED;
                        break;
                }
            }
            mMarginFlags &= ~NEED_RESOLUTION_MASK;
        }

上邊的註釋已經很清楚了,在targetSdkVersion<17下,走的是相容模式,所以我們的marginLeft和marginRight可以生效,而在targetSdkVersion>=17,marginLeft和marginRight會被marginStart、marginEnd覆蓋。

public void change(){
        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) button.getLayoutParams();
        int dy = 40;
        params.setMargins(params.leftMargin+dy, params.topMargin, params.rightMargin, params.bottomMargin+dy);
        button.setLayoutParams(params);
    }

因此上面動態設定佈局引數的方法,在targetSdkVersion>=17並且設定了marginStart和marginEnd時,你會發現marginLeft並沒有改變,但是marginTop可以正常改變。

解決方法,在在targetSdkVersion>=17並且設定了marginStart和marginEnd時,用

public void setMarginEnd(int end)
public void setMarginStart(int start)

而不要使用

public void setMargins(int left, int top, int right, int bottom)