DecorView层级
View
ViewRoot
对应于ViewRootImpl
类,它是连接WindowManager
和DecorView
的纽带,View
的三大流程均是通过ViewRoot
来完成的。在ActivityThread
中,当Activity
对象被创建完毕后,会将DecorView
添加到Window
中,同时会创建ViewRootImpl
对象,并将ViewRootImpl
对象和DecorView建立关联
View的整个绘制流程可以分为以下三个阶段:
- measure: 判断是否需要重新计算View的大小,需要的话则计算
- layout: 判断是否需要重新计算View的位置,需要的话则计算
- draw: 判断是否需要重新绘制View,需要的话则重绘制
MeasureSpec
MeasureSpec表示的是一个32位的整型值,它的高2位表示测量模式SpecMode,第30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明应该如何测量这个View
Mode | 说明 |
---|---|
UNSPECIFIED | 不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。 |
EXACTLY | 精确测量模式,视图宽高指定为match_parent或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下View的测量值就是SpecSize的值 |
AT_MOST | 最大值测量模式,当视图的宽高指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸 |
对于DecorView而言,它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定;对于普通的View,它的MeasureSpec由父视图的MeasureSpec和其自身的LayoutParams共同决定
childLayoutParams\parentSpecMode | EXACTLY | AT_MOST |
---|---|---|
dp/px | EXACTLY(childSize) | EXACTLY(childSize) |
match_parent | EXACTLY(childSize) | AT_MOST(parentSize) |
wrap_content | AT_MOST(parentSize) | AT_MOST(parentSize) |
直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,因为View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于父容器当前剩余的空间大小,就相当于使用match_parent。这解决方式如下:
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
MotionEvent
事件 | 说明 |
---|---|
ACTION_DOWN | 手指刚接触到屏幕 |
ACTION_MOVE | 手指在屏幕上移动 |
ACTION_UP | 手机从屏幕上松开的一瞬间 |
ACTION_CANCEL | 触摸事件取消 |
点击屏幕后松开,事件序列为 DOWN -> UP,点击屏幕滑动松开,事件序列为 DOWN -> MOVE -> …> MOVE -> UP。
getX/getY
返回相对于当前View左上角的坐标,getRawX/getRawY
返回相对于屏幕左上角的坐标
TouchSlop是系统所能识别出的被认为滑动的最小距离,不同设备值可能不相同,可通过 ViewConfiguration.get(getContext()).getScaledTouchSlop()
获取。
VelocityTracker
VelocityTracker 可用于追踪手指在滑动中的速度:
1 | view.setOnTouchListener(new View.OnTouchListener() { |
GestureDetector
GestureDetector 辅助检测用户的单击、滑动、长按、双击等行为:
1 | final GestureDetector mGestureDetector = new GestureDetector(this, new GestureDetector.OnGestureListener() { |
如果是监听滑动相关,建议在 onTouchEvent
中实现,如果要监听双击,那么就使用 GestureDectector
。
Scroller
弹性滑动对象,用于实现 View 的弹性滑动,Scroller 本身无法让 View 弹性滑动,需要和 View 的 computeScroll
方法配合使用。startScroll
方法是无法让 View 滑动的,invalidate
会导致 View 重绘,重回后会在 draw
方法中又会去调用 computeScroll
方法,computeScroll
方法又会去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo
方法实现滑动,接着又调用 postInvalidate
方法如此反复。
1 | Scroller mScroller = new Scroller(mContext); |
View的滑动
scrollTo/scrollBy
适合对 View 内容的滑动。scrollBy
实际上也是调用了scrollTo
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}mScrollX的值等于 View 的左边缘和 View 内容左边缘在水平方向的距离,mScrollY的值等于 View 上边缘和 View 内容上边缘在竖直方向的距离。
scrollTo
和scrollBy
只能改变 View 内容的位置而不能改变 View 在布局中的位置。使用动画
操作简单,主要适用于没有交互的 View 和实现复杂的动画效果。改变布局参数
操作稍微复杂,适用于有交互的 View.1
2
3
4
5ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
view.requestLayout();
//或者 view.setLayoutParams(params);
View的事件分发
点击事件达到顶级 View(一般是一个ViewGroup),会调用 ViewGroup 的 dispatchTouchEvent 方法,如果顶级 ViewGroup 拦截事件即 onInterceptTouchEvent 返回 true,则事件由 ViewGroup 处理,这时如果 ViewGroup 的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。也就是说如果都提供的话,onTouch 会屏蔽掉 onTouchEvent。在 onTouchEvent 中,如果设置了 mOnClickListenser,则 onClick 会被调用。如果顶级 ViewGroup 不拦截事件,则事件会传递给它所在的点击事件链上的子 View,这时子 View 的 dispatchTouchEvent会被调用。如此循环。
- ViewGroup 默认不拦截任何事件。ViewGroup 的 onInterceptTouchEvent 方法默认返回 false。
- View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,onTouchEvent 方法就会被调用。
- View 在可点击状态下,onTouchEvent 默认会消耗事件。
- ACTION_DOWN 被拦截了,onInterceptTouchEvent 方法执行一次后,就会留下记号(mFirstTouchTarget == null)那么往后的 ACTION_MOVE 和 ACTION_UP 都会拦截。
事件分发机制源码
在Activity中获取某个View的宽高
- Activity/View#onWindowFocusChanged
1
2
3
4
5
6
7
8
9
10// 此时View已经初始化完毕
// 当Activity的窗口得到焦点和失去焦点时均会被调用一次
// 如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasureWidth();
int height = view.getMeasuredHeight();
}
} - view.post(runnable)
1
2
3
4
5
6
7
8
9
10
11
12
13// 通过post可以将一个runnable投递到消息队列的尾部,// 然后等待Looper调用次runnable的时候,View也已经初
// 始化好了
protected void onStart() {
super.onStart();
view.post(new Runnable() {
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
} - ViewTreeObserver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 当View树的状态发生改变或者View树内部的View的可见// 性发生改变时,onGlobalLayout方法将被回调
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
Draw的基本流程
1 | // 绘制基本上可以分为六个步骤 |
View绘制从ViewRootImpl的performTraversals()说起以LinearLayout为例
View绘制流程-从activityThread的handleResumeActivity说起
从invalidate到onDraw
View的事件体系
《Android开发艺术探索》第3章 View的事件体系
View基础知识
什么是View
View的位置参数
View的位置主要由它的四个顶点来决定,对应View的四个属性:top(左上角纵坐标)、left(左上角横坐标)、right(右下角横坐标)、bottom(右下角纵坐标),这些坐标是相对View的父容器来说的,是相对坐标。
Android中,x轴和y轴的正方向分别是右和下
View的 width = right-left
View的 height = bottom-top
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
x和y是View的左上角坐标(x = left + translationX; y = top + translationY)
translationX和translationY是View左上角相对于父容器的偏移量(默认是0)
注意:View平移的过程中,top和left表示的是原始左上角的位置信息,其值不会发生改变。 改变的是x、y、translationX和translationY
MotionEvent、TouchSlop
MotionEvent有ACTION_DOWN、ACTION_MOVE、ACTION_UP事件
点击屏幕后松开:DOWN–>UP
点击屏幕滑动再松开:DOWN–>MOVE–>…–>MOVE–>UP
getX/getY是当前View左上角的x和y坐标
getRawX和getRawY是相对于手机屏幕左上角的x和y坐标
VelocityTracker
GestureDetector
Scroller对象
自定义View
继承View重写onDraw方法
主要用于实现一些不规则的效果,静态或者动态地显示一些不规则的图形,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。继承ViewGroup派生特殊的Layout
主要用于实现自定义布局,采用这种方式需要合适地处理ViewGroup的测量、布局两个过程,并同时处理子元素的测量和布局过程。继承特定的View
用于扩张某种已有的View的功能继承特定的ViewGroup
用于扩张某种已有的ViewGroup的功能
继承ViewGroup的自定义View
多个地方引用同一个布局的情况下,可以把此布局抽取到一个单独的xml中,在使用的地方用<include>
来引入。
抽取出的布局可以使用自定义View,方便统一管理属性(不同在各个activity/fragment(各自的布局文件中include了这个抽取出的布局)中写这些逻辑)
例子:自定义个状态栏
1 | public class TitleLayout extends LinearLayout{ |
1 | <LinearLayout |
属性动画(ValueAnimator)
属性动画的强大之处在于可以对任意对象的任意属性增加动画效果,并且可以自定义值的类型和变化过程(TypeEvaluator)和过渡速度(Interpolator)
- 最常用的是
ObjectAnimator
就是(ValueAnimator
)的子类。 ValueAnimator
以特定的方式(可以自定义)对值机型不断的修改,已达到某种想要的过渡效果。它提供设置播放次数、动画间隔、重复模式、开始动画以及设置动画监听器的方法。
例子:从 0 到 1,耗时 3000 毫秒的线性变化。监听器中输出值例子:线性动画,左右来回移动1
2
3
4
5
6
7
8
9
10
11
12
13ValueAnimator animator =
ValueAnimator.ofFloat(0f,1.0f);
animator.setDuration(3000);
animator.setInterpolator(new LinearInterpolator());
//添加监听器获取具体改变的值
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
public void onAnimationUpdate(ValueAnimator animation){
float value = (Float) animation.getAnimatedValue();
Log.d(TAG, value);
}
});
animator.start();1
2
3
4
5
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
canvas.drawCircle(XPoint, heightSpecSize / 2, 30, mPaint);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void start(){
final ValueAnimator animator = ValueAnimator.ofFloat(60, 600);
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.RESTART);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
public void onAnimationUpdate(ValueAnimator animation){
//获取到动画每次改变的 float 值,赋值给 xpoint
XPoint = (Float) animation.getAnimatedValue();
//通知 view 重绘
invalidate();
}
});
animator.start();
}- ValueAnimator.ofFloat(float…),还有 ofInt(int…)。它们内部已经使用了 FloatEvaluator、FloatArrayEvaluator、IntEvaluator、IntArrayEvaluator 这些系统已经实现好了的 TypeEvaluator。
- ValueAniamtor.ofObject(TypeEvaluator, Object…)第一个参数必须是 TypeEvaluator,用于设定自定义类型。
例子:1
2
3
4
5
6
7
8
9
10
11
12
13class PointEvaluator implements TypeEvaluator{
public Object evaluate(float fraction, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
int x = (int) (startPoint.x + fraction * (endPoint.x - startPoint.x));
int y = (int) (startPoint.y + fraction * (endPoint.y - startPoint.y));
return new Point(x, y);
}
}1
2
3
4protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mPoint.x, mPoint.y, 30, mPaint);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public void start() {
final ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(),
new Point(30, 30), new Point(600, 600));
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
mPoint = (Point)animation.getAnimatedValue();
invalidate();
}
});
animator.start();
} - TimeInterpolator 表示动画的速率
已经有很多已知的实现类,比如:
AccelerateDecelerateInterpolator 表示先加速后减速(不设置 setInterpolator 默认使用它)
AccelerateInterpolator 表示一直加速
DecelerateInterpolator 表示一直加速等
BounceInterpolator 可以模拟物理规律,实现反弹的效果
LinearInterpolator 表示按一定的比率持续变化(线性变化的内插器) - AnimatorSet 表示动画的集合,可几个动画一起播放,或依次序播放。提供 paly、with、after 等方法。(执行动画顺序 after>play; play>before; play>with)
例子:
完整代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82public class MyView extends View {
private Paint mPaint;
private Point mPoint;
private int mColor;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
public MyView(Context context) {
super(context);
initPaint();
}
private void initPaint() {
mPaint = new Paint();
mPaint.setColor(0xFFF00000);
mPaint.setAntiAlias(true); // 抗锯齿
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mPoint.x, mPoint.y, 60, mPaint);
}
public void start() {
final ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(),
new Point(60, 60), new Point(990, 1050));
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
mPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
final ValueAnimator animator1 = ValueAnimator.ofArgb(0xFFF00000,0xFFFFFF00);
animator1.setRepeatCount(ValueAnimator.INFINITE);
animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
mColor = (int) animation.getAnimatedValue();
mPaint.setColor(mColor);
}
});
AnimatorSet animationSet = new AnimatorSet();
animationSet.setDuration(3000);
animationSet.setInterpolator(new LgDecelerateInterpolator());
animationSet.play(animator).with(animator1);
animationSet.start();
}
class PointEvaluator implements TypeEvaluator {
public Object evaluate(float fraction, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
int x = (int) (startPoint.x + fraction * (endPoint.x - startPoint.x));
int y = (int) (startPoint.y + fraction * (endPoint.y - startPoint.y));
return new Point(x, y);
}
}
class LgDecelerateInterpolator implements TimeInterpolator {
private float background;
public LgDecelerateInterpolator() {
background = 10;
}
public float getInterpolation(float input) {
return (1 - (float) Math.pow(background, -input));
}
}
}
TypedArray自定义属性
在
res/values
下新建attrs.xml
文件(也可以别的名字)。attrs.xml
内容(编写我们需要的属性):1
2
3
4
5
6
7
8
9
10
11
12
13
<resources>
<declare-styleable name="burce">
<attr name="mHeight" format="integer"/>
<attr name="mWidth" format="integer"/>
<attr name="mName" format="string"/>
<attr name="sex" format="enum">
<enum name="man" value="0"/>
<enum name="woman" value="1"/>
</attr>
<attr name="student" format="boolean"/>
</declare-styleable>
</resources><declare-styleable name="burce">
其中的name的值随便定义一个,不要与系统的起冲突。<attr name="mHeight" format="integer"/>
name就是自定义的属性的名字(比如系统控件的android:layout_width) format 就是属性的类型,这里支持10种类型,常用的有string,integer,boolean等等,这次我们用到了整形,枚举和布尔format支持:
color
:颜色值;boolean
:布尔值;dimension
:尺寸值,注意,这里如果是dp那就会做像素转换;float
:浮点值;integer
:整型值;string
:字符串;fraction
:百分数;enum
:枚举值;flag
:是自己定义的,就是里面对应了自己的属性值;reference
:指向其它资源;reference|color
:颜色的资源文件;reference|boolean
:布尔值的资源文件.
对
attr
的获取不能使用switch-case
,要用if...else
注意:我们在自定义属性的名字的时候不能与系统的名字冲突,否则会报错
新建一个类继承View类,实现3个构造方法,然后获取我们自定义的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class MyView extends View {
private static final String TAG = "MyView";
private int heiget;
private int width;
private String name;
private int sex;
private boolean student;
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);
//获取declare-styleable标签。其中burce是自定义标签内的name
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.burce);
heiget=array.getInt(R.styleable.burce_mHeight,0);
width=array.getInt(R.styleable.burce_mWidth,0);
name=array.getString(R.styleable.burce_mName);
sex=array.getInt(R.styleable.burce_sex,0);
student=array.getBoolean(R.styleable.burce_student,true);
array.recycle();
Log.i(TAG, "height: "+heiget);
Log.i(TAG, "width: "+width);
Log.i(TAG, "name: "+name);
Log.i(TAG, "sex: "+sex);
Log.i(TAG, "student: "+student);
}
}在layout中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.myviewtest.MainActivity">
<com.myviewtest.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:mName="bruce"
app:sex="man"
app:mHeight="100"
app:mWidth="100"
app:student="true"/>
</RelativeLayout>- 注意:命名空间需要加上
xmlns:app="http://schemas.android.com/apk/res-auto"
才能使用自定义属性 - 使用自定义属性需要用
app:
为前缀 - 其中命名空间的名称
app
可以随意指定的,比如改成xmlns:dictview="http://schemas.android.com/apk/res-auto"
。那么底下使用的地方就是dictview:mHeight="100"
- 注意:命名空间需要加上
自定义View基础-坐标系
View的坐标系
注意:View的坐标系统是相对于父控件而言的.
1 | getTop(); //获取子View左上角距父View顶部的距离 |
MotionEvent中 get 和 getRaw 的区别
1 | event.getX(); //触摸点相对于其所在组件坐标系的坐标 |
自定义View基础-角度弧度
自定义View基础-颜色
安卓支持的颜色模式:
颜色模式 | 备注 |
---|---|
ARGB8888 | 四通道高精度(32位) |
ARGB4444 | 四通道低精度(16位) |
RGB565 | 屏幕默认模式(16位) |
Alpha8 | 仅有透明通道(8位) |
其中 A R G B 的取值范围均为0
255(即16进制的0x000xff)A 从0x00到0xff表示从透明到不透明。
RGB 从0x00到0xff表示颜色从浅到深。
当RGB全取最小值(0或0x000000)时颜色为黑色,全取最大值(255或0xffffff)时颜色为白色
颜色混合模式(Alpha通道相关)
默认情况下,当一个颜色绘制到Canvas上时的混合模式是这样计算的:
(RGB通道) 最终颜色 = 绘制的颜色 + (1 - 绘制颜色的透明度) × Canvas上的原有颜色。
注意:
1.这里我们一般把每个通道的取值从0(0x00)到255(0xff)映射到0到1的浮点数表示。
2.这里等式右边的“绘制的颜色”、“Canvas上的原有颜色” 都是经过预乘了自己的Alpha通道的值。如绘制颜色:0x88ffffff,那么参与运算时的每个颜色通道的值不是1.0,而是(1.0 * 0.5333 = 0.5333)。 (其中0.5333 = 0x88/0xff)
使用这种方式的混合,就会造成后绘制的内容以半透明的方式叠在上面的视觉效果。
其实还可以有不同的混合模式供我们选择,用Paint.setXfermode,指定不同的PorterDuff.Mode。
下表是各个PorterDuff模式的混合计算公式:(D指原本在Canvas上的内容dst,S指绘制输入的内容src,a指alpha通道,c指RGB各个通道)
混合模式 | 计算公式 |
---|---|
ADD | Saturate(S + D) |
CLEAR | [0, 0] |
DARKEN | [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] |
DST | [Da, Dc] |
DST_ATOP | [Sa, Sa * Dc + Sc * (1 - Da)] |
DST_IN | [Sa * Da, Sa * Dc] |
DST_OUT | [Da * (1 - Sa), Dc * (1 - Sa)] |
DST_OVER | [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] |
LIGHTEN | [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] |
MULTIPLY | [Sa * Da, Sc * Dc] |
SCREEN | [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] |
SRC | [Sa, Sc] |
SRC_ATOP | [Da, Sc * Da + (1 - Sa) * Dc] |
SRC_IN | [Sa * Da, Sc * Da] |
SRC_OUT | [Sa * (1 - Da), Sc * (1 - Da)] |
SRC_OVER | [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] |
XOR | [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] |
用示例图来查看使用不同模式时的混合效果如下(src表示输入的图,dst表示原Canvas上的内容):
自定义View进阶-分类与流程
代码:
复杂布局进行模块拆分
作用:模块顺序是固定的。拆分模块,各模块处理自己的逻辑避免全部逻辑都混在Activity中造成代码复杂难以维护。
难点:Activity与各模块间的交互相对较复杂
本质:自定义View
代码
ProDetailPriceUI.java 商品详情价格区域相关UI
1 | package xxx.xxx.view.productDetail.ui; |
如果自定义UI跟Activity有交互,那么要传递callback到自定义UI中,在自定义UI中合理位置设置回调
ProDetailActivity.java
1 | ... |
布局文件
Activity的布局文件
activity_pro_detail_new.xml –>其中content_pro_detail内有各个自定义样式布局
1 | <?xml version="1.0" encoding="utf-8"?> |
content_pro_detail.xml
1 | <?xml version="1.0" encoding="utf-8"?> |
实现通用的圆角布局
使用 canvas.drawPath(path, paint)
,为 paint 添加抗锯齿标记,并设置 XFermodes
在 Android P 上无法使用 canvas.drawPath(path, paint)
剪裁布局,原因是 Android P 上 XFermodes 行为变更导致的。暂且在 P 上使用 canvas.clipPath(path)
实现圆角,会有锯齿效果
1 | public class RoundRelativeLayout extends RelativeLayout { |
attrs.xml
中
1 | <declare-styleable name="RoundRelativeLayout"> |