View的工作原理
ViewRoot和DecorView
ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均通过ViewRoot来完成。
ActivityThread中,Activity创建完成后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联。
View的绘制流程从ViewRoot的performTraversals方法开始,经过measure、layout和draw三大流程。
绘制流程:
从ViewRoot的performTraversals方法开始,在performMeasure中调用measure方法,在measure方法中又会调用onMeasure,在onMeasure方法中对所有子元素进行measure过程,然后在这里将measure流程传递给子元素。子元素会重复measure过程,完成View树的遍历。
通俗的说:我们要画一幅肖像画,首先选择合适大小的画布,在测量出来人脸在画布中所占位置的大小,然后再测量出,眼睛、鼻子、嘴巴等在画布上是多大。总的来说,是先对顶层View进行测量,然后测量子View,一直到最底层的View.
performLayout和performDraw的传递流程和performMeasure是类似的。唯一不同的是performDraw的传递过程是在draw方法中通过dispatchDraw来实现的。
Measure决定的是View的宽/高,Layout决定的是View的顶点位置,而Draw则是将View绘制出来显示。
MeasureSpec
MeasureSpec代表一个32位int值,高两位代表SpecMode,低30位代表SpecSize。
SpecMode是指测量模式。测量模式有三种:
(1)UNSPECIFIED
子View的尺寸想多大就多大,父容器不限制。
(2)EXACTLY
精确模式,比如在子View在父布局中写的match_parent、以及精确值 300dp;
(3)AT_MOST
自适应,不能超过父布局最大宽度,对应属性:wrap_content
子View和父容器的MeasureSpec关系归纳:
a. 子View为精确宽高,如:300dp,无论父容器的MeasureSpec,子View的MeasureSpec都为精确值且遵循LayoutParams中的值。
b. 子View为match_parent时,如果父容器是精确模式,则子View也为精确模式且为父容器的剩余空间大小;如果父容器是wrap_content,则子View也是wrap_content且不会超过父容器的剩余空间。
c. 子View为wrap_content时,无论父View是精确还是wrap_content,子View的模式总是wrap_content,且不会超过父容器的剩余空间。
SpecSize是指在某种测量模式下的规格大小。
View的工作流程
View的工作流程主要是指:measure、layout、draw这三大流程。即测量、布局和绘制。
View的measure过程:
getSuggestedMinimumWidth的逻辑:View如果没有背景,那么返回android:minWidth这个属性指定的值,这个值可以为0;如果设置了背景,则返回背景的最小宽度和minWidth中的较大值。
ViewGroup的measure过程:
对于ViewGroup来说,除了完成自己的measure过程之外,还会遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个过程。ViewGroup是一个抽象类,并没有重写View的onMeasure方法,它提供了一个measureChildren方法。
注意事项:
如何在Activity初始化时获取View的宽高:
a. Activity或者View的onWindowFocusChanged方法(注意该方法会在Activity Pause和resume时被多次调用)。
b. view.post(new Runnable( {@Overiddepublic void run(){})});在run方法中获取。
c. ViewTreeObserver中的onGlobalLayoutListener中。
d. view.measure手动获取: match_parent:无法测量; 精确值:int wMeasureSpec = MeasureSpec.makeMeasureSpec(exactlyValue, MeasureSpec.EXACTLY); wrap_content:int wMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
Layout过程:
我们来看下ViewGroup,在ViewGroup进行layout确定位置时,循环遍历了子View,并且最终调用了子View的layout确定子View的位置。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/**子元素的数量*/
final int count = getChildCount();
int width = r - l;
int height = b - t;
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
final int scrollX = getScrollX();
int decorCount = 0;
/**遍历ViewGroup*/
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childLeft = 0;
int childTop = 0;
if (lp.isDecor) {
/**如果为水平方向*/
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
/**如果设置内容为竖直方向*/
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (hgrav) {
default:
childLeft = paddingLeft;
break;
case Gravity.LEFT:
childLeft = paddingLeft;
paddingLeft += child.getMeasuredWidth();
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
paddingLeft);
break;
case Gravity.RIGHT:
childLeft = width - paddingRight - child.getMeasuredWidth();
paddingRight += child.getMeasuredWidth();
break;
}
switch (vgrav) {
default:
childTop = paddingTop;
break;
case Gravity.TOP:
childTop = paddingTop;
paddingTop += child.getMeasuredHeight();
break;
case Gravity.CENTER_VERTICAL:
childTop = Math.max((height - child.getMeasuredHeight()) / 2,
paddingTop);
break;
case Gravity.BOTTOM:
childTop = height - paddingBottom - child.getMeasuredHeight();
paddingBottom += child.getMeasuredHeight();
break;
}
childLeft += scrollX;
/**通过子View的layout方法,确定子元素自身的位置*/
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());
decorCount++;
}
}
}
}
View的draw过程:
Draw的过程比较简单:
(1)绘制背景background.draw(canvas)
(2)绘制自己(onDraw)
(3)绘制Children(dispatchDraw)
(4)绘制装饰(onDrawScrollBars)
自定义view
自定义View的步骤
(1)自定义属性的声明与获取
(2)测量onMeasure
(3)布局onLayout(针对ViewGroup)
(4)绘制onDraw
(5)onTouchEvent(和用户交互)
(6)onInterceptTouchEvent(ViewGroup 事件拦截)
- Step1:自定义属性声明和获取*
a)分析需要的自定义属性
b)zaires/values/attrs.xml中定义声明
c)在Layout xml文件中进行使用
d)在View的构造方法中进行获取
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="icon" format="reference"></attr>
<attr name="color" format="color"></attr>
<attr name="text" format="string"></attr>
<attr name="text_size" format="dimension"></attr>
<declare-styleable name="MyView">
<attr name="icon"></attr>
<attr name="color"></attr>
<attr name="text"></attr>
<attr name="text_size"></attr>
</declare-styleable>
</resources>
在View的构造方法中获取:
public class MyView extends View{
private Context context;
private AttributeSet attrs;
private Bitmap btIcon;//图片
private int textColor;//字体颜色
private String text;//文字内容
private int text_size;//字体大小
public MyView(Context context) {
this(context,null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context=context;
this.attrs=attrs;
init();
}
private void init() {
TypedArray a=context.obtainStyledAttributes(attrs,R.styleable.MyView);
int n=a.getIndexCount();
for (int i = 0; i <n ; i++) {
int attr=a.getIndex(i);
switch (attr){
case R.styleable.MyView_icon:
BitmapDrawable bt= (BitmapDrawable) a.getDrawable(R.styleable.MyView_icon);
this.btIcon=bt.getBitmap();
break;
case R.styleable.MyView_text:
this.text=a.getString(R.styleable.MyView_text);
break;
case R.styleable.MyView_color:
this.textColor=a.getColor(R.styleable.MyView_color,0);
break;
case R.styleable.MyView_text_size:
this.text_size=a.getInt(R.styleable.MyView_text_size,16);
break;
}
}
/**不需要的时候释放内存*/
a.recycle();
}
}
Step2:测量onMeasure
(1)测量模式EXACTLY、AT_MOST、UNSPECIFIED
(2)MeasureSpec
(3)setMeasureDimension
(4)requestLayout(重新测量)
Step3:布局Layout(对于ViewGroup来说)
决定自View的位置
尽可能将onMeasure中的一下操作移动到此方法中。
requestLayout(重新测量)
Step4:绘制draw
绘制内容区域
invalidate(), UI线程中调用
postInvalidate();子线程中调用
Step5:与用户进行交互onTouchEvent
ACTION_DOWN
ACTION_UP
ACTION_MOVE
…
Step6:onInterceotTouchEvent()
是否拦截该手势等
自定义View的实现通常有三种方式
(1)继承现有控件,对现有控件进行扩展
我们来实现一个带有清除内容功能的文本输入框,我们可以使用Edittext+按钮组合来配合使用,也可以直接自定义Edittext,通过drawableRight添加一个清除按钮的图标,通过点击图标来实现清除输入框内容。
首先我们来实现自定义的Edittext:
public class ClearEdittext extends AppCompatEditText{
private Drawable mClearTextIcon;
public ClearEdittext(Context context) {
super(context);
init(context);
}
public ClearEdittext(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ClearEdittext(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
final Drawable drawable = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_mtrl_alpha);
final Drawable wrappedDrawable = DrawableCompat.wrap(drawable); //Wrap the drawable so that it can be tinted pre Lollipop
// Drawable 着色的后向兼容
DrawableCompat.setTint(wrappedDrawable, getCurrentHintTextColor());
mClearTextIcon = wrappedDrawable;
/**讲清除按钮的图标,放在最右边*/
mClearTextIcon.setBounds(0, 0, mClearTextIcon.getIntrinsicHeight(), mClearTextIcon.getIntrinsicHeight());
}
}
}
1、首先呢我们得监听输入框文字的变化,没有内容的时候,我们让清除图标隐藏,当用户输入内容时,让清除按钮显示,我们让这个类实现一个TextWatcher接口。
/**
* 动态改变清除按钮的显示和隐藏
*/
private void setClearIconVisible(final boolean visible) {
mClearTextIcon.setVisible(visible, false);
final Drawable[] compoundDrawables = getCompoundDrawables();
setCompoundDrawables(
compoundDrawables[0],
compoundDrawables[1],
visible ? mClearTextIcon : null,
compoundDrawables[3]);
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
/**在这里是否获取到焦点*/
if (isFocused()) {
/**判断是否有输入的内容,然后动态设置清除按钮的显示和隐藏*/
setClearIconVisible(charSequence.length()>0);
}
}
@Override
public void afterTextChanged(Editable editable) {
}
2、在清除按钮显示的时候,将输入框的内容置空。这里我们需要实现一个OnTouchListener接口。
@Override
public void setOnTouchListener(OnTouchListener l) {
this.mOnTouchListener=l;
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
/**获取到点击位置的X轴坐标*/
int x = (int) motionEvent.getX();
/**判断清除按钮是否显示,并且点击的位置在清除按钮上*/
if (mClearTextIcon.isVisible() && x > getWidth() - getPaddingRight() - mClearTextIcon.getIntrinsicWidth()) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
setText("");
}
}
return false;
}
3、焦点变换的,对清除按钮显示或隐藏的处理,实现OnFocusChangeListener接口
@Override
public void setOnFocusChangeListener(OnFocusChangeListener l) {
mOnFocusChangeListener=l;
}
@Override
public void onFocusChange(View view, boolean b) {
/**如果失去了焦点,重新设置清除按钮的隐藏和显示*/
if (b){
setClearIconVisible(getText().length()>0);
}else{
setClearIconVisible(false);
}
if (mOnFocusChangeListener!=null){
mOnFocusChangeListener.onFocusChange(view,b);
}
}
完整代码:
public class ClearEdittext extends AppCompatEditText implements View.OnFocusChangeListener, View.OnTouchListener, TextWatcher {
private Drawable mClearTextIcon;
private OnFocusChangeListener mOnFocusChangeListener;
private OnTouchListener mOnTouchListener;
public ClearEdittext(Context context) {
super(context);
init(context);
}
public ClearEdittext(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ClearEdittext(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
final Drawable drawable = ContextCompat.getDrawable(context, R.drawable.abc_ic_clear_mtrl_alpha);
final Drawable wrappedDrawable = DrawableCompat.wrap(drawable); //Wrap the drawable so that it can be tinted pre Lollipop
// Drawable 着色的后向兼容
DrawableCompat.setTint(wrappedDrawable, getCurrentHintTextColor());
mClearTextIcon = wrappedDrawable;
/**讲清除按钮的图标,放在最右边*/
mClearTextIcon.setBounds(0, 0, mClearTextIcon.getIntrinsicHeight(), mClearTextIcon.getIntrinsicHeight());
setClearIconVisible(false);
super.setOnTouchListener(this);
super.setOnFocusChangeListener(this);
addTextChangedListener(this);
}
/**
* 动态改变清除按钮的显示和隐藏
*/
private void setClearIconVisible(final boolean visible) {
mClearTextIcon.setVisible(visible, false);
final Drawable[] compoundDrawables = getCompoundDrawables();
setCompoundDrawables(
compoundDrawables[0],
compoundDrawables[1],
visible ? mClearTextIcon : null,
compoundDrawables[3]);
}
@Override
public void setOnFocusChangeListener(OnFocusChangeListener l) {
mOnFocusChangeListener=l;
}
@Override
public void onFocusChange(View view, boolean b) {
/**如果失去了焦点,重新设置清除按钮的隐藏和显示*/
if (b){
setClearIconVisible(getText().length()>0);
}else{
setClearIconVisible(false);
}
if (mOnFocusChangeListener!=null){
mOnFocusChangeListener.onFocusChange(view,b);
}
}
@Override
public void setOnTouchListener(OnTouchListener l) {
this.mOnTouchListener=l;
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
/**获取到点击位置的X轴坐标*/
int x = (int) motionEvent.getX();
/**判断清除按钮是否显示,并且点击的位置在清除按钮上*/
if (mClearTextIcon.isVisible() && x > getWidth() - getPaddingRight() - mClearTextIcon.getIntrinsicWidth()) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
setText("");
}
}
return false;
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if (isFocused()) {
setClearIconVisible(charSequence.length()>0);
}
}
@Override
public void afterTextChanged(Editable editable) {
}
}
显示:首先未输入时状态
点击清除按钮,输入框内容置空
(2)通过组合来实现新的控件
一般在项目中,我们每个activity都会有一个很类似的标题栏。通常有:左边一个返回按钮、中间一个标题、右边一个不确定有木有的按钮。我们来做一个标题栏的模板。
Step1:在项目的res资源目录的values目录下创建一个attrs.xml属性定义文件,并且在该文件中定义响应的属性即可。
这里说一下:比如说字体的大小,我们指定为format=”dimension”,颜色指定为:format=”color”
背景可以为图片、颜色的可以指定为:format=”color|reference”
<declare-styleable name="baseActionbar">
<attr name="title_text" format="string"></attr>
<attr name="title_text_size" format="dimension"></attr>
<attr name="title_text_color" format="color"></attr>
<attr name="btn_back" format="reference"></attr>
<attr name="btn_pad_left" format="dimension"></attr>
<attr name="btn_pad_right" format="dimension"></attr>
<attr name="tv_right" format="string"></attr>
<attr name="tv_right_size" format="dimension"></attr>
<attr name="tv_right_color" format="color"></attr>
<attr name="tv_pad_left" format="dimension"></attr>
<attr name="tv_pad_right" format="dimension"></attr>
</declare-styleable>
Step2:新建一个MyActionBar类继承自ViewGroup,在这里我们直接继承自RelativeLayout。
首先我们再控件的构造方法中,对自定义属性进行初始化。系统提供了TypeArray对象获取XML文件中的自定义属性。
TypedArray ta=context.obtainStyledAttributes(set, R.styleable.baseActionbar);
第一个参数为AttributeSet(由构造方法传入)
public class MyActionBar extends RelativeLayout{
private ImageView leftImageView;
private LayoutParams left_Params;
private TextView centerTextView;
private LayoutParams centerParams;
private TextView rightTextView;
private LayoutParams rightParams;
private Context context;
private AttributeSet set;
String title_text;
int title_text_size;
int title_text_color;
BitmapDrawable btn_back;
int btn_pad_left;
int btn_pad_right;
String tv_right;
int tv_right_size;
int tv_right_color;
int tv_pad_left;
int tv_pad_right;
public MyActionBar(Context context) {
this(context,null);
}
public MyActionBar(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyActionBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context=context;
this.set=attrs;
init();
}
private void init() {
TypedArray ta=context.obtainStyledAttributes(set, R.styleable.baseActionbar);
title_text=ta.getString(R.styleable.baseActionbar_title_text);
title_text_color=ta.getColor(R.styleable.baseActionbar_title_text_color,0);
title_text_size= (int) ta.getDimension(R.styleable.baseActionbar_title_text_size,20);
btn_back= (BitmapDrawable) ta.getDrawable(R.styleable.baseActionbar_btn_back);
btn_pad_left= (int) ta.getDimension(R.styleable.baseActionbar_btn_pad_left,10);
btn_pad_right= (int) ta.getDimension(R.styleable.baseActionbar_btn_pad_right,10);
tv_right=ta.getString(R.styleable.baseActionbar_tv_right);
tv_right_size= (int) ta.getDimension(R.styleable.baseActionbar_tv_right_size,16);
tv_right_color=ta.getColor(R.styleable.baseActionbar_tv_right_color,0);
tv_pad_left= (int) ta.getDimension(R.styleable.baseActionbar_tv_pad_left,10);
tv_pad_right= (int) ta.getDimension(R.styleable.baseActionbar_tv_pad_right,10);
/**内存回收*/
ta.recycle();
}
Step3:接下来,我们可以开始组合控件,首先是左边的ImageView返回键,然后是中间的文本框、还有右边的按钮。
leftImageView=new ImageView(context);
centerTextView=new TextView(context);
rightTextView=new TextView(context);
leftImageView.setImageDrawable(btn_back);
leftImageView.setPadding(btn_pad_left,0,btn_pad_right,0);
/**设置返回按钮的控件大小*/
left_Params=new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
/**设置这个返回按钮,在父控件中的位置*/
left_Params.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
/**将控件加入到当前控件中*/
addView(leftImageView,left_Params);
centerTextView.setText(title_text);
centerTextView.setTextSize(title_text_size);
centerTextView.setTextColor(title_text_color);
centerParams=new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
centerParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);
addView(centerTextView,centerParams);
rightTextView.setText(tv_right);
rightTextView.setTextColor(tv_right_color);
rightTextView.setTextSize(tv_right_size);
rightTextView.setPadding(tv_pad_left,0,tv_pad_right,0);
rightTextView.setGravity(Gravity.CENTER);
rightParams=new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
addView(rightTextView,rightParams);
Step4:左边按钮返回键和右边按钮的点击事件;我们来定义一个接口,然后再给当前控件设置此接口监听。
/**定义一个左边返回键和右边textview点击事件的接口*/
public interface topBarClickListener{
/**左边按钮点击*/
void backClick();
/**右边按钮点击*/
void rightClick();
}
暴露接口给调用者:
leftImageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.backClick();
}
});
rightTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.rightClick();
}
});
/**添加点击的监听事件*/
public void setListener(topBarClickListener listener) {
this.listener = listener;
}
暴露一个方法给调用者来注册接口回调,通过接口来获得会叼着对接口方法的实现:
public void setListener(topBarClickListener listener) {
this.listener = listener;
}
在activity界面中实现接口回调:
myToolbar.setListener(new MyActionBar.topBarClickListener() {
@Override
public void backClick() {
Toast.makeText(MainActivity.this,"点击了返回键",Toast.LENGTH_SHORT).show();
}
@Override
public void rightClick() {
Toast.makeText(MainActivity.this,"点击了右边按钮",Toast.LENGTH_SHORT).show();
}
});
Step5:这里呢,有个问题,有的界面可能不需要显示这个左边的返回按钮,也可能不显示右边的。这个时候,我们就要对这个两个控件进行显示和隐藏控制。
public void setButtonVisable(int id,boolean flag){
/**为true则是显示,然后再根据id判断具体是返回按钮还是右边的按钮*/
if (flag) {
if (id==0){
leftImageView.setVisibility(View.VISIBLE);
}else {
rightTextView.setVisibility(View.VISIBLE);
}
}else {
if (id==0){
leftImageView.setVisibility(View.GONE);
}else {
rightTextView.setVisibility(View.GONE);
}
}
}
动态设置显示和隐藏:如下代码
//设置左边按钮显示
myToolbar.setButtonVisable(0,true);
//设置右边按钮不显示
myToolbar.setButtonVisable(1,false);
完整代码:
public class MyActionBar extends RelativeLayout{
private ImageView leftImageView;
private LayoutParams left_Params;
private TextView centerTextView;
private LayoutParams centerParams;
private TextView rightTextView;
private LayoutParams rightParams;
private Context context;
private AttributeSet set;
String title_text;
int title_text_size;
int title_text_color;
BitmapDrawable btn_back;
int btn_pad_left;
int btn_pad_right;
String tv_right;
int tv_right_size;
int tv_right_color;
int tv_pad_left;
int tv_pad_right;
private topBarClickListener listener;
public MyActionBar(Context context) {
this(context,null);
}
public MyActionBar(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyActionBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context=context;
this.set=attrs;
init();
}
private void init() {
TypedArray ta=context.obtainStyledAttributes(set, R.styleable.baseActionbar);
title_text=ta.getString(R.styleable.baseActionbar_title_text);
title_text_color=ta.getColor(R.styleable.baseActionbar_title_text_color,0);
title_text_size= (int) ta.getDimension(R.styleable.baseActionbar_title_text_size,20);
btn_back= (BitmapDrawable) ta.getDrawable(R.styleable.baseActionbar_btn_back);
btn_pad_left= (int) ta.getDimension(R.styleable.baseActionbar_btn_pad_left,10);
btn_pad_right= (int) ta.getDimension(R.styleable.baseActionbar_btn_pad_right,10);
tv_right=ta.getString(R.styleable.baseActionbar_tv_right);
tv_right_size= (int) ta.getDimension(R.styleable.baseActionbar_tv_right_size,16);
tv_right_color=ta.getColor(R.styleable.baseActionbar_tv_right_color,0);
tv_pad_left= (int) ta.getDimension(R.styleable.baseActionbar_tv_pad_left,10);
tv_pad_right= (int) ta.getDimension(R.styleable.baseActionbar_tv_pad_right,10);
/**内存回收*/
ta.recycle();
leftImageView=new ImageView(context);
centerTextView=new TextView(context);
rightTextView=new TextView(context);
leftImageView.setImageDrawable(btn_back);
leftImageView.setPadding(btn_pad_left,0,btn_pad_right,0);
/**设置返回按钮的控件大小*/
left_Params=new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
/**设置这个返回按钮,在父控件中的位置*/
left_Params.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);
/**将控件加入到当前控件中*/
addView(leftImageView,left_Params);
centerTextView.setText(title_text);
centerTextView.setTextSize(title_text_size);
centerTextView.setTextColor(title_text_color);
centerParams=new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
centerParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);
addView(centerTextView,centerParams);
rightTextView.setText(tv_right);
rightTextView.setTextColor(tv_right_color);
rightTextView.setTextSize(tv_right_size);
rightTextView.setPadding(tv_pad_left,0,tv_pad_right,0);
rightTextView.setGravity(Gravity.CENTER);
rightParams=new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);
addView(rightTextView,rightParams);
leftImageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.backClick();
}
});
rightTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
listener.rightClick();
}
});
}
/**添加点击的监听事件*/
public void setListener(topBarClickListener listener) {
this.listener = listener;
}
/**定义一个左边返回键和右边textview点击事件的接口*/
public interface topBarClickListener{
void backClick();
void rightClick();
}
public void setButtonVisable(int id,boolean flag){
if (flag) {
if (id==0){
leftImageView.setVisibility(View.VISIBLE);
}else {
rightTextView.setVisibility(View.VISIBLE);
}
}else {
if (id==0){
leftImageView.setVisibility(View.GONE);
}else {
rightTextView.setVisibility(View.GONE);
}
}
}
}
布局代码:需要注意的是自定义命名空间:xmlns:custom=”http://schemas.android.com/apk/res-auto”
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="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="vertical"
tools:context="com.example.administrator.tansuo3_viewscrolled_error.MainActivity">
<com.example.administrator.tansuo3_viewscrolled_error.view.MyActionBar
android:id="@+id/my_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorAccent"
custom:btn_back="@mipmap/ic_launcher"
custom:btn_pad_left="10dp"
custom:btn_pad_right="10dp"
custom:title_text="我是标题"
custom:title_text_color="#ffffff"
custom:title_text_size="14sp"
custom:tv_pad_left="10dp"
custom:tv_pad_right="10dp"
custom:tv_right="选择"
custom:tv_right_color="#ffffff"
custom:tv_right_size="12sp"
></com.example.administrator.tansuo3_viewscrolled_error.view.MyActionBar>
</LinearLayout>
最后呢,我们也可以把此控件进行封装成为一个UI模板。在其他布局中通过标签进行引用。
(3)重写View来实现新的控件
当现有控件无法满足我们的需求时,我们需要完全自定义View来实现我们的功能。通常需要继承View类,并重写它的onDraw()、onMeasure()等方法来实现绘制逻辑,同时重写onTouchEvent()等触控事件来实现交互逻辑。
系统提供了Canvas作为绘制图形的直接对象,有以下几个非常有用的方法:
Canvas.save():将之前所有绘制的图像保存起来。
Canvas.restore():可以理解为PhotoShop中的合并图层操作,作用是在save之后绘制的所有图像与save()之前额图形进行合并。
Canvas.translate():绘制坐标进行平移。
Canvas.totate():绘制的坐标旋转一定的角度。
我们来实现一个仪表盘:
我们先来分析一下,将图形进行分解
1.仪表盘–外面的大圆
2.刻度线–包含四个长的刻度线和其他短的的刻度线。
3.刻度值–包含长刻度线对应的大的刻度值和其他小的刻度值。
4.指针–中间的两个指针。
Step1:绘制外面的大圆
outPaint=new Paint();
outPaint.setStyle(Paint.Style.STROKE);
outPaint.setColor(Color.RED);
outPaint.setAntiAlias(true);
outPaint.setStrokeWidth(5);
canvas.drawCircle(getWidth()/2,getHeight()/2, outRadios,outPaint);
Step2:然后是这个刻度值,需要进行计算,我们首先画出来一条线,然后通过旋转画布来分别画每一条刻度值Canvas.totate()。
/**画刻度*/
keduLinePaint=new Paint();
for (int i = 0; i <24 ; i++) {
//区分整点和非整点
if (i==6||i==12||i==18||i==0){
keduLinePaint.setStrokeWidth(5);
keduLinePaint.setTextSize(36);
canvas.drawLine(getWidth()/2,getHeight()/2-outRadios,getWidth()/2,getHeight()/2-outRadios+60,keduLinePaint);
String degree=String.valueOf(i);
canvas.drawText(degree,getWidth()/2-keduLinePaint.measureText(degree)/2,getHeight()/2-outRadios+90,keduLinePaint);
}else {
keduLinePaint.setStrokeWidth(3);
keduLinePaint.setTextSize(24);
canvas.drawLine(getWidth()/2,getHeight()/2-outRadios,getWidth()/2,getHeight()/2-outRadios+30,keduLinePaint);
String degree=String.valueOf(i);
canvas.drawText(degree,getWidth()/2-keduLinePaint.measureText(degree)/2,getHeight()/2-outRadios+60,keduLinePaint);
}
//通过旋转画布简化坐标运算
/**第一个参数为,每次旋转的角度*/
canvas.rotate(15,getWidth()/2,getHeight()/2 );
}
Step3:然后画里面的两个指针
longPointPaint=new Paint();
longPointPaint.setStrokeWidth(10);
shortPointPaint=new Paint();
shortPointPaint.setStrokeWidth(20);
canvas.save();//保存画出来的图像
canvas.translate(getWidth()/2,getHeight()/2);
canvas.drawLine(0,0,100,100,shortPointPaint);
canvas.drawLine(0,0,100,200,longPointPaint);
canvas.restore();//然后进行合并
显示结果:
这里只是进行简单的自定义View,还有待深入。