餓了麼UETool原理初探

zjw-swun發表於2018-05-21

最近看見一個好庫github.com/eleme/UEToo…

1.餓了麼UETool效果圖&&用途

餓了麼UETool原理初探

自從我把這個工具給我們設計同學安利之後,她們就愛的不要不要的,用過安卓系統開發者選項的同學知道,有一個檢視邊界的按鈕,但是有時候呢,覺得功能不夠,因為開發者選項顯示邊距的和android studio的Layout Inspector 或者DDMS 的uiautomator工具

餓了麼UETool原理初探
一樣都是靜態顯示view邊距以及相關狀態,但是!有了UEtool,不僅能看見view的各種屬性,你還能動態改!也就是說你不僅能看你還能摸,有了這個工具,我再也不用為了改1,2個dp的邊距我再編譯執行了!設計同學也能更好調整UI了。

2.餓了麼UETool原理初探

以UETool官方Demo的捕捉控制元件功能例吧。

餓了麼UETool原理初探
如何快速分析一個我們完全陌生的app呢,那上工具,第一步先看看目前activity是誰

餓了麼UETool原理初探
adb shell dumpsys window w | findstr mCurrent或者 adb shell dumpsys window w | grep mCurrent 該命令能區分activity和popupwindow (win用findstr mac/linux用 grep)

好目標就是me.ele.uetool.TransparentActivity

第二步,看目標View

餓了麼UETool原理初探

這就是捕捉控制元件功能對應的特殊ViewGroup了,它有一個成員變數叫AttrsDialog是一個自定義Dialog,展示的就是View屬性列表的RecyclerView,我們重點看它的adapter

  public static class Adapter extends RecyclerView.Adapter {

        private List<Item> items = new ItemArrayList<>();
        private AttrDialogCallback callback;

        public void setAttrDialogCallback(AttrDialogCallback callback) {
            this.callback = callback;
        }

        public void notifyDataSetChanged(Element element) {
            items.clear();
            for (String attrsProvider : UETool.getInstance().getAttrsProvider()) {
                try {
                    IAttrs attrs = (IAttrs) Class.forName(attrsProvider).newInstance();
                    items.addAll(attrs.getAttrs(element));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            notifyDataSetChanged();
        }
複製程式碼

好來一個下一個斷點,我們跳過複雜的封裝邏輯,直接看執行的函式看呼叫棧

餓了麼UETool原理初探
這裡我們看見了資料來源其實就是element,這個element存有一個View成員變數

public class UETCore implements IAttrs {

    @Override
    public List<Item> getAttrs(Element element) {
        List<Item> items = new ArrayList<>();

        View view = element.getView();

        items.add(new SwitchItem("Move", element, SwitchItem.Type.TYPE_MOVE));

        IAttrs iAttrs = AttrsManager.createAttrs(view);
        if (iAttrs != null) {
            items.addAll(iAttrs.getAttrs(element));
        }

        items.add(new TitleItem("COMMON"));
        items.add(new TextItem("Class", view.getClass().getName()));
        items.add(new TextItem("Id", Util.getResId(view)));
        items.add(new TextItem("ResName", Util.getResourceName(view.getResources(), view.getId())));
        items.add(new TextItem("Clickable", Boolean.toString(view.isClickable()).toUpperCase()));
        items.add(new TextItem("Focused", Boolean.toString(view.isFocused()).toUpperCase()));
        items.add(new AddMinusEditItem("Width(dp)", element, EditTextItem.Type.TYPE_WIDTH, px2dip(view.getWidth())));
        items.add(new AddMinusEditItem("Height(dp)", element, EditTextItem.Type.TYPE_HEIGHT, px2dip(view.getHeight())));
        items.add(new TextItem("Alpha", String.valueOf(view.getAlpha())));
        Object background = Util.getBackground(view);
        if (background instanceof String) {
            items.add(new TextItem("Background", (String) background));
        } else if (background instanceof Bitmap) {
            items.add(new BitmapItem("Background", (Bitmap) background));
        }
        items.add(new AddMinusEditItem("PaddingLeft(dp)", element, EditTextItem.Type.TYPE_PADDING_LEFT, px2dip(view.getPaddingLeft())));
        items.add(new AddMinusEditItem("PaddingRight(dp)", element, EditTextItem.Type.TYPE_PADDING_RIGHT, px2dip(view.getPaddingRight())));
        items.add(new AddMinusEditItem("PaddingTop(dp)", element, EditTextItem.Type.TYPE_PADDING_TOP, px2dip(view.getPaddingTop())));
        items.add(new AddMinusEditItem("PaddingBottom(dp)", element, EditTextItem.Type.TYPE_PADDING_BOTTOM, px2dip(view.getPaddingBottom())));

        return items;
    }
複製程式碼

有View物件當然dialog顯示View各個屬性沒問題,那麼很奇怪,這個view是MainActivity的,這個新開的TransparentActivity是怎麼拿到資料來源的呢 莫慌,看這個函式棧,注意到EditAttrLayout 類的triggerActionUp方法的element

@Override
        public void triggerActionUp(MotionEvent event) {
            final Element element = getTargetElement(event.getX(), event.getY());
            if (element != null) {
                EditAttrLayout.this.element = element;
                invalidate();
                if (dialog == null) {
                    dialog = new AttrsDialog(getContext());
                    dialog.setAttrDialogCallback(new AttrsDialog.AttrDialogCallback() {
                        @Override
                        public void enableMove() {
                            mode = new MoveMode();
                            dialog.dismiss();
                        }
                    });
                    dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
                        @Override
                        public void onDismiss(DialogInterface dialog) {
                            element.reset();
                            invalidate();
                        }
                    });
                }
                dialog.show(element);
            }
        }
複製程式碼

好發現關鍵句getTargetElement(event.getX(), event.getY()); 點進去會跳到父類CollectViewsLayout的getTargetElement方法

 protected Element getTargetElement(float x, float y) {
        Element target = null;
        for (int i = elements.size() - 1; i >= 0; i--) {
            final Element element = elements.get(i);
            if (element.getRect().contains((int) x, (int) y)) {
                if (element != childElement) {
                    childElement = element;
                    parentElement = element;
                } else if (parentElement != null) {
                    parentElement = parentElement.getParentElement();
                }
                target = parentElement;
                break;
            }
        }
        if (target == null) {
            Toast.makeText(getContext(), getResources().getString(R.string.uet_target_element_not_found, x, y), Toast.LENGTH_SHORT).show();
        }
        return target;
    }
複製程式碼

element和elements有直接關係再看到elements List<Element> elements怎麼來的呢?list資料填充無非2種常用的要麼add要麼addAll直接command +f或者ctrl + f搜尋elements.add就發現了資料來源設定的函式

private void traverse(View view) {
        if (UETool.getInstance().getFilterClasses().contains(view.getClass().getName())) return;
        if (view.getAlpha() == 0 || view.getVisibility() != View.VISIBLE) return;
        if (getResources().getString(R.string.uet_disable).equals(view.getTag())) return;
        elements.add(new Element(view));
        if (view instanceof ViewGroup) {
            ViewGroup parent = (ViewGroup) view;
            for (int i = 0; i < parent.getChildCount(); i++) {
                traverse(parent.getChildAt(i));
            }
        }
    }
複製程式碼

來搞個斷點看呼叫棧,或者就在element 構造方法下斷點就能省掉上面從element到elemnets的分析 來看下圖

餓了麼UETool原理初探

UETool拿到targetActivity也就是MainActivity,然後反射拿到decoreView,然後呼叫 EditAttrLayout類的父類CollectViewsLayout類traverse方法。

至此按時間順序總結一下,CollectViewsLayout類的onAttachedToWindow通過反射拿到目標MainActivity的decoreView,去給CollectViewsLayout的成員變數List<Element> elements add包裝了decoreView的Element,然後使用者也就是我,點了UETool的操作控制元件按鈕,UP事件的時候AttrsDialog的show方法呼叫adapter.notifyDataSetChanged(element);給Adapter設定被UETCore解開element各種屬性的List<Item>作為資料來源

好了,原理簡單初探到這裡。餓了麼大神的程式碼就這麼大概摸完了,程式碼封裝的很不錯,建議有興趣的同學可以看看,學習一下。

相關文章