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和文字,然后唤醒键盘,把标签变成内容,同时移除标签。

    

        
    
    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();
    }

你可能感兴趣的:(HorizontalScrollView介绍)