DoraemonKit是滴滴開源的研發助手組件,目前支持iOS和Android兩個平臺。經過接入DoraemonKit組件,能夠方便支持以下所示的多種調試工具:java
本文是DoraemonKit之Android版本技術實現系列文章的第一篇,主要介紹各個視覺工具的技術實現細節。android
取色器工具能夠經過顏色吸管獲取屏幕任意位置的像素值,因此實現的關鍵就是如何獲取像素點。獲取像素點的第一步是獲取屏幕截圖,獲取屏幕截圖在Android平臺主要有如下幾種方式:git
對比三種實現方式,方式一隻能獲取當前Window內DocorView的內容,不能獲取狀態欄或者脫離應用自己,且開啓DrawingCache會增長應用內存佔用;方式二中FrameBuffer不能直接讀取,須要得到系統Root權限,且兼容性差;方式三可脫離應用自己獲取應用外截屏,截圖取自系統Binder不佔用應用內存,只需請求錄屏權限。github
getDrawingCache函數 | 讀取系統FrameBuffer | MediaProjectionManager類 | |
---|---|---|---|
實現複雜度 | 簡單 | 複雜 | 較簡單 |
須要權限 | 無 | Root權限 | 錄屏權限 |
適用性 | 只能截取應用內 | 應用內外都支持 | 應用內外都支持 |
性能影響 | 大 | 小 | 小 |
經過對比,DoraemonKit選擇方式三做爲取色器的實現方案。canvas
private boolean requestCaptureScreen() {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
return false;
}
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
if (mediaProjectionManager == null) {
return false;
}
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), RequestCode.CAPTURE_SCREEN);
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RequestCode.CAPTURE_SCREEN && resultCode == Activity.RESULT_OK) {
showColorPicker(data);
...
} else {
...
}
}
複製代碼
經過createScreenCaptureIntent()方法能夠獲取請求系統錄屏權限的Intent,而後調用startActivityForResult,系統會自動彈出權限授予彈窗。若是授予權限則在onActivityResult中獲得系統回調成功,且返回錄屏的resultData。api
public void init(Context context, Bundle bundle) {
mMediaProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
if (mMediaProjectionManager != null) {
Intent intent = new Intent();
intent.putExtras(bundle);
mMediaProjection = mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, intent);
}
int width = UIUtils.getWidthPixels(context);
int height = UIUtils.getRealHeightPixels(context);
int dpi = UIUtils.getDensityDpi(context);
mImageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
mMediaProjection.createVirtualDisplay("ScreenCapture",
width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(), null, null);
}
複製代碼
經過onActivityResult中返回的resultData就能夠建立系統錄屏服務MediaProjection,而後建立ImageReader並與MediaProjection的Surface進行綁定,以後就能夠經過ImageReader獲取屏幕截圖了。bash
public void capture() {
if (isCapturing) {
return;
}
isCapturing = true;
Image image = mImageReader.acquireLatestImage();
if (image == null) {
return;
}
int width = image.getWidth();
int height = image.getHeight();
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPaddingStride = rowStride - pixelStride * width;
int rowPadding = rowPaddingStride / pixelStride;
Bitmap recordBitmap = Bitmap.createBitmap(width + rowPadding , height, Bitmap.Config.ARGB_8888);
recordBitmap.copyPixelsFromBuffer(buffer);
mBitmap = Bitmap.createBitmap(recordBitmap, 0, 0, width, height);
image.close();
isCapturing = false;
}
複製代碼
調用ImageReader的acquireLatestImage能夠獲取當前屏幕的截圖,而後將Image對象轉爲Bitmap對象就能夠方便地進行顯示和獲取像素點了。app
public static int getPixel(Bitmap bitmap, int x, int y) {
if (bitmap == null) {
return -1;
}
if (x < 0 || x > bitmap.getWidth()) {
return -1;
}
if (y < 0 || y > bitmap.getHeight()) {
return -1;
}
return bitmap.getPixel(x, y);
}
複製代碼
根據浮標的座標在Bitmap上就能夠很方便地獲取像素點的值了。ide
控件檢查的功能是經過浮標選取目標View,而後獲取目標View的相關信息,因此如何獲取這個View的引用就是實現這一功能的關鍵。函數
app.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityResumed(Activity activity) {
...
for (ActivityLifecycleListener listener : sListeners) {
listener.onActivityResumed(activity);
}
}
}
複製代碼
經過註冊監聽能夠在Activity進入Resumed狀態時得到通知,這樣在浮標移動時DoraemonKit就能夠持有最前臺的Activity。
private View traverseViews(View view, int x, int y) {
int[] location = new int[2];
view.getLocationInWindow(location);
int left = location[0];
int top = location[1];
int right = left + view.getWidth();
int bottom = top + view.getHeight();
if (view instanceof ViewGroup) {
int childCount = ((ViewGroup) view).getChildCount();
if (childCount != 0) {
for (int index = childCount - 1; index >= 0; index--) {
View v = traverseViews(((ViewGroup) view).getChildAt(index), x, y);
if (v != null) {
return v;
}
}
}
if (left < x && x < right && top < y && y < bottom) {
return view;
} else {
return null;
}
} else {
LogHelper.d(TAG, "class: " + view.getClass() + ", left: " + left
+ ", right: " + right + ", top: " + top + ", bottom: " + bottom);
if (left < x && x < right && top < y && y < bottom) {
return view;
} else {
return null;
}
}
}
複製代碼
由於View是以Tree的結構組織的,因此經過遍歷當前Activity的ViewTree就能夠獲取到目標View。以DocorView做爲根,遞歸調用同時判斷浮標座標是否在View的範圍便可以獲得目標View。由於View可能存在覆蓋關係,因此須要使用深度優先遍歷才能得到最頂端的View。
對齊標尺的實現比較簡單,只須要根據浮標座標繪製水平標尺線和豎直標尺線便可,實現效果以下圖。
DoraemonKit最開始實現佈局邊界,是經過遍歷ViewTree在懸浮Window上繪製邊框線的方式,但這種方式有一個問題就是若是Activity包含多個層級的時候,全部層級View的邊框都會被繪製在最頂層,致使顯示十分混亂,在複雜界面上基本是不可用的。
在調研了幾種方式以後,DoraemonKit使用了替換View的Background的方式。View的Background是Drawable類型的,而LayerDrawable這種Drawable是能夠包含一組Drawable的,因此取出View的原始Background後與繪製邊框的Drawable放進同一個LayerDrawable中,就能夠實現帶邊框的背景。
private void traverseChild(View view) {
if (view instanceof ViewGroup) {
replaceDrawable(view);
int childCount = ((ViewGroup) view).getChildCount();
if (childCount != 0) {
for (int index = 0; index < childCount; index++) {
traverseChild(((ViewGroup) view).getChildAt(index));
}
}
} else {
replaceDrawable(view);
}
}
private void replaceDrawable(View view) {
LayerDrawable newDrawable;
if (view.getBackground() != null) {
Drawable oldDrawable = view.getBackground();
if (oldDrawable instanceof LayerDrawable) {
for (int i = 0; i < ((LayerDrawable) oldDrawable).getNumberOfLayers(); i++) {
if (((LayerDrawable) oldDrawable).getDrawable(i) instanceof ViewBorderDrawable) {
// already replace
return;
}
}
newDrawable = new LayerDrawable(new Drawable[] {
oldDrawable,
new ViewBorderDrawable(view)
});
} else {
newDrawable = new LayerDrawable(new Drawable[] {
oldDrawable,
new ViewBorderDrawable(view)
});
}
} else {
newDrawable = new LayerDrawable(new Drawable[] {
new ViewBorderDrawable(view)
});
}
view.setBackground(newDrawable);
}
複製代碼
這種方式的好處就是實現簡單,且兼容性很好,不會出現顯示異常,包括多個層級的覆蓋也能正常顯示,能實時地伴隨組件隱藏和顯示,但也存在必定的侵入性,會對View的繪製形成必定的開銷。
佈局層級功能能夠很方便地查看當前頁面的Layout層級,是一個3D可視化的效果,能夠多個角度旋轉查看,這個功能是依賴jakewharton的scalpel項目實現的,這個項目的核心只有一個源碼文件ScalpelFrameLayout,經過把但願查看層級的頁面根View加到這個Layout中就能夠實現佈局層級功能。這個Layout的實現原理是重寫了Layout的draw方法,經過Camera進行了3D變換,從新進行了排布繪製。
@Override public void draw(@SuppressWarnings("NullableProblems") Canvas canvas) {
if (!enabled) {
super.draw(canvas);
return;
}
getLocationInWindow(location);
float x = location[0];
float y = location[1];
int saveCount = canvas.save();
float cx = getWidth() / 2f;
float cy = getHeight() / 2f;
camera.save();
camera.rotate(rotationX, rotationY, 0);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-cx, -cy);
matrix.postTranslate(cx, cy);
canvas.concat(matrix);
canvas.scale(zoom, zoom, cx, cy);
if (!layeredViewQueue.isEmpty()) {
throw new AssertionError("View queue is not empty.");
}
// We don't want to be rendered so seed the queue with our children.
for (int i = 0, count = getChildCount(); i < count; i++) {
LayeredView layeredView = layeredViewPool.obtain();
layeredView.set(getChildAt(i), 0);
layeredViewQueue.add(layeredView);
}
while (!layeredViewQueue.isEmpty()) {
LayeredView layeredView = layeredViewQueue.removeFirst();
View view = layeredView.view;
int layer = layeredView.layer;
// Restore the object to the pool for use later.
layeredView.clear();
layeredViewPool.restore(layeredView);
// Hide any visible children.
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
visibilities.clear();
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
View child = viewGroup.getChildAt(i);
//noinspection ConstantConditions
if (child.getVisibility() == VISIBLE) {
visibilities.set(i);
child.setVisibility(INVISIBLE);
}
}
}
int viewSaveCount = canvas.save();
// Scale the layer index translation by the rotation amount.
float translateShowX = rotationY / ROTATION_MAX;
float translateShowY = rotationX / ROTATION_MAX;
float tx = layer * spacing * density * translateShowX;
float ty = layer * spacing * density * translateShowY;
canvas.translate(tx, -ty);
view.getLocationInWindow(location);
canvas.translate(location[0] - x, location[1] - y);
viewBoundsRect.set(0, 0, view.getWidth(), view.getHeight());
canvas.drawRect(viewBoundsRect, viewBorderPaint);
if (drawViews) {
if (!(view instanceof SurfaceView)) {
view.draw(canvas);
}
}
if (drawIds) {
int id = view.getId();
if (id != NO_ID) {
canvas.drawText(nameForId(id), textOffset, textSize, viewBorderPaint);
}
}
canvas.restoreToCount(viewSaveCount);
// Restore any hidden children and queue them for later drawing.
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
if (visibilities.get(i)) {
View child = viewGroup.getChildAt(i);
//noinspection ConstantConditions
child.setVisibility(VISIBLE);
LayeredView childLayeredView = layeredViewPool.obtain();
childLayeredView.set(child, layer + 1);
layeredViewQueue.add(childLayeredView);
}
}
}
}
canvas.restoreToCount(saveCount);
}
複製代碼
佈局層級在添加SurfaceView等特殊繪製的View時可能出現繪製問題,出現黑白屏閃爍問題,須要屏蔽這些特殊View的繪製。
取色器組件的實現主要經過系統錄屏api,從截圖Bitmap中取得像素點。
控件檢查功能經過遍歷ViewTree實現,須要註冊全局Activity的生命週期監聽。
對齊標尺功能直接經過浮標的屏幕座標繪製水平和垂直標尺。
佈局邊界功能經過替換View的Background實現,由包含原始Background的LayerDrawable替換原有Background。
佈局層級主要是使用開源項目scalpel實現,對原有ViewTree進行3D變換,從新進行繪製。
經過這篇文章主要是但願你們可以對DoraemonKit視覺工具的技術實現有一個瞭解,若是有好的想法也能夠參與到DoraemonKit開源項目的建設中來,在項目頁面提交Issues或者提交Pull Requests,相信DoraemonKit項目在你們的努力下會愈來愈完善。
DoraemonKit項目地址:github.com/didi/Doraem…,以爲不錯的話就給項目點個star吧。