1,解决ScrollView和ListView冲突问题(1.3)
自定义一个MyListView继承自ListView,之后重写onMeasure()方法.具体的请看ScrollView源码和ListView源码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
heightMeasureSpec=MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
2,事件分发使用的是责任链模式(1.4)
重点关注ViewGroup的dispatchTouchEvent()方法,这个方法里面的while()循环。这里懂了就知道为什么必须返回onTouchEvent()必须要返回true才能响应后续事件。
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
3,自定义属性的一些细节(1.5,第二节)
1,在value文件夹下面新建attrs文件(名字可以随便取)
<resources>
<declare-styleable name="MyTextView">
<attr name="text" format="string"/>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="maxLength" format="integer"/>
<attr name="background" format="reference|color"/>
<!--枚举-->
<attr name="inputType">
<enum name="number" value="1"/>
<enum name="text" value="2"/>
<enum name="pasword" value="3"/>
</attr>
</declare-styleable>
</resources>
2,在xml布局总引用
<LinearLayout 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"
android:orientation="vertical">
<com.example.didi.myproject.MyTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:text="Draaen"
app:textSize="18sp"
app:textColor="@color/colorAccent"/>
</LinearLayout>
3,在MyTextView中得到属性
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
public class MyTextView extends View {
private String mText;
private int mTextSize=15;//默认大小是15
private int mTextColor=Color.BLACK;//默认是黑色
public MyTextView(Context context) {
this(context,null);
}
public MyTextView(Context context,AttributeSet attrs) {
this(context, attrs,0);
}
public MyTextView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.MyTextView);
mText=typedArray.getString(R.styleable.MyTextView_text);
mTextSize=typedArray.getDimensionPixelSize(R.styleable.MyTextView_textSize,mTextSize);
mTextColor=typedArray.getColor(R.styleable.MyTextView_textColor,mTextColor);
typedArray.recycle();
}
}
3,绘制自定义TextView
3.1,绘制宽高:有三种模式,根据不同的模式确定不同的宽高
public class MyTextView extends View {
private String mText;
private int mTextSize=15;//默认大小是15
private int mTextColor=Color.BLACK;//默认是黑色
private Paint mPaint;
public MyTextView(Context context) {
this(context,null);
}
public MyTextView(Context context,AttributeSet attrs) {
this(context, attrs,0);
}
public MyTextView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.MyTextView);
mText=typedArray.getString(R.styleable.MyTextView_ylytext);
mTextSize=typedArray.getDimensionPixelSize(R.styleable.MyTextView_ylytextSize,mTextSize);
mTextColor=typedArray.getColor(R.styleable.MyTextView_ylytextColor,mTextColor);
typedArray.recycle();
mPaint=new Paint();
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);//抗锯齿
mPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
//如果给的是确定值的话,不用计算,给的是多少就是多少
int width=MeasureSpec.getSize(widthMeasureSpec);
//如果给的是wrap_content的话就要计算
if (widthMode==MeasureSpec.AT_MOST){
//计算的火长度与字体的大小和字的长度有关,用画笔来测量
Rect bounds=new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
width=bounds.width();
Log.e("yly","---"+width+"---");
}
//计算高度,与计算宽度的方法一样,因为高度也有可能是包裹内容
int height=MeasureSpec.getSize(heightMeasureSpec);
if (heightMode==MeasureSpec.AT_MOST){
Rect bounds=new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
height=bounds.height();
Log.e("yly","---"+height+"---");
}
//设置控件的宽高
setMeasuredDimension(width,height);
}
}
3.2,使用onDraw()方法绘制文本
这一步最重要的是计算baseLine基线的位置
public class MyTextView extends View {
private String mText;
private int mTextSize=15;//默认大小是15
private int mTextColor=Color.BLACK;//默认是黑色
private Paint mPaint;
public MyTextView(Context context) {
this(context,null);
}
public MyTextView(Context context,AttributeSet attrs) {
this(context, attrs,0);
}
public MyTextView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.MyTextView);
mText=typedArray.getString(R.styleable.MyTextView_ylytext);
mTextSize=typedArray.getDimensionPixelSize(R.styleable.MyTextView_ylytextSize,sp2px(mTextSize));
mTextColor=typedArray.getColor(R.styleable.MyTextView_ylytextColor,mTextColor);
typedArray.recycle();
mPaint=new Paint();
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);//抗锯齿
mPaint.setAntiAlias(true);
}
//把sp转换成px
private int sp2px(int sp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
//如果给的是确定值的话,不用计算,给的是多少就是多少
int width=MeasureSpec.getSize(widthMeasureSpec);
//如果给的是wrap_content的话就要计算
if (widthMode==MeasureSpec.AT_MOST){
//计算的火长度与字体的大小和字的长度有关,用画笔来测量
Rect bounds=new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
width=bounds.width();
Log.e("yly","---"+width+"---");
}
//计算高度,与计算宽度的方法一样,因为高度也有可能是包裹内容
int height=MeasureSpec.getSize(heightMeasureSpec);
if (heightMode==MeasureSpec.AT_MOST){
Rect bounds=new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
height=bounds.height();
Log.e("yly","---"+height+"---");
}
//设置控件的宽高
setMeasuredDimension(width,height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint.FontMetricsInt fontMetrics=mPaint.getFontMetricsInt();
int dy= (fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom;
int baseLine=getHeight()/2+dy;
canvas.drawText(mText,0,baseLine,mPaint);
}
}
3.3,设置padding值,首先在xml文件中设置padding,然后分别改变width,和height
public class MyTextView extends View {
private String mText;
private int mTextSize=15;//默认大小是15
private int mTextColor=Color.BLACK;//默认是黑色
private Paint mPaint;
public MyTextView(Context context) {
this(context,null);
}
public MyTextView(Context context,AttributeSet attrs) {
this(context, attrs,0);
}
public MyTextView(Context context,AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.MyTextView);
mText=typedArray.getString(R.styleable.MyTextView_ylytext);
mTextSize=typedArray.getDimensionPixelSize(R.styleable.MyTextView_ylytextSize,sp2px(mTextSize));
mTextColor=typedArray.getColor(R.styleable.MyTextView_ylytextColor,mTextColor);
typedArray.recycle();
mPaint=new Paint();
mPaint.setColor(mTextColor);
mPaint.setTextSize(mTextSize);//抗锯齿
mPaint.setAntiAlias(true);
}
//把sp转换成px
private int sp2px(int sp){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
//如果给的是确定值的话,不用计算,给的是多少就是多少
int width=MeasureSpec.getSize(widthMeasureSpec);
//如果给的是wrap_content的话就要计算
if (widthMode==MeasureSpec.AT_MOST){
//计算的火长度与字体的大小和字的长度有关,用画笔来测量
Rect bounds=new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
width=bounds.width()+getPaddingLeft()+getPaddingRight();
}
//计算高度,与计算宽度的方法一样,因为高度也有可能是包裹内容
int height=MeasureSpec.getSize(heightMeasureSpec);
if (heightMode==MeasureSpec.AT_MOST){
Rect bounds=new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
height=bounds.height()+getPaddingBottom()+getPaddingTop();
}
//设置控件的宽高
setMeasuredDimension(width,height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint.FontMetricsInt fontMetrics=mPaint.getFontMetricsInt();
int dy= (fontMetrics.bottom-fontMetrics.top)/2-fontMetrics.bottom;
int baseLine=getHeight()/2+dy;
Rect bounds =new Rect();
mPaint.getTextBounds(mText,0,mText.length(),bounds);
int dx=(getWidth()-bounds.width())/2;
//还有一种方法计算:dx=getPaddingLeft()
canvas.drawText(mText,dx,baseLine,mPaint);
}
}
最后讲解一个面试题:
如果自定义的MyTextView继承自LinearLayout的话字体能够正常显示?答案是:如果MyTextView设置了背景色就会显示没如果不设置背景色就不会显示,下面解释为什么:
LinearLayout继承自ViewGroup,viewGroup继承自View,所以直接看View中的draw()方法,因为draw方法内部调用的是onDraw()。
在这个方法里主要关注三个方法:(这里使用到了模板设计模式)
// Step 3, draw the content最重要的还是画,
//可以看出只要dirtyOpaque为false就会执行onDraw()
if (!dirtyOpaque) onDraw(canvas);
dispatchDraw(canvas);
onDrawForeground(canvas);
那说了半天了直接继承自View就可以出来,但是继承自LinearLayout就不行了呢?
我们可以知道dirtyOpaque这个值是有下面决定的:
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
而privateFlags决定着整个,
final int privateFlags = mPrivateFlags;
直接找mPrivateFlgs在哪赋值就行了。在View构造方法中的最后一行调用了
computeOpaqueFlags()这个方法:
protected void computeOpaqueFlags() {
// Opaque if:
// - Has a background
// - Background is opaque
// - Doesn't have scrollbars or scrollbars overlay
if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
}
final int flags = mViewFlags;
if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
}
}
下面再来看ViewGrou为什么出不来,主要是因为ViewGroup调用了
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
setFlags会导致mPrivateFlags会重新赋值。那么if (!dirtyOpaque) onDraw(canvas);就进不去了,
但是为什么有了北京就有进去了呢?
在view中有这样一个方法:
public void setBackgroundDrawable(Drawable background) {
computeOpaqueFlags();
这里可以看到computeOpaqueFlags()又计算了一遍。所以就出来了
解决这个问题有以下三种方法:
1,设置透明背景色:
setBackgroundColor(Color.TRANSPARENT);
2,把onDraw()方法变为dispatchOnDraw()
3,改写setWillNotDraw()
wm.addView(decor,1)才开始把我们的DecorView加载到我们的Windows中去
这个时候才开始View的绘制流程
measure()——>layout()——>draw()流程才开始
对于View view=View.inflate(this,R.layout.activity,null/textView),这句话,是不可获取View的高度的,因为这只是把View实例化了而已,并没有把他加载到任何父布局.如果把第三个参数改成一个ViewGroup就能够获取到。
WindowManager是一个接口,其实现类是WindowMangerImpl,所以接下来就应该去WindowManagerImpl
wm.addView(decor, l);---->WindowManagerImpl.addView()---->mGlobal.addView()这就来到了WindowManagerGlobal的addView的方法中,首先关注一下:
ViewRootImpl root;是不是优点熟悉这个root,root到底是在哪里实例化的呢?
往下面看就能看到了:
root = new ViewRootImpl(view.getContext(), display);(ViewRootImpl类是继承自ViewParent,ViewParent是一个接口,其中requestLayout(),invalidateChildInParent()等方法均在这个接口里面)接着执行了下面三个,把他们加入集合:
在invalidate()源码的时候讲解了ViewRootImpl这个类
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
关键是下面一句话:
root.setView(view, wparams, panelParentView);接着会执行ViewRootImpl中的:
requestLayout();---->scheduleTraversals()---->postCallback(mTraversalRunnable)---->doTraversal()---->performTraversals()在这个方法中开始执行:
performMeasure()----->performLayout()------>perforDraw()
下面就开始讲activity_main.xml布局的绘制的流程:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.myproject.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
/>
<TextView
android:id="@+id/textView_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
/>
<TextView
android:id="@+id/textView_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
/>
</LinearLayout>
</LinearLayout>
在这个布局中三个TextView放在第二层LinearLayout中,在绘制View的时候首先执行performTraversals()中的performMeasure()方法:
performMeasure()调用:
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
mView(其实也就是Decore)会通过measure会调用第一层布局LinearLayout的onMeasure,这个时候就应该来到LinearLayout的onMeasure()方法中:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
在这里进入measureVertical()之后,会执行measureChildBeforeLayoute(),之后执行measureChildWithMargins,
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
由此可以看出,子View的大小和模式是父布局和子View共同决定的。
child.measure()方法会执行View中的measure()方法,在View的measure()方法中会执行onMeasure()方法,onMeasure()方法没有做什么特别的事情,主要就是赋值:
onMeasure---->setMeasuredDimension---->setMeasuredDimensionRaw在这个方法中进行了最终的赋值:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
至此View就能获取到高度和宽度了。
View的大小和模式确定之后接下来就要测量夫布局的宽高和模式了;总结起来测量可以这么说:
确定子View的尺寸的时候是从外往里走的:ViewRootImpl--->Decore--->ViewGroup--->子View
确定完成子View尺寸之后就要向外走了:ViewRootImpl<----Decore<----ViewGroup<----子View
下面讲解performLayout方法:会调用
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());(在这里host肯定是最外层的Decor布局)
view类中的onLayout是一个空方法,谁需要实就去按照自己的标准去实现(用于摆放子View),在这里举个例子看看LinearLayout中的onLayout方法,
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
在LinearLayout的onLayout方法中首先判断布局是垂直还是水平,因为垂直和水平的时候测量的是不一样的,然后会调用setChildFrame方法,在这个方法中会调用child.layout()
接下来就要讲解performDraw方法了,它主要的作用就是用来绘制自己了子View(当然也包括绘制背景):
performDraw调用了View的draw()方法,在View的draw()方法中,利用了模板设计模式,在draw的时候会遵循一套流程:
draw BackGround(画背景,如果设置的有背景,就要先绘制自己的背景,然后采取遍历子View绘制他们)
onDraw(canvas)画自己,ViewGroup默认不会调用这个方法
dispatchDraw(canvas) 画子View,肯定也是不断的循环调用子View的onDraw方法
然后dispatchDraw()这个方法中什么都没有写,这就是给了你一套模板,让你自己去实现,(模板设计模式在AsynTask中也使用到了)
ViewGroup默认情况下不会走onDraw方法,
总结一下:
第一步:performMeasure(),用于指定和测量layout中所有控件的宽高,对于ViewGroup要先去测量里面子孩子,然后根据子孩子的宽高再来计算和自定义自己的宽高,对于View,它的宽高是由自己和父布局决定的。
第二步:performLayout(),用于摆放子布局for循环所有子View,用child.layout()摆放ChildView
第三步:performDraw(),用于绘制自己还有子View,对于ViewGroup来说,首先绘制自己的背景,for循环绘制子view,调用子view的
draw()方法,对于View来说,只需要绘制自己的背景和绘制自己要显示的内容。