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

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

  • 相关阅读:
    tensorflow2.0 GPU和CPU 时间对比
    第一次使用FileZilla Server
    PremiumSoft Navicat 15 for Oracle中文破解版安装教程
    Unmapped Spring configuration files found. Please configure Spring facet or use 'Create Default Context' to add one including all unmapped files.
    ng : 无法加载文件 D: odejs ode_global g.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。
    angular
    Github上优秀的go项目
    win10---file explore 中remove quick access folder
    react--useEffect使用
    linux---cat 和 grep 的妙用
  • 原文地址:https://www.cnblogs.com/huansky/p/11937840.html
Copyright © 2011-2022 走看看