getViewTreeObserver().addOnGlobalLayoutListener() 与 getViewTreeObserver().removeOnGlobalLayoutListener() 在 View 的绘制流程中何时被调用
答案就在 ViewRootImpl#performTraversals()
private void performTraversals() {
final View host = mView;
...
// 重点看 layout 过程
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
// 若didLayout为true,则对view进行布局
// 同时,上面的triggerGlobalLayoutListener也就为 teue
if (didLayout) {
// 调用 performLayout() 对 view 进行布局
// 感兴趣的自己跟踪,最终会进入到 view 的 onLayout() 方法中
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
}
// 若进行了重新布局,则triggerGlobalLayoutListener 为 teue
if (triggerGlobalLayoutListener) {
mAttachInfo.mRecomputeGlobalAttributes = false;
// 则通过 dispatchOnGlobalLayout() 通知 全局布局发生改变
mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
}
// 省略 其他操作以及 draw 过程
...
}
复制代码
getSuggestedMinimumWidth()和getSuggestedMinimumHeight()返回的值是多少
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
复制代码
如果这个view没有设置背景,那么返回的是mMinWidth或者mMinHeight,这两个值对应的是android:minWidth和android:minHeight,但是如果设置了background,就取background.getMinimumHeight和mMinHeight的大值。 这里的 mBackground 是一个 Drawable 对象
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
复制代码
getMinimumHeight返回的就是drawable的原始高度。
Drawable 的宽高
Drawable 通过从drawable、drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等资源文件夹加载资源文件时,如果设备的屏幕密度高于当前drawable目录所代表的密度,则图片会被放大,否则会被缩小。图片放在drawable中,等同于放在drawable-mdpi中,原因为:drawable目录不具有屏幕密度特性,所以采用基准值,即mdpi。
放大或缩小比例 = 设备屏幕密度 / drawable目录所代表的屏幕密度。
图片大小以及dp和px关系一览表
为了更全面的适配所有设备,我们应该提供一套针对主流屏幕密度的图片(目前为hdpi或xhdpi),其他密度通过系统自动缩放得到图片。
布局文件中的dp与px的关系
px:对应于屏幕上的实际像素。
dp/dip:密度无关像素 - 基于屏幕物理密度的抽象单元。 这些单位相对于160 dpi的屏幕,因此一个dp是160 dpi屏幕上的一个像素。 dp与像素的比率将随着屏幕密度而变化,但不一定是成正比的。 注意:编译器同时接受“dip”和“dp”,但“dp”更符合“sp”。
android中的dp在渲染前会将dp转为px,计算公式:
- px = density * dp;
- density = dpi / 160;
- px = dp * (dpi / 160); 而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。
布局优化
:将不同布局中的公共布局抽取出来重复使用。
:将该标签当成该布局的根节点使用。而当在其他位置需要引用该布局时,则使用
标签进行引用,同时该节点会同步变成父容器的根节点。可以省略一些不必要的布局嵌套。
:只要在需要的时候显示它,才会进行加载。使用的时候findViewById()并调用setVisibility(View.VISIBLE)或者inflate()显示它就可以了。ViewStub所要替代的layout文件中不能含有
标签。一旦ViewStub被显示后,则ViewStub将从视图框架中移除,其id也会失效,此时findViewById()得到的也是空的。
scrollTo()和scrollBy()的功能
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public 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();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
复制代码
从源码中可以看出,scrollBy 方法调用了 scrollTo 方法。scrollTo 方法更新了 View 中的 mScrollX 和 mScrollY 这两个属性,回调了 View 的 onScrollChanged 方法,参数为之前的旧值和新设置的值。然后进行屏幕刷新。
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollY;
复制代码
mScrollX 和 mScrollY 分别代表的是 View 的内容的横纵坐标的偏移量。如果是 ViewGroup 的话作用的就是它的所有子 View,如果是 TextView 的话则作用的就是 TextView 的内容,如果是 ImageView 的话,作用的是 ImageView 的 src 属性,background 属性不受影响。这两个api作用的对象是 View 的内容而不是 View 本身。 postInvalidateOnAnimation 方法最终会调用到
/**
* Mark the area defined by dirty as needing to be drawn. If the view is
* visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
* point in the future.
*
* This must be called from a UI thread. To call from a non-UI thread, call
* {@link #postInvalidate()}.
*
* WARNING: In API 19 and below, this method may be destructive to
* {@code dirty}.
*
* @param dirty the rectangle representing the bounds of the dirty region
*/
public void invalidate(Rect dirty) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
dirty.right - scrollX, dirty.bottom - scrollY, true, false);
}
复制代码
都是减 scrollX,减 scrollY,所以导致 scrollTo 位移的方向都是相反的。 总结上述,scrollTo 会遍历所有子 View 进行重新绘制并把 scrollX,scrollY 算在其内容的绘制上,导致了作用的就是它内容,并且移动的方向是相反的。
View 的位置参数
由上图可总结如下:
①top、left、right、bottom代表View的初始坐标,在绘制完毕后就不会再改变
②translationX、translationY代表View左上角的偏移量(相对于父容器),比如上图是View进行了平移,那么translation代表水平平移的像素值,translationY代表竖直平移的像素值。
③x、y代表当前View左上角相对于父容器的坐标,即实时相对坐标。
④以上参数的关系可以用这两个公式表示:x = left + translationX 和y = top+ translationY。
View 位移 API
scrollTo或scrollBy
textview.scrollBy(-20,0) //将textview里面的文字向右滑动20px
复制代码
属性动画
ObjectAnimator.ofFloat(view,"translationX",0,100).setDuration(500).start();
复制代码
布局参数
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) imageView.getLayoutParams(); //获取imageView的布局参数
params.leftMargin += 100; //修改leftMargin的值,相当于xml布局文件中的margin_left的值
imageView.setLayoutParams(params); //将新的params值设置进imageView
复制代码
layout()
view.layout(view.getLeft()+offsetX,view.getTop()+offsetY,view.getRight()+offsetX,view.getBottom()+offsetY);
复制代码
offsetLeftAndRight 和 offsetTopAndBottom
imageView.offsetLeftAndRight(50); //将imageView沿水平正方向偏移50px
imageView.offsetTopAndBottom(50); //将iamgeView沿竖直正方向偏移50px
复制代码
Scroller 原理
Scroller有一个模板代码,基本上实现弹性滑动都是这样写的,以下给出模板代码:
//实例化Scroller对象,在自定义View中,mContext可以在自定义View的构造方法中获取
Scroller mScroller = new Scroller(mContext);
//在一个自定义View中实现该方法,方法名可以自定义
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int scrollY = getScrollY();
int dx = destX - scrollX;
int dy = destY - scrollY;
//前两个参数表示起始位置,第三第四个参数表示位移量,最后一个参数表示时间
mScroller.startScroll(scrollX,scrollY,dx,dy,1000);
invalidate();
}
//自定义View中重写该方法
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
复制代码
Scroller的构造方法:
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
复制代码
由构造方法可以知道,如果说只传递了一个Context值,那么会默认把Interpolator设置为null,这里的Interpolator是插值器,所谓插值器,就是让动画按照某一规律来演示,比如说加速、减速、抛物线等。
Scroller的 startScroll() 方法
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
复制代码
该方法并无做实际的滑动,只是为scroller作一下参数的赋值,mStartX、mStartY表示滑动的起点;mFinalX,mFinalY表示滑动的终点;mDeltalX,mDeltalY表示要滑动的距离;mStartTime记录了滑动开始的时间。这里没有实际滑动的方法,接着调用 View#invalidate() 方法,该方法最终会调用View 的 draw()方法,进行View的重绘。在 draw() 方法中会调用 ViewGroup#dispatchDraw(canvas)方法。接着调用对每个子 View 调用 ViewGroup#drawChild() 方法。这一次是子View对自身进行绘制。View#draw()方法:
...
int sx = 0;
int sy = 0;
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
...
复制代码
在这里,子View调用了computeScroll()方法,而View#computeScroll()是一个空实现,所以子View需要重写这个方法。比如说一个ViewGroupA里面有一个ViewA,那么需要对这个ViewA滑动,我们可以重写ViewGroupA的computeScroll()方法,这样就做到了ViewGroupA内容的滑动,即ViewA的滑动了;如果重写的是ViewA的computeScroll()方法,则不会达到滑动的效果,就是ScrollTo/By是针对View的内容滑动的。
重写的computeScroll()方法中调用了Scroller.computeScrollOffset()方法,这个方法判断是否还需要进行滑动:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
...
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
...
}
复制代码
这里主要判断了滑动时间与设置的时间的大小以及对mCurrX和mCurrY进行动态修改,这样每一次滑动都会向前滑动一点。此时经过computeScrollOffset()的判断,如果还要进行滑动,则执行scrollTo()方法,可以看到,这里才实际上进行了View的滑动,接着进行postInvalidate()方法,这个方法就是invalidate()方法,只不过是在不同线程的情况下会调用该方法,那么,现在就很明朗了,在computeScroll()的最后,再次调用了invalidate()方法,那么导致View树进行第二次重绘,流程重新执行一遍,再次调用computeScroll(),然后再次获取currX和currY值,再次scrollTo...不断重复,直到在Scroller.computeScrollOffset()方法处判断为false,跳出循环,此时便完成了View的滑动过程。
View#setWillNotDraw(boolean willNotDraw)
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
复制代码
如果一个View不需要绘制任何内容,那么系统会对View的绘制进行优化,即不会调用到onDraw()方法,而系统判定是否需要进行优化的参数是willNotDraw。默认地,一个View继承了Viwe则这个参数设置为false,此时不优化;但一个ViewGroup默认会设置willNotDraw为true,即View树重绘的时候不会调用到ViewGroup的onDraw()方法。这也是为什么在滑动的时候,进行了View树的重绘而ViewGroup的onDraw()方法始终没有调用。所以,如果要使ViewGroup的onDraw()方法得到调用,那么我们在实例化这个ViewGroup的时候应该调用这个方法:setWillNotDraw(false),设置不对ViewGroup进行优化,或者这样:为ViewGroup设置一个background属性(xml布局中),那么系统就会认为该ViewGroup存在内容了,此时就会每一次都调用onDraw()方法了。
ViewGroup重绘的时候,子View的onDraw()方法有没有调用呢?从理论上分析,我们在调用ViewGroup的重绘的时候是会调用到子View的draw()方法的,在draw()方法的内部又会调用onDraw()方法的,因此我们可以在子View的onDraw()方法内打印一下日志,事实上,在当前的Scroller背景下,子View的onDraw()方法是没有被调用的,但这个和上面说到的willNotDraw没有关系,因为子View是默认不开启优化的,那么到底为什么呢?其实在View的内部有一个标志参数,用来标志当前View是否需要重绘,如果这个View的内容没有改变,那么系统就会认为这个View不需要重新绘制,所以就不会调用子View的onDraw()方法了,由于当前的Scroller方法并没有对子View的内容作用,因此子View最终也没有调用这个onDraw()方法。
获取 View 的 Bitmap
方法一:
View实际上可以在Canvas类的任何实例上绘制自己(通常在测量和布局过程结束时,它将自己绘制到View类内部的Canvas实例上)。 所以你可以传递一个Canvas的新实例,它是由你创建的Bitmap对象构造的,如下所示:
Bitmap b = Bitmap.createBitmap(targetView.getWidth(),
targetView.getHeight(),
Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
targetView.draw(c);
BitmapDrawable d = new BitmapDrawable(getResources(), b);
canvasView.setBackgroundDrawable(d);
复制代码
方法二:
获取一个 View 的 drawingCache,它是一个 View 自己对其 bitmap 的引用。 请注意,drawingCaches 并不总是可用,因此可能需要实际让 View 生成它。 可以通过在View 上设置 setDrawingCacheEnabled(true) 或手动调用View.buildDrawingCache() 然后使用以前面提到的相同方式生成的Bitmap来完成此操作。 我发现的问题是生成的位图可以在您不知情的情况下进行处理。 由于建议调用 buildDrawingCache 应与destroyDrawingCache() 匹配,因此必须将该 bitmap 数据复制到拥有的 bitmap 中。如果使用DrawingCache,则对要截图的View有一个要求:View本身已经显示在界面上。如果View没有添加到界面上或者没有显示(绘制)过,则buildDrawingCache会失败。
targetView.buildDrawingCache();
//targetView.setDrawingCacheEnabled(true);这行代码和上面效果一样
Bitmap b1 = targetView.getDrawingCache();
Bitmap b = b1.copy(Bitmap.Config.ARGB_8888, false);//创建一个DrawingCache的拷贝,因为DrawingCache得到的位图在禁用后会被回收
BitmapDrawable d = new BitmapDrawable(b);
canvasView.setBackgroundDrawable(d);
targetView.destroyDrawingCache();
targetView.setDrawingCacheEnabled(false); //禁用DrawingCahce否则会影响性能
复制代码
为什么 getDrawingCache 性能很差
一旦开启 DrawingCache 之后,每次调用 getDrawingCache 时,都会重新调用 buildDrawingCache 方法来建立新的 DrawingCache。但是大部分情况下,View 的状态不会发生改变,因此如果在讲究效率的方法中调用 getDrawingCache 方法,将会降低效率(例如:View 的 onDraw 方法)。而且 DrawingCache 的品质使用的是 ARGB_8888,这将非常占用性能和内存。
为什么 getDrawingCache 常常返回 null
- 首先 View 需要启用 DrawingCache,或者调用 buildDrawingCache 方法。
- 调用 getDrawingCache 方法的时机需要在 View 执行 measure 和 layout 之后才可以被绘制出来。 如果 View 没有加入至 Activity 或 Fragment 的RootView中,也可以取得DrawingCache:
...
ViewGroup viewGroup = new FrameLayout(getContext());
ImageView image = new ImageView(getContext());
image.setImageResource(R.drawable.ic_launcher);
viewGroup.addView(image);
viewGroup.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
viewGroup.layout(0, 0, viewGroup.getMeasuredWidth(), viewGroup.getMeasuredHeight());
viewGroup.buildDrawingCache();
Bitmap bitmap = viewGroup.getDrawingCache();
...
复制代码
- 如果经过以上操作还是返回 null,那么就有可能是需要绘制的内容太大,超过 Android 系统预设的 drawingCacheSize,所以系统就不給画啦!
ViewConfiguration.get(context).getScaledMaximumDrawingCacheSize();
复制代码
对于WebView的截图有一点特殊,网页内容并不能在布局完成后立即渲染出来,因为WebView大小的变化就相当与桌面浏览器窗口大小的变化,Webkit需要根据窗口大小重新渲染所有的内容,这最多大概需要300ms的时间(对于不同性能的设备、网页复杂程度和Webkit版本可能不同)。如果创建后台的WebView需要截图的话,应该在创建时就对其进行布局操作,这样加载完成后大部分就已经渲染完毕了(除非有异步的js处理)。
不使用getDrawingCache的替代方法
大致上的概念是自行建立出 Bitmap,並且使用一个 Canvas 在这个 Bitmap 上作画,只要调用 View 的 draw 方法,将自己的 Canvas 作为参数传入,结果就会出现在 Bitmap 上了。
public Bitmap getMagicDrawingCache(View view) {
Bitmap bitmap = (Bitmap) view.getTag(cacheBitmapKey);
Boolean dirty = (Boolean) view.getTag(cacheBitmapDirtyKey);
int viewWidth = view.getWidth();
int viewHeight = view.getHeight();
if (bitmap == null || bitmap.getWidth() != viewWidth || bitmap.getHeight() != viewHeight) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
bitmap = Bitmap.createBitmap(viewWidth, viewHeight, bitmap_quality);
view.setTag(cacheBitmapKey, bitmap);
dirty = true;
}
if (dirty == true || !quick_cache) {
bitmap.eraseColor(color_background);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
view.setTag(cacheBitmapDirtyKey, false);
}
return bitmap;
}
复制代码
其中,cacheBitmapKey和cacheBitmapDirtyKey為相異的整數數值,分別用來指定View的Tag ID。cacheBitmapKey的位置會存放使用這個方法建立出來的DrawingCache;cacheBitmapDirtyKey的位置會存放這個View的DrawingCache是否已經髒掉了(dirty)而需要呼叫View的draw方法重新繪製。DrawingCache所用的Bitmap只在沒有Bitmap物件或是Bitmap物件的大小和View的大小不合的時候才重新建立,在建立新的Bitmap前會先將先前的Bitmap進行recycle,新的Bitmap物件的參考會再被存入至View的Tag中。quick_cache若設定為false,則不論DrawingCache是否dirty,都進行重繪,只有在View常常變化的時候才需要這樣做。bitmap_quality可以設定為Bitmap.Config.RGB_565或是Bitmap.Config.ARGB_8888,Bitmap.Config.ARGB_4444已經隨著Android API層級愈來愈高而慢慢被禁用了,在實際應用上,RGB_565雖然沒有透明層,但是效能會比ARGB_8888還要好很多。
如果要加入View不在Activity或是Fragment的RootView中的判斷的話,可以寫成以下程式:
public Bitmap getMagicDrawingCache(View view) {
Bitmap bitmap = (Bitmap) view.getTag(cacheBitmapKey);
Boolean dirty = (Boolean) view.getTag(cacheBitmapDirtyKey);
if (view.getWidth() + view.getHeight() == 0) {
view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
int viewWidth = view.getWidth();
int viewHeight = view.getHeight();
if (bitmap == null || bitmap.getWidth() != viewWidth || bitmap.getHeight() != viewHeight) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
bitmap = Bitmap.createBitmap(viewWidth, viewHeight, bitmap_quality);
view.setTag(cacheBitmapKey, bitmap);
dirty = true;
}
if (dirty == true || !quick_cache) {
bitmap.eraseColor(color_background);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
view.setTag(cacheBitmapDirtyKey, false);
}
return bitmap;
}
复制代码
View#setSystemUiVisibility
- SYSTEM_UI_FLAG_LOW_PROFILE
设置状态栏和导航栏中的图标变小,变模糊或者弱化其效果。这个标志一般用于游戏,电子书,视频,或者不需要去分散用户注意力的应用软件。同时,点击状态栏和导航栏相应的位置,这些图标的效果会还原成正常的状态。
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
复制代码
- SYSTEM_UI_FLAG_HIDE_NAVIGATION
隐藏导航栏,点击屏幕任意区域,导航栏将重新出现,并且不会自动消失。
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
复制代码
- SYSTEM_UI_FLAG_FULLSCREEN
隐藏状态栏,点击屏幕区域不会出现,需要从状态栏位置下拉才会出现。
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
复制代码
- SYSTEM_UI_FLAG_LAYOUT_STABLE
稳定布局,主要是在全屏和非全屏切换时,布局不要有大的变化。一般和View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN、View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION搭配使用。同时,android:fitsSystemWindows要设置为true。
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
复制代码
- SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
将布局内容拓展到导航栏的后面。
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
复制代码
- SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
将布局内容拓展到状态的后面。
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
复制代码
- SYSTEM_UI_FLAG_IMMERSIVE
使状态栏和导航栏真正的进入沉浸模式,即全屏模式,如果没有设置这个标志,设置全屏时,我们点击屏幕的任意位置,就会恢复为正常模式。所以,View.SYSTEM_UI_FLAG_IMMERSIVE都是配合View.SYSTEM_UI_FLAG_FULLSCREEN和View.SYSTEM_UI_FLAG_HIDE_NAVIGATION一起使用的。
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE);
复制代码
对比View.SYSTEM_UI_FLAG_HIDE_NAVIGATION的效果,可以看出来,在没有设置View.SYSTEM_UI_FLAG_IMMERSIVE时,随便点击屏幕就可以解除隐藏导航栏的状态。所以,设置View.SYSTEM_UI_FLAG_IMMERSIVE就是真正进入沉浸模式。
- SYSTEM_UI_FLAG_IMMERSIVE_STICKY
它的效果跟View.SYSTEM_UI_FLAG_IMMERSIVE一样。但是,它在全屏模式下,用户上下拉状态栏或者导航栏时,这些系统栏只是以半透明的状态显示出来,并且在一定时间后会自动消息。
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
复制代码
- 额外补充
在设置全屏和非全屏的时候,可以通过下面的方法实现,代码如下:
if (mVisible){ //全屏
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
} else { //非全屏
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
);
}
复制代码
WindowInsets
WindowInsets是应用于应用程序窗口的系统视图(例如状态栏,导航栏)的插图(或大小)。
android:fitsSystemWindows="true"
当布局具有此标志时,表示此布局要求系统应用窗口插图。ViewCompat.setOnApplyWindowInsetListener。 使用此API,可以将窗口插图应用于特定视图。
CoordinatorLayout 会更改默认的 WindowInsets 处理,将 WindowInsets 进行分发。
private void setupForInsets() {
if (Build.VERSION.SDK_INT < 21) {
return;
}
if (ViewCompat.getFitsSystemWindows(this)) {
if (mApplyWindowInsetsListener == null) {
mApplyWindowInsetsListener =
new android.support.v4.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
return setWindowInsets(insets);
}
};
}
// First apply the insets listener
ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener);
//设置 Layout FULLSCREEN,从而获取 WindowInsets 的处理
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
} else {
ViewCompat.setOnApplyWindowInsetsListener(this, null);
}
}
final WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
if (!objectEquals(mLastInsets, insets)) {
mLastInsets = insets;
mDrawStatusBarBackground = insets != null && insets.getSystemWindowInsetTop() > 0;
setWillNotDraw(!mDrawStatusBarBackground && getBackground() == null);
//将 WindowInset 优先分发给 Behaviors
insets = dispatchApplyWindowInsetsToBehaviors(insets);
requestLayout();
}
//如果 WindowInset 被 Behaviors 消费后,后续默认的分发流程就不会继续进行了
return insets;
}
private WindowInsetsCompat dispatchApplyWindowInsetsToBehaviors(WindowInsetsCompat insets) {
if (insets.isConsumed()) {
return insets;
}
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
if (ViewCompat.getFitsSystemWindows(child)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
insets = b.onApplyWindowInsets(this, child, insets);
if (insets.isConsumed()) {
//当 WindowInset 被消费后,停止分发
break;
}
}
}
}
return insets;
}
复制代码
像FrameLayout,LinearLayout或RelativeLayout这样的标准布局不会将窗口插图传递给它们的子节点,我们可以将标准布局子类化并将插图传递给布局的子级。 我们只需要覆盖onApplyWindowInsets方法。
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
int childCount = getChildCount();
for (int index = 0; index < childCount; ++index)
getChildAt(index).dispatchApplyWindowInsets(insets);
// let children know about WindowInsets
return insets;
}
复制代码
标签
在布局文件里面被此标签包裹的视图布局将会闪烁。原理就是使用Handler每隔500ms发送消息进行invalidate()
操作,以此来刷新界面造成闪烁的效果。
复制代码
LinearLayout#android:divider,LinearLayout#android:showDividers
同时使用 android:divider,android:showDividers,否则默认为none。 共有四个选项:middle , beginning , end and none。beginning 和 end 将分别仅应用于开始和结束。
android:showDividers="beginning|middle|end"
使用 |
合并多种设置。
"@drawable/divider"
android:showDividers="middle">
复制代码
CoordinatorLayout 子 View 的 app:layout_insetEdge 和 app:layout_dodgeInsetEdges 属性
layout_insetEdge:Gravity 值。描述 ChildView 如何插入 CoordinatorLayout。
dodgeInsetEdges:Gravity 值。描述 ChildView 是否躲避 layout:insetEdge 设置为该值的 ChildViews。
//FloatingActionButton
CoordinatorLayout.LayoutParams clp;
clp.insetEdge = Gravity.BOTTOM;
CoordinatorLayout.LayoutParams lp;
lp.dodgeInsetEdges = Gravity.BOTTOM;
复制代码
所以 FloatingActionButton 会随着 SnackBar 的滑动上移。
//inset 是由 onChildViewsChanged 方法计算的,各个 Gravity 插入的 ChildViews 所需要的最大 left,right,top,bottom 的值,这里可以理解为:各个方向的边距
private void offsetChildByInset(final View child, final Rect inset, final int layoutDirection) {
...
setInsetOffsetY();
...
setInsetOffsetX();
...
}
private void setInsetOffsetX(View child, int offsetX) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mInsetOffsetX != offsetX) {
final int dx = offsetX - lp.mInsetOffsetX;
ViewCompat.offsetLeftAndRight(child, dx);
lp.mInsetOffsetX = offsetX;
}
}
private void setInsetOffsetY(View child, int offsetY) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mInsetOffsetY != offsetY) {
final int dy = offsetY - lp.mInsetOffsetY;
ViewCompat.offsetTopAndBottom(child, dy);
lp.mInsetOffsetY = offsetY;
}
}
复制代码
ViewOutlineProvider 裁剪 View
可以用它来把 View 裁剪成一些特定(圆形、矩形、圆角矩形)的形状:
view.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 30);
}
});
view.setClipToOutline(true);
复制代码
也可以用来设置投影,但是投影的形状只能是凸多边形:
view.setElevation(5);//设置阴影
view.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
//你可以用 Path 指定任何的形状,前提是凸多边形
//这里设置投影的位置从右下角开始,投影形状是矩形
Path path = new Path();
path.moveTo(view.getWidth(), view.getHeight());
path.lineTo(view.getWidth(), view.getHeight() * 2);
path.lineTo(view.getWidth() * 2, view.getHeight() * 2);
path.lineTo(view.getWidth() * 2, view.getHeight());
path.close();
outline.setConvexPath(path);
}
});
复制代码
AppBarLayout 可以在 CoordinatorLayout 与 RecyclerView 联合使用实现滑动传递,但是却不能与 ListView 实现滑动传递
AppBarLayout 的源码上加了注解 @DefaultBehavior(AppBarLayout.Behavior.class)
实现了以下接口:
void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed)
void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)
void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target)
复制代码
在 CoordinatorLayout 中实现了以下代码,表明了 CoordinatorLayout 可以对其子 View 进行通知:
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
nestedScrollAxes);
handled |= accepted;
lp.acceptNestedScroll(accepted);
} else {
lp.acceptNestedScroll(false);
}
}
return handled;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
}
}
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onStopNestedScroll(this, view, target);
}
lp.resetNestedScroll();
lp.resetChangedAfterNestedScroll();
}
mNestedScrollingDirectChild = null;
mNestedScrollingTarget = null;
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
final int childCount = getChildCount();
boolean accepted = false;
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed);
accepted = true;
}
}
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
复制代码
现在的问题是当 CoordinatorLayout 的子 View 滚动发生时,CoordinatorLayout如何被其子 View 通知。
RecyclerView 实现了 NestedScrollingChild 接口。此接口应由希望支持将嵌套滚动操作分派给协作父 ViewGroup 的 View 子类实现。
为了要接收被调度的嵌套滚动操作,CoordinatorLayout 需要实现 NestedScrollingParent 接口。此接口应由希望支持嵌套子 View 委派的滚动操作的父 ViewGroup 实现。
NestedScrollingChild 与 NestedScrollingParent 的交互:
当子 View 即将开始滚动(对应于 onTouch 动作 down 事件)时,它会调用 onScrollStarted 方法。 对于每个滚动步骤,子 View 将调用 dispatchPreScroll 和 dispatchScroll 两个方法。 第一种方法为父 View 提供了在子 View 使用之前消耗部分或全部滚动操作的机会。 如果父 View 返回true,则子 View 应按父 View 使用的值减少其滚动。 调用方法 dispatchNestedScroll 以向父 View 报告滚动进度。 它包含消耗和未消耗的滚动值。 父 View 可以不同地对待这些值:
例如,实现类可以选择使用所消费的部分来匹配或追逐多个子 View 的滚动位置。 未消耗部分可用于允许连续拖动多个滚动或可拖动元素,例如在垂直抽屉内滚动列表,其中一旦到达内部滚动内容的边缘,抽屉开始拖动。
CoordinatorLayout充当代理。 它接收来自子 View 的回调并将其转发给其子 View 的行为类。
当滚动过程完成时,子 View 调用 onScrollStoped 回调。
ScrollView 嵌套 ListView 时,ListView 的滑动事件会被 ScrollView 消费,NestedScrollView 嵌套 RecyclerView 解决滑动冲突
NestedScroll 机制不同于之前的 Android 事件分发与处理都是控件单方面处理的,即要不就是子View处理,要不就是父View处理。有了NestedScroll机制,父View和子View就可以同时处理一个事件了。
有关 NestedScroll 嵌套滑动机制,非常关键的几个东西是:
NestedScrollingParent
NestedScrollingChild
NestedScrollingParentHelper
NestedScrollingChildHelper
前两个是接口,后两个是辅助类。为了实现外部的滚动,控件需要实现NestedScrollingParent接口;为了能在内部滚动,控件需要实现NestedScrollingChild接口。例如NestedScrollView实现了NestedScrollingParent接口,所以它可以套别人,例如RecyclerView实现了NestedScrollingChild接口,所以它能被别人套,当然了实际NestedScrollView也实现了NestedScrollingChild接口,即它既可以套别人,也可以被套。
NestedScrollingParent:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
复制代码
NestedScrollingChild:
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
复制代码
其中,NestedScrollingParentHelper 和 NestedScrollingChildHelper 两个帮助类帮我们实现大部分的方法,我们只需要关心几个特定的方法就可以了。
从Android5.0开始View和ViewGroup已经默认实现了 NestedScrollingParent 和 NestedScrollingChild 这两个接口了。
效果图
当向上滚动时,上面的图片可见的时候最外面的Parent滚动,如果图片不可见了,那么外面的Parent不要滚动,留下那个title显示,接着里面的Child滚动就可以了;当向下滚动时,Child滚动到最上面的时候让Parent滚动就可以了。
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
其中target就是内部嵌套的View,这里就是我们的Child,dx是x方向的滑动偏移,dy是y方向的滑动偏移,consumed的含义是消耗,说子控件滑动时的偏移父控件想要消耗多少。它是一个长度是2的int数组,consumed[0]代表x轴,consumed[1]代表y轴。
例如我们想要把所有的y轴的滑动事件都消耗掉,那么就让consumed[1] = dy;这样子控件会在父控件的这个方法执行后重新计算还剩余多少可以供自己用,如果父控件的consumed[1] = dy,那么子控件的剩下的就是dy-consumed[1]=0,也就是不进行任何滑动处理了。例如我们可以让consumed[1] = dy/2,这样我们手指滑动时父控件和子控件各滑动一半。
当向上滚动时(dy>0),上面的图片可见的时候(getScrollY() < mImgHeight)最外面的Parent滚动,如果图片不可见了,那么外面的Parent不要滚动,留下那个title显示,接着里面的Child滚动就可以了;当向下滚动时(dy < 0),Child滚动到最上面的时候(target.getScrollY() <= 0)让Parent滚动就可以了。
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
调用后会如果返回了true,那么证明Parent是要对滑动事件进行消耗的,那么消耗后剩余的就是dy - mScrollConsumed[1];这和我们之前说的Parent的是一致的。
滑动在onTouchEvent里,经过了三个过程:
-
从手指点击下去开始Down,在里面可以调用startNestedScroll(),告诉 Parent,Child准备进入滑动状态了,这时候Parent会被回调onStartNestedScroll(),如果这个方法返回true,代表它接受嵌套滑动,紧接着Parent会被回调onNestedScrollAccepted()来记录滑动的方向。
-
手指滑动Move,需要问一下Parent 是否需要滑动,即调用dispatchNestedPreScroll(),这时Parent会被回调onNestedPreScroll()。如果Parent要滑动,即消耗事件那么这个就会返回true,我们需要重新计算一下父类滑动后剩下给你的滑动距离,之后Child进行剩余的滑动,即调用overScrollByCompat,把滑动距离的参数传给mScroller进行弹性滑动,最后,如果滑动距离还有剩余,Child就再问一下,Parent 是否需要在继续滑动你剩下的距离,也就是调用dispatchNestedScroll()。这时候Child就会在滑动之后报告滑动的情况,即Parent的onNestedScroll会被调用。
-
手指离开Up,会调用dispatchNestedPreFling,从这里开始,分配fling和分配scroll的过程是一样的,fling也会有和scroll对应的方法。最后Child调用stopNestedScroll,然后Parent被调用onStopNestedScroll结束滑动。
https://github.com/jackzhengpinwen/kiwiViews/tree/master/app/src/main/java/com/zpw/views/exercise24
TextView 里按照不同的判断条件使用了不同的 Layout 继承类型
TextView 确定与调用 Layout 主要在这几个过程:
1.在 TextView 的 onMeasure 的时候,如果还没有 Layout,在 makeNewLayout 方法中开始选择需要的 Layout。
2.如果 TextView 设定为单行模式了,运行 makeSingleLayout 来选择单行情况下的 Layout。
3.如果是属于 Spannable 的文本对象,使用动态布局 DynamicLayout,否则,使用 isBoring 判断是不是单纯的单行布局,是则使用 BoringLayout,其他情况使用 StaticLayout。
4.如果不是单行的,同样按照这个逻辑区分出 BoringLayout,DynamicLayout 与 StaticLayout,参数略有不同。
5.获得测量结果的高宽,参与计算。
6.如果需要重新测量,运行 View 的 requestLayout 方法。
7.在 onDraw 的时候,暂存已有的 canvas。
8.调用 layout.draw 在 canvas 上进行绘制。
9.返回结果进行叠加。
protected Layout(
CharSequence text,
TextPaint paint,
int width,
Alignment align,
TextDirectionHeuristic textDir,
float spacingMult,
float spacingAdd)
复制代码
传入的包括文字,文字样式,需求宽度,对齐方式,文本方向,行间距的增加和倍数,这些参数足以构成绘制一段文字的级别样式与大小判定。
而layout基础类中,主要提供了大量的文字测量计算的方法,主要用于获得这段文本占有到高度getHeight(),还有很多中间测量值和对应到span的变化值。同时,有基本的draw方法,主要包括:
1.getLineRangeForDraw获取需要绘制的行数。
2.drawBackground绘制背景部分,主要是识别出拥有背景的span(LineBackgroundSpan)并加以处理。
3.drawText以行(TextLine对象)为单位绘制文本,实现预设的文本样式,同时识别各种各样和样式布局有关的span。
这样,就可以完成文本的测量与绘制。
- BoringLayout
// Override draw so it will be faster.
@Override
public void draw(Canvas c, Path highlight, Paint highlightpaint, int cursorOffset) {
if (mDirect != null && highlight == null) {
c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
} else {
super.draw(c, highlight, highlightpaint, cursorOffset);
}
}
复制代码
BoringLayout 最重要的地方是它提供了一个静态方法 isBoring 来判断一段文字是否能在一行放下。这个方法有广泛的使用场景。
- StaticLayout 在初始化的时候进行一次计算与渲染,之后再也不动它了。 这个对象一旦被初始化,就会运行 generate 内部方法开始计算。generate 的运算代码几乎和 Layout 类中的 draw 的计算方法一模一样,把所有需要计算的数值都在对象的属性中填写完成。最后,在调用draw方法的时候,由于各种参数已经计算完成,只需要直接开始画就ok。
public class StaticTextView extends View {
private Layout layout = null;
public void setLayout(Layout layout) {
this.layout = layout;
requestLayout();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
if (layout != null) {
layout.draw(canvas, null, null, 0);
}
canvas.restore();
}
}
复制代码
可以直接通过设置这个 View 的 Layout 来绘制文本,并在 onDraw 方法中直接使用这个 Layout 对象来绘制文本。在这里我们摒弃了setText方法,直接通过Layout来绘制文本,而这里的Layout对象,我们可以通过预先创建之后才设置进去(这里可以放到单独的一个线程中创建),这样对比起普通TextView的setText方法,我们减少了setText中的许多消耗,可以大幅度的提升效率。 同样,如果我们有在其他自定义控件中绘制文本的需求,直接调用staticlayout来绘制就可以完美解决换行和样式问题了。
- DynamicLayout
它在绘制的时候把字符拆成以块(Block)为单位进行渲染,而块的区分是按照段落和最小块长来设置的,目的是动态渲染提高性能(因为这个本来就很次性能)。 它的每一次渲染的参数其实是复用的StaticLayout的生成器来设置的,同时使用TextLayoutCache来缓存住。同时updateBlocks来更新样式。 同时,内部拥有TextWatcher, SpanWatcher来监听文本和span的变动以进行实时刷新。
Spans
https://rocko.xyz/2015/03/04/%E3%80%90%E8%AF%91%E3%80%91Spans%EF%BC%8C%E4%B8%80%E4%B8%AA%E5%BC%BA%E5%A4%A7%E7%9A%84%E6%A6%82%E5%BF%B5/
http://lrdcq.com/me/read.php/37.htm
Xfermode 和 PorterDuff
1.Xfermode: Xfermode有三个子类 :
AvoidXfermode 指定了一个颜色和容差,强制Paint避免在它上面绘图(或者只在它上面绘图)。
PixelXorXfermode 当覆盖已有的颜色时,应用一个简单的像素异或操作。
PorterDuffXfermode 这是一个非常强大的转换模式,使用它,可以使用图像合成的16条Porter-Duff规则的任意一条来控制Paint如何与已有的Canvas图像进行交互。
要应用转换模式,可以使用setXferMode方法,如下所示:
AvoidXfermode avoid = new AvoidXfermode(Color.BLUE, 10, AvoidXfermode.Mode. AVOID); borderPen.setXfermode(avoid);
复制代码
2.PorterDuff: PorterDuff.Mode为枚举类,一共有16个枚举值:
1.PorterDuff.Mode.CLEAR:所绘制不会提交到画布上。
2.PorterDuff.Mode.SRC 显示上层绘制图片
3.PorterDuff.Mode.DST 显示下层绘制图片
4.PorterDuff.Mode.SRC_OVER 正常绘制显示,上下层绘制叠盖。
5.PorterDuff.Mode.DST_OVER 上下层都显示。下层居上显示。
6.PorterDuff.Mode.SRC_IN 取两层绘制交集。显示上层。
7.PorterDuff.Mode.DST_IN 取两层绘制交集。显示下层。
8.PorterDuff.Mode.SRC_OUT 取上层绘制非交集部分。
9.PorterDuff.Mode.DST_OUT 取下层绘制非交集部分。
10.PorterDuff.Mode.SRC_ATOP 取下层非交集部分与上层交集部分
11.PorterDuff.Mode.DST_ATOP 取上层非交集部分与下层交集部分
12.PorterDuff.Mode.XOR 异或:去除两图层交集部分
13.PorterDuff.Mode.DARKEN 取两图层全部区域,交集部分颜色加深
14.PorterDuff.Mode.LIGHTEN 取两图层全部,点亮交集部分颜色
15.PorterDuff.Mode.MULTIPLY 取两图层交集部分叠加后颜色
16.PorterDuff.Mode.SCREEN 取两图层全部区域,交集部分变为透明色
复制代码
Listview 的 setSelectionFromTop
ListView.setSelectionFromTop(int position, int y);其中position指的是指定的item的在ListView中的索引, 注意如果有Header存在的情况下,索引是从Header就开始算的。y指的是到ListView可见范围内最上边边缘的距离。
ListView.getChildAt(int position), 这个position指的是在可视的item中的索引,跟cursor里的位置是大不一样的。 可以看看ListView.getChildCount()函数得到个数是小于或等于Cursor里的个数的(不考虑header的话)。 虽然一共可能有20条数据,但是界面只能看到8条,那么这个ChildCount大约就是8了。 另一方面, FirstVisiblePosition取出的是第一个可见的item在总的条数中的索引,再将会消失的header考虑进来。
public void setSelectionFromTop(int position, int y) {
if (mAdapter == null) {
return;
}
if (!isInTouchMode()) {
position = lookForSelectablePosition(position, true);
if (position >= 0) {
setNextSelectedPositionInt(position);
}
} else {
mResurrectToPosition = position;
}
if (position >= 0) {
mLayoutMode = LAYOUT_SPECIFIC;
mSpecificTop = mListPadding.top + y;
if (mNeedSync) {
mSyncPosition = position;
mSyncRowId = mAdapter.getItemId(position);
}
if (mPositionScroller != null) {
mPositionScroller.stop();
}
requestLayout();
}
}
复制代码
从上面的代码可以得知,setSelectionFromTop()的作用是设置ListView选中的位置,同时在Y轴设置一个偏移量(padding值)。 ListView还有一个方法叫setSelection(),传入一个index整型数值,就可以让ListView定位到指定Item的位置。
@Override
public void setSelection(int position) {
setSelectionFromTop(position, 0);
}
复制代码
setSelection()内部就是调用了setSelectionFromTop(),只不过是Y轴的偏移量是0而已。
另外,可以通过别的方式可以记录listView滚动到的位置的坐标,然后利用listView.scrollTo精确的进行恢复 。
listView.setOnScrollListener(new OnScrollListener() {
/**
* 滚动状态改变时调用
*/
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// 不滚动时保存当前滚动到的位置
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
if (currentMenuInfo != null) {
scrolledX = statusListView.getScrollX();
scrolledY = statusListView.getScrollY();
}
}
}
/**
* 滚动时调用
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
复制代码
在恢复位置时调用。
listView.scrollTo(scrolledX, scrolledY);
复制代码
Canvas 的 clip 函数
clipPath(@NonNull Path path)
clipPath(@NonNull Path path, @NonNull Region.Op op)
clipRect(float left, float top, float right, float bottom) clipRect(float left, float top, float right, float bottom,@NonNull Region.Op op)
clipRect(int left, int top, int right, int bottom)
clipRect(@NonNull Rect rect)
clipRect(@NonNull Rect rect, @NonNull Region.Op op)
clipRect(@NonNull RectF rect)
clipRect(@NonNull RectF rect, @NonNull Region.Op op)
clipRegion(@NonNull Region region)
clipRegion(@NonNull Region region, @NonNull Region.Op op)
复制代码
结论
TextView#BufferType
TextView.BufferType 用于在运行时改变 TextView 的状态,例如插入,设置颜色和样式。
EDITABLE -> 返回 Spannable 和 Editable。
yourTextView.setText("is a textView of Editable BufferType",TextView.BufferType.EDITABLE);
/* 在运行时插入值*/
Editable editable = youTextView.getEditableText();
editable.insert(0,"This ");
复制代码
Ouput:
This is a textView of Editable BufferType
复制代码
NORMAL -> 返回 CharSequence。
SPANNABLE -> 返回 Spannable。
yourTextView.setText("textView of Spannable BufferType",TextView.BufferType.SPANNABLE);
/* 在运行时改变颜色*/
Spannable span = (Spannable)yourTextView.getText();
span.setSpan(new ForegroundColorSpan(0xff0000ff),11,"textView of Spannable BufferType".length(),Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
复制代码
Output:
textView of `Spannable BufferType`(Spannable BufferType are in blue color)
复制代码
手势识别
- GestureDetector
GestureDetector 是最方便的处理手势的方式,创建它的时候需要实现一个 listener,这是 GestureDetector 的核心处理接口。
public interface OnGestureListener {
//当按下的时候
boolean onDown(MotionEvent var1);
//当按下之后且未立刻滑动
void onShowPress(MotionEvent var1);
//轻触,当按下后立刻起来
boolean onSingleTapUp(MotionEvent var1);
//滑动,手指在屏幕上移动
boolean onScroll(MotionEvent var1, MotionEvent var2, float var3, float var4);
//长按,按下后久未动
void onLongPress(MotionEvent var1);
//快速划,手指在屏幕上滑动并离开时
boolean onFling(MotionEvent var1, MotionEvent var2, float var3, float var4);
}
复制代码
通过这个 listenr,可以基本处理大部分单点触摸和滑动的操作。
public interface OnDoubleTapListener {
//双击判断超时,确认是单击
boolean onSingleTapConfirmed(MotionEvent var1);
//双击
boolean onDoubleTap(MotionEvent var1);
//双击第二次点击的event
boolean onDoubleTapEvent(MotionEvent var1);
}
复制代码
通过这个 listenr,可以判断出这个点击是不是双击并且处理双击中的事件。
sdk提供了一个合并的SimpleOnGestureListener作为基类且同时实现了这两个listener,只需要挑自己要使用的处理方法自己去实现内容就可以了。
多点操作可以使用 ScaleGestureDetector,它可以处理两个手指精细缩放的手势。它提供的listener是:
public interface OnScaleGestureListener {
//当缩放改变时
boolean onScale(ScaleGestureDetector var1);
//当开始缩放
boolean onScaleBegin(ScaleGestureDetector var1);
//当结束缩放
void onScaleEnd(ScaleGestureDetector var1);
}
复制代码
在onScaleBegin和onScale返回false会强制中断本次缩放手势识别与处理。
旋转角度可以通过判断两个点相对位置的偏差来识别。MotionEvent中多点相关的api是:
getPointerCount();//获得触控点个数
getPointerId(int pointerIndex);//返回触摸点的id
getX(int pointerIndex);//返回触摸点X坐标
getY(int pointerIndex);//返回触摸点Y坐标
复制代码
- GestureLibrary 比如音乐播放器实现划左右的箭头"<"">"来实现上一首下一首切换,或者浏览器通过各种方向的L型手势来进行操作,或者通过打勾打叉画圈圈在确认操作。这种复杂的东西就交给GestureLibrary处理好了。
它可以用一个文件中读取一个或者一些列手势模版,Gesture对象,然后我们提供一些用户绘制的Gesture对象进行比对,得出一个相似度,我们再设一个阈值判断这个相似度就可以匹配到当前这个Gesture对象是不是某一个手势了。
1.手势模版哪儿来。
2.用户绘制操作怎么变成Gesture对象。
事实上都是Gesture对象,而且来源只有一个——GestureOverlayView。
GestureOverlayView是一个framelayout的子类,它可以显示出在它上面正在绘制的手势并通过OnGesturePerformedListener返回一个Gesture对象给代码,这就是代码中Gesture对象的主要来源。GestureOverlayView和触控相关的属性主要都是和界面绘制手势的样式相关的,不过一般情况下我们会把绘制的线条隐藏掉。
怎么储存模版文件呢?回到GestureLibrary,它的功能除了比对,最主要的是,从sd卡或者raw中读取一个模版文件,或者向sd卡中写入一个模版文件。
事实上我们开发中要做的是,手动绘制出模版的手势并且通过GestureLibrary存储到sd卡中。从开发机的sd卡中把模版文件拷出来并放到项目的raw进行使用。
GestureOverlayView gestureOverlayView = (GestureOverlayView) findViewById(R.id.main_gol);
final GestureLibrary library = GestureLibraries.fromRawResource(MainActivity.this, R.raw.gestures);//获取手势文件
library.load();
gestureOverlayView.addOnGesturePerformedListener(new GestureOverlayView.OnGesturePerformedListener() {
@Override
public void onGesturePerformed(GestureOverlayView arg0, Gesture gesture) {
//读出手势库中内容 识别手势
ArrayList mygesture = library.recognize(gesture);
Prediction predction = mygesture.get(0);
if (predction.score >= 3.5) {//阈值匹配
if (predction.name.equals("test_down")) {
Toast.makeText(MainActivity.this, "向下手势", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(MainActivity.this, "没有该手势", Toast.LENGTH_SHORT).show();
}
}
});
复制代码
通过library.recognize(gesture)是将当前的手势和这个手势模版文件中所有的手势进行匹配,并且把所有手势的相似度作为Prediction对象(分数和手势名字)的list返回回来。list是安装相似度分数从高到低排列的,所以一般来说我们只看第一个即分数最高的就可以了。然后我们看看我们最相似的这个手势——的相似度达到我们的阈值没用——经过实测,阈值在3.5到5左右比较合适。当然,除了相似度,还要判断当前这个手势到底是哪一个,就通过名字判断吧,最终得到结果。
3.ViewDragHelper
- ViewDragHelper 可以检测到是否触及到边缘
- ViewDragHelper 并不是直接作用于要被拖动的 View,而是使其控制的视图容器中的子 View 可以被拖动
- ViewDragHelper.Callback 是连接 ViewDragHelper 与 View 之间的桥梁
- ViewDragHelper 的本质其实是分析 onInterceptTouchEvent 和 onTouchEvent 的 MotionEvent 参数,然后根据分析的结果去改变一个容器中被拖动子 View 的位置。onInterceptTouchEvent 中通过使用 mDragger.shouldInterceptTouchEvent(event) 来决定我们是否应该拦截当前的事件。onTouchEvent 中通过 mDragger.processTouchEvent(event) 处理事件。
ViewDragHelper.Callback 中提供了以下回调:
- onViewCaptured (当 captureView 被捕获时回调)
- tryCaptureView (是否需要 capture 这个 View)
- clampViewPositionHorizontal (横向移动的时候回调)
- clampViewPositionVertical (纵向移动的时候回调)
- onViewDragStateChanged (当ViewDragHelper状态发生变化时回调(IDLE,DRAGGING,SETTING[自动滚动时]))
- onViewPositionChanged (当 captureView 的位置发生改变时回调)
- onEdgeTouched (当触摸到边界时回调)
- onEdgeLock (true 的时候会锁住当前的边界,false 则 unLock )
- onEdgeDragStarted (边界拖动开始的时候回调)
- getOrderedChildIndex (改变同一个坐标( x , y )去寻找 captureView 位置的方法)
- getViewHorizontalDragRange (最大横滑动的滑动距离)
- getViewVerticalDragRange (最大纵向滑动的距离)
- onViewReleased (当 captureView 被释放的时候回调)
如果在同一个位置有两个子 View 重叠,想要让下层的子 View 被选中, 那么就要实现 Callback 里的 getOrderedChildIndex(int index) 方法来改变查找子View的顺序;例如 topView( 上层View )的 index 是 4, bottomView(下层View)的 index 是 3,按照正常的遍历查找方式( getOrderedChildIndex() 默认直接返回 index ),会选择到 topView , 要想让 bottomView 被选中就得这么写:
public int getOrderedChildIndex(int index) {
int indexTop = mParentView.indexOfChild(topView);
int indexBottom = mParentView.indexOfChild(bottomView);
if (index == indexTop) {
return indexBottom;
}
return index;
}
复制代码
ViewDragHelper 通过调用 offsetLeftAndRight() 和 offsetTopAndBottom() 来完成对 mCapturedView 移动。
settleCapturedViewAt(int finalLeft, int finalTop) 以松手前的滑动速度为初速动,让捕获到的 View 自动滚动到指定位置。只能在 Callback 的 onViewReleased() 中调用。flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) 以松手前的滑动速度为初速动,让捕获到的 View 在指定范围内 fling ,也只能在 Callback 的 onViewReleased() 中调用。
同时复写:
@Override
public void computeScroll() {
if (viewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this); // 滚动还未停止继续刷新
}
}
复制代码
smoothSlideViewTo(View child, int finalLeft, int finalTop) 指定某个 View 自动滚动到指定的位置,初速度为 0 ,可在任何地方调用。
根据 x,y 坐标返回 ListView 的点击条目位置
public int pointToPosition(int x, int y) {
Rect frame = mTouchFrame;
if (frame == null) {
mTouchFrame = new Rect();
frame = mTouchFrame;
}
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
child.getHitRect(frame);
if (frame.contains(x, y)) {
return mFirstPosition + i;
}
}
}
return INVALID_POSITION;
}
复制代码
获取 View 在屏幕上的 x,y 坐标
public void getLocationOnScreen(@Size(2) int[] outLocation) {
getLocationInWindow(outLocation);
final AttachInfo info = mAttachInfo;
if (info != null) {
outLocation[0] += info.mWindowLeft;
outLocation[1] += info.mWindowTop;
}
}
复制代码
ListView#setSelectionFromTop
将 ListView 的 position 条目移动到第一显示位,然后再竖直平移 y 单位。
public void setSelectionFromTop(int position, int y) {
if (mAdapter == null) {
return;
}
if (!isInTouchMode()) {
position = lookForSelectablePosition(position, true);
if (position >= 0) {
setNextSelectedPositionInt(position);
}
} else {
mResurrectToPosition = position;
}
if (position >= 0) {
mLayoutMode = LAYOUT_SPECIFIC;
mSpecificTop = mListPadding.top + y;
if (mNeedSync) {
mSyncPosition = position;
mSyncRowId = mAdapter.getItemId(position);
}
if (mPositionScroller != null) {
mPositionScroller.stop();
}
requestLayout();
}
}
复制代码
ImageSwitcher
ImageSwitcher的原理就是有两个子View:ImageView,当左右滑动的时候,就在这两个ImageView之间来回切换来显示图片。首先需要通过ImageSwitcher.setFactory()方法,创建ViewSwitcher.ViewFactory的实现类来提供ImageView。
public class ViewSwitcher extends ViewAnimator {
...
public interface ViewFactory {
/**
* Creates a new {@link android.view.View} to be added in a
* {@link android.widget.ViewSwitcher}.
*
* @return a {@link android.view.View}
*/
View makeView();
}
}
复制代码
setFactory()方法的具体代码如下
public void setFactory(ViewFactory factory) {
mFactory = factory;
obtainView();
obtainView();
}
复制代码
obtainView()方法就是给ImageSwitcher添加子ImageView的,调用两遍就是添加了两个子ImageView。再来看看obtainView()方法的具体代码
private View obtainView() {
View child = mFactory.makeView();
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null) {
lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT);
}
addView(child, lp);
return child;
}
复制代码
可以看到obtainView()方法的的职责就是:通过makeView()方法创建View,然后把创建出来的View添加到ImageSwitcher上。 然后通过ImageSwitcher.setImageResource()方法进行初始化
public void setImageResource(int resid) {
ImageView image = (ImageView)this.getNextView();
image.setImageResource(resid);
showNext();
}
public View getNextView() {
int which = mWhichChild == 0 ? 1 : 0;//交替显示两个View
return getChildAt(which);
}
public void showNext() {
setDisplayedChild(mWhichChild + 1);
}
public void setDisplayedChild(int whichChild) {
mWhichChild = whichChild;
if (whichChild >= getChildCount()) {
mWhichChild = 0;
} else if (whichChild < 0) {
mWhichChild = getChildCount() - 1;
}
boolean hasFocus = getFocusedChild() != null;
// This will clear old focus if we had it
showOnly(mWhichChild);
if (hasFocus) {
// Try to retake focus if we had it
requestFocus(FOCUS_FORWARD);
}
}
void showOnly(int childIndex) {
final boolean animate = (!mFirstTime || mAnimateFirstTime);
showOnly(childIndex, animate);
}
void showOnly(int childIndex, boolean animate) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {//显示一个View的同时隐藏另一个View
final View child = getChildAt(i);
if (i == childIndex) {
if (animate && mInAnimation != null) {
child.startAnimation(mInAnimation);
}
child.setVisibility(View.VISIBLE);
mFirstTime = false;
} else {
if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
child.startAnimation(mOutAnimation);
} else if (child.getAnimation() == mInAnimation)
child.clearAnimation();
child.setVisibility(View.GONE);
}
}
}
复制代码
同理,可以使用ViewSwitcher来实现相同效果。TextSwitcher可以用来显示文字。
ViewPager动画
从3.0开始,ViewPager开始支持自定义切换动画,暴露的接口为PageTransformer,因此只要实现PageTransformer接口和其唯一的方法transformPage(View view, float position)即可。
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
public void transformPage(View page, float position);
}
复制代码
参数page :给定界面的View对象。可以根据该对象进行findViewById()检索对应的View进行动画操作。
参数position :给定界面的位置相对于屏幕中心的偏移量。在用户滑动界面的时候,是动态变化的。那么我们可以将position的值应用于setAlpha(), setTranslationX(), or setScaleY()方法,从而实现自定义的动画效果。 另 外在ViewPager滑动时,内存中存活的Page都会执行transformPage方法,在滑动过程中涉及到两个Page,当前页和下一页,而它们 的position值是相反的(因为是相对运动,一个滑入一个滑出),比如,页面A向右滑动到屏幕一半,页面B也正好处于一半的位置,那么A和B的 position为:0.5 和 -0.5
- position == 0 :当前界面位于屏幕中心的时候
- position == 1 :当前Page刚好滑出屏幕右侧
- position == -1 :当前Page刚好滑出屏幕左侧
Bitmap缓存
public class BackgroundBitmapCache {
private LruCache mBackgroundsCache;
private static BackgroundBitmapCache instance;
public static BackgroundBitmapCache getInstance() {
if (instance == null) {
instance = new BackgroundBitmapCache();
instance.init();
}
return instance;
}
private void init() {
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 5;
mBackgroundsCache = new LruCache(cacheSize) {
@Override
protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
@Override
protected int sizeOf(Integer key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than number of items.
return bitmap.getByteCount() / 1024;
}
};
}
public void addBitmapToBgMemoryCache(Integer key, Bitmap bitmap) {
if (getBitmapFromBgMemCache(key) == null) {
mBackgroundsCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromBgMemCache(Integer key) {
return mBackgroundsCache.get(key);
}
}
复制代码