• Android 如何动态添加 View 并显示在指定位置。


    引子

    最近,在做产品的需求的时候,遇到 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 上面。

    步骤:

    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 的逻辑漏洞,应该在每次添加前都做一个检查,确保不会重复添加。

    到这里,整个知识点就讲完了。 

  • 相关阅读:
    页面滚屏截图工具推荐
    java总结第二次(剩余内容)//类和对象1
    happy birthday to tbdd tomorrow
    数组增删改查及冒泡
    三个循环方面程序
    三个入门小程序
    java总结第二次//数组及面向对象
    Java总结第一次//有些图片未显示,文章包含基础java语言及各种语句
    后台验证url是不是有效的链接
    img 鼠标滑上后图片放大,滑下后图片复原
  • 原文地址:https://www.cnblogs.com/huansky/p/11937840.html
Copyright © 2020-2023  润新知