Android 如何動態添加 View 並顯示在指定位置。

引子

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

看完後,PM 提了個問題,這個動畫效果範圍能不能再大一點?PM 解釋到按鈕自己大小不是很大,會致使引導效果不夠明顯,也會致使用戶的點擊慾望不夠。我想了想,彷佛頗有道理啊,可是這個能作到嗎?android

答案是固然能夠呢。若是單純從如今的佈局上去將動畫的尺寸去擴大,得改變本來的佈局。這個引導只出現幾回,爲了引導,而去改動原有的佈局,我的以爲改動仍是蠻大的。不值得!ide

因而想用 clipChildren 屬性來試着讓 子 view 突破父佈局,可是這樣一樣會影響其餘子 view,也很差去與按鈕的中心進行定位。佈局

那還有沒有其餘儘量不去改動原有佈局就能夠實現的方案呢?post

有的!動畫

準備知識

相信你們都對下面這段代碼會很熟悉:ui

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

 這段代碼執行後,將 activity_main 這個佈局添加到了 DecorView 。對於 activity 與 DecorView 之間的關係,你們能夠看這篇文章:Android DecorView 與 Activity 綁定原理分析this

DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有惟一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另外一個是 ContentView(窗口內容的容器)也是一個 FrameLayout(android.R.id.content),日常用的 setContentView 就是設置它的子 View 。後面咱們就是在 ContentView 上作文章。url

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

         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 的邏輯漏洞,應該在每次添加前都作一個檢查,確保不會重複添加。

到這裏,整個知識點就講完了。 

相關文章
相關標籤/搜索