如何在自定义ViewGroup中实现子View的top/left偏移且不被裁剪?
问题:自定义ViewGroup实现子View的top/left偏移(类似React Native View)
我有如下JSON布局结构:
{ "name": "view", "attributes": { "style": { "backgroundColor": "yellow" } }, "children": [ { "name": "view", "attributes": { "style": { "backgroundColor": "red", "width": 400, "height": 400 } } }, { "name": "view", "attributes": { "style": { "backgroundColor": "yellow", "top": 150, "left": 100, "width": 200, "height": 200 } } } ] }
我通过递归方式基于该JSON渲染布局,自定义了继承自ViewGroup的CustomLayout,功能类似LinearLayout:
public static class CustomLayout extends ViewGroup { private int orientation; // 0 for horizontal, 1 for vertical public CustomLayout(Context context) { super(context); } public CustomLayout(Context context, AttributeSet attrs) { super(context, attrs); } // Set the orientation (horizontal or vertical) public void setOrientation(int orientation) { this.orientation = orientation; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height = 0; // Measure each child view for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); if (orientation == 0) { // Horizontal layout width += child.getMeasuredWidth(); height = Math.max(height, child.getMeasuredHeight()); } else { // Vertical layout width = Math.max(width, child.getMeasuredWidth()); height += child.getMeasuredHeight(); } } // Take padding into account width += getPaddingLeft() + getPaddingRight(); height += getPaddingTop() + getPaddingBottom(); // Respect the given measure spec width = resolveSizeAndState(width, widthMeasureSpec, 0); height = resolveSizeAndState(height, heightMeasureSpec, 0); setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int childLeft = getPaddingLeft(); int childTop = getPaddingTop(); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (orientation == 0) { // Horizontal layout child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); childLeft += child.getMeasuredWidth(); } else { // Vertical layout child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); childTop += child.getMeasuredHeight(); } } } }
递归渲染后的底层结构如下:
LinearLayout rootView = activity.findViewById(R.id.root); CustomLayout view = new CustomLayout(activity); view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); view.setOrientation(1); view.setBackgroundColor(Color.parseColor("yellow")); View child1 = new CustomLayout(activity); View child2 = new CustomLayout(activity); child1.setLayoutParams(new LayoutParams(400, 400)); child1.setBackgroundColor(Color.parseColor("red")); child2.setLayoutParams(new LayoutParams(200, 200)); child2.setBackgroundColor(Color.parseColor("green")); view.addView(child1); view.addView(child2); rootView.addView(view);
我需要为子View应用top和left属性,实现类似React Native的效果:
function App(): React.JSX.Element { return ( <View style={{backgroundColor: 'yellow'}}> <View style={{backgroundColor: 'red', width: 200, height: 200}}></View> <View style={{ backgroundColor: 'green', top: 100, left: 50, width: 150, height: 150, }}></View> </View> ); }
要求:黄色父布局根据子View自适应宽高,绿色子View可偏移至父布局外部且不被裁剪。
解决方案
1. 自定义LayoutParams存储偏移属性
扩展ViewGroup.LayoutParams,添加topOffset和leftOffset字段存储子View的偏移值:
public static class CustomLayoutParams extends ViewGroup.LayoutParams { public int topOffset = 0; public int leftOffset = 0; public CustomLayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public CustomLayoutParams(int width, int height) { super(width, height); } public CustomLayoutParams(ViewGroup.LayoutParams source) { super(source); } }
同时在CustomLayout中重写LayoutParams生成方法:
@Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new CustomLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateDefaultLayoutParams() { return new CustomLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new CustomLayoutParams(p); }
2. 修改onMeasure逻辑,计算包含偏移的父布局尺寸
父布局在wrap_content模式下,需要计算子View偏移后的最大边界,确保自适应尺寸能覆盖所有子View(包括偏移到外部的部分):
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int minWidth = getPaddingLeft() + getPaddingRight(); int minHeight = getPaddingTop() + getPaddingBottom(); int maxRight = minWidth; int maxBottom = minHeight; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); CustomLayoutParams lp = (CustomLayoutParams) child.getLayoutParams(); // 计算子View偏移后的边界 int childRight = getPaddingLeft() + lp.leftOffset + child.getMeasuredWidth(); int childBottom = getPaddingTop() + lp.topOffset + child.getMeasuredHeight(); // 更新父布局的最大边界 maxRight = Math.max(maxRight, childRight); maxBottom = Math.max(maxBottom, childBottom); } int finalWidth = resolveSizeAndState(maxRight, widthMeasureSpec, 0); int finalHeight = resolveSizeAndState(maxBottom, heightMeasureSpec, 0); setMeasuredDimension( widthMode == MeasureSpec.EXACTLY ? widthSize : finalWidth, heightMode == MeasureSpec.EXACTLY ? heightSize : finalHeight ); }
3. 修改onLayout逻辑,应用偏移
布局子View时,将偏移值加到布局坐标上:
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); CustomLayoutParams lp = (CustomLayoutParams) child.getLayoutParams(); int layoutLeft = getPaddingLeft() + lp.leftOffset; int layoutTop = getPaddingTop() + lp.topOffset; child.layout(layoutLeft, layoutTop, layoutLeft + child.getMeasuredWidth(), layoutTop + child.getMeasuredHeight()); } }
如果需要保留原有的线性排列逻辑(子View先按水平/垂直排列,再叠加偏移),可调整为:
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { int childLeft = getPaddingLeft(); int childTop = getPaddingTop(); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); CustomLayoutParams lp = (CustomLayoutParams) child.getLayoutParams(); int layoutLeft = childLeft + lp.leftOffset; int layoutTop = childTop + lp.topOffset; child.layout(layoutLeft, layoutTop, layoutLeft + child.getMeasuredWidth(), layoutTop + child.getMeasuredHeight()); // 更新线性排列的基准位置 if (orientation == 0) { childLeft += child.getMeasuredWidth(); } else { childTop += child.getMeasuredHeight(); } } }
4. 递归渲染时设置偏移属性
从JSON的style中取出top和left值,赋值给CustomLayoutParams:
// 示例:渲染child2时的修改 CustomLayout child2 = new CustomLayout(activity); CustomLayoutParams lp2 = new CustomLayoutParams(200, 200); // 从JSON解析top和left,这里直接写死示例值 lp2.topOffset = 150; lp2.leftOffset = 100; child2.setLayoutParams(lp2); child2.setBackgroundColor(Color.parseColor("green"));
5. 关闭子View裁剪
让子View超出父布局的部分不被裁剪,需要在父布局及上层容器中设置:
view.setClipChildren(false); rootView.setClipChildren(false);
也可以在XML中给容器添加android:clipChildren="false"属性。
内容的提问来源于stack exchange,提问作者Jose Carlos Ramírez
相关产品推荐
相关产品推荐

