HorizontalScrollView介绍

想做一个功能,把纯搜索框做成支持编辑加添加标签,并可以横向滑动,当有标签后点击输入框整体展示成文字,返回再变成文字加标签样式。

这里提供几种方案,由EditText变成支持横向滑动可以有以下几种方案:

1.添加父容器HorizontalScrollView,内部嵌套EditText和LinearLayout,LinearLayout用来添加标签;

2.输入框采用RecyclerView方案,有2中type,第一种展示EditText,第二种展示标签;

这里我用了第一种方案实现,布局如下图

布局

给HorizontalScrollView配置属性,去除装饰

android:fadeScrollbars="false"
android:overScrollMode="never"
android:scrollbars="none"

EditText宽度设置为wrap_content,LinearLayout有个右边距,因为每添加一个标签就要自动滑动到最后一项,预留margin可以展示出更好的效果,可以参考京东添加标签的效果。

京东效果

功能1:添加标签后自动滑到最后一个元素
起初我是计算的宽度,求EditText的宽度和LinearLayout的宽度和,然后平滑滑动到指定的位置;

HorizontalScrollView.smoothScrollTo(EditText.getMeasureWidth()+ LinearLayout.getMeasureWidth(),0);

这里注意用的是smoothScrollTo,不是smoothScrollBy,前者是滑到指定位置,这样如果标签没超出视野,也不会滑出去。smoothScrollBy则是滑动一定的距离,ScrollTo则没有平滑的动画效果。如果要平滑的滑到起点则是HorizontalScrollView.smoothScrollTo(0,0);

后来我看到HorizontalScrollView提供了其他更好的API,如fullScroll(View.FOCUS_RIGHT),表示滑动最右边,View.FOCUS_LEFT则是滑到最左边,代码实现如下:

LinearLayout.post(new Runnable() {
    @Override
    public void run() {
        HorizontalScrollView.fullScroll(View.FOCUS_RIGHT);
    }
});

这里注意下是给LinearLayout加标签也就是addView,所以需要在LinearLayout绘制完成后执行HorizontalScrollView滑动操作,放到post里面。我们知道View的绘制不是立刻完成的,想要获取测量宽度,一般有这些方法:

A. 通过ViewTreeObserver监听View的全局变化事件(addOnGlobalLayoutListener或addOnPreDrawListener),但用完后要移除监听,避免后续每一次发生全局 View 变化均触发该事件,影响性能。

view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        if (Build.VERSION.SDK_INT >= 16) {
          view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
        }else {
          view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        }
        int width = view.getWidth();
    }
});

B. 利用Handler通信机制,添加一个Runnable到MessageQueue 中,当View layout处理完成时,自动发送消息,通知 UI 线程,巧妙获取View 的宽高属性。

view.post(new Runnable() {
    @Override
    public void run() {
        int width = view.getWidth();
    }
});

C. 在Activity的onWindowFocusChanged中获取宽高。


问题1:
EditText正常测量是没有问题,但是如果hint很长,那么EditText.getMeasureWidth()就会返回初始hint的值,即便后边输入了文字且内容很短,宽度还是那么长,这就会导致添加的标签距离文字很远。

我们期望的是这样的效果:

期望

但hint很长的效果:

hint很长

解决方法是手动去设置EditText的宽度,给EditText添加addTextChangedListener,在afterTextChanged里面动态设置宽度。

       if (TextUtils.isEmpty(text)) {
            ViewGroup.LayoutParams layoutParams = EditText.getLayoutParams();
            layoutParams.width = LayoutParams.MATCH_PARENT;
            EditText.setLayoutParams(layoutParams);
        } else {
            ViewGroup.LayoutParams layoutParams = EditText.getLayoutParams();
            layoutParams.width = (int) EditText.getPaint().measureText(EditText.getText().toString());
            EditText.setLayoutParams(layoutParams);
        }

这里判断了如果没有hint就是充满父容器,有文字则按文字的宽度来设置,用了getPaint().measureText()来获得结果。起初我是自己根据EditText的长度乘以12来计算宽度,12是设置的sp值,文字缩放值设置成为1保证长度固定。

// 在Activity中设置,取消系统设置文字大小时对sp的影响
configContextConfiguration(this);
configContextConfiguration(getApplicationContext());

private void configContextConfiguration(Context context) {
    try {
        Resources resources = context.getResources();
        Configuration configuration = resources.getConfiguration();
        configuration.fontScale = 1f;
        resources.updateConfiguration(configuration, resources.getDisplayMetrics());
    } catch (Throwable ignored) {
    }
}

但是这样很不准确,比如空格,数字占的宽度就和汉字不一样,中文符号和英文符号也不一样,由于自己计算的误差导致滑动的位置也不准确,后来看API用measureText就没有这些问题了。

问题2:
点击整个输入框都需要弹出键盘,但是现在EditText变小了,那么需要让LinearLayout来承接点击事件,来唤起键盘,模拟出EditText被点击的效果,把光标移到最后一位。

InputMethodManager mInputMgr = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
LinearLayout.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        EditText.setFocusable(true);
        EditText.setFocusableInTouchMode(true);
        EditText.setCursorVisible(true);
        EditText.requestFocus();
        mInputMgr.showSoftInput(EditText, 0);
        EditText.setSelection(mSearchEdit.getText().toString().length());
    }
});

起初给HorizontalScrollView设置点击事件,但不生效,这里讲解的很详细

HorizontalScrollView#onTouchEvent方法总结起来有两点:
1、代码中没有调用OnClickListener的地方,也没有调用super.onTouchEvent方法,所以说我们给HorizontalScrollView设置的OnClickListener都是无效的,不会被调用。
2、在看返回值,如果HorizontalScrollView没有子View,那么onTouchEvent就返回false,表示不消耗事件;其余情况都会返回true,表示消耗事件。

网友提供了一种解决方案,给HorizontalScrollView设置touch监听,放开手就弹出键盘,这个适合搭配一个全屏的EditText使用,我们希望技能编辑又能滑动,很显然这样改完之后如果滑动标签后也会弹出键盘,不是想要的效果。

scrollView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                //通知父控件请勿拦截本控件touch事件
                view.getParent().requestDisallowInterceptTouchEvent(true);
                switch (motionEvent.getAction()){
                    case MotionEvent.ACTION_UP:
                        //点击整个页面都会让内容框获得焦点,且弹出软键盘
                        content.setFocusable(true);
                        content.setFocusableInTouchMode(true);
                        content.requestFocus();
                        AddFlagActivity.this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
                        break;
                }
                 return false;
            }
        });

由于HorizontalScrollView有子View,并且HorizontalScrollView自身是match_parent,所以它的父容器无法接受到点击事件,只能让子LinearLayout来接受点击事件了。起初没有用到HorizontalScrollView的fillViewport属性,使得LinearLayout设置match_parent不生效,于是设置了一个最小宽度,保证可以接受到点击事件,但是这存在适配性问题,而且搜索框有变大变小的动画,当变小后,内容只显示了一行,但是存在右边的边距导致可以滑动,体验也不好,于是考虑在动画完成监听里手动给LinearLayout设置宽度,宽度为HorizontalScrollView的父容器的大小,但是设置不生效,其实和ScrollView下的LinearLayout的高度只能内容自适应是一样的。

我写了个demo,在setLayoutParams之后重新回调了onMeasure和onLayout,这里打印出来的高度都是内容的大小,而不是设置的值。

        Log.e("sss", "1:" + mLinearLayout.getHeight());
        ViewGroup.LayoutParams p = mLinearLayout.getLayoutParams();
        p.height = 1000;
        mLinearLayout.setLayoutParams(p);
        mLinearLayout.post(new Runnable() {
            @Override
            public void run() {
                Log.e("sss", "2:" + mLinearLayout.getHeight());
            }
        });

最后解决办法采用android:fillViewport = "true",直接看HorizontalScrollView的源码,当设置mFillViewport为true后会把子View填满。


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            final int widthPadding;
            final int heightPadding;
            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
            if (targetSdkVersion >= Build.VERSION_CODES.M) {
                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
            } else {
                widthPadding = mPaddingLeft + mPaddingRight;
                heightPadding = mPaddingTop + mPaddingBottom;
            }

            int desiredWidth = getMeasuredWidth() - widthPadding;
            if (child.getMeasuredWidth() < desiredWidth) {
                final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        desiredWidth, MeasureSpec.EXACTLY);
                final int childHeightMeasureSpec = getChildMeasureSpec(
                        heightMeasureSpec, heightPadding, lp.height);
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

这里有篇文章分析ScrollView的源码,包括布局和事件。到这里这个功能就算是实现了,右边的标签容器直接addView就可以了。TagView通过tag和数据绑定,这样取数据的时候遍历view,通过tag拿到数据。由于有margin,可以设置gone和visiable,删除标签的时候判断是否是最后一个,是的话隐藏容器。

if (mFilterContainer.getChildCount() < 1) {
    mFilterContainer.setVisibility(View.GONE);
}

由于布局比较多,为了查看方便,可以给每个容器设置一个背景,或打开布局边界,来查看容器的大小,算是开发调试小技巧把。


上面列举了采用HorizontalScrollView方案面临的问题:
1.hint太长导致EditText变大,导致添加标签时无法紧跟文字;
2.EditText变小了,所以需要让父容器填满宽度并捕获事件;

如果采用RecyclerView来实现呢,也不复杂,首先更换布局为RecyclerView,并编写两个Item的xml,一个包含EditText,一个包含标签样式的TextView。
(1)多type样式:
通过getItemViewType根据位置返回不同的type,然后在onBindViewHolder的时候通过getItemViewType(position)获取到type,再返回对应的ViewHolder;

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_EDIT;
        } else {
            return TYPE_TAG;
        }
    }

(2)添加一个标签:
这里操纵数据集List,add数据后只刷新单条数据即可,同时滑动到最后一条;

notifyItemInserted(List.size()-1);
recyclerView.smoothScrollToPosition(list.size() - 1);

(3)删除一个标签:
在ViewHolder里面执行;

           mTextView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    int i = getAdapterPosition();
                    genData.remove(i);
                    notifyItemRemoved(i);
                }
            });

(4)点击其他空白处可以唤醒键盘,这里可以给RecyclerView添加一个父容器来接受事件,在Adapter中存下EditText和文字,然后唤醒键盘,把标签变成内容,同时移除标签。

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:descendantFocusability="afterDescendants"
        android:onClick="text3">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#456" />
    </LinearLayout>
    public void onclick() {
        mTextView.setFocusable(true);
        mTextView.setFocusableInTouchMode(true);
        mTextView.setCursorVisible(true);
        mTextView.requestFocus();
        mInputMgr.showSoftInput(mTextView, 0);
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = genData.size() - 1; i >= 1; i--) {
            stringBuilder.append(" ");
            stringBuilder.append(((MultiData1) genData.get(i)).getStr1());
            genData.remove(i);
        }
        mTextView.setText(mString + stringBuilder.toString());
        mTextView.setSelection(mTextView.getText().length());
        notifyDataSetChanged();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容