1. 程式人生 > >Android 如何動態新增 View 並顯示在指定位置。

Android 如何動態新增 View 並顯示在指定位置。

引子

最近,在做產品的需求的時候,遇到 PM 要求在某個按鈕上新增一個新手引導動畫,引導使用者去點選。作為 RD,我嘩啦啦的就寫好相關邏輯了。自測完成後,提測,PM Review 效果。

看完後,PM 提了個問題,這個動畫效果範圍能不能再大一點?PM 解釋到按鈕本身大小不是很大,會導致引導效果不夠明顯,也會導致使用者的點選慾望不夠。我想了想,似乎很有道理啊,但是這個能做到嗎?

答案是當然可以呢。如果單純從現在的佈局上去將動畫的尺寸去擴大,得改變原本的佈局。這個引導只出現幾次,為了引導,而去改動原有的佈局,個人覺得改動還是蠻大的。不值得!

於是想用 clipChildren 屬性來試著讓 子 view 突破父佈局,但是這樣同樣會影響其他子 view,也不好去與按鈕的中心進行定位。

那還有沒有其他儘可能不去改動原有佈局就可以實現的方案呢?

有的!

準備知識

相信大家都對下面這段程式碼會很熟悉:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

 這段程式碼執行後,將 activity_main 這個佈局新增到了 DecorView 。對於 activity 與 DecorView 之間的關係,大家可以看這篇文章:Android DecorView 與 Activity 繫結原理分析

DecorView 是一個應用視窗的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(視窗內容的容器)也是一個 FrameLayout(android.R.id.content),平常用的 setContentView 就是設定它的子 View 。後面我們就是在 ContentView 上做文章。

另外,對於 FrameLayout,他的子 view 如果沒有指定 Gravity 的話,那麼就會堆積再左上角,誰是後面新增的誰在上面。其實使用也可以下面兩個方法來決定放置的位置:

         public void setX(float x) {
        setTranslationX(x - mLeft);
    }

    public void setY(float y) {
        setTranslationY(y - mTop);
    }

 可以發現這兩個方法其實是都通過設定平移的偏移的量來實現的。這樣我們就可以指定 View 所顯示的位置的。

那如何去獲取 PM 需求中所要求的位置呢?如果這個按鈕是 wrap_content 的,按鈕的寬度是無法確定的?那就只能拿到按鈕對應的 View 例項,通過該例項就可以獲取到按鈕的寬高。

獲取 view 的顯示位置

按鈕的寬高知道後,結合前面介紹的兩個設定顯示位置方法,有些人應該已經猜到要怎麼做了。如果能夠知道按鈕的顯示位置,這時候只要呼叫這兩個方法,就可以將動畫 view 顯示位置確定下來。那我要怎麼去獲取按鈕的顯示位置呢。下面就得介紹另一個方法呢。

    public final boolean getLocalVisibleRect(Rect r) {
        final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
        if (getGlobalVisibleRect(r, offset)) {
            r.offset(-offset.x, -offset.y); // make r local
            return true;
        }
        return false;
    }

 在來看看 getGlobalVisibleRect 的實現,

   public boolean getGlobalVisibleRect(Rect r, Point globalOffset) {
        int width = mRight - mLeft;
        int height = mBottom - mTop;
        if (width > 0 && height > 0) {
            r.set(0, 0, width, height);
            if (globalOffset != null) {
                globalOffset.set(-mScrollX, -mScrollY);
            }
            return mParent == null || mParent.getChildVisibleRect(this, r, globalOffset);
        }
        return false;
    }

 

簡單來說,就是 rect 是 View 的寬高和 View 的偏移量綜合的結果,具體計算過程咱就不糾結了,下面說下每個數字代表的含義:

其中對於 getLocalVisibleRect 來說:

  • rect.left 大於0,表示左邊已經處於不可見,否則是等於0;

  • rect.top 大於0,表示上邊已經處於不可見,否則是等於0;

  • rect.right 小於 View 的寬度,表是處於不可見,否則是等於 View 的寬度;

  • rect.bottom 小於 View 的高度,表是處於不可見,否則是等於 View 的高度;

  • View 的可見高度 = rect.bottom - rect.top;View 的可見寬度 = rect.right - rect.left;

對於 getGlobalVisibleRect 來說:就是其在螢幕當中的位置。具體可見下面的 gif 圖

相信大家在有了上述知識基礎之後,就知道要怎麼做了。下一步就是實戰。

實踐

目標:將一個 imageView 居中顯示在一個 TextView 上面。

步驟:

  1. 獲取錨點 TextView 例項物件;

  2. 根據例項物件獲取 ContentView;

  3. 根據 ContentView 和 TextView 的顯示位置確定 TextView 在 ContentView 中的位置;

  4. 將 imageView 新增到 ContentView 上,根據位置調整位置。

經過上面四步即可將一個 view 新增到任何一個位置呢。

最終實現效果:

 

 原始碼

下面是具體實現程式碼,為了便於該邏輯的重複利用,我稍微進行了封裝。採用的是 builder 模式,雖然我的變數比較少,但是真的當封裝的功能足夠強大的時候,需要用到屬性就會很多,這時候就能體會到 builder 模式的強大呢。比如可以支援設定 Gravity,支援傳入不同的 targetView。現在我是直接 imageView 寫死的。

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

       
        mText = findViewById(R.id.text);
        mText.setClickable(true);
        mText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showCenterView(mText);
            }
        });
   }

   public void showCenterView(View view) {
        FloatingManager.Builder builder = FloatingManager.getBuilder();
        builder.setAnchorView(view);
        FloatingManager manager = builder.build();
        manager.showCenterView();
    }

 下面是 採用的是 builder 模式簡單封裝的一個管理類:

public class FloatingManager {

    private View mAnchorView;

    private String mTitle;

    private ViewGroup mRootView;

    public static Builder getBuilder() {
        return new Builder();
    }

    static class Builder {
        private FloatingManager mManager;

        public FloatingManager build() {
            return mManager;
        }

        public Builder() {
            mManager = new FloatingManager();
        }

        public Builder setAnchorView(View view) {
            mManager.setAnchorView(view);
            return this;
        }

        public Builder setTitle(String title) {
            mManager.setTitle(title);
            return this;
        }

    }

    public void setAnchorView(View view) {
        mAnchorView = view;
    }

    public void setTitle(String title) {
        this.mTitle = title;
    }

    public void showCenterView() {
        if (mAnchorView == null) {
            return;
        }
        Activity activity = (Activity) mAnchorView.getContext();
        mRootView = activity.findViewById(android.R.id.content);

        Rect anchorRect = new Rect();
        Rect rootViewRect = new Rect();

        mAnchorView.getGlobalVisibleRect(anchorRect);
        mRootView.getGlobalVisibleRect(rootViewRect);

        // 建立 imageView
        ImageView imageView = new ImageView(activity);
        imageView.setImageDrawable(activity.getResources().getDrawable(R.drawable.ic_launcher));
        mRootView.addView(imageView);

        // 調整顯示區域大小
        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) imageView.getLayoutParams();
        params.width = 100;
        params.height = 100;
        imageView.setLayoutParams(params);

        // 設定居中顯示
        imageView.setY(anchorRect.top - rootViewRect.top + (mAnchorView.getHeight() - 100) / 2);
        imageView.setX(anchorRect.left + (mAnchorView.getWidth()  - 100) / 2);
    }

}

其實新增以後,還得考慮事件的點選之類的,比如可以通過設定回撥,當點選引導動畫的時候,先隱藏動畫,再去主動促發按鈕的點選邏輯等。

還有就是上面寫的管理類存在重複新增 imageView 的邏輯漏洞,應該在每次新增前都做一個檢查,確保不會重複新增。

到這裡,整個知識點就講完了。&n