Android开发之漫漫长途 番外篇——自定义View的各种姿势1
来源:互联网 发布:安装linux内核源代码 编辑:程序博客网 时间:2024/05/17 00:14
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索》以及《深入理解Android 卷Ⅰ,Ⅱ,Ⅲ》中的相关知识,另外也借鉴了其他的优质博客,在此向各位大神表示感谢,膜拜!!!另外,本系列文章知识可能需要有一定Android开发基础和项目经验的同学才能更好理解,也就是说该系列文章面向的是Android中高级开发工程师。
第六篇了,,这一篇我们来看自定义View的各种姿势。前面几篇文章中我们介绍了Acitivity的启动流程以及生命周期,还介绍了Activity显示的各种原理。那么这篇文章呢,我们来实战一下。(读者可能看了好多关于Android Activity相关的知识,也看了View的实现原理。可是对于自定义View还是感觉隔着一层膜,那么今天我们试着捅破这层隔膜。)
上篇文章中我们详细讲解了ViewRootImpl,我们知道了其5大过程,知道了View的测量、布局以及绘制。那么这些知识对我们有何用处呢。下面我们就来自定义View
Android本身的控件系统可以实现我们开发中的一些基本需求,可是我们在处理实际业务的时候却催生出了Android控件系统不能很好的需求。这时,自定义控件应运而生。
在进行自定义View之前我们先来看一下View的坐标系。
上图引自刘望舒大神的博客
第1种自定义View的姿势——直接继承自View,重写其onDraw方法
直接继承自View,重写其onDraw方法,这个方式主要用来实现一些不规则的效果。比如显示一个圆。需要注意的是直接继承自View的控件需要对支持wrap_content和padding做处理。所以本例中也重写了onMeasure方法。以及在onDraw方法中加入了自身padding的处理。读者可试着去除onMeasure方法或者onDraw方法中的对padding的处理看看效果
自定义的属性xml
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="CircleView"> <attr name="circle_color" format="color" /> </declare-styleable></resources>
自定义CircleView
import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.util.AttributeSet;import android.view.View;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) { this(context, attrs, 0); } public CircleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); a.recycle(); init(); } private void init() { mPaint.setColor(mColor); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, 200); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(200, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, 200); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); int width = getWidth() - paddingLeft - paddingRight; int height = getHeight() - paddingTop - paddingBottom; int radius = Math.min(width, height) / 2; canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint); }}
使用自定义CircleView
布局文件activity_main1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical" > <com.mafeibiao.testapplication.CircleView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="20dp" android:background="@color/light_green"/></LinearLayout>
MainActivity.java
package com.mafeibiao.testapplication;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main1); }}
效果如下图
注:我们在这里直接继承了View并重写其onMeasure和onDraw方法,我们从上几篇文章详细分析了Activity的创建以及显示。我们在梳理一下,首先程序的入口函数是ActivityThread.main函数,从这个函数开始,然后回调我们MainActivity的attach函数,我们在这里没有重写这个函数,但是该函数内部会创建一个至关重要的对象PhoneWindow,然后会回调我们MainActivity的onCreate函数,我们在MainActivity的onCreate函数中调用了setContentView(R.layout.activity_main1);这个函数内部会创建Android 的顶级View DecorView,把我们的布局文件R.layout.activity_main1解析成相关View并关联到DecorView下。然后会调用WindowManager的addView方法把DecorView添加到PhoneWindow上,实际上完成这个过程的是ViewRootImpl,它会对我们的DecorView依次进测量、布局、绘制等工作,在这些工作的过程中会依次回调我们在View以及其子类中重写的onMeasure、onLayout、onDraw等方法。以我们上面的CircleView为例,,我们在布局文件中定义了一个LinearLayout并在LinearLayout内使用了我们自定义的CircleView,那么按照上一章讲解ViewRootImpl的工作流程。会沿着控件树从上到下依次调用到我们自定义的CircleView onMeasure(我们重写了该方法)然后沿着控件树从下向上依次回调。上文也讲过,测量过程是后根遍历,布局过程是先根遍历。(要理解Android View的层级结构是树结构)
第2种自定义View的姿势——直接继承自Android中控件View,如TextView或者EditText等。
下面我们来实现渐变的TextView。这个我们效果我们经常在锁屏应用上看到。
import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.LinearGradient;import android.graphics.Matrix;import android.graphics.Paint;import android.graphics.Shader;import android.support.v7.widget.AppCompatTextView;import android.util.AttributeSet;import android.util.Log;public class CustomTextView extends AppCompatTextView { private final static String TAG = CustomTextView.class.getSimpleName(); private Paint paint1; private Paint paint2; private int mWidth; private LinearGradient gradient; private Matrix matrix; //渐变的速度 private int deltaX; public CustomTextView(Context context) { super(context, null); } public CustomTextView(Context context, AttributeSet attrs) { super(context, attrs); initView(context, attrs); } private void initView(Context context, AttributeSet attrs) { paint1 = new Paint(); paint1.setColor(getResources().getColor(android.R.color.holo_blue_dark)); paint1.setStyle(Paint.Style.FILL); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if(mWidth == 0){ Log.e(TAG,"*********************"); mWidth = getMeasuredWidth(); paint2 = getPaint(); //颜色渐变器 gradient = new LinearGradient(0, 0, mWidth, 0, new int[]{Color.GRAY,Color.WHITE,Color.GRAY}, new float[]{ 0.3f,0.5f,1.0f }, Shader.TileMode.CLAMP); paint2.setShader(gradient); matrix = new Matrix(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(matrix !=null){ deltaX += mWidth / 5; if(deltaX > 2 * mWidth){ deltaX = -mWidth; } } //关键代码通过矩阵的平移实现 matrix.setTranslate(deltaX, 0); gradient.setLocalMatrix(matrix); postInvalidateDelayed(100); }}
下面我们来实现支付宝上手机号和银行卡号写入分段的效果。
如上图,在作为手机号或者银行卡时输入的数字会按照不同规则分段,并且右侧出现清空按钮。很明显,我们需要自定义一个控件符合上述要求。
style格式attrs.xml
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="MyEditText"> <!-- 设置自定义输入框的模式 当值为1:普通输入框模式 2:银行卡号输入框模式 3:电话号码模式 默认为1--> <attr name="editTextMode" format="integer" /> <!-- 配置自定义控件在银行卡号模式下分隔位数 ,默认为4位 --> <attr name="splitNumber" format="integer" /> </declare-styleable></resources>
布局代码activity_main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffff" android:orientation="vertical" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="5dp" android:layout_marginLeft="5dp" android:layout_marginRight="5dp" android:layout_marginTop="20dp" android:text="请绑定持卡人本人的银行卡" /> <LinearLayout android:id="@+id/ll_name" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="1dp" android:background="#ffffff" android:gravity="center_vertical" android:orientation="horizontal" android:paddingBottom="5dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:paddingTop="5dp" > <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="left" android:text="持卡人" /> <com.mafeibiao.testapplication.MyEditText android:id="@+id/et_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="5" android:hint="请输入姓名" android:padding="5dp" > </com.mafeibiao.testapplication.MyEditText> </LinearLayout> <LinearLayout android:id="@+id/ll_card_number" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ffffff" android:gravity="center_vertical" android:orientation="horizontal" android:paddingBottom="5dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:paddingTop="5dp" > <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="left" android:text="卡号" /> <com.mafeibiao.testapplication.MyEditText android:id="@+id/et_card_number" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="5" android:hint="请输入银行卡号" android:padding="5dp" android:inputType="number" app:editTextMode="2" app:splitNumber="4"> </com.mafeibiao.testapplication.MyEditText> </LinearLayout> <LinearLayout android:id="@+id/ll_phone_number" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ffffff" android:gravity="center_vertical" android:orientation="horizontal" android:paddingBottom="5dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:paddingTop="5dp" > <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="left" android:text="手机号" /> <com.mafeibiao.testapplication.MyEditText android:id="@+id/et_phone_number" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="5" android:hint="请输入手机号" android:inputType="number" android:padding="5dp" app:editTextMode="3" > </com.mafeibiao.testapplication.MyEditText> </LinearLayout></LinearLayout>
代码MainActivity.java
package com.mafeibiao.testapplication;/** * Created by mafeibiao on 2017/11/21. */import android.content.Context;import android.content.res.TypedArray;import android.graphics.Rect;import android.graphics.drawable.Drawable;import android.support.v7.widget.AppCompatEditText;import android.text.Editable;import android.text.TextWatcher;import android.util.AttributeSet;import android.view.MotionEvent;public class MyEditText extends AppCompatEditText { // 每隔多少位以空格进行分隔一次,卡号一般都是每4位以空格分隔一次 public int splitNumber = 4; // 自定义输入框的模式 当值为true:银行卡号输入框模式,false:普通输入框模式 private int editTextMode = 1; public MyEditText(Context context) { this(context, null); } public MyEditText(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } // 内容清除图标 private Drawable mClearDrawable; /** * 初始化方法 */ private void init(AttributeSet attrs) { // 设置单行显示所有输入框内容 setSingleLine(); // 设置输入框可获得焦点 setFocusable(true); setFocusableInTouchMode(true); TypedArray t = this.getResources().obtainAttributes(attrs, R.styleable.MyEditText); editTextMode = t.getInt(R.styleable.MyEditText_editTextMode, editTextMode); splitNumber = t.getInt(R.styleable.MyEditText_splitNumber, splitNumber); t.recycle(); mClearDrawable = this.getResources().getDrawable(R.drawable.clear); mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight()); initEvent(); } // 输入框内容改变后onTextChanged方法会调用多次,设置一个变量让其每次改变之后只调用一次 private boolean isTextChanged = false; /** * 处理事件的方法 */ private void initEvent() { addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (isTextChanged) { isTextChanged = false; return; } isTextChanged = true; // 处理输入内容空格与位数以及光标位置的逻辑 handleInputContent(s, start,before,count); // 处理清除图标的显示与隐藏逻辑 handleClearIcon(true); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { } }); } // 卡号内容 private String content; // 卡号最大长度,卡号一般最长19位 public static final int MAX_CARD_NUMBER_LENGHT = 19; //手机号 public static final int MAX_PHONE_NUMBER_LENGHT = 11; // 缓冲分隔后的新内容串 private String result = ""; /** * 处理输入内容空格与位数的逻辑 */ private void handleInputContent(CharSequence s, int start, int before, int count) { content = s.toString(); // 先缓存输入框内容 result = content; // 去掉空格,以防止用户自己输入空格 content = content.replace(" ", ""); switch (editTextMode){ case 1://普通模式 break; case 2://银行卡号模式 // 限制输入的数字位数最多21位(银行卡号一般最多21位) if (content != null && content.length() <= MAX_CARD_NUMBER_LENGHT) { result = ""; int i = 0; // 先把splitNumber倍的字符串进行分隔 while (i + splitNumber < content.length()) { result += content.substring(i, i + splitNumber) + " "; i += splitNumber; } // 最后把不够splitNumber倍的字符串加到末尾 result += content.substring(i, content.length()); } else { // 如果用户输入的位数 result = result.substring(0, result.length() - 1); } break; case 3://手机号模式 if (content != null && content.length() <= MAX_PHONE_NUMBER_LENGHT) { int length = s.toString().length(); if (length == 3 || length == 8){ result += " "; } } else { // 如果用户输入的位数 result = result.substring(0, result.length() - 1); } break; } // 获取光标开始位置 // 必须放在设置内容之前 int j = getSelectionStart(); setText(result); // 处理光标位置 handleCursor(before, j); } /** * 处理光标位置 * * @param before * @param j */ private void handleCursor(int before, int j) { // 处理光标位置 try { if (j + 1 < result.length()) { // 添加字符 if (before == 0) { // 遇到空格,光标跳过空格,定位到空格后的位置 if (j % splitNumber + 1 == 0) { setSelection(j + 1); } else { // 否则,光标定位到内容之后 (光标默认定位方式) setSelection(result.length()); } // 回退清除一个字符 } else if (before == 1) { // 回退到上一个位置(遇空格跳过) setSelection(j); } } else { MyEditText.this.setSelection(result.length()); } } catch (Exception e) { } } /** * 处理清除图标的逻辑 */ private void handleClearIcon(boolean focused) { if (content != null && content.length() > 0) { // 显示 if (focused) { setEditTextIcon(null, null, mClearDrawable, null); } else { // 隐藏 setEditTextIcon(null, null, null, null); } } else { // 隐藏 setEditTextIcon(null, null, null, null); } } @Override public boolean onTouchEvent(MotionEvent event) { // 获取用户点击的坐标,这里只对X轴做了判断, float x = event.getX(); // 当用户抬起手指时,判断坐标是否在图标交互区域,如果在则清空输入框内容,同时隐藏图标自己 if (event.getAction() == MotionEvent.ACTION_UP) { if (x > (getWidth() - getPaddingRight() - mClearDrawable.getIntrinsicWidth())) { // 清空输入框内容 setText(""); // 隐藏图标 setEditTextIcon(null, null, null, null); } } return super.onTouchEvent(event); } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); handleClearIcon(focused); //刷新界面,防止有时候出现的不刷新界面情况 invalidate(); } /** * 设置输入框的左,上,右,下图标 * * @param left * @param top * @param right * @param bottom */ private void setEditTextIcon(Drawable left, Drawable top, Drawable right, Drawable bottom) { setCompoundDrawables(left, top, right, bottom); } /** * 重写onMeasure,主要目的是让EditText的高度与我们显示在右侧的清空图标的高度相同,否则输入的时候可能会动态改变EditText的高度以适应清空图标的高度 * 用户体验不好 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mClearDrawable.getIntrinsicWidth(), heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mClearDrawable.getIntrinsicHeight()); } }}
本篇总结
本篇文章上了一些例子来巩固我们之前学过的知识。我们自定义了CircleView来实现一个圆,该CircleView直接继承自View,需要注意的是直接继承自View的控件需要自己实现对wrap_content和padding的支持,另外我们继承了TextView和EditText来分别实现了渐变的TextView和银行卡手机号输入分隔的问题。可以看到我们继承自这些系统控件时,并单独没有实现对wrap_content和padding的支持(后面的自定义EditText不实现onMeasure方法也不会对wrap_content和padding造成影响)。
下篇预告
在下一篇文章中我们将来分析Android View的事件体系,这篇文章已经涉及到了一些,不过读者对Android View的事件体系可能还是有些模糊。并且读者可能已经发现了,本篇自定义View并没有继承ViewGroup和其子类LinearLayout,FrameLayout等,因为这部分内容我们会在分析了Android View的事件体系之后再做解释。
此致,敬礼
- Android开发之漫漫长途 番外篇——自定义View的各种姿势1
- Android开发之漫漫长途 番外篇——自定义View的各种姿势2
- Android 开发之漫漫长途 Ⅲ—Activity 的显示之 Window和View(2)
- Android开发之漫漫长途 Ⅱ——Activity的显示之Window和View(1)
- Android 开发之漫漫长途 Ⅱ——Activity的显示之Window和View(1)
- Android开发之漫漫长途 Ⅲ——Activity的显示之Window和View(2)
- Android开发之漫漫长途 Ⅳ——Activity的显示之ViewRootImpl初探
- Android 开发之漫漫长途 Ⅳ——Activity 的显示之 ViewRootImpl 初探
- Android开发之漫漫长途 番外篇——内存泄漏分析与解决
- Android开发之漫漫长途 Ⅰ——Android系统的创世之初以及Activity的生命周期
- Android 开发之漫漫长途Ⅰ—Android 系统的创世之初以及 Activity 的生命周期
- Android 开发之漫漫长途Ⅰ—Android 系统的创世之初以及 Activity 的生命周期
- Android开发之漫漫长途 Ⅴ——Activity的显示之ViewRootImpl的PreMeasure、WindowLayout、EndMeasure、Layout、Draw
- Android开发之漫漫长途 Ⅷ——Android Binder(也许是最容易理解的)
- Android开发之漫漫长途 VII——Android消息机制(Looper Handler MessageQueue Message)
- Android开发之漫漫长途 Ⅵ——图解Android事件分发机制(深入底层源码)
- 长途漫漫
- android开发之自定义View
- Spring MVC中xxx-servlet.xml与ApplicationContext.xml
- php.ini 设置error_reporting不起作用
- 架构师的学习知识
- Oracle 触发器 插入,更新,删除,数据同步,两表同步
- Java BIO NIO AIO
- Android开发之漫漫长途 番外篇——自定义View的各种姿势1
- CSS中!important的作用
- 程序员,30年后,你打算拿什么养活自己?
- 转载学习ucos的历程
- 投资,什么是真正的风险?
- Python操作MongoDB
- 表单input中disabled提交后得不到值的解决办法
- matlab 识别灰度图像与彩色图像
- 用户体验设置和 UI 设计的 10 个不同点