最近,在作产品的需求的时候,遇到 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 的逻辑漏洞,应该在每次添加前都作一个检查,确保不会重复添加。
到这里,整个知识点就讲完了。