引子

最近,在做产品的需求的时候,遇到 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 的逻辑漏洞,应该在每次添加前都做一个检查,确保不会重复添加。

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

Android 如何动态添加 View 并显示在指定位置。的更多相关文章

  1. Mono for android 如何动态添加View,线程内部如何更新UI.

    貌似所有设计到UI的程序原理都是一样的,子线程是不能够更新UI状态的,所以就必须使用UI自身或者第三方来更新UI. 如 在WinForm 中 就可以使用Control.Invoke(Action ac ...

  2. 动态添加布局、动态添加View、LinearLayout动态添加View;

    LinearLayout提供了几个方法,用作动态添加View特别好用: 可以添加View.删除View.删除指定位置View.删除全部View: 看代码: public class MainActiv ...

  3. 微信小程序之一:动态添加view(view包含picker,input)

    <view wx:for="{{array}}" wx:key="this" class="borderContainer"> ...

  4. Android 在程序中动态添加 View 布局或控件

    有时我们需要在程序中动态添加布局或控件等,下面用程序来展示一下相应的方法: 1.addView 添加View到布局容器 2.removeView 在布局容器中删掉已有的View 3.LayoutPar ...

  5. 往Layout中动态添加View

    需要注意几个方法:基本上所有的方法参数单位是px 1.设置View的宽高: LinearLayout.LayoutParams params = new LinearLayout().LayoutPa ...

  6. vue-element-admin登录逻辑,以及动态添加路由,显示侧边栏

    这段时间在研究element-admin,感觉这个库有许多值得学习的地方,我学习这个库的方法是,先看它的路由,顺着路由,摸清它的逻辑,有点像顺藤摸瓜. 这个库分的模块非常清晰,适合多人合作开发项目,但 ...

  7. Android Fragment动态添加 FragmentTransaction FragmentManager

    Fragment常用的三个类:android.app.Fragment 主要用于定义Fragmentandroid.app.FragmentManager 主要用于在Activity中操作Fragme ...

  8. GridView动态添加View

    activity_main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ...

  9. BootStrap-select插件动态添加option无法显示

    问题描述: 在使用bootstrap-select插件时出现下拉框无法显示动态追加的option,经过查看element元素发现,select标签已经append进去了所需的option选项,但是页面 ...

随机推荐

  1. 函数进阶(二) day13

    目录 昨日内容 闭包函数 装饰器 二层装饰器 装饰器模板 三层装饰器 今日内容 迭代器 可迭代对象 迭代器对象 for循环原理(迭代循环) 三元表达式 列表推导式 字典生成式 生成器 yield关键字 ...

  2. 记录一些常用的python库、软件或者网址

    1.数据收集 BeautifulSoup.scrapy.selenium.requests 2.数据分析 pandas.numpy.pyDD.spacy 3.数据可视化 matplotlib.seab ...

  3. fenby C语言 P31 使用数组的指针

    ++p代表p=p+1; #include <stdio.h> int main(void){ int a[5],i; for(i=0;i<5;i++) *(a+i)=1; print ...

  4. 2018.8.20 Python之路---常用模块

    一.re模块 查找: re.findall(‘正则表达式’,‘字符串’) 匹配所有符合正则表达式的内容,形成一个列表,每一项都是列表中的一个元素. ret = re.findall('\d+','sj ...

  5. SpringBoot 整合 Elasticsearch深度分页查询

    es 查询共有4种查询类型 QUERY_AND_FETCH: 主节点将查询请求分发到所有的分片中,各个分片按照自己的查询规则即词频文档频率进行打分排序,然后将结果返回给主节点,主节点对所有数据进行汇总 ...

  6. JDK JRE JVM的区别与关系

    JDK JAVA开发工具包    他包含了JRE   JAVA运行环境   JVM JAVA虚拟机他是跨平台的核心主件   他将Java源文件编译成 .class结尾字节码文件交由不同计算机执行    ...

  7. MAVEN(一) 安装和环境变量配置

    一.安装步骤 1.安装maven之前先安装jdk,并配置好环境变量.确保已安装JDK,并 “JAVA_HOME” 变量已加入到 Windows 环境变量. 2.下载maven 进入官方网站下载网址如下 ...

  8. python——函数的形参和实参、参数

    python的参数分类 python参数可以分为两类:1.定义时的参数--形参(形式参数).2.调用时的参数--实参(实际参数,传参) 实参的规则 实参就是在函数调用的时候,通过函数后面的括号传递给函 ...

  9. SpringBoot系列之@Conditional注解用法简介

    SpringBoot系列之@Conditional注解用法简介 引用Spring官方文档的说法介绍一下@Conditional注解:Spring5.0.15版本@Conditional注解官方文档 @ ...

  10. Cauchy-Binet公式的证明 及 对Denton et al. (2019)的个人注(1)

    ------------恢复内容开始------------ 据新闻报道数学天才陶哲轩和3个物理学家研究出一个只用特征值就可以计算矩阵特征向量的公式, 我感觉很有趣, 这应该能够应用在很多领域中, 所 ...