android开发艺术探索(四)

来源:互联网 发布:读书思考 知乎 编辑:程序博客网 时间:2024/04/29 00:05

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,还有待深入。

0 0
原创粉丝点击