引子
最近, 在做产品的需求的时候, 遇到 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 的逻辑漏洞, 应该在每次添加前都做一个检查, 确保不会重复添加.
到这里, 整个知识点就讲完了.
来源: https://www.cnblogs.com/huansky/p/11937840.html