最近,在作產品的需求的時候,遇到 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 顯示位置肯定下來。那我要怎麼去獲取按鈕的顯示位置呢。下面就得介紹另外一個方法呢。
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 上面。
步驟:
獲取錨點 TextView 實例對象;
根據實例對象獲取 ContentView;
根據 ContentView 和 TextView 的顯示位置肯定 TextView 在 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 的邏輯漏洞,應該在每次添加前都作一個檢查,確保不會重複添加。
到這裏,整個知識點就講完了。