跟着徐宜生学Android——

[TOC]

Android 控件架构与自定义控件详解

Android控价架构

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

为什么要自定义控件:

特定的显示风格
处理特有的用户交互
优化我们的布局
封装等...

如何自定义控件

自定义属性的声明
测量onMeasure
布局onLayout(ViewGroup)
绘制onDraw
onTouchEvent
onInterceptTouchEvent(ViewGroup)

View的测量——onMeasure

如果该Vewi还需要使用wrap_content属性,那么还需要重写onMeasure()方法。

EXACTLY、AT_MOST、UMSPECIFIED
MeasureSpec类
setMeasuerdDimension()
requestLayout()

|Mode|描述
|-------------|
| EXACTLY | 精确模式,控件布局中,指定为100dp或者match_parent
| AT_MOST | 最大值模式,不超过父控件的最大尺寸,需要重写onMeasure()方法,Math.min(result,specSize),指定为wrap_content
| UNSPECIFIED | View有多大就多大,自定义View绘制使用

源代码中,在重写onMeasure()方法后,就是把测量后的宽高作为参数设置给setMeasuredDimension().

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

源代码

/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
//从提供的测量规范中提取模式。
 
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
//绘制的大小

测量模板代码

    public void onMeasure(int widthMeasureSpec,int heightMeasuureSpec){
        setMeasureDimension(measureWidth(widthMeasureSpec),measureHeight(heightMeasureSpec));
}

private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

if (specMode == MeasureSpec.EXACTLY){
result = specSize;
}else {
result = 400;
if (specMode == MeasureSpec.AT_MOST){
result = Math.min(result,specSize);
}
}
return result;
}

布局onLayout(VIewGroup)

决定子View的位置
尽可能将onMeasure中一些操作移动到此方法中
requestLayout()

绘制onDraw

绘制内容区域
invaliudate(),postInvalidate()invalidate(),主线程
postInvalidate(),子线程

Canvas.drawXXX
translate、rotate、scale、skew
save() 、restore()

Canvas就像是一个画板,使用Paint就可以在上面作画。
Canvas canvas= new Canvas(bitmap);穿进去的bitmap与通过这个bitmap创建的Canvas画布是紧紧联系在一起的,这个过程我们称之为装载画布。这个bitmap用来存储所有绘制在Canvas上的像素信息

4d84a1c8-493f-4360-87dc-89a85df12435.jpg

事件拦截机制分析

Android为触摸事件封装了一个类 ——MotionEvent,本质就是动作类型加坐标。当同一个事件,子View和父ViewGroup都有可能想要处理的时候就产生了“事件拦截”。整个流程就是事件的分发、拦截、处理。

对ViewGroup来说,会重写三个方法

public boolean dispatchTouchEvent(MotionEvent ev)//一般不重写这个方法
public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)

而对于View来说,只重写两个方法

public boolean dispatchTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)

intercept 拦截 v. 拦截者 n.
dispatch 派遣,调度,分发 v. 新闻报道n.

事件的传递顺序是最外层的父ViewGroup一层一层的派遣然后决定是否拦截任务;
事件的处理顺序是最里层的子View依次递交这个onTouchEvent()的处理结果直至任务完成。
初始情况下,返回值都是false。

parent.requestDisallow-InterceptTouchEvent(true);告诉父控件不要拦截我的手势

38d15620-9444-4c4b-bd5e-bf3ef70b6a30.jpg
d1a7a5ac-f9b8-42ef-87b4-312dbbc85012.jpg

自定义View的方式:

对现有控件进行扩展

public class AmazingTV extends TextView {
    private Paint mPaint1;
    private Paint mPaint2;
    public AmazingTV(Context context,AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        initView();
        //绘制外层
        canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint1);
        //绘制内层
        canvas.drawRect(5,5,getMeasuredWidth()-5,getMeasuredHeight()-5,mPaint2);
        canvas.save();//save()方法的目的是什么?
        canvas.translate(10,0);//绘制文字前平移10像素
//在回调父类方法前,实现自己的逻辑,对TextView来说即是在绘制文本内容前
        super.onDraw(canvas);
        canvas.restore();
    }

    private void initView() {
        mPaint1 = new Paint();
        mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_bright));
        mPaint1.setStyle(Paint.Style.FILL);
        mPaint2 = new Paint();
        mPaint2.setColor(Color.YELLOW);
        mPaint2.setStyle(Paint.Style.FILL);
    }
}

动态的闪光灯效果:

6e8d79c9-20da-4ce1-91ff-04cf9ba18a06.png
26a842a4-0df7-43d3-b23d-857741672044.jpg

其中最关键的就是getPaint()方法中获取当前Textview的paint对象,并且设置LinearGradient属性,最后用矩阵不断平移渐变效果,就实现了这个效果。

public class CustomTextView extends TextView {
    int mViewWidth = 0;
    private Paint mPaint;
    private LinearGradient mLinearGradient;
    private Matrix matrix;
    private int mTranslate;

    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @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();
                //Shader渲染器来设置一个不断变化的LinearGradient
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
                        null, Shader.TileMode.CLAMP);
                mPaint.setShader(mLinearGradient);
                //矩阵
                matrix = new Matrix();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (matrix != null) {
            mTranslate += mViewWidth + 5;
            if (mTranslate > 2 * mViewWidth / 5) {
                mTranslate = -mViewWidth;
            }
            matrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(matrix);
            //每隔100毫秒闪动一下
            postInvalidateDelayed(100);
        }
    }

}

通过组合来显示新的控件

设计需要的属性(res/value/attrs.xml)
实现一个我们的View(继承View并实现带属性参数的构造方法)
引用我们的View(xml导包并添加xmlns)

设计UI模板的时候,还可以给TopBar增加相应的接口,让调用者能够更加灵活地控制TopBar,修改UI时能够快速修改。

<resources><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="reference|color"/><attr name="rightText" format="string"/></declare-styleable></resources>
<!--<declare-styleable>标签嵌套<attr>--><!--format 属性的类型(类似数据类型) dimension 文字大小属性 color 颜色属性 reference 引用属性-->
public class TopBar extends RelativeLayout {private String title;private int titleTextSize;private int titleTextColor;private int leftTextColor;private Drawable leftBackground;private String leftText;private int rightTextColor;private Drawable rightBackground;private String rightText;private Button leftButton;private Button rightButton;private TextView tvTitle;private LayoutParams leftParams,rightParams,titleParams;private topbarClickListener listener;public interface topbarClickListener{public void leftClick();public void rightClick();}
   //定义接口
public void setOntopbarClikListener(topbarClickListener listener){this.listener = listener;}public TopBar(Context context){super(context);}public TopBar(Context context, AttributeSet attrs) {super(context, attrs);TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.Toolbar);
     //TypedArray 存放键值对,通过下划线链接一个新的名字leftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor,0);leftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);leftText = ta.getString(R.styleable.TopBar_leftText);rightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor,0);rightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);rightText = ta.getString(R.styleable.TopBar_rightText);title = ta.getString(R.styleable.TopBar_title);titleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor,0);titleTextSize = ta.getInt(R.styleable.TopBar_titleTextSize,0);ta.recycle();//防止资源浪费或者缓存引起的错误 //组合模式,把几个存在的建立为一个新的控件 leftButton = new Button(context);rightButton = new Button(context);tvTitle = new TextView(context);leftButton.setBackground(leftBackground);leftButton.setTextColor(leftTextColor);leftButton.setText(leftText);rightButton.setTextColor(rightTextColor);rightButton.setBackground(rightBackground);rightButton.setText(rightText);tvTitle.setText(title);tvTitle.setTextColor(titleTextColor);tvTitle.setTextSize(titleTextSize);tvTitle.setGravity(Gravity.CENTER);setBackgroundColor(0xfff59563);leftParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);leftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE);addView(leftButton,leftParams);rightParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);rightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE);addView(rightButton,rightParams);titleParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);//宽和高 titleParams.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE);//添加规则,是根据相对布局的方式 addView(tvTitle,titleParams);//加载布局的方式
    //暴露接口给调用者 leftButton.setOnClickListener(new OnClickListener() {@Override public void onClick(View v) {listener.leftClick();}});rightButton.setOnClickListener(new OnClickListener() {@Override public void onClick(View v) {listener.rightClick();}});}}

Activity中调用的时候实现接口回调

TopBar topbar = (TopBar) findViewById(R.id.topBar);topbar.setOntopbarClikListener(new TopBar.topbarClickListener() {@Override public void leftClick() {Toast.makeText(MainActivity.this,"left",Toast.LENGTH_SHORT).show();}@Override public void rightClick() {Toast.makeText(MainActivity.this,"left",Toast.LENGTH_SHORT).show();}});

引用UI模板

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"><example.com.view3.TopBar android:id="@+id/topBar" android:layout_width="match_parent" android:layout_height="40dp" custom:leftBackground="#16c7ca" custom:leftText="Back" custom:leftTextColor="#FFFFFF" custom:rightBackground="#16c7ca" custom:rightText="More" custom:rightTextColor="#FFFFFF" custom:title="@string/title" custom:titleTextColor="#123412" custom:titleTextSize="10sp"></example.com.view3.TopBar></RelativeLayout>

重写View来实现全新的控件

这里画一个半弧圆,分解图形,当脑海中有了一副设计图之后,剩下的事情就只是对坐标的计算了。

public class CircleView extends View {//圆的长度 private int mCircleXY;//屏幕高宽 private int w, h;//圆的半径 private float mRadius;//圆的画笔 private Paint mCirclePaint;//弧线的画笔 private Paint mArcPaint;//文本画笔 private Paint mTextPaint;//需要显示的文字 private String mShowText = "半弧圆";//文字大小 private int mTextSize = 50;//圆心扫描的弧度 private int mSweepAngle = 270;public CircleView(Context context, AttributeSet attrs) {super(context, attrs);//获取屏幕高宽 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);w = wm.getDefaultDisplay().getWidth();h = wm.getDefaultDisplay().getHeight();init();}private void init() {mCircleXY = w / 2;mRadius = (float) (w * 0.5 / 2);mCirclePaint = new Paint();mCirclePaint.setColor(Color.BLUE);mArcPaint = new Paint();//设置线宽 mArcPaint.setStrokeWidth(100);//设置空心 mArcPaint.setStyle(Paint.Style.STROKE);//设置颜色 mArcPaint.setColor(Color.BLUE);mTextPaint = new Paint();mTextPaint.setColor(Color.WHITE);mTextPaint.setTextSize(mTextSize);}@Override protected void onDraw(Canvas canvas) {super.onDraw(canvas);//绘制矩形(左,上,右,下) RectF mArcRectF = new RectF((float) (w * 0.1), (float) (w * 0.1), (float) (w * 0.9), (float) (w * 0.9));//绘制圆,(圆心x,圆心y,半径,画笔) canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);//绘制弧线(圆弧的外轮廓矩形区域,起始角度,圆弧扫过的角度,圆心包括与否,画笔)
    //正右边为0°正上角为270°,第四个参数是是否和圆心有连线 canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);//绘制文本(文字,开始,结束,x,y,画笔) canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY - (mTextSize / 4), mCircleXY + (mTextSize / 4), mTextPaint);}//设置一些其他的状态,设置他的弧度,我们写一个对外的方法 public void setSweepValues(float sweepValues){if(sweepValues !=- 0){mSweepAngle = (int) sweepValues;}else{//如果没有,我们默认设置 mSweepAngle = 30;}//记得刷新 invalidate();}}

sweep 打扫,掠过 v.

d72d8313-90b3-42be-a08c-1d63a9e628c4.png

canvas.drawText文字居中

cd40177e-a1cd-47c7-ad0d-a387c6ce56bd.png

Android是基于基线(Baseline),top+bottom是字体高度

de7c2185-3eaa-4685-bbde-d38e55b57f9f.png

绘制的时候取决于基线,默认为left,但是可以通过paint.setTextAlign(Paint.Align.CENTER);设置

e69cb58b-440e-4845-b1e1-7e116aff4f3d.png

首先找到矩形的中心,已知矩形中心根据top和bottom可以算出Baseline的y位置,这样文字的位置就确定了。

int baseLineY = (int) (rect.centerY() - top/2 - bottom/2);//基线中间点的y轴计算公式
canvas.drawText("你好世界",rect.centerX(),baseLineY,textPaint);

这里有点要注意textPaint.getFontMetrics()这个方法一定要在设置字体大小或者样式等等一系列会影响字体的方法后在调用,不然获取到的top和bottom值不准.

Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();float top = fontMetrics.top;//为基线到字体上边框的距离,即上图中的topfloat bottom = fontMetrics.bottom;//为基线到字体下边框的距离,即上图中的bottom

待: onTouchEvent

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,400评论 25 707
  • 6、View的绘制 (1)当测量好一个View之后,我们就可以简单的重写 onDraw()方法,并在 Canvas...
    b5e7a6386c84阅读 1,875评论 0 3
  • 贵州赤水 · 赤水丹霞大瀑布 赤水桫椤自然保护区 赤水桫椤国家级自然保护区位于贵州省赤水中部葫市镇...
    崔建强阅读 500评论 0 1
  • 【昨日感悟】 好领导是什么样的? 工作中能把任务划分到最小可执行步骤生活中多帮助员工解决问题,不推卸责任能给员工谋...
    爱游泳的Danny阅读 225评论 0 1
  • 今天听到老大说老妈已确诊癌症三期,心里怕极了,发了信息给子君,她说别慌,我无法心安,无法不慌,内心惶恐不安,忍不住...
    冲破屋顶的呐喊阅读 413评论 0 0