引子
最近,在做產品的需求的時候,遇到 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 上面。
步驟:
-
獲取錨點 TextView 實例對象;
-
根據實例對象獲取 ContentView;
-
根據 ContentView 和 TextView 的顯示位置確定 TextView 在 ContentView 中的位置;
- 將 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 的邏輯漏洞,應該在每次添加前都做一個檢查,確保不會重復添加。
到這里,整個知識點就講完了。