Android自定义控件(十三)——实现CSDN搜索框文字提示容器

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Android自定义控件(十三)——实现CSDN搜索框文字提示容器

分析CSDN搜索文字布局控件


首先,我们需要来分析一下,CSDN搜素框文字提示控件的布局,下图为最新CSDN的搜索框布局提示图:

经过前面的自定义控件,布局等学习后,我们来简单的来分析上面这个控件,可以看到文字的提示是由很多的文字(TextView)组成,而每排的文字(TextView)数量不固定,根据长度自适应每行多少个文字,同时他们有上间距,也有下间距,也就是布局中常用的Margin。


ok,我们看到的也就这些,主要实现的就是这个容器规则,但是我们前面只介绍了如果布局容器,并没有介绍如何使用Margin,所以我们先来了解一下ViewGroup中是如何获取,以及设置Margin的。


ViewGroup获取子控件的Margin


我们先来看看,ViewGroup需要重写获取设置Margin值的三个方法:

@Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
    }
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }
    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }


每个自定义控件都需要这样写,至于原因,我们还需要来看一段源代码,比如new MarginLayoutParams(getContext(),attrs),你可以通过Android Studio开发工具点进去看下,就会发现其实跟前面获取自定义属性的代码基本一样,代码如下:

public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
            setBaseAttributes(a,
                R.styleable.ViewGroup_MarginLayout_layout_width,
                R.styleable.ViewGroup_MarginLayout_layout_height);
        int margin = a.getDimensionPixelSize(
                com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        } else {
            int horizontalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            int verticalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                if (leftMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                    leftMargin = DEFAULT_MARGIN_RESOLVED;
                }
                rightMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                        UNDEFINED_MARGIN);
                if (rightMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                    rightMargin = DEFAULT_MARGIN_RESOLVED;
                }
            }
            startMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                    DEFAULT_MARGIN_RELATIVE);
            endMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                    DEFAULT_MARGIN_RELATIVE);
            if (verticalMargin >= 0) {
                topMargin = verticalMargin;
                bottomMargin = verticalMargin;
            } else {
                topMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                        DEFAULT_MARGIN_RESOLVED);
                bottomMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                        DEFAULT_MARGIN_RESOLVED);
            }
            if (isMarginRelative()) {
               mMarginFlags |= NEED_RESOLUTION_MASK;
            }
        }
        final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport();
        final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
        if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) {
            mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK;
        }
        // Layout direction is LTR by default
        mMarginFlags |= LAYOUT_DIRECTION_LTR;
        a.recycle();
    }


简单的来理解,这段代码先是提取了layout_margin的值并进行设置,然后,如果用户没有设置layout_margin,而是单独设置的,就一个一个提取。


需要注意的是,之所以需要重写这些函数,是因为默认的ViewGroup方法只会获取layout_width和layout_height,只有MarginLayoutParams()方法才具有提取margin值的功能。


实现CSDN搜索文字提示布局


今天讲解的Margin加上前面讲解的自定义布局的知识,就可以实现CSDN搜索文字提示布局,首先,我们还是来分析一下博文开始的图片,是不是发现所有的TextView的style除了文字不一样,其他的都一样呢?所以我为了减少代码额冗余,我们先自定义一个style,代码如下:

<style name="textview">
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:layout_margin">4dp</item>
    <item name="android:background">@drawable/textviewshap</item>
    <item name="android:textColor">#FF1E90FF</item>
    <item name="android:textSize">16sp</item>
</style>


接着,我们设置我们的布局文件,主Activity的XML布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.liyuanjinglyj.csdnserachapplication.CSDNSerachView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            style="@style/textview"
            android:text="Python"/>
        <TextView
            style="@style/textview"
            android:text="Java"/>
        <TextView
            style="@style/textview"
            android:text="Spring Boot"/>
        <TextView
            style="@style/textview"
            android:text="PHP"/>
        <TextView
            style="@style/textview"
            android:text="Vue"/>
        <TextView
            style="@style/textview"
            android:text="Flutter"/>
        <TextView
            style="@style/textview"
            android:text="Python基础教程"/>
        <TextView
            style="@style/textview"
            android:text="Java学习路线"/>
        <TextView
            style="@style/textview"
            android:text="C语言"/>
  <!--省略其他TextView-->
    </com.liyuanjinglyj.csdnserachapplication.CSDNSerachView>


然后就是常规的自定义ViewGroup步骤,测量onMeasure(),布局onLayout(),首先肯定是测量所有子控件的大小,计算整个自定义ViewGroup的宽高,代码如下(还是常规套路):

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode=MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec);
        int width=0;
        int height=0;
        //中间代码
        setMeasuredDimension((measureWidthMode==MeasureSpec.EXACTLY)?measureWidth:width,
                (measureHeightMode==MeasureSpec.EXACTLY)?measureHeight:height);
    }


最重要的还是中间计算的代码,但是我们现在增加了Margin值,所以我们需要多申请2个变量代码如下:

int lineWidth=0;//记录每一行的宽度
int lineHeight=0;//记录每一行的高度


定义变量后,我们需要遍历每个子控件,然后根据子空间的大小是否还能放在当前行,不行的话跳转到下一行,并记录当前行使用的宽度,以便后续判断其他子控件是否装的下,完整代码如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width = 0;//整体宽度
        int height = 0;//整体高度
        int lineWidth = 0;//记录每一行的宽度
        int lineHeight = 0;//记录每一行的高度
        int count = getChildCount();//获取子控件数量
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (lineWidth + childWidth > measureWidth) {
                //如果当前行占位Textview+新占位如果当前行占位Textview大于整体宽度,就需要换行设置控件
                width = Math.max(lineWidth, width);//获取最大值的宽度值
                height += lineHeight;//换行后直接加上宽度就是整体宽度
                //因为当前行放不下控件,所以宽高直接等于新换行的控件宽高
                lineWidth = childWidth;
                lineHeight = childHeight;
            } else {
                //否则
                lineWidth += childWidth;//如果控件没有换行,就是之前的宽度加上新TextView宽度
                lineHeight = Math.max(lineHeight, childHeight);//而如果之前高度高于当前子控件高度,那么该行还是之前的宽度,否则为新控件高度
            }
            //因为最后一行不管填不填满,高度都需要加上最后一行所以,没经过最后换行的操作,上面是不会计算加最后一行的高度的,所以必须单独写出来
            if (i == count - 1) {
                height += lineHeight;
                width = Math.max(width, lineWidth);
            }
        }
        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
                (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
    }


上面的注释应该够详细了,这里就不再赘述了,不过需要说明的是子控件的宽度是自身宽度加上margin_right和margin_left,子空间高度是自身高度加上margin_top和margin_bottom,上面自定义ViewGroup也就这个值的获取与之前博文不同,其他的都是一些常规操作与判断。

测量onMeasure()写完了,我们还需要进行布局代码的编写,因为我们在控件中加入了margin属性,所以我们需要标记当前子控件的top坐标和left坐标,为什么不需要bottom,以及right呢?因为定位一个控件是其左上角坐标(如上图所示,看完就会明白),具体的代码如下:

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int lineWidth = 0;
        int lineHeight = 0;
        int top = 0, left = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (childWidth + lineWidth > getMeasuredWidth()) {
                //如果换行,与onMeasure基本类同
                top += lineHeight;
                left = 0;
                lineHeight = childHeight;
                lineWidth = childWidth;
            } else {
                lineHeight = Math.max(lineHeight, childHeight);
                lineWidth += childWidth;
            }
            //计算子空间的left,top,right,bottom
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + child.getMeasuredWidth();
            int bc = tc + child.getMeasuredHeight();
            child.layout(lc, tc, rc, bc);
            left += childWidth;//将left更改为下个子空间的起始点
        }
    }


这样我们就是实现了CSDN搜索框下文字布局的容器控件效果,当然,我这里还设置了边框等样式,本文主要介绍自定义ViewGroup,边框样式属于基础,这里就不再介绍了。(边框效果最后源代码里面有),实现效果如下:



本文GitHub下载地址:点击下载

相关文章
|
2月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。
|
15天前
|
搜索推荐 Android开发 开发者
安卓应用开发中的自定义控件实践
在安卓应用开发的广阔天地中,自定义控件如同璀璨的星辰,点亮了用户界面设计的夜空。它们不仅丰富了交互体验,更赋予了应用独特的个性。本文将带你领略自定义控件的魅力,从基础概念到实际应用,一步步揭示其背后的原理与技术细节。我们将通过一个简单的例子——打造一个具有独特动画效果的按钮,来展现自定义控件的强大功能和灵活性。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇通往更高阶UI设计的大门。
|
2月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件基础与进阶
【10月更文挑战第5天】在Android应用开发中,自定义控件是提升用户体验和界面个性化的重要手段。本文将通过浅显易懂的语言和实例,引导你了解自定义控件的基本概念、创建流程以及高级应用技巧,帮助你在开发过程中更好地掌握自定义控件的使用和优化。
48 10
|
1月前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
2月前
|
前端开发 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的世界里,自定义控件如同画家的画笔,能够绘制出独一无二的界面。通过掌握自定义控件的绘制技巧,开发者可以突破系统提供的界面元素限制,创造出既符合品牌形象又提供卓越用户体验的应用。本文将引导你了解自定义控件的核心概念,并通过一个简单的例子展示如何实现一个基本的自定义控件,让你的安卓应用在视觉和交互上与众不同。
|
3月前
|
缓存 前端开发 Android开发
安卓应用开发中的自定义控件
【9月更文挑战第28天】在安卓应用开发中,自定义控件是提升用户界面和交互体验的关键。本文通过介绍如何从零开始构建一个自定义控件,旨在帮助开发者理解并掌握自定义控件的创建过程。内容将涵盖设计思路、实现方法以及性能优化,确保开发者能够有效地集成或扩展现有控件功能,打造独特且高效的用户界面。
|
3月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义控件
【9月更文挑战第5天】在安卓开发的海洋中,自定义控件如同一艘精致的小船,让开发者能够乘风破浪,创造出既独特又高效的用户界面。本文将带你领略自定义控件的魅力,从基础概念到实战应用,一步步深入理解并掌握这一技术。
|
3月前
|
Android开发
Android经典实战之Textview文字设置不同颜色、下划线、加粗、超链接等效果
本文介绍了 `SpannableString` 在 Android 开发中的强大功能,包括如何在单个字符串中应用多种样式,如颜色、字体大小、风格等,并提供了详细代码示例,展示如何设置文本颜色、添加点击事件等,助你实现丰富文本效果。
295 3
|
4月前
|
数据处理 开发工具 数据安全/隐私保护
Android平台RTMP推送|轻量级RTSP服务|GB28181接入之文字、png图片水印的精进之路
本文探讨了Android平台上推流模块中添加文字与PNG水印的技术演进。自2015年起,为了满足应急指挥及安防领域的需求,逐步发展出三代水印技术:第一代为静态文字与图像水印;第二代实现了动态更新水印内容的能力,例如实时位置与时间信息;至第三代,则优化了数据传输效率,直接使用Bitmap对象传递水印数据至JNI层,减少了内存拷贝次数。这些迭代不仅提升了用户体验和技术效率,也体现了开发者追求极致与不断创新的精神。
|
4月前
|
Android开发 UED 开发者
安卓开发中的自定义控件基础
【8月更文挑战第31天】在安卓应用开发过程中,自定义控件是提升用户界面和用户体验的重要手段。本文将通过一个简易的自定义按钮控件示例,介绍如何在安卓中创建和使用自定义控件,包括控件的绘制、事件处理以及与布局的集成。文章旨在帮助初学者理解自定义控件的基本概念,并能够动手实践,为进一步探索安卓UI开发打下坚实的基础。