学习目标:
了解Android框架与自定义控件
学习内容:
Android控件框架与自定义控件详解 3.1 Android控件架构 3.2 View的测量与绘制 3.3 ViewGroup的测量与绘制 3.4 自定义控件的三种方式 3.5 事件的拦截机制
3.1 Android控件架构
程序在OnCreate()中通过调用setContentView()后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorView添加到PhoneWindow中,让其显示出来从而完成绘制。
3.2 View的测量
该过程在onMeasure()方法中进行,通过调用MeasureSpec类来对View进行测量。MeasureSpec是一个32位的int值,其中高2位为测量的模式,低30位为测量的大小,在计算中使用位运算是为了提高并优化效率。测量模式有以下三种:
- EXACTLY:精确值模式。有具体的宽高值或match_parent
- AT_MOST:最大值模式。wrap_content
- UNSPECIFIED:不指定模式。自定义View中使用
以measureWidth为例,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasuredDimension(measuredWidth(widthMeasureSpec), measuredHeight(heightMeasureSpec));
}
private int measureWidth(int measureSpec){
if result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){
result = specSize;
}else if(specMode == MeasureSpec.AT_MOST){
result = Math.min(200, specSize);
}else{
result = 200;
}
return result;
}
3.3 View的绘制
该过程在onDraw()中进行、通过重写该方法在Canvas上使用Paint实现View的绘制。
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(bitmap1, 0, 0, null);
canvas.drawBitmap(bitmap2, 0, 0, null);
Canvas mCanvas = new Canvas(bitmap2);
mCanvas.drawXXX
3.4 ViewGroup的测量
遍历子View进行测量,调用子View的Measure方法来获得每一个子View的测量结果,测量完毕后调用Layout方法放置在具体显示的位置。
3.5 ViewGroup的绘制
通常不需要绘制,除非是指定了背景颜色否则不会调用onDraw()。ViewGroup会使用dispatchDraw()方法来绘制其子View,及遍历所有的子View,然后调用子View的方法来绘制。
3.6 自定义View
在View中通常有以下一些比较重要的回调。
- onFinishInflate():从XML加载组件后回调
- onSizeChanged():组件大小改变时回调
- onMeasure():回调该方法来进行测量
- onLayout():回调该方法来确定显示的位置
- onTouchEvent:监听到触摸事件时回调
通常有以下三种方式来实现自定义控件:
- 对现有的控件进行拓展
- 通过组合来实现新的控件
- 重写View来实现全新控件
3.6.1 对现有控件进行拓展
以“抛光”效果的TextView为例,实现一段文字的颜色渐变。需要对其onSizeChanged方法中设置LinearGradient和onDraw方法中对矩阵进行平移的操作来重写。
public PolishedTextView(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();
mLinearGradient = new LinearGradient(-mViewWidth, 0, 0, 0,
new int[]{0xFF9EADD9, 0xffffffff, 0xFF9EADD9},
new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
mGradientMatrix = new Matrix();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mAnimating && mGradientMatrix != null) {
mTranslate += mViewWidth / 10;
if (mTranslate > 2 * mViewWidth) {
mTranslate = -mViewWidth;
}
mGradientMatrix.setTranslate(mTranslate, 0);
mLinearGradient.setLocalMatrix(mGradientMatrix);
postInvalidateDelayed(50);
}else{
setTextColor(0xFF9EADD9);
}
}
3.6.2 创建复合控件
该方法为通常需要继承一个合适的ViewGroup,并给它添加指定的功能的控件,从而组合成新的复合控件。通过该方式的创建的控件,我们一般人为给它指定一些可配置的属性,使其具有更强的拓展性。
3.6.2.1 定义属性
为一个View提供属性,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件。然后再标签声明了使用自定义属性,并通过name属性来确定引用的字体大小等。确定属性之后即可创建一个自定义控件——TopBar,并让它继承ViewGroup。在构造方法中,通过如下代码来获取XML布局文件中自定义的那些属性。
TypeArray ta = context.obtainStyleAttributes(attrs,R.style.TopBar);
系统提供了TypedArray这样的数据结构来获取自定义属性集,后面引用的styleable的TopBar,就是我们在XML中通过所制定的name名。通过ta.getColor等方法可获取这些定义的属性值。需要注意的是,当获取完所有的饿属性值后,需要调用TypeaArray的recycle方法完成资源回收。
3.6.2.2 组合控件
也可由现成的多个控件进行组合,再通过接口调用来实现具体的实现,
3.6.2.3 引用UI模板
xmlns:android="http://schemas.android.com/apk/res/android"
指定引用的名字空间。使用系统属性时才可使用"android: "来引用Android的系统属性。若要使用自定义属性,就需要创建自己的名字空间,第三方控件都使用如下代码引入名字空间。
xmlns:custom="http://schemas.android.com/apk/res-auto"
之后在XML文件中使用自定义属性,都可以通过这个名字空间来引用。使用自定义View时需要指定完整的包名。而在引用自定义的属性时,需要使用自定义的xlms的名字。
3.7 自定义ViewGrop
自定义ViewGroup的修改实际上是通过改变其子View来实现、先onMeasure() 对子View大小进行测量、重写onLayout()确定子View的位置,重写onTouchEvent()方法增加响应事件。 例如:使用ViewGroup实现类似ScrollView的功能。但在滑动过程中增加一个黏性效果,即当一个子View向上滑动大于一定距离之后松开手指,它将自动向上滑显示下一个View.。下滑同理。
1、首先对ViewGroup的子类大小进行测量
@Overrrie
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for(int i = 0; i < count; i++){
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
2、对子View进行防治位置的设定。让每个子View都显示完整的一屏。因此整个ViewGroup的高度即子View的个数乘以屏幕的高度。获取之后通过遍历来设定每个子View需要放置的位置、直接调用layout()方法。代码如下所示
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
int childCount = getChildCount();
MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
for(int i = 0; i < childCount; i++){
View child = getChildAt(i);
if(child.getVisibility()!= View.GONE){
child.layout(1, i*mScreenHeight, r, (i+1)*mScreenHeight);
}
}
}
3、重写onTouchEvent()方法,为ViewGroup重写触摸事件。通常使用scrollBy()来辅助滑动。
case MotionEvent.ACTION_DOWN:
mLastY = y;
break;
case MotionEvent.ACYION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY-y;
if(getScrollY() < 0){
dy = 0;
}
if(getScrollY() > getHeight() - mScreenHeight()){
dy = 0;
}
scrollBy(0, dy);
mLastY = Y;
break;
4、实现ViewGroup黏性效果。在ACTION_UP事件中判断手指滑动的距离,如果超过一定距离,则使用Scroller类来平滑移动到下一个View,若小于一定距离,则回滚到原来的位置。
@Override
public boolean onTouchEvent(MotionEvent event){
int y = (int)event.getY();
swtich(event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y;
if(getScrollY() < 0){
dy = 0;
}
if(getScrollY() > getHeight() - mScreenHeight()){
dy = 0;
}
scrollBy(0, dy);
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
if(dScrollY > 0){
int dScrollY = mEnd - mStart;
if(dScrollY > 0){
if(dScrollY < mScreenHeight/3){
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
}else{
mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
}
}else{
if(-dScrollY < mScreenHeight/3){
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
}else{
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
}
}
}
break;
}
postInvalidate();
return true;
}
@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0,mScroller.getCurY());
postInvalidate();
}
}
3.8 事件拦截机制分析
方法与控件的对应关系如下: 事件传递过程:从上之下依次为父类与子类。父类通过dispatchTouchEvent()将事件传递给子类,其中onInterceptTouchEvent()决定是否拦截传递的事件,若不拦截则在子类通过onTouchEvent()实现具体的操作事件。
学习时间:
2021/1/27-2021/1/28
学习产出:
1、 技术博客 1 篇
|