在Android应用中很多应用都已经由集成滑动退出的效果,比如QQ,比如UC,又比如微信。QQ的滑动退出仅仅监听手势,没有任何动画效果,在手势触发成功后结束当前的页面。UC效果会好一些,会有根据手势慢慢移除的效果,但之前页面没有联动效果,这一点微信做的相对而言好些。
微信的滑动退出,在当前页面滑动,之前的页面也会有个跟随的效果,想到的方案有在当前页面保持之前页面的引用,滑动过程中一起做联动效果,这种实现方案,我尝试了下,在Android4.4及以上的手机下可以实现,但是之前的版本却无法产生效果,主要原因是,之前版本在调用OnPause后就不在继续接受刷新请求,这是一种效率上的保护。
而且在高API手机上,性能表现也不是很好,因为同一帧要刷新两个界面,系统负担较重。
目前的实现方案是,在当前页面启动时候,取上一个页面的快照,这样在滑动退出的时候,可以以快照为背景,达到联动的效果。标准截图API如下
public Bitmap myShot(Activity activity) {
// 获取windows中最顶层的view
View view = activity.getWindow().getDecorView();
view.buildDrawingCache();
// 获取状态栏高度
Rect rect = new Rect();
view.getWindowVisibleDisplayFrame(rect);
int statusBarHeights = rect.top;
Display display = activity.getWindowManager().getDefaultDisplay();
// 获取屏幕宽和高
int widths = display.getWidth();
int heights = display.getHeight();
// 允许当前窗口保存缓存信息
view.setDrawingCacheEnabled(true);
// 去掉状态栏
Bitmap bmp = Bitmap.createBitmap(view.getDrawingCache(), 0,
statusBarHeights, widths, heights - statusBarHeights);
// 销毁缓存信息
view.destroyDrawingCache();
return bmp;
}
不过这种方式导致,在截图时会阻塞主线程,造成卡顿。我大概测试了下每个快照在100到300毫秒间,大概会阻塞10到20帧的样子。这会带来很不好的用户体验。
那主线程不行,子线程截图呢?这也是之后采用的方式,不过后来发现了问题,如果之前页面是有动画,便会造成线程冲突,使当前View被废弃,不再能接收到事件。只能在页面失去焦点的时候,关闭动画,不过这也不是一个很好的方法,会使得框架使用起来太过于麻烦。
只能想办法让快照的时间缩短,分析view.getDrawingCache()代码,发现每一次截图会重新创建bitmap对象,创建Canvas,手动调用draw方法获得截图,而创建bitmap对象是相当费性能的。复用bitmap一定是个不错的注意,因为全局我只需要一张图片,这种方式保证了效率,也保证了内存的稳定。
Canvas canvas = new CacheCanvas(bitmap);
canvas.clipRect(0, 0, bitmap.getWidth(), bitmap.getHeight());
canvas.drawColor(Color.WHITE);
cacheBitmap.clear();
cacheBitmap.put(layout.hashCode(), bitmap);
layout.dispatchDraw(canvas);
最后大概每次快照会花费20毫秒左右,这个时间用户一般感觉不出来了。解决了功能上的实现,要考虑让集成更简单,在每次Activity设置视图的时候开启?太麻烦了,而且以后相关也太费事。不过我们可以定义一个Activity的基类,在基类里面做,还是太麻烦。
大概说下,视图是如何绘制到窗口的,以及窗口如何管理View;
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
Activity的setContentView最终是调用Window的setContentView,而Android中的具体实现类是PhoneWindow
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
这部分代码比较长,有兴趣的可以自己看下,我大概来讲解下,如果窗口的dectorView为空,会创建一个 installDecor(),里面会根据主题创建不同的ContentParent ,逻辑简单,代码比较长就不贴了。这一块主要是为了说明,每一个Window其实都会有一个DectorView,这是根View。如果我们可以拿到根View就可以达到我们想要的目的了。
幸运的是,我们的根视图都是由WindowManagerGlobal统一管理的,创建的DectorView会保存在 private final ArrayList
class HoldArrayList extends ArrayList {
public interface OnRootViewChange {
void onChange(List rootViews);
}
private OnRootViewChange onRootViewChange;
private List activityRootViews;
public HoldArrayList(OnRootViewChange onRootViewChange) {
this.onRootViewChange = onRootViewChange;
this.activityRootViews = new ArrayList<>();
}
@Override
public boolean add(View t) {
boolean add = super.add(t);
onChange();
return add;
}
@Override
public boolean remove(Object o) {
boolean remove = super.remove(o);
onChange();
return remove;
}
@Override
public View remove(int index) {
View remove = super.remove(index);
onChange();
return remove;
}
private void onChange() {
activityRootViews.clear();
for (View view : this) {
if (view.getContext() instanceof Activity) {
activityRootViews.add(view);
}
}
if (onRootViewChange != null) {
onRootViewChange.onChange(activityRootViews);
}
}
}
这是我创建的holdArrayList替换系统的ArrayList,便可以监听到View的添加和移除。
github地址:https://github.com/long8313002/FastSlidingExit