如何在ConstraintLayout中使用ViewStub?

25

ConstraintLayout中填充一个ViewStub时,似乎导致生成的视图失去了所有约束。我猜我们可以使用ConstraintSet为填充的视图定义约束,但这有点违背了ViewStub的初衷。

有什么好的方法解决这个问题吗?

3个回答

64

有一个简单的解决方案:

在您的ViewStub中,让inflatedId属性与id保持相同,就像这样:

如下所示:

<ViewStub
    android:id="@+id/pocket_view"
    android:inflatedId="@+id/pocket_view"
    android:layout="@layout/game_pocket_view"
    android:layout_width="@dimen/game_pocket_max_width"
    android:layout_height="@dimen/game_pocket_max_height"
    app:layout_constraintLeft_toLeftOf="@id/avatar_view"
    app:layout_constraintRight_toRightOf="@id/avatar_view"
    app:layout_constraintTop_toTopOf="@id/avatar_view"
    app:layout_constraintBottom_toBottomOf="@id/avatar_view"
    />

17
工作但毫无意义。就应该在安卓上是这样的。 - Ghedeon
@Ghedeon,idViewSub的句柄,而inflatedId是代替它的结果布局的句柄。 - Shazbot

1

我遇到了同样的问题,并找到了一个代码解决方法。只需迭代ConstraintLayoutViewStub的父级)的每个子项,并将引用从存根id更改为已填充id。

我实现了自己的ViewStub,因为否则IDE预览无法正常工作。这是我正在使用的类。我从android项目中复制了代码,并更改了它以使IDE预览正确显示。

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewStub;

import androidx.annotation.LayoutRes;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintHelper;
import androidx.constraintlayout.widget.ConstraintLayout;

import java.lang.ref.WeakReference;

import rocks.tbog.tblauncher.R;

/**
 * Copy of {@link android.view.ViewStub} so that we can see something in the preview
 */
public final class ViewStubPreview extends View {
    private int mLayoutResource;
    private int mInflatedId;
    private WeakReference<View> mInflatedViewRef = null;
    private LayoutInflater mInflater = null;
    private OnInflateListener mInflateListener = null;

    public ViewStubPreview(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ViewStubPreview(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewStubPreview,
            defStyle, 0);
        mInflatedId = a.getResourceId(R.styleable.ViewStubPreview_inflatedId, NO_ID);
        mLayoutResource = a.getResourceId(R.styleable.ViewStubPreview_layout, 0);
        setId(a.getResourceId(R.styleable.ViewStubPreview_id, NO_ID));
        a.recycle();
        if (!isInEditMode()) {
            setVisibility(GONE);
            setWillNotDraw(true);
        }
    }

    /**
     * Returns the id taken by the inflated view. If the inflated id is
     * {@link View#NO_ID}, the inflated view keeps its original id.
     *
     * @return A positive integer used to identify the inflated view or
     * {@link #NO_ID} if the inflated view should keep its id.
     * @attr name android:inflatedId
     * @see #setInflatedId(int)
     */
    public int getInflatedId() {
        return mInflatedId;
    }

    /**
     * Defines the id taken by the inflated view. If the inflated id is
     * {@link View#NO_ID}, the inflated view keeps its original id.
     *
     * @param inflatedId A positive integer used to identify the inflated view or
     *                   {@link #NO_ID} if the inflated view should keep its id.
     * @attr name android:inflatedId
     * @see #getInflatedId()
     */
    public void setInflatedId(int inflatedId) {
        mInflatedId = inflatedId;
    }

    /**
     * Returns the layout resource that will be used by {@link #setVisibility(int)} or
     * {@link #inflate()} to replace this StubbedView
     * in its parent by another view.
     *
     * @return The layout resource identifier used to inflate the new View.
     * @attr name android:layout
     * @see #setLayoutResource(int)
     * @see #setVisibility(int)
     * @see #inflate()
     */
    public int getLayoutResource() {
        return mLayoutResource;
    }

    /**
     * Specifies the layout resource to inflate when this StubbedView becomes visible or invisible
     * or when {@link #inflate()} is invoked. The View created by inflating the layout resource is
     * used to replace this StubbedView in its parent.
     *
     * @param layoutResource A valid layout resource identifier (different from 0.)
     * @attr name android:layout
     * @see #getLayoutResource()
     * @see #setVisibility(int)
     * @see #inflate()
     */
    public void setLayoutResource(int layoutResource) {
        mLayoutResource = layoutResource;
    }

    /**
     * Set {@link LayoutInflater} to use in {@link #inflate()}, or {@code null}
     * to use the default.
     */
    public void setLayoutInflater(LayoutInflater inflater) {
        mInflater = inflater;
    }

    /**
     * Get current {@link LayoutInflater} used in {@link #inflate()}.
     */
    public LayoutInflater getLayoutInflater() {
        return mInflater;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (isInEditMode()) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            return;
        }

        setMeasuredDimension(0, 0);
    }

    @Override
    public void draw(Canvas canvas) {
        if (isInEditMode())
            super.draw(canvas);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
    }

    /**
     * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
     * {@link #inflate()} is invoked and this StubbedView is replaced in its parent
     * by the inflated layout resource. After that calls to this function are passed
     * through to the inflated view.
     *
     * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
     * @see #inflate()
     */
    @Override
    public void setVisibility(int visibility) {
        if (mInflatedViewRef != null) {
            View view = mInflatedViewRef.get();
            if (view != null) {
                view.setVisibility(visibility);
            } else {
                throw new IllegalStateException("setVisibility called on un-referenced view");
            }
        } else {
            super.setVisibility(visibility);
            if (visibility == VISIBLE || visibility == INVISIBLE) {
                inflate();
            }
        }
    }

    /**
     * Inflates the layout resource identified by {@link #getLayoutResource()}
     * and replaces this StubbedView in its parent by the inflated layout resource.
     *
     * @return The inflated layout resource.
     */
    public View inflate() {
        final ViewParent viewParent = getParent();
        if (viewParent instanceof ViewGroup) {
            if (mLayoutResource != 0) {
                final ViewGroup parent = (ViewGroup) viewParent;
                final LayoutInflater factory;
                if (mInflater != null) {
                    factory = mInflater;
                } else {
                    factory = LayoutInflater.from(getContext());
                }
                final View view = factory.inflate(mLayoutResource, parent, false);
                if (mInflatedId != NO_ID) {
                    view.setId(mInflatedId);
                }
                final int index = parent.indexOfChild(this);
                parent.removeViewInLayout(this);
                final ViewGroup.LayoutParams layoutParams = getLayoutParams();
                if (layoutParams != null) {
                    parent.addView(view, index, layoutParams);
                } else {
                    parent.addView(view, index);
                }

                // update parent ConstraintLayout constraints
                if (parent instanceof ConstraintLayout)
                    updateConstraintsAfterStubInflate((ConstraintLayout) parent, getId(), view.getId());

                mInflatedViewRef = new WeakReference<>(view);
                if (mInflateListener != null) {
                    mInflateListener.onInflate(this, view);
                }
                return view;
            } else {
                throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
            }
        } else {
            throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
        }
    }

    /**
     * Specifies the inflate listener to be notified after this ViewStub successfully
     * inflated its layout resource.
     *
     * @param inflateListener The OnInflateListener to notify of successful inflation.
     * @see android.view.ViewStub.OnInflateListener
     */
    public void setOnInflateListener(OnInflateListener inflateListener) {
        mInflateListener = inflateListener;
    }

    @Nullable
    public static View inflateStub(@Nullable View view) {
        return inflateStub(view, 0);
    }

    @Nullable
    public static View inflateStub(@Nullable View view, @LayoutRes int layoutRes) {
        if (view instanceof ViewStubPreview) {
            if (layoutRes != 0)
                ((ViewStubPreview) view).setLayoutResource(layoutRes);
            // ViewStubPreview already calls updateConstraintsAfterStubInflate
            return ((ViewStubPreview) view).inflate();
        }

        if (!(view instanceof ViewStub))
            return view;

        ViewStub stub = (ViewStub) view;
        int stubId = stub.getId();

        // get parent before the call to inflate
        ConstraintLayout constraintLayout = stub.getParent() instanceof ConstraintLayout ? (ConstraintLayout) stub.getParent() : null;

        if (layoutRes != 0)
            stub.setLayoutResource(layoutRes);
        View inflatedView = stub.inflate();
        int inflatedId = inflatedView.getId();

        updateConstraintsAfterStubInflate(constraintLayout, stubId, inflatedId);

        return inflatedView;
    }

    private static void updateConstraintsAfterStubInflate(@Nullable ConstraintLayout constraintLayout, int stubId, int inflatedId) {
        if (inflatedId == View.NO_ID)
            return;
        // change parent ConstraintLayout constraints
        if (constraintLayout != null && stubId != inflatedId) {
            int childCount = constraintLayout.getChildCount();
            for (int childIdx = 0; childIdx < childCount; childIdx += 1) {
                View child = constraintLayout.getChildAt(childIdx);
                if (child instanceof ConstraintHelper) {
                    // get a copy of the id list
                    int[] refIds = ((ConstraintHelper) child).getReferencedIds();
                    boolean changed = false;
                    // change constraint reference IDs
                    for (int idx = 0; idx < refIds.length; idx += 1) {
                        if (refIds[idx] == stubId) {
                            refIds[idx] = inflatedId;
                            changed = true;
                        }
                    }
                    if (changed)
                        ((ConstraintHelper) child).setReferencedIds(refIds);
                }
                ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) child.getLayoutParams();
                if (changeConstraintLayoutParamsTarget(params, stubId, inflatedId))
                    child.setLayoutParams(params);
            }
        }
    }

    private static boolean changeConstraintLayoutParamsTarget(ConstraintLayout.LayoutParams params, int fromId, int toId) {
        boolean changed = false;
        if (params.leftToLeft == fromId) {
            params.leftToLeft = toId;
            changed = true;
        }
        if (params.leftToRight == fromId) {
            params.leftToRight = toId;
            changed = true;
        }
        if (params.rightToLeft == fromId) {
            params.rightToLeft = toId;
            changed = true;
        }
        if (params.rightToRight == fromId) {
            params.rightToRight = toId;
            changed = true;
        }
        if (params.topToTop == fromId) {
            params.topToTop = toId;
            changed = true;
        }
        if (params.topToBottom == fromId) {
            params.topToBottom = toId;
            changed = true;
        }
        if (params.bottomToTop == fromId) {
            params.bottomToTop = toId;
            changed = true;
        }
        if (params.bottomToBottom == fromId) {
            params.bottomToBottom = toId;
            changed = true;
        }
        if (params.baselineToBaseline == fromId) {
            params.baselineToBaseline = toId;
            changed = true;
        }
        if (params.baselineToTop == fromId) {
            params.baselineToTop = toId;
            changed = true;
        }
        if (params.circleConstraint == fromId) {
            params.circleConstraint = toId;
            changed = true;
        }
        if (params.startToEnd == fromId) {
            params.startToEnd = toId;
            changed = true;
        }
        if (params.startToStart == fromId) {
            params.startToStart = toId;
            changed = true;
        }
        if (params.endToStart == fromId) {
            params.endToStart = toId;
            changed = true;
        }
        if (params.endToEnd == fromId) {
            params.endToEnd = toId;
            changed = true;
        }
        return changed;
    }

    /**
     * Listener used to receive a notification after a ViewStub has successfully
     * inflated its layout resource.
     *
     * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener)
     */
    public interface OnInflateListener {
        /**
         * Invoked after a ViewStub successfully inflated its layout resource.
         * This method is invoked after the inflated view was added to the
         * hierarchy but before the layout pass.
         *
         * @param stub     The ViewStub that initiated the inflation.
         * @param inflated The inflated View.
         */
        void onInflate(ViewStubPreview stub, View inflated);
    }
}

注意:静态方法inflateStub也适用于android.view.ViewStub


0

考虑不要使用相同的idinflatedId,因为这可能导致未定义的行为和崩溃,例如:

java.lang.IllegalArgumentException: Wrong state class... This usually happens when two views of different type have the same id in the same hierarchy.(这种情况可能发生在旋转或从后台返回后恢复状态时,具体取决于您的膨胀位置。)

如果您不希望修改父ConstraintLayoutConstraintSet,则有一种替代方案 - {{link1:androidx.costraintlayout.widget.Barrier}}

例如,假设您的视图是:

  <ScrollView
      android:id="@+id/some_scroll_view"
      android:layout_width="0dp"
      android:layout_height="wrap_content"      
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintBottom_toTopOf="@+id/some_view_stub">

  <ViewStub
      android:id="@+id/some_view_stub"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:inflatedId="@+id/some_view"
      android:layout="@layout/some_view_layout"
      app:layout_constraintBottom_toBottomOf="parent" />

当视图存根被填充时,some_scroll_view的约束将不再有效。
我们可以这样引入一个Barrier
  <ScrollView
      android:id="@+id/some_scroll_view"
      android:layout_width="0dp"
      android:layout_height="wrap_content"      
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintBottom_toTopOf="@+id/some_barrier">

  <androidx.constraintlayout.widget.Barrier
      android:id="@+id/some_barrier"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:barrierDirection="top"
      app:constraint_referenced_ids="some_view_stub,some_view" />

  <ViewStub
      android:id="@+id/some_view_stub"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:inflatedId="@+id/some_view"
      android:layout="@layout/some_view_layout"
      app:layout_constraintBottom_toBottomOf="parent" />

请注意,app:constraint_referenced_ids 包括 ViewStub 的 id 和其 inflatedId

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接