Android开发技巧——实现设计师给出的视觉居中的布局

简介: 本篇主要是对自定义控件的测量方法(onMeasure(int widthMeasureSpec, int heightMeasureSpec))在实际场景中的运用。在移动应用的设计中,经常有这样的界面:某个界面的元素非常少,比如空列表界面,或者某某操作成功的界面,只有一两个元素在中间。

本篇主要是对自定义控件的测量方法(onMeasure(int widthMeasureSpec, int heightMeasureSpec))在实际场景中的运用。

在移动应用的设计中,经常有这样的界面:某个界面的元素非常少,比如空列表界面,或者某某操作成功的界面,只有一两个元素在中间。但是它们在某个布局里又不是数学上的那个居中,而是经过设计师调出来的“视觉居中”。这种“视觉居中”内部是怎么计算的,我大致也不懂,反正结果就是设计师们看起来要显示的信息给人有感觉是在中间的(通常是比中间偏上一点)。
既是这样,那我们在布局中就不能用gravity="center"layout_gravity="center"等这样的属性来设置了。而使用固定的padding或margin来调整它的位置,也难以在不同的屏幕分辨率中实现同样的效果,那就只好按钮设计图的标注,按比例来计算它的位置了。

按比例来调整子view与layout中的距离,在约束布局(ConstraintLayout)中是可以做到的,但是在我个人看来相对这样简单的需求,约束布局有点重了,并且它的依赖在不同方式的编译下总是很容易出问题(比如在自己电脑编译通过,在travis-ci上编译就提示找不到该库的某个版本),还有拖拽生成的代码格式不是很整齐(我有代码洁癖),总需要自己再去格式化一下代码。那么自定义实现一下也是可以的嘛。

首先像这样简单的界面,通常来说,使用LinearLayout就足够了。我们所需要的只是按比例计算出padding然后设进去,那么内容就能够按我们想要的位置去显示了。在这里,我把这个布局定义为PaddingWeightLinearLayout,即可以按权重来设计它的padding。它提供了四个属性:

    <declare-styleable name="PaddingWeightLinearLayout">
        <attr name="ppLeftPadding" format="integer"/>
        <attr name="ppRightPadding" format="integer"/>
        <attr name="ppTopPadding" format="integer"/>
        <attr name="ppBottomPadding" format="integer"/>
    </declare-styleable>

分别代表每个方向上的padding的权重。
在构造方法里获取这些属性的值:

    private final int mTopWeight;
    private final int mBottomWeight;
    private final int mLeftWeight;
    private final int mRightWeight;

    public PaddingWeightLinearLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PaddingWeightLinearLayout);
        mTopWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppTopPadding, 0);
        mBottomWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppBottomPadding, 0);
        mLeftWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppLeftPadding, 0);
        mRightWeight = ta.getInteger(R.styleable.PaddingWeightLinearLayout_ppRightPadding, 0);
        ta.recycle();
    }

那么接下来,我们只需要计算出所有子View所占的空间,然后计算出水平和竖直方向上剩余的空间,按比例分给这四个padding就可以了。之所以使用LinearLayout是因为它是线性布局,子View按线性排列,比较利于计算。如下图(电脑的图画不好,献丑了):
LinearLayout内容示意图
图1表示的是水平方向的布局,那么它的内容所占的大小是:

  • 宽度为所有子View的宽度加上其左右Margin的总和。
  • 高度为子View高度加上其上下Margin中最高的一个。

图2 是竖直方向的布局,它的内容所占的大小是:

  • 宽度为子View宽度加上其左右Margin中最大的一个。
  • 高度为所有子View的高度加上其上下Margin的总和。

因此,我们要先测量出子View的大小,然后再进行计算。
测试是在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中进行的。其中参数分别表示父布局能够提供给它的空间。这个int类型的参数分为两部分,高2位表示的是模式(mode),后面的30位表示的是具体的大小。这里的mode一共有三个:

  • UNSPECIFIED 父View对子View没有任何约束,可以随意指定大小
  • EXACTLY 父View给子View一个固定的大小,子View会被这些边界限制,不管它自己想要多大
  • AT_MOST 子视图的大小可以是自己想要的值,但是不能超过指定的值

当我们要计算子View时,我们需要调用子View的measure(widthMeasureSpec, int heightMeasureSpec)方法,为了能够得到子View的确定大小,我们需要将widthMeasureSpec mode指定为AT_MOST,代码如下(以下代码都是在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法内):

        int layoutWidth = MeasureSpec.getSize(widthMeasureSpec);
        int layoutHeight = MeasureSpec.getSize(heightMeasureSpec);

        int widthSpec = MeasureSpec.makeMeasureSpec(widthMeasureSpec, MeasureSpec.AT_MOST);
        int heightSpec = MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.AT_MOST);

然后遍历所有子View,计算子View宽高的总和及最大值:

        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            child.measure(widthSpec, heightSpec);
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            int width = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
            int height = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;
            totalWidth += width;
            totalHeight += height;
            if (width > maxWidth) {
                maxWidth = width;
            }
            if (height > maxHeight) {
                maxHeight = height;
            }
        }

然后计算出在水平及竖直方向上剩余的空间:

        int spaceHorizontal;
        int spaceVertical;
        if (getOrientation() == VERTICAL) {
            spaceHorizontal = layoutWidth - maxWidth;
            spaceVertical = layoutHeight - totalHeight;
        } else {
            spaceHorizontal = layoutWidth - totalWidth;
            spaceVertical = layoutHeight - maxHeight;
        }
        if (spaceHorizontal < 0) {
            spaceHorizontal = 0;
        }
        if (spaceVertical < 0) {
            spaceVertical = 0;
        }

最后算出各个方向的padding,设置进去,然后重新调用父类的onMeasure(int, int)方法:

        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int horizontalWeight = mLeftWeight + mRightWeight;
        if (spaceHorizontal > 0 && horizontalWeight > 0) {
            paddingLeft = mLeftWeight * spaceHorizontal / horizontalWeight;
            paddingRight = spaceHorizontal - paddingLeft;
        }

        int verticalWeight = mTopWeight + mBottomWeight;
        if (spaceVertical > 0 && verticalWeight > 0) {
            paddingTop = mTopWeight * spaceVertical / verticalWeight;
            paddingBottom = spaceVertical - paddingTop;
        }

        setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

下面我们只需要在写布局代码的时候,按照设计图填入标注的padding值,就可以按比例计算出内边距并设置,从而让我们的内容按照比例位置显示了:

<com.parkingwang.widget.PaddingWeightLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical"
    android:paddingLeft="@dimen/screen_padding"
    android:paddingRight="@dimen/screen_padding"
    app:ppBottomPadding="287"
    app:ppTopPadding="85">
    ... 内容布局代码
</com.parkingwang.widget.PaddingWeightLinearLayout>

下面分別是实现的效果以及设计图,可以看到,在内容区域它们所在的位置是相同的(由于屏幕分辨率的关系,大小会有微小差异)。
效果图对比

完整代码请参见Github项目:https://github.com/msdx/androidsnippet

目录
相关文章
|
3天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
8天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
1月前
|
ARouter Android开发
Android不同module布局文件重名被覆盖
Android不同module布局文件重名被覆盖
|
10天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
12天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
10天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
11天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
23 2
|
12天前
|
Android开发
布谷语音软件开发:android端语音软件搭建开发教程
语音软件搭建android端语音软件开发教程!
|
20天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
19天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
29 5
下一篇
无影云桌面