Android中级——自定义控件

控件架构

控件分为两大类,即ViewGroup和View

ViewGroup又可包含View,形成控件树,常用的fingViewById()方法依据深度优先遍历查找控件

上层控件负责下层子控件的测量和绘制,并传递交互事件

setContentView()

Android界面框架图如下

在这里插入图片描述

  • 每个Activity都包含一个PhoneWindow对象
  • PhoneWindow将一个DecorView设置为根View,其分为TitleView和ContentView(Framelayout)
  • setContentView()将activity_main.xml设置到ContentView,ActivityManagerService回调onResume()将DecorView添加到PhoneWindow显示出来
  • DecorView中的View监听事件,通过WindowManagerService接收并回调到Activity的onClickListener

将上图转为视图树如下

在这里插入图片描述

  • DecoreView以LinearLayout上下布局
  • 上面为ActionBar显示标题,下面为Content显示内容
  • 若设置requestWindowFeature(Window.FEATURE_NO_TITLE)全屏显示,则只存在Content,故其需要在setContView()之前调用才能生效

MeasureSpec

MeasureSpec是一个32位int值,高2位为测量模式,低30位为测量大小,模式分为

  • EXACTLY:layout_width、layout_height 为具体数值或match_parent
  • AT_MOST:layout_width、layout_height 为 wrap_content,在父控件宽高范围内,控件大小随子控件或内容变化而变化
  • UNSPECIFIED,不指定其大小和测量模式,内部使用无需关注

measure

调用过程为:

  • Activity创建时通过ViewRootImpl的setView()设置DecorView,调用performTraversals()、performMeasure()
  • 调用DecorView的measure(),会调用到父类View的measure()
  • 随后调用View的onMeasure(),这里会根据多态,会调用到DecorView的onMeasure()
  • 再调用父类FrameLayout的onMeasure(),其通过getChildMeasureSpec()获取每一个child的MeasureSpec,并调child的measure()、onMeasure()
  • 若child是ViewGroup则重复上述过程,否则调用child的setMeasuredDimension()设置mMeasuredWidth、mMeasuredHeight

View的measure

通过getChildMeasureSpec()获取child的MeasureSpec,其与下面属性有关

  • 父容器的MeasureSpec
  • child的LayoutParams
  • child的Margin和Padding

可以总结为下图

在这里插入图片描述

由View的getDefaultSize()方法和上表可知,当View为wrap_content时,默认大小就为父容器中剩余空间的大小,如下图

在这里插入图片描述

继承View的自定义控件,需要重写onMeasure()并设置wrap_content下的大小,否则其相当于match_parent

public class MyView extends View {

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidthOrHeight(widthMeasureSpec), measureWidthOrHeight(heightMeasureSpec));
    }

    private int measureWidthOrHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }
}

如上自定义View,其测量过程如下

  • 从MeasureSpec获取具体的模式specMode和大小specSize
  • 当若specMode为EXACTLY,使用指定的大小specSize
  • 当specMode为AT_MOST取(specSize或默认值)中的最小值

如下设置为wrap_content时,默认为200,而不是填充整个父布局

在这里插入图片描述

ViewGroup的measure

ViewGroup继承自View,但其是抽象类,并没有实现onMeasure()方法,需要其子类去具体实现,接下来以LinearLayout的onMeasure()为例

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     if (mOrientation == VERTICAL) {
         measureVertical(widthMeasureSpec, heightMeasureSpec);
     } else {
         measureHorizontal(widthMeasureSpec, heightMeasureSpec);
     }
 }

纵向布局的测量过程如下

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	
	.....
	
    final int count = getVirtualChildCount();
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
   	......
   	
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        
      	......
      	
      	//下面测量child,判断是否使用weight分割
      	
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();	
        totalWeight += lp.weight;
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
        
			//1.当前LinearLayout高度为EXACTLY ,先加上child的margin,但不测量child,稍后会将多余空间分配给child
			
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            skippedMeasure = true;
        } else {
            if (useExcessSpace) {
            
                //2. 当前LinearLayout高度为UNSPECIFIED或AT_MOST,child.height设置为WRAP_CONTENT,先测量出原始高度,否则测量高度会为0
                                
                lp.height = LayoutParams.WRAP_CONTENT;
            }

			//下面调用measureChildWithMargins()测量child

            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
                    
            final int childHeight = child.getMeasuredHeight();
            if (useExcessSpace) {
            
                //恢复child.height,记录weight消耗的高度
                
                lp.height = 0;
                consumedExcessSpace += childHeight;
            }

			//加上child的margin
			
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                   lp.bottomMargin + getNextLocationOffset(child));
           	
           	......
        }
        
		...... 
    }
 	......
    
    //加上自己的Padding,将计算出的宽高和LinearLayout的MeasureSpec传到resolveSizeAndState()
    
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
	......
    maxWidth += mPaddingLeft + mPaddingRight;
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
 	......
}

resolveSizeAndState()代码如下

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;	//若计算出来的宽高超过LinearLayout,仍使用LinearLayout宽高
            } else {
                result = size;	//若计算出来的宽高不超过LinearLayout,则用计算出来的宽高
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;	//宽高固定为LinearLayout宽高
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

获取宽高

宽高需要在measure结束后,通过getMeasuredWidth() / getMeasuredHeight()获取,但在某些情况(如多次measure)无法获取正确的宽高

onWindowFocusChanged

Activity / View的回调方法,当其被调用时,View已经初始化完毕,但需要注意,当窗口每次失去/获取焦点时,这个方法都会被调用

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
}

View.post

当Looper调用此runnable时,View已经初始化好了

@Override
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int measuredWidth = view.getMeasuredWidth();
            int measuredHeight = view.getMeasuredHeight();

        }
    });
}

ViewTreeObserver

当View树状态改变或内部View可见性改变时,onGlobalLayout()会被回调,但此方法可能也会被调用多次

@Override
protected void onStart() {
    super.onStart();
    View view = new View(this);
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            int measuredWidth = view.getMeasuredWidth();
            int measuredHeight = view.getMeasuredHeight();
        }
    });
}

View.measure

可手动对View进行measure以获取宽高,但要根据LayoutParams分具体情况

当为match_parent时,无法获取,因为measure时需要知道父容器的剩余空间

当为具体数值时,可以获取,如100px时

 int widthMeasureSpc = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
 int HeightMeasureSpc = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
 view.measure(widthMeasureSpc, HeightMeasureSpc);

 int measuredWidth = view.getMeasuredWidth();
 int measuredHeight = view.getMeasuredHeight();

当为wrap_content时,可以获取,利用最大的SepcSize去构造MeasureSpec

int widthMeasureSpc = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
int HeightMeasureSpc = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpc, HeightMeasureSpc);
int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();

layout

调用过程为:

  • 调用ViewRootImpl的performTraversals()、performLayout()
  • 调用DecorView的layout(),故会调用到父类ViewGroup和View的layout()
  • 随后调用View的onLayout(),这里会根据多态,会调用到DecorView的onLayout()
  • 再调用父类FrameLayout的onLayout()、layoutChildren()处理上下左右边距
  • 调用child的layout()
  • 若child是ViewGroup则重复上述过程,否则调用自身的onLayout()完成布局

View的layout

layout()通过setFrame()设置mLeft、mRight、mTop、mBottom确认自身在父容器的位置,随后调用onLayout(),View的onLayout是个protected空方法,需要子类具体实现

public void layout(int l, int t, int r, int b) {

    ......
    
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        
      	.....
      	
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    
    ......
    
}

ViewGroup的layout

ViewGroup继承自View,但其是抽象类,并没有实现onLayout()方法,需要其子类去具体实现,接下来以LinearLayout的onLayout()为例

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

layoutVertical代码如下,通过不断增加childTop让child下移,在setChildFrame()调用child的layout()方法

void layoutVertical(int left, int top, int right, int bottom) {
  	
  	......

    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

           	......
           	
            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            i += getChildrenSkipCount(child, i);
        }
    }
}

getMeasuredWidth()和getWidth()区别

从源码来看,View的默认实现中,测量宽高和最终宽高是相等的,只不过测量宽高在measure中赋值,最终宽高在layout中赋值

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

public final int getWidth() {
    return mRight - mLeft;
}

但在某些情况可能会导致两者不同,如下重写了layout()方法会导致最终宽高+100,还有就是当View需要多次measure时,前几次measure和最终的宽高可能不一致

@Override
public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r + 100, b + 100);
}

draw

调用流程为

  • 调用ViewRootImpl的performTraversals()、perforemDraw()
  • 调用ViewRootImpl的draw()、drawSoftware()
  • 调用DecorView的draw(),调用父类View的draw()、onDraw()、dispatchDraw()
  • 根据多态调用ViewGroup的dispatchDraw()、drawChild()
  • 再调用child的draw(),若是Viewgroup则重复上面过程,否则调用自身的onDraw()完成绘制

View的draw

View的绘制由onDraw()方法完成,并在其参数canvas上调用drawXXX()方法绘制图像

canvas通过bitmap用于存储所有绘制信息,可让onDraw()中的canvas绑定bitmap并暴露出去,实现通过修改共享bitmap的方法更新UI

public class MyView extends View {

    public static Bitmap bitmap;

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888);	//这里未转换单位
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidthOrHeight(widthMeasureSpec), measureWidthOrHeight(heightMeasureSpec));
    }

    private int measureWidthOrHeight(int measureSpec) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = 200;
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(bitmap, 0, 0, null);
        super.onDraw(canvas);
    }
}

如上,修改MyView,构造函数中创建bitmap,onDraw方法中绑定bitmap

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="替换背景" />

    <com.demo.demo0.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ff0000" />
</LinearLayout>

如上,添加按钮和自定义View

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button test = findViewById(R.id.test);
        test.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Canvas canvas = new Canvas(MyView.bitmap);
                canvas.drawColor(Color.GREEN);
            }
        });
    }
}

如上,在点击事件中,根据MyView.bitmap初始化一个Canvas,执行drawColor()方法,通过改变bitmap让View重绘实现更新颜色

ViewGroup的绘制

ViewGroup通常情况下不需要绘制,只有在指定背景色时其onDraw()方法才会调用,使用dispatchDraw()方法遍历并绘制child

public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

ViewGroup会默认开启WILL_NOT_DRAW 标志位进行优化(View不默认开启),当ViewGroup需要onDraw()时需要手动关闭此标志位

实例

自定义View需要注意如下事项

  • 让View支持wrap_content,否则其相当于match_parent
  • 让View支持padding,如果不在ondraw()处理padding会导致其无效
  • 让ViewGroup支持padding和child的margin,同上
  • 不要在View中使用Handle,内部有post()等方法
  • View中的线程或动画需要及时停止,可利用onDetachedFromWindow()方法
  • 滑动嵌套需要处理滑动冲突

Android实例——自定义控件

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
设计自定义控件需要以下步骤: 1.创建一个新的 Android Studio 项目。 2.在项目中创建自定义控件的类。该类应继承自 View 或其子类(例如 Button、EditText 等)。 3.在自定义控件类中实现构造函数和必要的方法,例如 onMeasure()、onLayout() 和 onDraw() 等。 4.在布局文件中使用自定义控件。 下面是一个示例代码: ``` public class MyCustomView extends View { private Paint paint; private Rect rect; public MyCustomView(Context context) { super(context); init(); } public MyCustomView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { paint = new Paint(); paint.setColor(Color.BLUE); rect = new Rect(0, 0, 100, 100); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int desiredWidth = 200; int desiredHeight = 200; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); } else { width = desiredWidth; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desiredHeight, heightSize); } else { height = desiredHeight; } setMeasuredDimension(width, height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(rect, paint); } } ``` 在布局文件中使用自定义控件: ``` <com.example.myapplication.MyCustomView android:layout_width="wrap_content" android:layout_height="wrap_content"/> ``` 以上代码是一个简单的自定义 View,它绘制一个蓝色的矩形。当然,你可以根据需要修改自定义控件的代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值