声明:本文中使用的Demo的Git地址:https://github.com/NoClay/TestView.git
1.自定义View中需要知道的几个类
1.MeasureSpec
简述:作为一个尺寸类,将测量模式和尺寸合而为一,在我们自定义的控件,MeasureSpce的四种测量模式分别对应如下:
UNSPECIFIED --> 系统内部使用,要多大给多大,父容器对子View没有任何影响,要多大给多大
EXACTLY -->精确值,对应例如34dp,或者math_parent
AT_MOST -->父容器给定子View了一个可用大小的SpecSize, View的大小相当于wrap_content
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//父容器不对View有任何限制,要多大给多大,一般用于系统内部
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//父容器已经检测出View所需要的精确大小,这个时候view的最终大小就是指定的SpecSize的值
//对应于使用match_parent和具体的数值如37dp这种
public static final int EXACTLY = 1 << MODE_SHIFT;
//父容器指定了一个可用大小的SpecSize,View的大小不能大于这个值,相当于wrap_content
public static final int AT_MOST = 2 << MODE_SHIFT;
//获取一个SpecSize的模式
@MeasureSpecMode
public static int getMode(int measureSpec) ;
//获取一个SpecSize的大小
public static int getSize(int measureSpec) ;
//对一个SpecSize的大小进行调整
static int adjust(int measureSpec, int delta);
}
2.Paint
简述:Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色, 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法, 大体上可以分为两类,一类与图形绘制相关Paint,一类与文本绘制相关TextPaint。相当于我们常用的ps软件中的画笔,我们可以对其设置许多东西,比如画笔颜色、抗锯齿、画笔粗细、画笔线形等
/**
* Paint类介绍
*
* Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色,
* 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,
* 大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。
*
* 1.图形绘制
* setARGB(int a,int r,int g,int b);
* 设置绘制的颜色,a代表透明度,r,g,b代表颜色值。
*
* setAlpha(int a);
* 设置绘制图形的透明度。
*
* setColor(int color);
* 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。
*
* setAntiAlias(boolean aa);
* 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。
*
* setDither(boolean dither);
* 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰
*
* setFilterBitmap(boolean filter);
* 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示
* 速度,本设置项依赖于dither和xfermode的设置
*
* setMaskFilter(MaskFilter maskfilter);
* 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等 *
* setColorFilter(ColorFilter colorfilter);
* 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果
*
* setPathEffect(PathEffect effect);
* 设置绘制路径的效果,如点画线等
*
* setShader(Shader shader);
* 设置图像效果,使用Shader可以绘制出各种渐变效果
*
* setShadowLayer(float radius ,float dx,float dy,int color);
* 在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色
*
* setStyle(Paint.Style style);
* 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE
*
* setStrokeCap(Paint.Cap cap);
* 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式
* Cap.ROUND,或方形样式Cap.SQUARE
*
* setSrokeJoin(Paint.Join join);
* 设置绘制时各图形的结合方式,如平滑效果等
*
* setStrokeWidth(float width);
* 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度
*
* setXfermode(Xfermode xfermode);
* 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果
*
* 2.文本绘制
* setFakeBoldText(boolean fakeBoldText);
* 模拟实现粗体文字,设置在小字体上效果会非常差
*
* setSubpixelText(boolean subpixelText);
* 设置该项为true,将有助于文本在LCD屏幕上的显示效果
*
* setTextAlign(Paint.Align align);
* 设置绘制文字的对齐方向
*
* setTextScaleX(float scaleX);
* 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果
*
* setTextSize(float textSize);
* 设置绘制文字的字号大小
*
* setTextSkewX(float skewX);
* 设置斜体文字,skewX为倾斜弧度
*
* setTypeface(Typeface typeface);
* 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
*
* setUnderlineText(boolean underlineText);
* 设置带有下划线的文字效果
*
* setStrikeThruText(boolean strikeThruText);
* 设置带有删除线的效果
*
*/
3.Canvas
简述:相当于我们的ps软件,设置好画笔,即可通过代码在画布上画圆等
public class Canvas{
弧线(arcs)、填充颜色(argb和color)、
Bitmap、圆(circle和oval)、点(point)、
线(line)、矩形(Rect)、图片(Picture)、
圆角矩形 (RoundRect)、文本(text)、
顶点(Vertices)、路径(path)。
Canvas位置转换的方法:
rorate(旋转)、
scale(缩放)、
translate(变换)、
skew(扭曲)等
}
2.View绘制的三大流程
首先先看各种View的继承关系。
在一个Activity手机屏幕的View树,顶层View即我们看到的View。
1.View绘制的三大流程--measure
主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:
mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。
具体的调用链如下:
ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:
1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth) ;
2 、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。
整个measure调用流程就是个树形的递归过程
看流程图如下,如果View树申请重新测量,则进行重新测量,从顶层View依次向下递归测量整个View树
伪代码如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//....
//回调onMeasure()方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
//more
}
//回调View视图里的onMeasure过程
private void onMeasure(int height , int width){
//设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)
//1、该方法必须在onMeasure调用,否者报异常。
setMeasuredDimension(h , l) ;
//2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//2.1、获得每个子View对象引用
View child = getChildAt(i) ;
//整个measure()过程就是个递归过程
//该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法都
measureChildWithMargins(child , h, i) ;
//其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:
//child.measure(h, l)
}
}
//该方法具体实现在ViewGroup.java里 。
protected void measureChildWithMargins(View v, int height , int width){
v.measure(h,l)
}
2.View绘制的三大流程--layout
主要作用 :为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。
具体的调用链如下:
host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下
1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;
2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。
流程图如下:如果请求View树重新布局,则进行重新布局,与测量流程类似,依次自顶层View递归向下测量
伪代码:
/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
// layout()过程 ViewRoot.java
// 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()
private void performTraversals(){
//...
View mView ;
mView.layout(left,top,right,bottom) ;
//....
}
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void onLayout(int left , int top , right , bottom){
//如果该View不是ViewGroup类型
//调用setFrame()方法设置该控件的在父视图上的坐标轴
setFrame(l ,t , r ,b) ;
//--------------------------
//如果该View是ViewGroup类型,则对它的每个子View进行layout()过程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//2.1、获得每个子View对象引用
View child = getChildAt(i) ;
//整个layout()过程就是个递归过程
child.layout(l, t, r, b) ;
}
}
3.View绘制的三大流程--draw
由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。
调用流程 :
mView.draw()开始绘制,draw()方法实现的功能如下:
1 、绘制该View的背景
2 、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)
3、调用onDraw()方法绘制视图本身 (每个View都需要重载该方法,ViewGroup不需要实现该方法)
4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个 地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。
伪代码:
// draw()过程 ViewRoot.java
// 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图
private void draw(){
//...
View mView ;
mView.draw(canvas) ;
//....
}
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现
private void draw(Canvas canvas){
//该方法会做如下事情
//1 、绘制该View的背景
//2、为绘制渐变框做一些准备操作
//3、调用onDraw()方法绘制视图本身
//4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。
// 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。
//5、绘制渐变框
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
@Override
protected void dispatchDraw(Canvas canvas) {
//
//其实现方法类似如下:
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
View child = getChildAt(i) ;
//调用drawChild完成
drawChild(child,canvas) ;
}
}
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法
protected void drawChild(View child,Canvas canvas) {
// ....
//简单的回调View对象的draw()方法,递归就这么产生了。
child.draw(canvas) ;
//.........
}
3.自定义View的一些重要方法
1.invalidate()方法 :
一般引起invalidate()操作的函数如下:
1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
2、setSelection()方法:请求重新draw(),但只会绘制调用者本身。
3、setVisibility()方法 :当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,
继而绘制该View。
4 、setEnabled()方法 :请求重新draw(),但不会重新绘制任何视图包括该调用者本身。
2.requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。
说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。
一般引起操作的方法如下:
setVisibility()方法:
当View的可视状态在INVISIBLE/VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。 同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。
3.requestFocus()函数说明:
说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。
4.实例
1.继承自View重写onDraw
继承自View制作一个心电图的自定义View
public class HeartWavesView extends View {
private int mTableLineColor = Color.RED;
private int mWavesLineColor = Color.BLACK;
private int mTitleColor = Color.BLACK;
private int mTitleSize = 30;
private int mXYTextSize = 20;
private Context context;
private static final String TAG = "HeartWavesView";
private Paint paintWavesLine;
private Paint paintTableLine;
private TextPaint paintTitle;
private TextPaint paintXYText;
private boolean isFirstDrawPoint = true;
private boolean isFirstDrawBackground = true;
private int height;
private int width;
private int leftPadding;
private int rightPadding;
private int topPadding;
private int bottomPadding;
private int maxY = 2100;
private int minY = -2100;
private int maxX = 120;
private int x_num = 25;
private int y_num;
private int grid_width;
//x轴每个小格子对应的秒
//y轴每个小格子对应的指数
private int grid_second = 5;
private float grid_num;
private int zeroCurY;
private int yStartNum;
private int workWidth;
private int workHeight;
//几秒钟一次数据,默认为1秒1次
private float dataHz = 1;
private List<PointXY> pointList;
private String title = "心电图";
public HeartWavesView(Context context) {
super(context);
this.context = context;
initView();
}
public HeartWavesView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
resolveAttrs(attrs);
initView();
}
private void resolveAttrs(AttributeSet attrs) {
TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.HeartWavesView);
mTableLineColor = typeArray.getColor(R.styleable.HeartWavesView_tableLineColor, Color.RED);
mTitleColor = typeArray.getColor(R.styleable.HeartWavesView_titleColor, Color.BLACK);
mWavesLineColor = typeArray.getColor(R.styleable.HeartWavesView_wavesLineColor, Color.BLACK);
mTitleSize = typeArray.getDimensionPixelSize(R.styleable.HeartWavesView_titleSize, 30);
mXYTextSize = typeArray.getDimensionPixelSize(R.styleable.HeartWavesView_xyTextSize, 20);
typeArray.recycle();
}
public HeartWavesView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
resolveAttrs(attrs);
initView();
}
private void initView() {
//生成抗锯齿的画笔
pointList = new ArrayList<>();
paintWavesLine = new Paint(Paint.ANTI_ALIAS_FLAG);
//设置画笔粗细
paintWavesLine.setStrokeWidth(2.5f);
//设置画笔颜色
paintWavesLine.setColor(mWavesLineColor);
paintTableLine = new Paint();
paintTableLine.setColor(mTableLineColor);
paintTableLine.setAntiAlias(true);
paintWavesLine.setStrokeWidth(4);
paintTitle = new TextPaint();
paintTitle.setTextSize(mTitleSize);
paintTitle.setColor(mTitleColor);
paintXYText = new TextPaint();
paintXYText.setColor(mTitleColor);
paintXYText.setTextSize(mXYTextSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isFirstDrawBackground) {
height = getHeight();
width = getWidth();
leftPadding = rightPadding = bottomPadding = topPadding = 100;
}
drawBackground(canvas);
drawWaves(canvas);
}
private void drawWaves(Canvas canvas) {
PointXY start = new PointXY();
PointXY end = new PointXY();
for (int i = 0; i < pointList.size() - 1; i++) {
start = pointList.get(i);
end = pointList.get(i + 1);
canvas.drawLine(start.getX(), start.getY(), end.getX(), end.getY(), paintWavesLine);
}
}
private void drawBackground(Canvas canvas) {
if (isFirstDrawBackground) {
isFirstDrawBackground = false;
x_num = maxX / grid_second;
x_num = x_num % 5 == 0 ? x_num : (x_num % 5 > 3 ? (x_num / 5 + 1) * 5 : x_num / 5 * 5);
grid_width = (width - leftPadding - rightPadding) / x_num;
y_num = (height - topPadding - rightPadding) / grid_width;
y_num = y_num % 5 == 0 ? y_num : (y_num % 5 > 3 ? (y_num / 5 + 1) * 5 : y_num / 5 * 5);
//获取工作区的宽和高
workWidth = grid_width * x_num;
workHeight = grid_width * y_num;
//获取xy轴比例尺
//获得y轴0标识位的位置
if (maxY > 0 && minY >= 0) {
yStartNum = maxY;
grid_num = maxY / y_num;
zeroCurY = y_num;
} else if (maxY <= 0 && minY < 0) {
yStartNum = 0;
grid_num = -minY / y_num;
zeroCurY = 0;
} else {
zeroCurY = y_num / 2;
zeroCurY = zeroCurY % 5 == 0 ? zeroCurY :
(zeroCurY % 5 > 3) ? (zeroCurY / 5 + 1) * 5 : (zeroCurY / 5 * 5);
grid_num = Math.max(maxY, minY) / Math.min(y_num - zeroCurY, zeroCurY);
yStartNum = (int) (zeroCurY * grid_num);
}
}
for (int i = 0; i <= x_num; i++) {
paintTableLine.setStrokeWidth(1f);
if (i % 5 == 0) {
paintTableLine.setStrokeWidth(3f);
String label = grid_second * i + "";
canvas.drawText(label,
leftPadding + i * grid_width - mXYTextSize / 2,
workHeight + bottomPadding / 2 + topPadding,
paintXYText);
}
canvas.drawLine(leftPadding + i * grid_width, topPadding,
leftPadding + i * grid_width, topPadding + workHeight, paintTableLine);
}
for (int i = 0; i <= y_num; i++) {
paintTableLine.setStrokeWidth(1f);
if (i % 5 == 0) {
paintTableLine.setStrokeWidth(3f);
String label = yStartNum - i * grid_num + "";
canvas.drawText(label, leftPadding / 5, topPadding + i * grid_width, paintXYText);
}
canvas.drawLine(leftPadding, topPadding + i * grid_width,
leftPadding + workWidth, topPadding + i * grid_width, paintTableLine);
}
canvas.drawText(title, width / 2 - mTitleSize * title.length() / 2,
topPadding / 2, paintTitle);
}
public void drawNextPoint(float y) {
if (!isFirstDrawBackground) {
if (isFirstDrawPoint) {
isFirstDrawPoint = false;
PointXY point = new PointXY();
point.setX(leftPadding);
point.setY(zeroCurY * grid_width + topPadding);
pointList.add(point);
}
PointXY lastPoint = pointList.get(pointList.size() - 1);
Log.d(TAG, "drawNextPoint: size" + pointList.size());
if (pointList.size() == maxX- 1) {
pointList.clear();
lastPoint.setX(leftPadding);
}
PointXY nowPoint = new PointXY();
nowPoint.setX(dataHz / grid_second * grid_width + lastPoint.getX());
nowPoint.setY((yStartNum - y) / grid_num * grid_width + topPadding);
pointList.add(nowPoint);
invalidate();
}
}
public Paint getPaintWavesLine() {
return paintWavesLine;
}
public void setPaintWavesLine(Paint paintWavesLine) {
this.paintWavesLine = paintWavesLine;
}
public Paint getPaintTableLine() {
return paintTableLine;
}
public void setPaintTableLine(Paint paintTableLine) {
this.paintTableLine = paintTableLine;
}
public TextPaint getPaintTitle() {
return paintTitle;
}
public void setPaintTitle(TextPaint paintTitle) {
this.paintTitle = paintTitle;
}
public TextPaint getPaintXYText() {
return paintXYText;
}
public void setPaintXYText(TextPaint paintXYText) {
this.paintXYText = paintXYText;
}
public int getLeftPadding() {
return leftPadding;
}
public void setLeftPadding(int leftPadding) {
this.leftPadding = leftPadding;
}
public int getRightPadding() {
return rightPadding;
}
public void setRightPadding(int rightPadding) {
this.rightPadding = rightPadding;
}
public int getTopPadding() {
return topPadding;
}
public void setTopPadding(int topPadding) {
this.topPadding = topPadding;
}
public int getBottomPadding() {
return bottomPadding;
}
public void setBottomPadding(int bottomPadding) {
this.bottomPadding = bottomPadding;
}
public int getMaxY() {
return maxY;
}
public void setMaxY(int maxY) {
this.maxY = maxY;
}
public int getMinY() {
return minY;
}
public void setMinY(int minY) {
this.minY = minY;
}
public int getMaxX() {
return maxX;
}
public void setMaxX(int maxX) {
this.maxX = maxX;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public class PointXY {
private float x;
private float y;
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
}
2.继承自ViewGroup实现特定的layout
实现一个水平的滑动栏容器
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
//记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
//动画滚动到最终位置结束动画,这里代表本身HorizontalScrollViewEx处理触摸事件
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
//水平滑动则处理此事件
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "onInterceptTouchEvent: intercepted = " + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
/**
* 本身在处理触摸事件
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP:{
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if(Math.abs(xVelocity) >= 50){
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
}else{
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:break;
}
mLastX = x;
mLastY = y;
return true;
}
/**
* 重写onMeasure方法,对容器内的测量进行修正
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* super.onMeasure方法内部调用了setMeasuredDimension(getDefaultSize(
* getSuggestedMinimumWidth(), widthMeasureSpec),
* getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
*/
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
if(childCount == 0){
//no child -- no width, no height
setMeasuredDimension(0, 0);
}else if(widthSpaceMode == MeasureSpec.AT_MOST &&
heightSpaceMode == MeasureSpec.AT_MOST){
//width is wrap and height is wrap
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
}else if(heightSpaceMode == MeasureSpec.AT_MOST){
//height is wrap but width not
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, measuredHeight);
}else if(widthMeasureSpec == MeasureSpec.AT_MOST){
//width is wrap but height not
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth();
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for(int i = 0; i < childCount; i ++){
final View childView = getChildAt(i);
if(childView.getVisibility() != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
/**
* 两个startScrollBy方法,一个加上了时间间隔,一个并没有
* 之前整错了,用错了方法
* @param dx
* @param dy
*/
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
3.继承自特定的ViewGroup
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_gravity="center"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="3dp">
<ImageView
android:id="@+id/image_view_image"
android:layout_gravity="center_horizontal"
android:src="@drawable/image"
android:layout_width="match_parent"
android:layout_height="50dp"
android:padding="5dp"/>
<TextView
android:textSize="10sp"
android:text="测试"
android:id="@+id/image_view_title"
android:layout_width="match_parent"
android:layout_height="10dp"
android:gravity="center_horizontal" />
</LinearLayout>
实现代码:
package noclay.testview;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* Created by 82661 on 2016/10/18.
*/
public class MyImageView extends LinearLayout {
private static final String TAG = "MyImageView";
private TextView textView;
private ImageView imageView;
private LinearLayout linearLayout;
private static Context context;
public MyImageView(Context context) {
super(context);
this.context = context;
init();
}
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyImageView);
textView.setText(array.getText(R.styleable.MyImageView_title));
imageView.setImageDrawable(getResources().
getDrawable(array.getResourceId(R.styleable.MyImageView_image, R.drawable.image)));
array.recycle();
}
public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyImageView);
textView.setText(array.getText(R.styleable.MyImageView_title));
imageView.setImageDrawable(getResources().
getDrawable(array.getResourceId(R.styleable.MyImageView_image, R.drawable.image)));
array.recycle();
}
public void init(){
LayoutInflater.from(getContext()).inflate(R.layout.my_iamge_view, this, true);
textView = (TextView) findViewById(R.id.image_view_title);
imageView = (ImageView) findViewById(R.id.image_view_image);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
Log.d(TAG, "onMeasure: width = " + widthSpaceSize);
Log.d(TAG, "onMeasure: height = " + heightSpaceSize);
if(widthSpaceMode == MeasureSpec.EXACTLY && heightSpaceMode == MeasureSpec.EXACTLY){
//在有具体数值的时候调整子项的大小
if(7 * widthSpaceSize <= 5 * heightSpaceSize){//规定长宽比例为5:7
setMeasure(widthSpaceSize);
}else{
setMeasure(heightSpaceSize);
}
}
}
private void setMeasure(int value){
Log.d(TAG, "onMeasure: true");
LayoutParams lp = (LayoutParams) imageView.getLayoutParams();
lp.width = value * 4 / 5 ;
lp.height = value * 4 / 5;
lp.bottomMargin = lp.topMargin = value * 1 / 10;
imageView.setLayoutParams(lp);
lp = (LayoutParams) textView.getLayoutParams();
lp.width = value;
lp.height = value * 2 / 5;
textView.setLayoutParams(lp);
textView.setTextSize(px2sp(lp.height / 2));
}
public static int px2sp(float value) {
final float scale =context.getResources().getDisplayMetrics().scaledDensity;
return (int) (value / scale + 0.5f);
}
}
4.继承自特定的View
一个圆形图片的View
package noclay.testview;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* Created by 82661 on 2016/10/25.
*/
public class MyCircleImageView extends ImageView {
private Paint paint;
public MyCircleImageView(Context context) {
this(context, null);
}
public MyCircleImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyCircleImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
paint = new Paint();
}
/**
* 绘制圆形图片
*
* @author caizhiming
*/
@Override
protected void onDraw(Canvas canvas) {
Drawable drawable = getDrawable();
if (drawable != null) {
Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
Bitmap b = getCircleBitmap(bitmap);
final Rect rectSrc = new Rect(0, 0, b.getWidth(), b.getHeight());
final Rect rectDest = new Rect(0, 0, getWidth(), getHeight());
paint.reset();
canvas.drawBitmap(b, rectSrc, rectDest, paint);
} else {
super.onDraw(canvas);
}
}
/**
* 获取圆形图片方法
*
* @param bitmap
* @return Bitmap
* @author caizhiming
*/
private Bitmap getCircleBitmap(Bitmap bitmap) {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
final int color = 0xff424242;
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
paint.setAntiAlias(true);
//设置背景色
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
int x = bitmap.getWidth();
canvas.drawCircle(x / 2, x / 2, x / 2, paint);
//设置遮罩
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
return output;
}
}