• 【Android


      首先来介绍一下这个自定义View:

    • (1)这个自定义View的名称叫做 FlowLayout ,继承自ViewGroup类;
    • (2)在这个自定义View中,用户可以放入所有继承自View类的视图,这个布局会自动获取其宽高并排列在布局中,保证每一个视图都完整的显示在界面上;
    • (3)如果用户放入布局的视图的总高度大于设置给这个视图的高度,那么视图就可以支持上下滚动;
    • (4)可以在XML布局文件或JAVA文件中设置布局的padding属性和子元素的margin属性。

      接下来简单介绍一下在这个自定义View中用到的技术点:

    • (1)用到了自定义View三大流程中的测量和布局流程,分别体现在 onMeasure() 和 onLayout() 两个方法中;
    • (2)在onMeasure()方法中,测量所有子元素的宽高,最后通过累加判断得到自身要显示的宽高;
    • (3)在onMeasure()方法中还为所有子元素进行了分行,保证每个子元素都能完整的显示在布局中,达到“流式布局”的功能需求;
    • (4)在onLayout()方法中,一次取出所有子元素,获取onMeasure()方法中测量的宽高,开始布局;
    • (5)在上面的测量和布局过程中,都有将布局的 padding 属性和元素的 margin 属性考虑在内;
    • (6)设置了这个布局中的子元素具有的LayoutParams:通过 generateLayoutParams() 方法设置;
    • (7)在 onInterceptTouchEvent() 方法中对事件进行拦截,保证布局滚动和元素点击不会产生冲突;
    • (8)在 onTouchEvent() 方法中处理了触摸事件,实现布局的滚动功能。

      下面是这个自定义View—— FlowLayout 的实现代码:

      自定义View类 FlowLayout.java 中的代码:

    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 自定义流式布局
     */
    public class FlowLayout extends ViewGroup {
        private List<List<View>> views; // 存放所有子元素(一行一行存储)
        private List<View> lineViews; // 存储每一行中的子元素
        private List<Integer> heights; // 存储每一行的高度
    
        private boolean scrollable; // 是否可以滚动
        private int measuredHeight; // 测量得到的高度
        private int realHeight; // 整个流式布局控件的实际高度
        private int scrolledHeight = 0; // 已经滚动过的高度
        private int startY; // 本次滑动开始的Y坐标位置
        private int offsetY; // 本次滑动的偏移量
        private boolean pointerDown; // 在ACTION_MOVE中,视第一次触发为手指按下,从第二次触发开始计入正式滑动
    
        public FlowLayout(Context context) {
            super(context);
        }
    
        public FlowLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        /**
         * 初始化
         */
        private void init() {
            views = new ArrayList<>();
            lineViews = new ArrayList<>();
            heights = new ArrayList<>();
        }
    
        /**
         * 计算布局中所有子元素的宽度和高度,累加得到整个布局最终显示的宽度和高度
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            // 当前行的宽度和高度(宽度是子元素宽度的和,高度是子元素高度的最大值)
            int lineWidth = 0;
            int lineHeight = 0;
            // 整个流式布局最终显示的宽度和高度
            int flowLayoutWidth = 0;
            int flowLayoutHeight = 0;
            // 初始化各种参数(列表)
            init();
            // 遍历所有子元素,对子元素进行排列
            int childCount = this.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = this.getChildAt(i);
                // 获取到子元素的宽度和高度
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                // 如果当前行中剩余的空间不足以容纳下一个子元素,则换行
                // 换行的同时,保存当前行中的所有元素,叠加行高,然后将行宽和行高重置为0
                if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin > widthSize - getPaddingLeft() - getPaddingRight()) {
                    views.add(lineViews);
                    lineViews = new ArrayList<>();
                    flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth); // 以最宽的行的宽度作为最终布局的宽度
                    flowLayoutHeight += lineHeight;
                    heights.add(lineHeight);
                    lineWidth = 0;
                    lineHeight = 0;
                }
                // 无论换不换行,都需要将元素添加到列表中、处理宽度和高度的值
                lineViews.add(child);
                lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
                lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);
                // 处理最后一行,否则最后一行不能显示
                if (i == childCount - 1) {
                    flowLayoutHeight += lineHeight;
                    flowLayoutWidth = Math.max(flowLayoutWidth, lineWidth);
                    heights.add(lineHeight);
                    views.add(lineViews);
                }
            }
            // 得到最终的宽高
            // 宽度:如果是EXACTLY模式,则遵循测量值,否则使用我们计算得到的宽度值
            // 高度:只要布局中内容的高度大于测量高度,就使用内容高度(无视测量模式);否则才使用测量高度
            int width = widthMode == MeasureSpec.EXACTLY ? widthSize : flowLayoutWidth + getPaddingLeft() + getPaddingRight();
            realHeight = flowLayoutHeight + getPaddingTop() + getPaddingBottom();
            if (heightMode == MeasureSpec.EXACTLY) {
                realHeight = Math.max(measuredHeight, realHeight);
            }
            scrollable = realHeight > measuredHeight;
            // 设置最终的宽高
            setMeasuredDimension(width, realHeight);
        }
    
        /**
         * 对所有子元素进行布局
         */
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // 当前子元素应该布局到的X、Y坐标
            int currentX = getPaddingLeft();
            int currentY = getPaddingTop();
            // 遍历所有子元素,对每个子元素进行布局
            // 遍历每一行
            for (int i = 0; i < views.size(); i++) {
                int lineHeight = heights.get(i);
                List<View> lineViews = views.get(i);
                // 遍历当前行中的每一个子元素
                for (int j = 0; j < lineViews.size(); j++) {
                    View child = lineViews.get(j);
                    // 获取到当前子元素的上、下、左、右的margin值
                    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                    int childL = currentX + lp.leftMargin;
                    int childT = currentY + lp.topMargin;
                    int childR = childL + child.getMeasuredWidth();
                    int childB = childT + child.getMeasuredHeight();
                    // 对当前子元素进行布局
                    child.layout(childL, childT, childR, childB);
                    // 更新下一个元素要布局的X、Y坐标
                    currentX += lp.leftMargin + child.getMeasuredWidth() + lp.rightMargin;
                }
                currentY += lineHeight;
                currentX = getPaddingLeft();
            }
        }
    
        /**
         * 滚动事件的处理,当布局可以滚动(内容高度大于测量高度)时,对手势操作进行处理
         */
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            // 只有当布局可以滚动的时候(内容高度大于测量高度的时候),才会对手势操作进行处理
            if (scrollable) {
                int currY = (int) event.getY();
                switch (event.getAction()) {
                    // 因为ACTION_DOWN手势可能是为了点击布局中的某个子元素,因此在onInterceptTouchEvent()方法中没有拦截这个手势
                    // 因此,在这个事件中不能获取到startY,也因此才将startY的获取移动到第一次滚动的时候进行
                    case MotionEvent.ACTION_DOWN:
                        break;
                    // 当第一次触发ACTION_MOVE事件时,视为手指按下;以后的ACTION_MOVE事件才视为滚动事件
                    case MotionEvent.ACTION_MOVE:
                        // 用pointerDown标志位只是手指是否已经按下
                        if (!pointerDown) {
                            startY = currY;
                            pointerDown = true;
                        } else {
                            offsetY = startY - currY; // 下滑大于0
                            // 布局中的内容跟随手指的滚动而滚动
                            // 用scrolledHeight记录以前的滚动事件中滚动过的高度(因为不一定每一次滚动都是从布局的最顶端开始的)
                            this.scrollTo(0, scrolledHeight + offsetY);
                        }
                        break;
                    // 手指抬起时,更新scrolledHeight的值;
                    // 如果滚动过界(滚动到高于布局最顶端或低于布局最低端的时候),设置滚动回到布局的边界处
                    case MotionEvent.ACTION_UP:
                        scrolledHeight += offsetY;
                        if (scrolledHeight + offsetY < 0) {
                            this.scrollTo(0, 0);
                            scrolledHeight = 0;
                        } else if (scrolledHeight + offsetY + measuredHeight > realHeight) {
                            this.scrollTo(0, realHeight - measuredHeight);
                            scrolledHeight = realHeight - measuredHeight;
                        }
                        // 手指抬起后别忘了重置这个标志位
                        pointerDown = false;
                        break;
                }
            }
            return super.onTouchEvent(event);
        }
    
        /**
         * 调用在这个布局中的子元素对象的getLayoutParams()方法,会得到一个MarginLayoutParams对象
         */
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MarginLayoutParams(getContext(), attrs);
        }
    
        /**
         * 事件拦截,当手指按下或抬起的时候不进行拦截(因为可能这个操作只是点击了布局中的某个子元素);
         * 当手指移动的时候,才将事件拦截;
         * 因此,我们在onTouchEvent()方法中,只能将ACTION_MOVE的第一次触发作为手指按下
         */
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercepted = false;
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    intercepted = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    intercepted = true;
                    break;
                case MotionEvent.ACTION_UP:
                    intercepted = false;
                    break;
            }
            return intercepted;
        }
    }

      布局文件 activity_main.xml 中的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <my.itgungnir.flowlayout.FlowLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="20.0dip">
    
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="My name is ITGungnir" />
    
            ......(省略N个Button)
    
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="20.0dip"
                android:text="Hello" />
    
            ......(省略N个Button)
    
            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="搜嘎" />
        </my.itgungnir.flowlayout.FlowLayout>
    
    </LinearLayout>

      这里注意:我给FlowLayout布局设置了padding为20.0dip;Hello那个按钮的margin属性也设置成了20.0dip,具体的效果见最后的运行效果图。

      主界面JAVA代码中不需要书写任何代码。当然,我们也可以在Activity中通过JAVA代码,动态生成View后添加到这个布局中。这里就不演示了。

      项目的运行效果图如下图所示:

  • 相关阅读:
    深拷贝
    属性showoverflowtooltip内容过多时,如何设置tooltip的宽度,让其多行显示
    vue 实现点击全屏和退出全屏
    大数计算 BigInt()
    vue3 添加 vuex
    width:inherit
    Object 方法整理
    Python逆向爬虫之requests
    爬虫及浏览器开发者工具
    Python逆向爬虫之urllib
  • 原文地址:https://www.cnblogs.com/itgungnir/p/6742150.html
Copyright © 2020-2023  润新知