Android进阶 :实现自定义View

news2024/11/25 10:10:26

Android进阶:实现自定义View

在这里插入图片描述

导语

有时候我们会想要实现一些复杂或者是独特的组件效果,这时候系统提供的组件可能不能满足我们的需求,这个时候我们一般就会有两个解决办法,一是上网查找开源的控件库,一些流行的开源库可以满足我们的绝大部分需求。不过这些开源库也具有时效性,因为它们的更新得不到保证。二就是我们自己实现我们需要的控件,开源库本质也是自定义View。

不过实现自定义View需要我们对View的事件分发以及View的工作流程有一定了解,可以查看之前的博客:

  1. Android进阶 View事件体系(一):概要介绍和实现View的滑动
  2. Android进阶 View事件体系(二):从源码解析View的事件分发
  3. Android进阶 View事件体系(三):典型的滑动冲突情况和解决策略
  4. Android进阶 View的工作原理:源码分析View的三大工作流程

本篇文章将会主要介绍如何初步实现自定义View。

主要内容

本篇文章涉及到的内容主要有:

  • View的构造方法
  • 自定义View的分类
  • 以绘制一个自定义圆形为例
  • 自定义View时的注意事项
  • 简易ViewPager类的实现
  • 组合自定义组件的实现

View的构造方法

在正式进入自定义View的内容之前我们有必要简单说明一下View的构造方法,这对我们初始化一些参数具有作用。View的构造方法主要是有四种重载:

  1. public View(Context context):
    这个构造方法通常在代码中使用,用于在给定的上下文(Context)中创建一个新的View实例。它是最常用的构造方法之一,用于在代码中动态创建View。这个构造方法在实例化View时使用,但是在View被添加到父容器之前,需要设置视图的属性和布局参数。

  2. public View(Context context, AttributeSet attrs):
    这个构造方法通常在布局文件中使用,用于在XML布局文件中定义的View元素被实例化时调用。在XML布局文件中,每个View元素都对应一个View实例,这个构造方法用于实例化这些View。

  3. public View(Context context, AttributeSet attrs, int defStyleAttr):
    这个构造方法通常在主题(Theme)中使用,用于在指定的主题样式(Theme Style)中创建一个新的View实例。它允许通过主题样式为View设置默认的属性和样式。

  4. public View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes):
    这个构造方法通常在主题(Theme)中使用,用于在指定的主题样式(Theme Style)中创建一个新的View实例,并指定一个默认样式资源。它与上一个构造方法类似,但是可以指定一个默认样式资源,用于在主题样式中未指定的情况下为View设置属性和样式。

当我们依次点开View构造方法的源码时,就会发现他们之间是存在调用关系的:

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

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

	public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);
		......
    }

	public View(Context context) {
		mContext = context;
        mResources = context != null ? context.getResources() : null;
        .....
	}

可以看到这个调用关系还是比较清晰的,显然有两个参数的构造方法是我们正常使用xml实例化时的构造入口,调用顺序如下:
在这里插入图片描述
因此,在需要对一些参数进行初始化时(比如说Scroller等),可以将其放到第三个方法中,然后构造方法依次进行调用即可,具体在后面的例子中会进行演示。

自定义View的分类

我们要实现自定义View的话,也可以将其进行一定的分类,主要可以分为以下三种:

  1. 自定义View
    1)继承系统控件:一般用于拓展某种已有的View的功能
    2)继承View:主要用于实现一些不规则的效果,需要重写onDraw;还需要自己支持wrap_content属性和padding属性。

  2. 自定义ViewGroup
    1)继承系统控件:当某种效果看起来很像几种View组合在一起的时候;这种方法不需要自己处理测量布局这两个过程。
    2)继承ViewGroup:主要用于实现自己的布局,需要合适地处理ViewGroup的测量布局两个过程。

  3. 自定义组合控件

自定义View又可以分为继承View的和继承系统控件的(比如TextView);同理,自定义ViewGroup也可以分为继承ViewGroup和继承系统控件的(比如滑动冲突文章里写的HorzontalScrollViewEx就是继承了LinearLayout);而自定义组合控件又是什么呢?可以理解为由多个控件组合而成的控件,将其视作一个整体。

绘制一个自定义圆

接下来我们继承View实现一个自定义View,这个自定义View的显示很简单,就是绘制一个圆形,那在这里就需要提前了解两个比较关键,分别是Paint类和Canvas类,我一般会将前者Paint理解为画笔,它用于描述绘制的样式、颜色和效果等属性。而Canvas类可以理解为画布,它提供了一系列的绘制方法,用于在其中绘制图形、文本和位图等内容。通过 Canvas 类,我们可以实现对 View 或其他绘制目标的绘制操作。这两个方法具体会在onDraw方法绘制中用到。

我们先给出自定义圆的源码:

public class CircleView extends View {

    private int mColor = Color.RED;//取红色
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//画笔
    public CircleView(Context context) {
        super(context);
        //init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        //super(context, attrs);
        this(context,attrs,0);

    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs,defStyleAttr,0);

        //init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
        //init();

    }

    private void init(){
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获得了测量的尺寸和模式
        int widSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        int widResultSize = 0;
        int heightResultSize = 0;
        //这里我们如果想实现标准模式,可以参考别的View的测量流程
        if(widSpecMode == MeasureSpec.AT_MOST){
            widResultSize = 0;
        }else if(widSpecMode == MeasureSpec.EXACTLY){
            widResultSize = widSpecSize;
        }else{
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        }
        if(heightSpecMode == MeasureSpec.AT_MOST){
            heightResultSize = 0;
        }else if(widSpecMode == MeasureSpec.EXACTLY){
            heightResultSize = heightSpecSize;
        }else{
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        }
        setMeasuredDimension(widResultSize,heightResultSize);
    }

	.....

}

直接先从构造方法中看起来,可以看到我们遵守了View类中的初始化流程,初始化init方法放到最后一个构造方法中调用,但是其他的构造方法最终会调用到最后一个构造方法。

这个类中还声明了两个私有成员变量,分别是mColor和mPaint,mColor是用来设置画出圆的颜色的,而mPaint就是我们之前提到的画笔,它的构造方法中传入的是一些标志位,用来指定绘制出来的效果的,以下是常用的标志位和它们的含义:

  • ANTI_ALIAS_FLAG:抗锯齿标志,用于使绘制的边缘更加平滑,减少锯齿效果。
  • DITHER_FLAG:抖动标志,用于在绘制过程中使用抖动算法,使颜色过渡更平滑。
  • LINEAR_TEXT_FLAG:线性文本标志,用于启用线性文本处理,使文本绘制更加清晰。
  • SUBPIXEL_TEXT_FLAG:亚像素文本标志,用于启用亚像素文本处理,使文本绘制更加平滑。
  • EMBEDDED_BITMAP_TEXT_FLAG:嵌入位图文本标志,用于在文本绘制时,将字形转换为位图进行绘制,提高绘制性能。
  • FILTER_BITMAP_FLAG:位图过滤标志,用于启用位图过滤器,使绘制的位图更加平滑。
  • FULL_COLOR_LAYER_FLAG:全色层标志,用于启用全色层绘制模式,提高绘制质量和准确性。
  • STRIKE_THRU_TEXT_FLAG:删除线文本标志,用于给文本添加删除线效果。
  • UNDERLINE_TEXT_FLAG:下划线文本标志,用于给文本添加下划线效果。

接着再init初始化方法中,主要就是设置了画笔的颜色,这点很简单,我们往后看。根据View的工作流程来说,是需要经历onMeasure到onLayout最后再到onDraw的。之前的文章分析中,我们也知道默认情况下设置View的长和宽为wrap_content属性是无效的,所以需要重写onMeasure方法。在新的onMeasure方法中,我们做的就是根据自身的测量模式和测量尺寸来设置最终的测量尺寸。具体做的操作就是当属性设置为wrap_content时,将其大小设置为0,这样当我们设置属性为wrap_content时就有效果了。

由于这是View不是ViewGroup,所以我们可以不用管onLayout方法。

接下来看比较重要,也是实现圆的绘制的onDraw方法方法:

    @Override
    protected void onDraw(Canvas canvas) { //padding属性:内间距--属于该View的自身边界内,所以需要自己处理
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth()-paddingRight-paddingLeft ;
        if (width < 0) width = 0;
        int height = getHeight()-paddingTop-paddingBottom;
        if(height < 0) height = 0;
        int radius = Math.min(width,height)/2;
        canvas.drawCircle(getWidth()/2,getHeight()/2,radius,mPaint);
    }

最重要的就是最后一行的drawCircle方法,这是调用了canvas类的方法,看名字也知道这个方法就是用来绘制圆形的,传入的四个参数分别是圆心的X值,圆心的Y值,半径值,用来绘制圆形的画笔。上面也提到过了,要我们自己处理padding属性,这里也将padding属性考虑进去了,这样当我们在布局文件中设置padding属性时就有用了,最终实现的效果:
在这里插入图片描述

启用自定义参数

接下来我们升级一下这个自定义圆,让他可以设置自定义参数并绘制一个双色的同心圆。自定义参数就是我们自己定义的属性,这个属性可以影响最终绘制出来的效果,就和padding等参数一样。

创建自定义参数的做法也很简单,首先我们在values文件夹中新建一个xml文件用来存储我们自己的自定义属性,然后在里面定义两个自定义属性:
在这里插入图片描述

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
        <attr name="circle_outer_color" format="color"/>
    </declare-styleable>
</resources>

两个declare-styleable 标签后面跟的name就是我们写的自定义View的类,标签之前的参数就是我们自己定义的参数,这里我们定义了两个参数,都是代表颜色属性的,第一个代表同心圆内部的颜色,第二个代表同心圆外部的颜色。接下来我们在自定义View中使用这些参数,给出修改完后的自定义View的完整代码:

public class CircleView extends View {

    private int mColor = Color.RED;//取红色
    private int outerColor = Color.BLUE;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//应该是画笔
    private Paint mOuterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    public CircleView(Context context) {
        super(context);
        //init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        //super(context, attrs);
        this(context,attrs,0);

    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs,defStyleAttr,0);

        //init();
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);//获取CircleView下所有的自定义属性
        mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);
        outerColor = a.getColor(R.styleable.CircleView_circle_outer_color,Color.BLUE);
        a.recycle();
        init();
        //init();

    }

    private void init(){
        mPaint.setColor(mColor);
        mOuterPaint.setColor(outerColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获得了测量的尺寸和模式
        int widSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int widSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        int widResultSize = 0;
        int heightResultSize = 0;
        //这里我们如果想实现标准模式,可以参考别的View的测量流程
        if(widSpecMode == MeasureSpec.AT_MOST){
            widResultSize = 0;
        }else if(widSpecMode == MeasureSpec.EXACTLY){
            widResultSize = widSpecSize;
        }else{
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        }

        if(heightSpecMode == MeasureSpec.AT_MOST){
            heightResultSize = 0;
        }else if(widSpecMode == MeasureSpec.EXACTLY){
            heightResultSize = heightSpecSize;
        }else{
            super.onMeasure(widthMeasureSpec,heightMeasureSpec);
        }

        setMeasuredDimension(widResultSize,heightResultSize);
    }

    @Override
    protected void onDraw(Canvas canvas) { //padding属性:内间距--属于该View的自身边界内,所以需要自己处理
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth()-paddingRight-paddingLeft ;
        if (width < 0) width = 0;
        int height = getHeight()-paddingTop-paddingBottom;
        if(height < 0) height = 0;

        int radius = Math.min(width,height)/2;
        canvas.drawCircle(getWidth()/2,getHeight()/2,radius,mOuterPaint);//在画布上用mPaint画笔画一个圆心在中间,半径为r的圆
        canvas.drawCircle(getWidth()/2,getHeight()/2,(int)(radius*0.75),mPaint);
    }
}

我们看第四个构造参数中是怎么取出自定义View的属性的,首先是通过TypedArray 属性数组取出自定义属性,具体是调用
context的obtainStyledAttributes函数,传入的第二个参数就是我们写的标签的name,这样就取出了自定义属性,最后赋值给成员变量。getColor方法中的第二个参数就是默认参数(即当自定义属性没有设置时的默认值)。最后使用完TypedArray后还要将其回收。

至于如何画出一个同心圆,也很简单,我们可以先画一个大一点的圆,然后再画一个小一点的圆,当小圆遮挡住大圆的中心部分时,就达到了一个双色同心圆的效果:

        canvas.drawCircle(getWidth()/2,getHeight()/2,radius,mOuterPaint);//在画布上用mPaint画笔画一个圆心在中间,半径为r的圆
        canvas.drawCircle(getWidth()/2,getHeight()/2,(int)(radius*0.75),mPaint);

最后看看效果:
在这里插入图片描述

自定义View时的注意事项

这里我们也总结出一些在自定义View时需要注意的事项:

  1. 让View支持wrap_content
    因为直接继承了View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么这个属性默认是不起作用的。

  2. 如果有必要,让View支持Padding
    padding属性也和上面的wrap_content属性类似,如果我们自己不对padding属性进行处理,那么它默认也是无效的;另外,直接继承ViewGroup的控件需要在onMeasure方法和onLayout方法中考虑padding和子元素的margin属性,不然也会导致这两个属性设置了也失效。

  3. 尽量不要在View中使用Handler
    因为View本身就有post方法,所以说没必要用Handler

  4. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
    如果有线程或者动画需要停止时,onDetachedFromWindow方法是一个很好的时机,它会在包含次View的Activity退出时和当前View被移除时被调用;与之对应的方法是onAttachedToWindow。

  5. View带有滑动嵌套情形时,需要处理好滑动冲突
    这个情况我们在滑动冲突的文章里已经探讨过了,具体可以看之前的博客。

简易ViewPager类的实现

这里我们根据我们之前的HorzontalScrollViewEx进行改造,不同的是这里我们不是继承LinearLayout而是ViewGroup,这样需要我们自己实现onMeasure和onLayout方法,这里直接给出全部代码:

public class HorizontalScrollViewEx extends ViewGroup {

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    private int mLastX = 0;
    private int mLastY = 0;
    private int mLastInterceptX = 0;
    private int mLastInterceptY = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    private static final String TAG = "HZT";
    //自己写的伪ViewPager
    public HorizontalScrollViewEx(Context context) {
        super(context);
    }

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

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context,attrs,defStyleAttr,0);
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init(){
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();//获得子View的数量
        measureChildren(widthMeasureSpec,heightMeasureSpec);//先测出子View

        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if(childCount == 0){
            setMeasuredDimension(0,0);//如果没有子View的话,就直接设置大小为零
        }else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);//获得第一个子View的实例
            measuredWidth = childView.getMeasuredWidth() * childCount;//伪ViewPager
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth,measuredHeight);
        }else if(heightSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpecSize,measuredHeight);
        }else if(widthSpecMode == MeasureSpec.AT_MOST){
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth,heightSpecSize);
        }

    }

    @Override //测量完毕之后开始放置 -- 从左向右放置
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;
        Log.d(TAG, "Count: "+mChildrenSize);
        for(int i = 0; i < childCount;i++){ //遍历,然后一个一个依次开始放置
            final View childView = getChildAt(i);
            if(childView.getVisibility() != View.GONE){
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight());
                childLeft += childWidth;//依次向右放置
            }
        }
    }

    //开始处理 滑动冲突

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        boolean intercepted = false;
        switch (ev.getAction()){
            case ACTION_DOWN:{
                intercepted = false;
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                int deltaX = x - mLastInterceptX;
                int deltaY = y - mLastInterceptY;
                if(Math.abs(deltaX)>Math.abs(deltaY)){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP:{
                intercepted = false;
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        mLastInterceptX = x;
        mLastInterceptY = y;
        return intercepted;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case ACTION_DOWN:{
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            }
            case ACTION_MOVE:{
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX,0);
                break;
            }
            case ACTION_UP:{
                int scrollX = getScrollX();
                mVelocityTracker.addMovement(event);
                float xVelocity = mVelocityTracker.getXVelocity();//获得水平滑动速度
                if(Math.abs(xVelocity) >= 50){
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1: mChildIndex + 1;
                }else{
                    mChildIndex = (scrollX + mChildWidth / 2)/mChildWidth;
                }
                mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize-1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx,0);
                mVelocityTracker.clear();
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    public void computeScroll() {
        if(mScroller.computeScrollOffset()){
            scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            postInvalidate();
        }
    }

    private void smoothScrollBy(int dx,int dy){
        mScroller.startScroll(getScrollX(),getScrollY(),dx,dy,700);
        invalidate();
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

onMeasure测量流程中的测量我们主要用的还是高度为match_parent,宽度为wrap_content的模式,这种模式下,依照我们的经验来说的话,容器的高度应该是是会填满整个屏幕的,直接设置高度为自身的高度,宽度就是每个子View的宽度乘以子View的数量。

onLayout中我们横向依次放置子View,每次放置完一个子View就将放置的起始点给叠加,达到横向ViewPager的效果。

	for(int i = 0; i < childCount;i++){ //遍历,然后一个一个依次开始放置
            final View childView = getChildAt(i);
            if(childView.getVisibility() != View.GONE){
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight());
                childLeft += childWidth;//依次向右放置
            }
    }

我们可以在约束布局中测试这个布局实现的效果,我们在这个自定义ViewPager中放置三个宽度为500dp的TextView,并给其设置不同的背景色,需要注意的是这里给自定义ViewPager的横向约束只要给左边界一条就可以了:
在这里插入图片描述
再实际运行一下,就可以看到实际效果了:
在这里插入图片描述

自定义组合控件

自定义组合控件其实就是将多个组件结合起来形成一个整体的控件,首先要确定整体自定义控件的样式,在这里我们就写一个TitleBar,自带两个按钮可以设置点击事件:
在这里插入图片描述
下面是xml代码:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/layout_titlebar_rootlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/iv_titlebar_left"
        android:layout_width="wrap_content"
        android:layout_height="fill_parent"
        android:layout_alignParentLeft="true"
        android:layout_centerInParent="true"
        android:paddingLeft="15dp"
        android:paddingRight="15dp"
        android:src="@drawable/baseline_agriculture_24"/>

    <TextView
        android:id="@+id/it_titlebar_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:ellipsize="end"
        android:maxEms="11"
        android:singleLine="true"
        android:textStyle="bold" />

    <ImageView
        android:id="@+id/iv_titlebar_right"
        android:layout_width="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_height="fill_parent"
        android:padding="15dp"
        android:gravity="center"
        android:src="@drawable/baseline_airport_shuttle_24"/>

</RelativeLayout>

接着,我们需要将这个xml转化成View,主要是调用inflate方法:

LayoutInflater.from(context).inflate(R.layout.titlebar_layout,this,true);

与之前以往大多数不同的是,这里将这个xml创建出来之后直接加载进了这个类的根目录下,最后一个参数为true,这是最重要的一个部分,其他的代码部分就很简单了,大家可以自行理解,总的来说自定义组合控件本质上和其他自定义ViewGroup没有什么不同:

public class TitleBar extends RelativeLayout {

    private ImageView iv_titlebar_left;
    private ImageView iv_titlebar_right;
    private TextView tv_titlebar_title;
    private RelativeLayout layout_titlebar_rootlayout;
    private int mColor = Color.BLUE;
    private int mTextColor = Color.WHITE;
    private String mTitle = "Title";
    public TitleBar(Context context) {
        super(context);
        initView(context);
    }

    public TitleBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.TitleBar);
        mColor = mTypedArray.getColor(R.styleable.TitleBar_title_bg,Color.BLUE);
        mTextColor = mTypedArray.getColor(R.styleable.TitleBar_title_text_color,Color.WHITE);
        mTitle = mTypedArray.getString(R.styleable.TitleBar_title_text);
        mTypedArray.recycle();
        initView(context);
    }

    public TitleBar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    public TitleBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public void initView(Context context){
        LayoutInflater.from(context).inflate(R.layout.titlebar_layout,this,true);
        iv_titlebar_left = findViewById(R.id.iv_titlebar_left);
        iv_titlebar_right = findViewById(R.id.iv_titlebar_right);
        tv_titlebar_title = findViewById(R.id.it_titlebar_title);
        layout_titlebar_rootlayout = findViewById(R.id.layout_titlebar_rootlayout);
        //设置背景颜色
        layout_titlebar_rootlayout.setBackgroundColor(mColor);
        //设置标题的字体颜色
        tv_titlebar_title.setTextColor(mTextColor);
        setTitle(mTitle);
    }

    public void setTitle(String titleName){//设置标题内容
        if(!TextUtils.isEmpty(titleName)){
            tv_titlebar_title.setText(titleName);
        }
    }

    //设置左右图标的监听器
    public void setLeftListener(OnClickListener onClickListener){
        iv_titlebar_left.setOnClickListener(onClickListener);
    }

    public void setRightListener(OnClickListener onClickListener){
        iv_titlebar_right.setOnClickListener(onClickListener);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/599148.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JUnit技术专题】「入门到精通系列」手把手+零基础带你玩转单元测试,让你的代码更加“强壮”(夯实功底篇)

手把手零基础带你玩转单元测试&#xff0c;让你的代码更加“强壮” 前言介绍JUnit是什么&#xff1f;JUnit和xUnit之间的关系 JUnit的基本概念JUnit的特点什么是一个单元测试用例 JUnit的用法JUnit的最佳实践案例分析创建一个类创建 Test Case 类创建 Test Runner 类 JUnit总体…

Web实验二 CSS基本样式实验

实验原理 通过创建CSS样式文件&#xff0c;理解CSS样式基本属性作用及意义。 实验目的 理解CSS基本概念及功能 理解CSS样式的设计原则 理解并掌握CSS样式的基本声明方法 理解并掌握多种CSS选择器的使用方法 理解并掌握字文本、表格、超链接等元素常用属性的使用方法 理解并掌握…

机器人学:DH参数总结(传统DH方法和改进DH方法)

1. 传统DH参数方法 1.1 确定坐标系的方法 定义&#xff1a;杆 i i i的近端是关节 i i i&#xff0c;远端是关节 i 1 i1 i1. 【下面的规则参考上面的图看得更清楚】 对于 n n n自由度机器人&#xff0c;可用以下步骤建立与各杆件 i ( i 0 , 1 , … , n ) i(i0,1,…,n) i(i0,…

【iOS底层探索- Block原理分析-循环引用】

文章目录 前言准备工作1. Block的分类2. Block的内存分析捕获外部变量引用计数的变化堆栈释放的差异总结 3. Block的循环引用3.1 什么是循环引用&#xff1f;案例引入 循环引用解决方法1. 强弱共舞2. 手动中断循环引用3. 参数形式解决循环引用&#xff08;block传参&#xff09…

汇编指令执行过程及CS与IP和DS寄存器关系与变化

内存指令及寄存器初始值: CS:2000H IP:0 DS:1000H AX:0 BX:0 上面在内存中的汇编指令是如何执行的? 验证: 在debug下用a指令先向内存写入下面指令,然后用u指令查看 mov ax,2000 mov ds,ax mov ax,[0008] mov ax,[0002] 在debug下用a指令先向内存写入下面指令,然后用u指…

Computer之Compilation:Cmake的简介、安装、案例应用之详细攻略

Computer之Compilation&#xff1a;Cmake的简介、安装、案例应用之详细攻略 目录 Cmake的简介 Cmake的安装 1、官方下载 2、执行安装程序&#xff0c;并按照提示进行安装 3、验证测试 Cmake的案例应用 Cmake的简介 CMake&#xff08;Cross-platform Make&#xff09;是一…

【嵌入式烧录/刷写文件】-1.7-将一个文本文件转换为Motorola S-record(S19/SREC/mot/SX)文件

案例背景(共5页精讲)&#xff1a; 有如下两个文本文件&#xff08;*.txt&#xff0c;*.ini&#xff0c;*.asc…&#xff09;转换成Motorola S-record(S19/SREC/mot/SX)文件。常用于Key密钥&#xff0c;signature签名…的导入&#xff0c;或对一段数据计算出hex记录的最后一个字…

服务器性能优化方法

文章目录 服务器性能优化方法什么是服务器并发处理能力&#xff1f;什么方法衡量服务器的并发能力&#xff1f;怎么提高服务器的并发处理能力&#xff1f;**1&#xff0c;提高CPU并发计算能力**&#xff08;1&#xff09;多进程&多线程&#xff08;2&#xff09;减少进程切…

spring cloud搭建(eureka)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习新东西是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习…

力扣高频SQL50题(基础版)——第二天

力扣高频SQL50题(基础版)——第二天 1 文章浏览Ⅰ 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # Write your MySQL query statement below SELECT distinct author_id id FROM Views WHERE author_idviewer_id ORDER BY id asc1.3 运行截图 2 无…

基于MSP430送药小车 ----- 基础篇【2021年全国电赛(F题)】

文章目录 一、赛题1. 任务2. 要求3. 说明 二、构思 分析1. 引脚利用2. PID算法3. 灰度循迹及标志位4. 视觉模块5. 直角转弯、原地转向 三、硬件清单四、逻辑设计1. 近端送药2. 中端送药3. 远端送药 五、程序设计1. OpenMV2. 灰度循迹3. 装药卸药 总结 一、赛题 1. 任务 设计并…

Linux Socket 分包 和 粘包 问题 - 解决方案

分包和粘包在TCP网络编程中是非常常见的&#xff0c;分包会造成 接收端 接收的数据不全的问题&#xff0c;粘包会造成接收多余的数据的文件。 这里做一个举例&#xff0c;例如客户端A要发送字符串“helloworld”给服务器B&#xff0c;A是一次性发送&#xff0c;但TCP有可能会将…

A Unified Conditional Framework for Diffusion-based Image Restoration

A Unified Conditional Framework for Diffusion-based Image Restoration (Paper reading) Yi Zhang, CUHK, CN, arXiv2023, Cited:0, Code, Paper 1. 前言 最近&#xff0c;扩散概率模型&#xff08;Diffusion Probabilistic Models&#xff0c;DPMs&#xff09;在图像生成…

Android 自定义View 之 饼状进度条

饼状进度条 前言正文一、XML样式二、构造方法三、测量四、绘制① 绘制描边① 绘制进度 五、API方法六、使用七、源码 前言 前面写了圆环进度条&#xff0c;这次我们来写一个饼状进度条&#xff0c;首先看一下效果图&#xff1a; 正文 效果图感觉怎么样呢&#xff1f;下面我们…

GLTF/GLB模型轻量化简明教程

GLB 文件格式很方便&#xff0c;因为它包含渲染所需的所有文件&#xff0c;包括纹理。 但是&#xff0c;根据用途&#xff0c;你可能希望简化文件&#xff0c;因为它有时非常详细。 在本文中&#xff0c;我将使用 gltf-transform 来执行简化&#xff0c;并且假设你使用的是 Wi…

0x23 Read Version Information Service

0x23 Read Version Information Service ReadMemoryByAddress服务允许客户端通过提供的起始地址和要读取的内存大小向服务器请求内存数据。 ReadMemoryByAddress请求消息用于请求由参数memoryAddress和memorySize标识的服务器的内存数据。 用于memoryAddress和memorySize参数的…

SAP-MM-原始接受订单

业务场景&#xff1a; 供应商是强势供应商&#xff0c;产品紧缺&#xff0c;订购货物需要自提&#xff0c;运损也归我们公司&#xff0c;而且立刻付款&#xff0c;那么就不能按以往操作&#xff0c;等供应商送货&#xff0c;再开票 我们在付款&#xff0c;那么SAP如何快速实现…

JS 排序算法

在前端工作中算法不常用&#xff0c;但是排序可能会经常会用&#xff0c;下面学习几种常用算法。 引用借鉴&#xff1a;js的五种排序方法_js排序_木可生森的博客-CSDN博客 JS 常见的排序算法_js排序算法_东风过境F的博客-CSDN博客 1.冒泡排序&#xff1a; 思路&#xff1a;逐次…

儿童节快乐,基于CSS3绘制一个游乐场动效界面

0️⃣写在前面 让代码创造童话&#xff0c;共建快乐世界。六一儿童节——这是属于孩子们的节日&#xff0c;也是属于我们大人的节日。让我们一起「致童真」&#xff0c;用代码&#xff08;HTMLCSSJS&#xff09;创造出一个游乐场&#xff0c;让这个世界多一份快乐和惊喜&#x…

如何把vue项目部署服务器(宝塔面板)上

一&#xff0c;vue项目打包 首先我们把准备好的vue项目进行打包&#xff1a; 输入命令&#xff1a;npm run build 生成dist文件 二、进入宝塔管理界面&#xff0c;点击网站&#xff0c;然后点击添加站点 三。按下面输入 点设置 四。 输入好点添加&#xff0c;注意&#x…