https://blog.csdn.net/fancylovejava/article/details/45787729java
https://blog.csdn.net/dunqiangjiaodemogu/article/details/72956291canvas
飛豬上的doraemon一直對過分繪製和佈局深度有監控,不合理的佈局和過深得過分繪製影響頁面渲染速度。雖然發現了很多問題,多處可見以下圖的紅紅的頁面,可是一直很難推進解決,主要有兩個緣由。xcode
感謝@睿牧提供的外部開源參考工具
因而doraemon裏就多了同樣新的工具,將當前頁面的佈局3D化,有點像xcode上的view ui hierarchy工具,以下圖所示。新工具能夠輔助分析頁面佈局的合理性和影響過分繪製的關鍵點:緩存
按照上面的打開方式,而後進入門票首頁,再點擊「3D」Icon,能夠看到以下圖。能夠看到全部控件的背景色都被塗上了標識過分繪製深度的顏色。ide
從最外層佈局向內看,致使背景色突變的控件是設置了背景色,以下圖標記。其中5和6的背景色變化是由於加載了圖片,這種狀況能夠不修改。咱們主要看下一、二、三、4這4個地方。工具
以下代碼,在根佈局裏刷了一次全屏的白色。不合理性:標題欄自己也是白色,所在標題欄的像素區域內,根佈局的全屏白色是多餘的。佈局
整個頁面的佈局能夠當作是上面一個標題欄,下面一個列表控件(listview),代碼中爲這個列表控件再一次刷了白色,以下代碼所示:post
list的cell單元代碼中再次刷了個白色底色,很顯然這是多餘的性能
又一個list的cell單元這裏也刷了個白色底色,很顯然這也是多餘的,前面的e6透明度更是畫蛇添足。字體
去掉listview中的cell的白色底色
過分繪製數值由原先的4.04下降到2.63,提高53.6%。下圖是初步優化先後顏色對比。
以下黃色箭頭指向的位置,4個圖片控件(ImageView)並排放着,用了3層佈局,對此表示質疑。
3D佈局工具結合過渡繪製開關能夠有效地提高定位過分繪製問題,此外還容易發現多餘的不合理的佈局,以提高native的性能體驗。
下面是源碼,歡迎討論共建。
public class ScalpelFrameLayout extends FrameLayout { /** * 傳入當前頂部的Activity */ public static void attachActivityTo3dView(Activity activity) { if (activity == null) { return; } ScalpelFrameLayout layout; ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView(); /** * 在ids.xml裏定義一個id */ if (decorView.findViewById(R.id.id_scalpel_frame_layout) == null) { layout = new ScalpelFrameLayout(activity); layout.setId(R.id.id_scalpel_frame_layout); View rootView = decorView.getChildAt(0); decorView.removeView(rootView); layout.addView(rootView); decorView.addView(layout); } else { layout = (ScalpelFrameLayout) decorView.findViewById(R.id.id_scalpel_frame_layout); } if (!layout.isLayerInteractionEnabled()) { layout.setLayerInteractionEnabled(true); } else { layout.setLayerInteractionEnabled(false); } } /** * 標記位:當前多點觸摸滑動方向未肯定 */ private final static int TRACKING_UNKNOWN = 0; /** * 標記位:當前多點觸摸滑動方向是垂直方向 */ private final static int TRACKING_VERTICALLY = 1; /** * 標記位:當前多點觸摸滑動方向是橫向方向 */ private final static int TRACKING_HORIZONTALLY = -1; /** * 旋轉的最大角度 */ private final static int ROTATION_MAX = 60; /** * 反方向旋轉的最大角度 */ private final static int ROTATION_MIN = -ROTATION_MAX; /** * 默認X軸旋轉角度 */ private final static int ROTATION_DEFAULT_X = -10; /** * 默認Y軸旋轉角度 */ private final static int ROTATION_DEFAULT_Y = 15; /** * 默認縮放比例 */ private final static float ZOOM_DEFAULT = 0.6f; /** * 最小縮放比例 */ private final static float ZOOM_MIN = 0.33f; /** * 最大縮放比例 */ private final static float ZOOM_MAX = 2f; /** * 圖層默認間距 */ private final static int SPACING_DEFAULT = 25; /** * 圖層間最小距離 */ private final static int SPACING_MIN = 10; /** * 圖層間最大距離 */ private final static int SPACING_MAX = 100; /** * 繪製id的文案的偏移量 */ private final static int TEXT_OFFSET_DP = 2; /** * 繪製id的文案的字體大小 */ private final static int TEXT_SIZE_DP = 10; /** * view緩存隊列初始size */ private final static int CHILD_COUNT_ESTIMATION = 25; /** * 是否繪製view的內容,如TextView上的文字和ImageView上的圖片 */ private boolean mIsDrawingViews = true; /** * 是否繪製view的id */ private boolean mIsDrawIds = true; /** * 打印debug log開關 */ private boolean mIsDebug = true; /** * view大小矩形 */ private Rect mViewBoundsRect = new Rect(); /** * 繪製view邊框和id */ private Paint mViewBorderPaint = new Paint(ANTI_ALIAS_FLAG); private Camera mCamera = new Camera(); private Matrix mMatrix = new Matrix(); private int[] mLocation = new int[2]; /** * 用來記錄可見view * 可見view須要繪製 */ private BitSet mVisibilities = new BitSet(CHILD_COUNT_ESTIMATION); /** * 對id轉字符串的緩存 */ private SparseArray<String> mIdNames = new SparseArray<String>(); /** * 隊列結構實現廣度優先遍歷 */ private ArrayDeque<LayeredView> mLayeredViewQueue = new ArrayDeque<LayeredView>(); /** * 複用LayeredView */ private Pool<LayeredView> mLayeredViewPool = new Pool<LayeredView>( CHILD_COUNT_ESTIMATION) { @Override protected LayeredView newObject() { return new LayeredView(); } }; /** * 屏幕像素密度 */ private float mDensity = 0f; /** * 對移動最小距離的合法性的判斷 */ private float mSlop = 0f; /** * 繪製view id的偏移量 */ private float mTextOffset = 0f; /** * 繪製view id字體大小 */ private float mTextSize = 0f; /** * 3D視圖功能是否開啓 */ private boolean mIsLayerInteractionEnabled = false; /** * 第一個觸摸點索引 */ private int mPointerOne = INVALID_POINTER_ID; /** * 第一個觸摸點的座標X */ private float mLastOneX = 0f; /** * 第一個觸摸點的座標Y */ private float mLastOneY = 0f; /** * 當有多點觸摸時的第二個觸摸點索引 */ private int mPointerTwo = INVALID_POINTER_ID; /** * 第二個觸摸點的座標X */ private float mLastTwoX = 0f; /** * 第二個觸摸點的座標Y */ private float mLastTwoY = 0f; /** * 當前多點觸摸滑動方向 */ private int mMultiTouchTracking = TRACKING_UNKNOWN; /** * Y軸旋轉角度 */ private float mRotationY = ROTATION_DEFAULT_Y; /** * X軸旋轉角度 */ private float mRotationX = ROTATION_DEFAULT_X; /** * 縮放比例,默認是0.6 */ private float mZoom = ZOOM_DEFAULT; /** * 圖層之間距離,默認是25單位 */ private float mSpacing = SPACING_DEFAULT; public ScalpelFrameLayout(Context context) { super(context, null, 0); mDensity = getResources().getDisplayMetrics().density; mSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); mTextSize = TEXT_SIZE_DP * mDensity; mTextOffset = TEXT_OFFSET_DP * mDensity; mViewBorderPaint.setStyle(STROKE); mViewBorderPaint.setTextSize(mTextSize); if (Build.VERSION.SDK_INT >= JELLY_BEAN) { mViewBorderPaint.setTypeface(Typeface.create("sans-serif-condensed", NORMAL)); } } /** * 設置是否讓當前頁面佈局3D化 * 使用該方法前先調用attachActivityTo3dView方法 * * @param enabled */ public void setLayerInteractionEnabled(boolean enabled) { if (mIsLayerInteractionEnabled != enabled) { mIsLayerInteractionEnabled = enabled; setWillNotDraw(!enabled); invalidate(); } } /** * 當前頁面佈局是否已經3D化 * * @return */ public boolean isLayerInteractionEnabled() { return mIsLayerInteractionEnabled; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mIsLayerInteractionEnabled || super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (!mIsLayerInteractionEnabled) { return super.onTouchEvent(event); } int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: int index = action == ACTION_DOWN ? 0 : event.getActionIndex(); if (mPointerOne == INVALID_POINTER_ID) { mPointerOne = event.getPointerId(index); mLastOneX = event.getX(index); mLastOneY = event.getY(index); if (mIsDebug) { log("Got pointer 1! id: %s x: %s y: %s", mPointerOne, mLastOneY, mLastOneY); } } else if (mPointerTwo == INVALID_POINTER_ID) { mPointerTwo = event.getPointerId(index); mLastTwoX = event.getX(index); mLastTwoY = event.getY(index); if (mIsDebug) { log("Got pointer 2! id: %s x: %s y: %s", mPointerTwo, mLastTwoY, mLastTwoY); } } else { if (mIsDebug) { log("Ignoring additional pointer. id: %s", event.getPointerId(index)); } } break; case MotionEvent.ACTION_MOVE: if (mPointerTwo == INVALID_POINTER_ID) { /** * 單觸點滑動是控制3D佈局的旋轉角度 */ int i = 0; int count = event.getPointerCount(); while (i < count) { if (mPointerOne == event.getPointerId(i)) { float eventX = event.getX(i); float eventY = event.getY(i); float dx = eventX - mLastOneX; float dy = eventY - mLastOneY; float drx = 90 * (dx / getWidth()); float dry = 90 * (-dy / getHeight()); /** * 屏幕上X的位移影響的是座標系裏Y軸的偏移角度,屏幕上Y的位移影響的是座標系裏X軸的偏移角度 * 根據實際位移結合前面定義的旋轉角度區間算出應該旋轉的角度 */ mRotationY = Math.min(Math.max(mRotationY + drx, ROTATION_MIN), ROTATION_MAX); mRotationX = Math.min(Math.max(mRotationX + dry, ROTATION_MIN), ROTATION_MAX); if (mIsDebug) { log("Single pointer moved (%s, %s) affecting rotation (%s, %s).", dx, dy, drx, dry); } mLastOneX = eventX; mLastOneY = eventY; invalidate(); } i++; } } else { /** * 多觸點滑動是控制佈局的縮放和圖層間距 */ int pointerOneIndex = event.findPointerIndex(mPointerOne); int pointerTwoIndex = event.findPointerIndex(mPointerTwo); float xOne = event.getX(pointerOneIndex); float yOne = event.getY(pointerOneIndex); float xTwo = event.getX(pointerTwoIndex); float yTwo = event.getY(pointerTwoIndex); float dxOne = xOne - mLastOneX; float dyOne = yOne - mLastOneY; float dxTwo = xTwo - mLastTwoX; float dyTwo = yTwo - mLastTwoY; /** * 首先判斷是垂直滑動仍是橫向滑動 */ if (mMultiTouchTracking == TRACKING_UNKNOWN) { float adx = Math.abs(dxOne) + Math.abs(dxTwo); float ady = Math.abs(dyOne) + Math.abs(dyTwo); if (adx > mSlop * 2 || ady > mSlop * 2) { if (adx > ady) { mMultiTouchTracking = TRACKING_HORIZONTALLY; } else { mMultiTouchTracking = TRACKING_VERTICALLY; } } } /** * 若是是垂直滑動調整縮放比 * 若是是橫向滑動調整層之間的距離 */ if (mMultiTouchTracking == TRACKING_VERTICALLY) { if (yOne >= yTwo) { mZoom += dyOne / getHeight() - dyTwo / getHeight(); } else { mZoom += dyTwo / getHeight() - dyOne / getHeight(); } /** * 算出調整後的縮放比例 */ mZoom = Math.min(Math.max(mZoom, ZOOM_MIN), ZOOM_MAX); invalidate(); } else if (mMultiTouchTracking == TRACKING_HORIZONTALLY) { if (xOne >= xTwo) { mSpacing += (dxOne / getWidth() * SPACING_MAX) - (dxTwo / getWidth() * SPACING_MAX); } else { mSpacing += (dxTwo / getWidth() * SPACING_MAX) - (dxOne / getWidth() * SPACING_MAX); } /** * 算出調整後的圖層間距 */ mSpacing = Math.min(Math.max(mSpacing, SPACING_MIN), SPACING_MAX); invalidate(); } if (mMultiTouchTracking != TRACKING_UNKNOWN) { mLastOneX = xOne; mLastOneY = yOne; mLastTwoX = xTwo; mLastTwoY = yTwo; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: index = action != ACTION_POINTER_UP ? 0 : event.getActionIndex(); int pointerId = event.getPointerId(index); if (mPointerOne == pointerId) { /** * 多觸點狀態切換到單觸點狀態 * 即若是原先是調整縮放和圖層間距的狀態,放開一個手指後轉爲控制圖層旋轉狀態 */ mPointerOne = mPointerTwo; mLastOneX = mLastTwoX; mLastOneY = mLastTwoY; if (mIsDebug) { log("Promoting pointer 2 (%s) to pointer 1.", mPointerTwo); } /** * reset多觸點狀態 */ mPointerTwo = INVALID_POINTER_ID; mMultiTouchTracking = TRACKING_UNKNOWN; } else if (mPointerTwo == pointerId) { if (mIsDebug) { log("Lost pointer 2 (%s).", mPointerTwo); } /** * reset多觸點狀態 */ mPointerTwo = INVALID_POINTER_ID; mMultiTouchTracking = TRACKING_UNKNOWN; } break; default: break; } return true; } @Override public void draw(Canvas canvas) { if (!mIsLayerInteractionEnabled) { super.draw(canvas); return; } getLocationInWindow(mLocation); /** * 頁面左上角座標 */ float x = mLocation[0]; float y = mLocation[1]; int saveCount = canvas.save(); /** * 頁面中心座標 */ float cx = getWidth() / 2f; float cy = getHeight() / 2f; mCamera.save(); /** * 先旋轉 */ mCamera.rotate(mRotationX, mRotationY, 0F); mCamera.getMatrix(mMatrix); mCamera.restore(); mMatrix.preTranslate(-cx, -cy); mMatrix.postTranslate(cx, cy); canvas.concat(mMatrix); /** * 再縮放 */ canvas.scale(mZoom, mZoom, cx, cy); if (!mLayeredViewQueue.isEmpty()) { throw new AssertionError("View queue is not empty."); } { int i = 0; int count = getChildCount(); while (i < count) { LayeredView layeredView = mLayeredViewPool.obtain(); layeredView.set(getChildAt(i), 0); mLayeredViewQueue.add(layeredView); i++; } } /** * 廣度優先進行遍歷 */ while (!mLayeredViewQueue.isEmpty()) { LayeredView layeredView = mLayeredViewQueue.removeFirst(); View view = layeredView.mView; int layer = layeredView.mLayer; /** * 在draw期間儘可能避免對象的反覆建立 * 回收LayeredView一會再複用 */ layeredView.clear(); mLayeredViewPool.restore(layeredView); /** * 隱藏viewgroup內可見的view */ if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; mVisibilities.clear(); int i = 0; int count = viewGroup.getChildCount(); while (i < count) { View child = viewGroup.getChildAt(i); /** * 將可見的view記錄到mVisibilities中 */ if (child.getVisibility() == VISIBLE) { mVisibilities.set(i); child.setVisibility(INVISIBLE); } i++; } } int viewSaveCount = canvas.save(); /** * 移動出圖層的距離 */ float translateShowX = mRotationY / ROTATION_MAX; float translateShowY = mRotationX / ROTATION_MAX; float tx = layer * mSpacing * mDensity * translateShowX; float ty = layer * mSpacing * mDensity * translateShowY; canvas.translate(tx, -ty); /** * 畫view的邊框 */ view.getLocationInWindow(mLocation); canvas.translate(mLocation[0] - x, mLocation[1] - y); mViewBoundsRect.set(0, 0, view.getWidth(), view.getHeight()); canvas.drawRect(mViewBoundsRect, mViewBorderPaint); /** * 畫view的內容 */ if (mIsDrawingViews) { view.draw(canvas); } /** * 畫view的id */ if (mIsDrawIds) { int id = view.getId(); if (id != NO_ID) { canvas.drawText(nameForId(id), mTextOffset, mTextSize, mViewBorderPaint); } } canvas.restoreToCount(viewSaveCount); /** * 把剛剛應該顯示但又設置了不可見的view從隊列裏取出來,後面再繪製 */ if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; int i = 0; int count = viewGroup.getChildCount(); while (i < count) { if (mVisibilities.get(i)) { View child = viewGroup.getChildAt(i); child.setVisibility(VISIBLE); LayeredView childLayeredView = mLayeredViewPool.obtain(); childLayeredView.set(child, layer + 1); mLayeredViewQueue.add(childLayeredView); } i++; } } } canvas.restoreToCount(saveCount); } /** * 根據id值反算出在佈局文件中定義的id名字 * * @param id * @return */ private String nameForId(int id) { String name = mIdNames.get(id); if (name == null) { try { name = getResources().getResourceEntryName(id); } catch (NotFoundException e) { name = String.format("0x%8x", id); } mIdNames.put(id, name); } return name; } private static void log(String message, Object... object) { TLog.i("Scalpel", String.format(message, object)); } private static class LayeredView { private View mView = null; /** * mView所處的層級 */ private int mLayer = 0; void set(View view, int layer) { mView = view; mLayer = layer; } void clear() { mView = null; mLayer = -1; } } private static abstract class Pool<T> { private Deque<T> mPool; Pool(int initialSize) { mPool = new ArrayDeque<T>(initialSize); for (int i = 0; i < initialSize; i++) { mPool.addLast(newObject()); } } T obtain() { return mPool.isEmpty() ? newObject() : mPool.removeLast(); } void restore(T instance) { mPool.addLast(instance); } protected abstract T newObject(); } }