【Android】Android自定义控件详解

开学前提前来校,结束了寒假学习的尾巴工作。现在可以有时间来啃《Android群英传》,所以还是老习惯,在md上做好笔记写好总结。第三章主要讲自定义View。先把源码贴出来供大家参考与fork哈哈。

—-Android自定义View源码github地址,欢迎fork—-

Activity的setContentView()方法将布局显示到手机屏幕上的实现原理

每个Activity都有一个Window对象,这个对象由PhoneWindow来实现,PhoneWindowDecorView设置为整个应用窗口的根ViewDecorView作为窗口界面的顶层视图,通过其封装的一系列方法就可以操作整个窗口。

  • View的测量

Android中View的测量在onMeasure()方法中进行,方法接收两个参数int widthMeasureSpec, int heightMeasureSpec,分别表示宽度MeasureSpec对象和高度MeasureSpec对象,这里涉及到MeasureSpec类,MeasureSpec对象是一个32位的int值,其中高2位表示测量的模式,即EXACTY,AT_MOSTT,和UNSPEECIFIED三种:

  • EXACTY表示精确模式,当控件的layout_widthlayouut_height属性被指定为具体的数值时,或者指定为match_parent时,系统使用的是EXACTY模式;
  • AT_MOST表示最大值模式,当控件的layout_widthlayouut_height属性被指定为wrap_content时,只要空间的尺寸不超过父控件的尺寸,就会随着内容的大小而变化;
  • UNSPEECIFIED表示不指定大小,这种模式通常在自定义View的时候才使用。

View类默认的onMeasure()方法只支持EXACTY模式,自定义控件的时候需要重写onMeasure()方法。通过Ctrl+鼠标左键点进去,我们知道onMeasure()方法底层调用的是setMeasuredDimension()方法。所以在onMeasure()方法中,我们需要把测量值传给setMeasuredDimension()方法。然而int widthMeasureSpec, int heightMeasureSpec两个参数都是默认值,所以我们需要根据测量模式来重新确定测量值,就是下面的自定义的measureHeight()方法、measureWidth()方法。代码的解释都已经注释,如下:

/**
 * 下面是自定义的measureHeight()方法、measureWidth()方法实现对宽、高值的自定义
 *
 * @param measureSpec
 * @return
 */
private int measureWidth(int measureSpec) {
    int result = 0;
    // MeasureSpec.getMode()提取measureSpec对象中包含的测量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    // MeasureSpec.getSize()提取measureSpec对象中包含的测量值大小
    int specSize = MeasureSpec.getSize(measureSpec);

    // 判断测量类型,根据不同的测量模式specMode来指定不同的测量值specSize
    if (specMode == MeasureSpec.EXACTLY) {
        // 模式为EXACTLY,直接使用指定的specSize
        result = specSize;
    } else {
        // 否则,需要制定默认的测量值为200
        result = 200;
        // 判断,如果测量模式为AT_MOST,测量值指定为result, specSize中的最小值
        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}

/**
 * measureHeight()方法的含义同上面的measureWidth()一样
 *
 * @param measureSpec
 * @return
 */
private int measureHeight(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;
}

所以重写的onMeasure()方法代码就很简单啦,只需要在setMeasuredDimension()方法中调用自定义的measureHeight()方法、measureWidth()方法计算测量值就可以啦。代码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
  • View的绘制

然后简单的重写View类的onDraw()方法就可以在Canvas对象上面绘制自定义View啦。可以看到onDraw(Canvas canvas)方法接收一个Canvas对象,创建Canvas对象代码如下:

Canvas canvas = new Canvas(bitmap);

具体怎么使用Canvas对象来绘制自定义View,请参考我的另外一篇博客

《Android图片资源的加载、简单处理》

这里直接贴出代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.GRAY);
    int width = getWidth();
    int height = getHeight();
    Log.d("wondertwo", "width : " + width + " height : " + height);
}
  • ViewGroup的测量

ViewGroup控件作为父控件可以包含多个View控件,并可以管理它包含的所有View控件。当ViewGroup的大小指定为wrap_content的时候,ViewGroup会对子View进行遍历,通过调用子ViewMeasure方法获得所有View的大小,从而来决定自己的大小。其他情况时则会通过具体的指定值来设置自己的大小。然后ViewGroup遍历所有字ViewLayout方法来把他们放到指定的位置上,自定义View就是通过重写onLayout()方法来实现控制子View显示位置的逻辑。同样,如果需要支持wrap_content属性,还必须重写onMeasure()方法才可以。

  • 自定义View

通常实现自定义View有三种方法:

  • 对现有控件进行拓展
  • 通过组合来实现新控件
  • 重写View来实现全新控件

涉及到View中比较重要的几个回调方法:

onFinishInflate()----从XML加载组件后回调
onSizeChanged()----组件大小改变时回调
onMeasure()----回调该方法来进行View组件的测量
onLayout()----回调该方法来显示View组件的显示位置
onTouchEvent()----监听触摸事件
  • 对现有控件进行拓展:

TextView控件为例来展开说明,TextView之所以能够显示文本信息是因为调用了TextView类的onDraw()方法来把文本信息绘制到View控件上,代码如下所示:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
}

我们可以通过在super.onDraw(canvas)的前面或者后面加上我们自己的控制逻辑,来实现在绘制文本信息的前后完成自定义的操作。如下所示:

@Override
protected void onDraw(Canvas canvas) {
    // 回调父类方法之前实现自己的逻辑,对TextView来说实在绘制文本之前
    super.onDraw(canvas);
    // 回调父类方法之前实现自己的逻辑,对TextView来说实在绘制文本之后
}

比如我们可以利用Android 中Paint对象的Shader渲染器来实现TextView文字闪动的效果。要实现这个效果首先通过getPaint()方法获取Paint对象mPaint = getPaint(),然后通过mPaint.setShader(mLinearGradient)把定义好的Shader效果通过属性LinearGradient设置给Paint对象。示例代码如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (mViewWidth == 0) {
        mViewWidth = getMeasuredWidth();
        if (mViewWidth > 0) {
            mPaint = getPaint();
            mLinearGradient = new LinearGradient(
                    0,
                    0,
                    mViewWidth,
                    0,
                    new int[]{
                            Color.BLUE, 0xffffffff,
                            Color.BLUE},
                    null,
                    Shader.TileMode.CLAMP);
            mPaint.setShader(mLinearGradient);
            mGradientMatrix = new Matrix();
        }
    }
}

然后在onDraw()方法中通过矩阵不断平移渐变效果,从而产生动态的闪动效果。onDraw()方法代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (mGradientMatrix != null) {
        mTranslate += mViewWidth / 5;
        if (mTranslate > 2 * mViewWidth) {
            mTranslate = -mViewWidth;
        }
        mGradientMatrix.setTranslate(mTranslate, 0);
        mLinearGradient.setLocalMatrix(mGradientMatrix);
        postInvalidateDelayed(100);
    }
}

创建复合控件

这种方式需要继承一个合适的ViewGroup,再给他添加指定功能的控件,从而组合成新的复合控件。并指定一些可配置的属性让它具有更强的扩展性。一般分为定义属性,组合控件,引用模板三步。下面是以TopBar,一个标题栏自定义控件为例。自定义控件的实现思路是:

  • 定义属性就是把要自定义的控件的所有的属性都定义在在res/values目录下的attrs.xml配置文件中,然后在控件类TopBar类中获取定义的属性值。
  • 组合控件就是通过在TopBar类的构造方法中动态获取要组合的控件的实例化对象。比如按钮、TextView的实例等等,把获取到的定义好的属性值设置给控件对象,并且在TopBar类中定义接口暴露给空间的调用者。
  • 引用模板也就是使用我们的自定义控件。只是我们平时是大多都是引用系统提供的系统组件,引用自定义控件和引用系统组件的区别是要定义第三方控件的命名空间,并且要使用包名全路径。

res/values目录下创建一个attrs.xml配置文件来定义相关属性,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--
        declare-styleable标签声明使用自定义属性
        name属性确定引用名称
        <attr>标签声明具体的自定义属性,比如字体、大小、颜色、背景
        format属性指定自定义属性的类型
     -->
    <declare-styleable name="TopBar">
        <attr name="title" format="string"/>
        <attr name="titleTextSize" format="dimension"/>
        <attr name="titleTextColor" format="color"/>
        <attr name="leftTextColor" format="color"/>
        <attr name="leftBackground" format="reference|color"/>
        <attr name="leftText" format="string"/>
        <attr name="rightTextColor" format="color"/>
        <attr name="rightBackground" format="integer|color"/>
        <attr name="rightText" format="string"/>
    </declare-styleable>
</resources>

以上各个标签的含义是:

  • declare-styleable标签声明使用自定义属性
  • name属性确定引用名称
  • 标签声明具体的自定义属性,比如字体、大小、颜色、背景
  • format属性指定自定义属性的类型

然后是创建TopBar控件类继承Relativelayout。在其构造方法中实现组合控件的逻辑。TopBar控件类代码如下:

package com.wondertwo.app.application;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

/**
 * Created by Allenieo on 2016/2/25.
 */
public class TopBar extends RelativeLayout {

    // 包含topbar上的元素:左按钮、右按钮、标题
    private Button mLeftButton, mRightButton;
    private TextView mTitleView;

    // 布局属性,用来控制组件元素在ViewGroup中的位置
    private LayoutParams mLeftParams, mTitlepParams, mRightParams;

    // 左按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mLeftTextColor;
    private Drawable mLeftBackground;
    private String mLeftText;
    // 右按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mRightTextColor;
    private Drawable mRightBackground;
    private String mRightText;
    // 标题的属性值,即我们在atts.xml文件中定义的属性
    private float mTitleTextSize;
    private int mTitleTextColor;
    private String mTitle;

    // 映射传入的接口对象
    private topbarClickListener mListener;

    public TopBar(Context context) {
        super(context);
    }

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

    public TopBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        setBackgroundColor(0xFFF59563);
        // 通过这个方法,将在attrs中定义的declare-styleable的所有属性的值存储到TypedArray中
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);

        // 从TypedArray中取出相应的值来为要设置的属性赋值
        mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
        mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
        mLeftText = ta.getString(R.styleable.TopBar_leftText);
        mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
        mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
        mRightText = ta.getString(R.styleable.TopBar_rightText);
        mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
        mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
        mTitle = ta.getString(R.styleable.TopBar_title);

        // 获取完TypedArray的属性值后,一般要调用recyle方法来避免重新创建的时候发生错误
        ta.recycle();

        // 开始组合控件
        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);

        // 用我们引用的属性值来为左、右的点击按钮和中间的标题栏三个组件元素赋值
        mLeftButton.setTextColor(mLeftTextColor);
        mLeftButton.setBackground(mLeftBackground);
        mLeftButton.setText(mLeftText);

        mRightButton.setTextColor(mLeftTextColor);
        mRightButton.setBackground(mRightBackground);
        mRightButton.setText(mRightText);

        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleTextColor);
        mTitleView.setTextSize(mTitleTextSize);
        mTitleView.setGravity(Gravity.CENTER);

        // 为组件元素设置相应的布局元素
        mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
        // 添加到ViewGroup
        addView(mLeftButton, mLeftParams);

        mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
        addView(mRightButton, mRightParams);

        mTitlepParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitlepParams);

        // 按钮的点击事件,不需要具体的实现,只需调用接口的方法,回调时再具体去实现
        mLeftButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mListener.leftClick();
            }
        });
        mRightButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mListener.rightClick();
            }
        });
    }

    /**
     * 设置按钮的显示与否,通过id区分按钮,flag区分是否显示
     *
     * @param id
     * @param flag
     */
    public void setButtonVisable(int id, boolean flag) {
        if (flag) {
            if (id == 0) {
                mLeftButton.setVisibility(View.VISIBLE);
            } else {
                mRightButton.setVisibility(View.VISIBLE);
            }
        } else {
            if (id == 0) {
                mLeftButton.setVisibility(View.GONE);
            } else {
                mRightButton.setVisibility(View.GONE);
            }
        }
    }

    /**
     * 暴露一个方法给调用者来注册接口回调,通过接口来获得回调者对接口方法的实现
     *
     * @param mListener
     */
    public void setOntopbarClickListener(topbarClickListener mListener) {
        this.mListener = mListener;
    }

    /**
     * 接口对象,实现回调机制,在回调方法中通过映射的接口对象调用接口中的
     * 方法,而不用考虑具体实现,具体实现由回调者自己去实现
     */
    public interface topbarClickListener {
        // 左按钮点击事件
        void leftClick();
        // 右按钮点击事件
        void rightClick();
    }

}

组合控件要完成三件事情:

  • 获取定义好的属性,比如颜色,字体,等属性值;
  • 创建要添加到自定义控件中的组件的实例化对象,比如ButtonTextView等等组件。并把获取的属性值设置给这些组件的实例化对象;
  • 封装接口,让自定义控件的调用者去实现具体逻辑;

系统提供了TypedArray这个api来获取自定义属性集,R.styleable.TopBar就是attrs文件<declare-styleable name= "TopBar">标签指定的name。获取代码如下:

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);

先来看看TopBar类中定义的接收变量有哪些:

// 包含topbar上的元素:左按钮、右按钮、标题
private Button mLeftButton, mRightButton;
private TextView mTitleView;

// 布局属性,用来控制组件元素在ViewGroup中的位置
private LayoutParams mLeftParams, mTitlepParams, mRightParams;

// 左按钮的属性值,即我们在atts.xml文件中定义的属性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
// 右按钮的属性值,即我们在atts.xml文件中定义的属性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
// 标题的属性值,即我们在atts.xml文件中定义的属性
private float mTitleTextSize;
private int mTitleTextColor;
private String mTitle;

// 映射传入的接口对象
private topbarClickListener mListener;

再看看TopBar类的完整代码如下:

package com.wondertwo.app.application;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.TextView;

/**
 * Created by Allenieo on 2016/2/25.
 */
public class TopBar extends RelativeLayout {

// 包含topbar上的元素:左按钮、右按钮、标题
private Button mLeftButton, mRightButton;
private TextView mTitleView;

// 布局属性,用来控制组件元素在ViewGroup中的位置
private LayoutParams mLeftParams, mTitlepParams, mRightParams;

// 左按钮的属性值,即我们在atts.xml文件中定义的属性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
// 右按钮的属性值,即我们在atts.xml文件中定义的属性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
// 标题的属性值,即我们在atts.xml文件中定义的属性
private float mTitleTextSize;
private int mTitleTextColor;
private String mTitle;

// 映射传入的接口对象
private topbarClickListener mListener;

public TopBar(Context context) {
    super(context);
}

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

public TopBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    setBackgroundColor(0xFFF59563);
    // 通过这个方法,将在attrs中定义的declare-styleable的所有属性的值存储到TypedArray中
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);

    // 从TypedArray中取出相应的值来为要设置的属性赋值
    mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
    mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
    mLeftText = ta.getString(R.styleable.TopBar_leftText);
    mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
    mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
    mRightText = ta.getString(R.styleable.TopBar_rightText);
    mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
    mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);
    mTitle = ta.getString(R.styleable.TopBar_title);

    // 获取完TypedArray的属性值后,一般要调用recyle方法来避免重新创建的时候发生错误
    ta.recycle();

    // 开始组合控件
    mLeftButton = new Button(context);
    mRightButton = new Button(context);
    mTitleView = new TextView(context);

    // 用我们引用的属性值来为左、右的点击按钮和中间的标题栏三个组件元素赋值
    mLeftButton.setTextColor(mLeftTextColor);
    mLeftButton.setBackground(mLeftBackground);
    mLeftButton.setText(mLeftText);

    mRightButton.setTextColor(mLeftTextColor);
    mRightButton.setBackground(mRightBackground);
    mRightButton.setText(mRightText);

    mTitleView.setText(mTitle);
    mTitleView.setTextColor(mTitleTextColor);
    mTitleView.setTextSize(mTitleTextSize);
    mTitleView.setGravity(Gravity.CENTER);

    // 为组件元素设置相应的布局元素
    mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
    // 添加到ViewGroup
    addView(mLeftButton, mLeftParams);

    mRightParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
    addView(mRightButton, mRightParams);

    mTitlepParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
    addView(mTitleView, mTitlepParams);

    // 按钮的点击事件,不需要具体的实现,只需调用接口的方法,回调时再具体去实现
    mLeftButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mListener.leftClick();
        }
    });
    mRightButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mListener.rightClick();
        }
    });
}

/**
 * 设置按钮的显示与否,通过id区分按钮,flag区分是否显示
 *
 * @param id
 * @param flag
 */
public void setButtonVisable(int id, boolean flag) {
    if (flag) {
        if (id == 0) {
            mLeftButton.setVisibility(View.VISIBLE);
        } else {
            mRightButton.setVisibility(View.VISIBLE);
        }
    } else {
        if (id == 0) {
            mLeftButton.setVisibility(View.GONE);
        } else {
            mRightButton.setVisibility(View.GONE);
        }
    }
}

/**
 * 暴露一个方法给调用者来注册接口回调,通过接口来获得回调者对接口方法的实现
 *
 * @param mListener
 */
public void setOntopbarClickListener(topbarClickListener mListener) {
    this.mListener = mListener;
}

/**
 * 接口对象,实现回调机制,在回调方法中通过映射的接口对象调用接口中的
 * 方法,而不用考虑具体实现,具体实现由回调者自己去实现
 */
    public interface topbarClickListener {
        // 左按钮点击事件
        void leftClick();
        // 右按钮点击事件
        void rightClick();      
    }
}

值得注意的是,在暴露接口时我们只把接口中未实现的方法(按钮点击事件)的具体实现逻辑交给实现类,也就是自定义控件的调用者去做就好。接口方法的回调依然由接口的映射对象在TopBar类中调用。而接口的映射对象只考虑调用接口中的方法而不用去考虑如何实现,使得我们的自定义控件本身和它的调用者之间完全解耦,其实这也是作为一个控件模板最基本的要求。那么分开来看一下代码是怎样实现这个思路的。

TopBar类中定义公有抽象接口,代码如下:

    /**
 * 接口对象,实现回调机制,通过映射的接口对象调用接口中的
 * 方法,而不用考虑具体实现,具体实现由回调者自己去实现
 */
public interface topbarClickListener {
    // 左按钮点击事件
    void leftClick();
    // 右按钮点击事件
    void rightClick();
}

然后暴露一个方法给调用者来实现接口的抽象方法,也就是按钮的点击监听事件的具体处理逻辑。代码如下:

    /**
 * 暴露一个方法给调用者来实现接口的抽象方法,也就是按钮的点击监听
 *
 * @param mListener
 */
public void setOntopbarClickListener(topbarClickListener mListener) {
    this.mListener = mListener;
}

调用者在具体的实现类中就是通过mTopbar = (TopBar) findViewById(R.id.topBar)得到TopBar控件的实例,然后由TopBar控件的实例调用上面的setOntopbarClickListener(topbarClickListener mListener)方法实现接口抽象方法的具体处理逻辑的。我们来看一下实现类的代码如下:

        // 获得我们创建的TopBar对象
    mTopbar = (TopBar) findViewById(R.id.topBar);

    mTopbar.setOntopbarClickListener(new TopBar.topbarClickListener() {
        @Override
        public void leftClick() {
            Toast.makeText(TopbarTestActivity.this, "left", Toast.LENGTH_SHORT).show();
        }
        @Override
        public void rightClick() {
            Toast.makeText(TopbarTestActivity.this, "right", Toast.LENGTH_SHORT).show();
        }
    });

然后接口方法的回调依然由接口的映射对象在TopBar类中调用。具体到我们的自定义控件,回调接口方法的接口映射对象就是mLeftButtonmRightButton这两个按钮。回调的代码在TopBar类的构造方法中的最后。代码如下:

        // 按钮的点击事件,不需要具体的实现,只需调用接口的方法,回调时再具体去实现
    mLeftButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mListener.leftClick();
        }
    });
    mRightButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            mListener.rightClick();
        }
    });

另外,我们还在TopBar类中封装了设置按钮显示与否的方法setButtonVisable(int id, boolean flag)。代码如下:

    /**
 * 设置按钮的显示与否,通过id区分按钮,flag区分是否显示
 *
 * @param id
 * @param flag
 */
public void setButtonVisable(int id, boolean flag) {
    if (flag) {
        if (id == 0) {
            mLeftButton.setVisibility(View.VISIBLE);
        } else {
            mRightButton.setVisibility(View.VISIBLE);
        }
    } else {
        if (id == 0) {
            mLeftButton.setVisibility(View.GONE);
        } else {
            mRightButton.setVisibility(View.GONE);
        }
    }
}

setButtonVisable(int id, boolean flag)方法还是由控件调用者在具体的实现类中通过控件的实例去调用。调用的代码如下:

        // 设置按钮的显示状态
    mTopbar.setButtonVisable(0, true);
    mTopbar.setButtonVisable(1, true);

到这里自定义控件的封装可以说已经完成了。接下来就是使用我们自定义的控件TopBar了,是不是有点小激动呢?你只要把它当做一个普通的控件去使用就好了,就和平时使用ButtonTextViewListView这些控件一样去使用就好。区别有以下两点:

  • 需要为自定义控件创建命名空间;
  • 在声明自定义控件时需要制定完整的包名全路径;

为自定义孔家创建命名空间很简单,代码如下:

xmlns:custom="http://schemas.android.com/apk/res-auto"

其中xmlns就是在指定命名空间,这里的命名空间指定为custom。其他部分格式都是固定不变的,http://schemas.android.com/apk/res-auto这么地址就便是自定义控件。所以当调用者引入第三方控件,也就是自定义控件,的时候是通过命名空间来引用的。最后也是最重要的就是怎样使用自定义控件了。使用方法和系统控件一样,只是要求我们在标签上使用控件的完整包名全路径名即可,是不是很简单呢?代码如下:

    <?xml version="1.0" encoding="utf-8"?>
<com.wondertwo.app.application.TopBar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:id="@+id/topBar"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    custom:leftBackground="@drawable/blue_button"
    custom:leftText="Back"
    custom:leftTextColor="#FFFFFF"
    custom:rightBackground="@drawable/blue_button"
    custom:rightText="More"
    custom:rightTextColor="#FFFFFF"
    custom:title="自定义标题"
    custom:titleTextColor="#123412"
    custom:titleTextSize="15sp">

</com.wondertwo.app.application.TopBar>

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值