由于项目很多地方需要搜索框,就自定义了一个SearchView控件,顺便复习下自定义View的操作。
一.复用性
虽然我自己在多个地方进行复制粘贴也很省时,但是总觉得这样的做法太Low了,所以还是抽出来自定义一个view,看看效果。
这是一个默认样式下的搜索框,当然也可以改
抽离出以后再使用的话会比较方便。
二.默认输入框结构
目录
第一个是view,三个接口分别表示监听搜索框的焦点,监听搜索框的搜索操作和扩展自定义View时的行为。
View结构
默认的View的布局如下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_search"
android:focusable="true"
android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:id="@+id/ll_search"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/iv_search"
android:layout_toLeftOf="@+id/edt_search"
android:layout_marginRight="10dp"
/>
<EditText
android:gravity="top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/get_gray_code"
android:background="@null"
android:id="@+id/edt_search"
android:maxLines="1"
/>
</LinearLayout>
</RelativeLayout>
简单说说这样设计的理由,本来图片和Edit是可以直接用一个EditText来完成的,但是为了考虑扩展性所以分成了ImageView和EditText。
然后我是不想在中间多加一层的LinearLayout布局的,但是如果显示在中间的情况布局就会看着挺别扭,而且直接用RelativeLayout 去动态改变两个子控件的布局的话就会做很多操作,所以在中间加了一层,我觉得性能方面也不会影响很大。
再讲讲这样设计是为了确保存在ImageView和EditText,布局可以自定义进行扩展,不一定要使用默认的,但是一定要有ImageView和EditText,这个之后会说。
三.自定义属性
添加部分自定义属性,方便改变一些常用的样式
<!-- 搜索框 -->
<declare-styleable name="kylin_search_style">
<attr name="img_src" format="reference"/><!-- 图片地址 -->
<attr name="img_size" format="dimension"/><!-- 图片大小 -->
<attr name="img_visibility" format="boolean"/><!-- 图片显示/隐藏 -->
<attr name="show_location" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="2"/>
<enum name="centre" value="1"/>
</attr>
<attr name="edt_hint" format="string"/><!-- 提示文字 -->
<attr name="edt_size" format="dimension"/><!-- 提示文字大小 -->
<attr name="edt_hint_color" format="color"/><!-- 提示文字的颜色 -->
<attr name="search_backgroup" format="reference"/><!-- 搜索框背景 -->
<attr name="search_padding" format="reference"/><!-- 搜索框背景 -->
</declare-styleable>
show_location表示展示的位置,其它都有注解。
不仅如此,还会在View内部加入返回子控件的操作,可以在外部设置,因为我觉得如果在内部定义太多属性的话,要在View内写很多变量,这种做法我觉得很不划算,所以只写了一些常变化的,下面的方法返回控件。
public EditText getSearchEditText() {
return edtSearch;
}
public ImageView getSearchImageView() {
return ivSearch;
}
public ViewGroup getSearchFrameView() {
return rlSearch;
}
四.初始化操作
protected void initView() {
// 初始化搜索边框
rlSearch.setBackgroundResource(seaBackgroup);
rlSearch.setPadding((int) seaPadding, (int) seaPadding, (int) seaPadding, (int) seaPadding);
ViewGroup.LayoutParams llLp = llSearch.getLayoutParams();
if (showType == 0){
((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_LEFT);
}else if (showType == 1) {
((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.CENTER_IN_PARENT);
}else if (showType == 2){
((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
}
llSearch.setLayoutParams(llLp);
// 初始化图片
ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
lp.width = (int) imgSize;
lp.height = (int) imgSize;
ivSearch.setLayoutParams(lp);
ivSearch.setImageResource(imgRid);
// 初始化输入框
edtHint = (edtHint == null || edtHint == "" || edtHint.equals(null) || edtHint.equals(""))
? "请输入搜索内容" : edtHint;
edtSearch.setHint(edtHint);
edtSearch.setHintTextColor(edtHintColor);
edtSearch.setTextSize(edt_size);
}
就是初始化设置那些常用的属性。关键是下面的操作,为了增加扩展性,我添加了一步类似钩子的操作。
private void init(){
// 提供自定义样式的钩子
int layoutId = getLayoutId();
if (layoutId == -1){
seachView = LayoutInflater.from(getContext()).inflate(R.layout.layout_base_seach,this,false);
this.addView(seachView);
ivSearch = (ImageView) seachView.findViewById(R.id.iv_search);
edtSearch = (EditText) seachView.findViewById(R.id.edt_search);
rlSearch = (RelativeLayout) seachView.findViewById(R.id.rl_search);
llSearch = (LinearLayout) seachView.findViewById(R.id.ll_search);
initView();
}else {
seachView = LayoutInflater.from(getContext()).inflate(layoutId,this,false);
this.addView(seachView);
ivSearch = getImageView();
edtSearch = getEditText();
rlSearch = getSearchFrame();
}
// 初始化事件监听
initAllListener();
}
@Override
public int getLayoutId(){
return -1;
}
@Override
public ImageView getImageView(){
return null;
}
@Override
public EditText getEditText(){
return null;
}
@Override
public ViewGroup getSearchFrame(){
return null;
}
开发者可以写个类继承这个控件,然后重写上边说的SearchExtendImpl接口的4个方法
public interface SearchExtendImpl {
int getLayoutId();
ImageView getImageView();
EditText getEditText();
ViewGroup getSearchFrame();
}
如果布局和默认布局很大程度不同的话,可以使用继承的方式,但是要把搜索框的图标用getImageView来返给父类,输入框用getEditText传,搜索边框用getSearchFrame传。我这里这样设计的目的是因为,这东西子控件就两三个,所以在布局方面我没必要做太多的扩展,就几个子控件,做过多的扩展不如重做一个新的,但是逻辑是可以复用的。
五.搜索逻辑
这部分是可以服用的,所以不管是使用我写的默认的搜索框样式,还是开发者自定义的布局都可以服用这个逻辑。
protected void initAllListener(){
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(getContext().INPUT_METHOD_SERVICE);
if (edtSearch != null) {
// 点击事件
edtSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
edtSearch.setFocusable(true);//设置输入框可聚集
edtSearch.setFocusableInTouchMode(true);//设置触摸聚焦
edtSearch.requestFocus();//请求焦点
edtSearch.findFocus();//获取焦点
}
});
// 监听焦点
edtSearch.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); //显示软键盘
} else {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0); //隐藏软键盘
}
if (onSearchFocusListener != null){
onSearchFocusListener.searchFocusChange(v,hasFocus);
}
}
});
// 监听软键盘的按键
edtSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
//回车等操作
if (actionId == EditorInfo.IME_ACTION_SEND
|| actionId == EditorInfo.IME_ACTION_DONE
|| actionId == EditorInfo.IME_ACTION_SEARCH
|| actionId == EditorInfo.IME_ACTION_GO
|| (event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode()
&& KeyEvent.ACTION_DOWN == event.getAction())) {
// 搜索
search();
}
return true;
}
});
}
}
这里涉及两个比较啰嗦的东西,光标(焦点)和软键盘,软键盘的操做相关的一个类InputMethodManager,我没有很好去理解,只是查了一些大概的用法。
(1)我先给输入框设置点击之后获取焦点
edtSearch.setFocusable(true);//设置输入框可聚集
edtSearch.setFocusableInTouchMode(true);//设置触摸聚焦
edtSearch.requestFocus();//请求焦点
edtSearch.findFocus();//获取焦点
为什么要这样做,因为我之后要做失焦操作,如果不写这个代码的话,我这边会出个BUG,失焦后就无法再重新获取焦点。
(2)失焦操作
/**
* 让EditText失去焦点
*/
public void lostRocus(){
if(edtSearch != null) {
edtSearch.setFocusable(false);
}
}
(3)关联软键盘
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); //显示软键盘
} else {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0); //隐藏软键盘
}
}
获取焦点后软键盘弹起,失去焦点后软键盘消失。
六.全部代码
项目没有放到gayhub,一是有些BUG,我等下会说,二是我没写自定义布局的demo,三是我没写文档。所以我先写文章,过后完善了再把项目丢到gayhub。
1.默认样式布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_search"
android:focusable="true"
android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:id="@+id/ll_search"
>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/iv_search"
android:layout_toLeftOf="@+id/edt_search"
android:layout_marginRight="10dp"
/>
<EditText
android:gravity="top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/get_gray_code"
android:background="@null"
android:id="@+id/edt_search"
android:maxLines="1"
/>
</LinearLayout>
</RelativeLayout>
2.自定义属性
在attrs中添加
<!-- 搜索框 -->
<declare-styleable name="kylin_search_style">
<attr name="img_src" format="reference"/><!-- 图片地址 -->
<attr name="img_size" format="dimension"/><!-- 图片大小 -->
<attr name="img_visibility" format="boolean"/><!-- 图片显示/隐藏 -->
<attr name="show_location" format="enum">
<enum name="left" value="0"/>
<enum name="right" value="2"/>
<enum name="centre" value="1"/>
</attr>
<attr name="edt_hint" format="string"/><!-- 提示文字 -->
<attr name="edt_size" format="dimension"/><!-- 提示文字大小 -->
<attr name="edt_hint_color" format="color"/><!-- 提示文字的颜色 -->
<attr name="search_backgroup" format="reference"/><!-- 搜索框背景 -->
<attr name="search_padding" format="reference"/><!-- 搜索框背景 -->
</declare-styleable>
3.三个定义的接口
public interface OnSearchFocusListener {
void searchFocusChange(View v, boolean hasFocus);
}
public interface OnSearchListener {
void search(String content);
}
public interface SearchExtendImpl {
int getLayoutId();
ImageView getImageView();
EditText getEditText();
ViewGroup getSearchFrame();
}
4.View代码
/**
* Created by kylin on 2018/2/23.
*/
public class KylinSearchView extends FrameLayout implements SearchExtendImpl{
protected Context context;
protected View seachView;
protected ImageView ivSearch;
protected EditText edtSearch;
protected ViewGroup rlSearch;
protected LinearLayout llSearch;
/**
* 属性定义
*/
protected int seaBackgroup;
protected int imgRid;
protected float imgSize;
protected String edtHint;
protected int edtHintColor;
protected float seaPadding;
protected int showType;
protected float edt_size;
/**
* 事件
*/
protected OnSearchListener onSearchListener;
protected OnSearchFocusListener onSearchFocusListener;
public KylinSearchView(Context context) {
super(context);
init();
}
public KylinSearchView(Context context, AttributeSet attrs) {
super(context, attrs);
initAttrs(context, attrs);
init();
}
public KylinSearchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs);
init();
}
/**
* 设置属性
*/
private void initAttrs(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.kylin_search_style);
imgRid = typedArray.getInteger(R.styleable.kylin_search_style_img_src,R.mipmap.product_serch);
// 默认的float是px,所以要转成dp
imgSize = typedArray.getDimension(R.styleable.kylin_search_style_img_size, DimensionUtils.dip2px(context,24));
edtHint = typedArray.getString(R.styleable.kylin_search_style_edt_hint);
edtHintColor = typedArray.getColor(R.styleable.kylin_search_style_edt_hint, getResources().getColor(R.color.get_gray_code));
seaBackgroup = typedArray.getInteger(R.styleable.kylin_search_style_search_backgroup,R.drawable.bg_search_default);
seaPadding = typedArray.getDimension(R.styleable.kylin_search_style_img_size, DimensionUtils.dip2px(context,8));
showType = typedArray.getInteger(R.styleable.kylin_search_style_show_location,0);
edt_size = typedArray.getDimension(R.styleable.kylin_search_style_edt_size, 18);
typedArray.recycle();
}
/**
* 初始化操作
*/
private void init(){
// 提供自定义样式的钩子
int layoutId = getLayoutId();
if (layoutId == -1){
seachView = LayoutInflater.from(getContext()).inflate(R.layout.layout_base_seach,this,false);
this.addView(seachView);
ivSearch = (ImageView) seachView.findViewById(R.id.iv_search);
edtSearch = (EditText) seachView.findViewById(R.id.edt_search);
rlSearch = (RelativeLayout) seachView.findViewById(R.id.rl_search);
llSearch = (LinearLayout) seachView.findViewById(R.id.ll_search);
initView();
}else {
seachView = LayoutInflater.from(getContext()).inflate(layoutId,this,false);
this.addView(seachView);
ivSearch = getImageView();
edtSearch = getEditText();
rlSearch = getSearchFrame();
}
// 初始化事件监听
initAllListener();
}
@Override
public int getLayoutId(){
return -1;
}
@Override
public ImageView getImageView(){
return null;
}
@Override
public EditText getEditText(){
return null;
}
@Override
public ViewGroup getSearchFrame(){
return null;
}
protected void initView() {
// 初始化搜索边框
rlSearch.setBackgroundResource(seaBackgroup);
rlSearch.setPadding((int) seaPadding, (int) seaPadding, (int) seaPadding, (int) seaPadding);
ViewGroup.LayoutParams llLp = llSearch.getLayoutParams();
if (showType == 0){
((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_LEFT);
}else if (showType == 1) {
((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.CENTER_IN_PARENT);
}else if (showType == 2){
((RelativeLayout.LayoutParams) llLp).addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
}
llSearch.setLayoutParams(llLp);
// 初始化图片
ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
lp.width = (int) imgSize;
lp.height = (int) imgSize;
ivSearch.setLayoutParams(lp);
ivSearch.setImageResource(imgRid);
// 初始化输入框
edtHint = (edtHint == null || edtHint == "" || edtHint.equals(null) || edtHint.equals(""))
? "请输入搜索内容" : edtHint;
edtSearch.setHint(edtHint);
edtSearch.setHintTextColor(edtHintColor);
edtSearch.setTextSize(edt_size);
}
protected void initAllListener(){
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(getContext().INPUT_METHOD_SERVICE);
if (edtSearch != null) {
// 点击事件
edtSearch.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
edtSearch.setFocusable(true);//设置输入框可聚集
edtSearch.setFocusableInTouchMode(true);//设置触摸聚焦
edtSearch.requestFocus();//请求焦点
edtSearch.findFocus();//获取焦点
}
});
// 监听焦点
edtSearch.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
imm.showSoftInput(v, InputMethodManager.SHOW_FORCED); //显示软键盘
} else {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0); //隐藏软键盘
}
if (onSearchFocusListener != null){
onSearchFocusListener.searchFocusChange(v,hasFocus);
}
}
});
// 监听软键盘的按键
edtSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
//回车等操作
if (actionId == EditorInfo.IME_ACTION_SEND
|| actionId == EditorInfo.IME_ACTION_DONE
|| actionId == EditorInfo.IME_ACTION_SEARCH
|| actionId == EditorInfo.IME_ACTION_GO
|| (event != null && KeyEvent.KEYCODE_ENTER == event.getKeyCode()
&& KeyEvent.ACTION_DOWN == event.getAction())) {
// 搜索
search();
}
return true;
}
});
}
}
/**
* 获取搜索框的内容
*/
public String getSearchContent(){
if (edtSearch == null){
return null;
}
return edtSearch.getText().toString();
}
/**
* 清空搜索框
*/
public void clearSearch(){
edtSearch.setText("");
}
/**
* 让EditText失去焦点
*/
public void lostRocus(){
if(edtSearch != null) {
edtSearch.setFocusable(false);
}
}
/**
* 搜索
*/
public void search(){
lostRocus();
if (onSearchListener != null){
onSearchListener.search(getSearchContent());
}
}
public void setOnSearchListener(OnSearchListener onSearchListener) {
this.onSearchListener = onSearchListener;
}
public void setOnSearchFocusListener(OnSearchFocusListener onSearchFocusListener) {
this.onSearchFocusListener = onSearchFocusListener;
}
public EditText getSearchEditText() {
return edtSearch;
}
public ImageView getSearchImageView() {
return ivSearch;
}
public ViewGroup getSearchFrameView() {
return rlSearch;
}
public void setImgSize(float size){
ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
lp.width = (int) size;
lp.height = (int) size;
ivSearch.setLayoutParams(lp);
}
public void setTextSize(float size){
edtSearch.setTextSize(size);
}
}
之中有些操作,比如 DimensionUtils.dip2px就是转尺寸单位,这些命名就很明显,自己写个工具类转就行。
伸手党要用的话只能全抄了,gayhub没这么快。
模拟一下,软键盘回车后失去焦点。
六.BUG
我在玩的时候玩出个BUG,关键是这个BUG我还不知道要怎么去表达,设置图片的尺寸
imgSize = typedArray.getDimension(R.styleable.kylin_search_style_img_size, DimensionUtils.dip2px(context,24));
我这里默认填24dp是正常,我如果在控件中设置
app:img_size="24dp"
图片大小没变,但是间距莫名其妙变大了。还有文字也是,用sp的话比正常情况的sp大得多。所以我现在还不懂这个format="dimension"出了什么毛病,这是其中一个问题。
还有一个问题是软键盘,这个东西比较容易出BUG,我无法保证不同的软键盘或者不同的版本不会出问题,我是感觉之后可能要做适配的问题。
暂时就这些,简单封装了一下,完善也只能在之后碰到问题再去完善,然后之后我有时间写个demo写个文档再放到gayhub
更新
项目地址
https://github.com/994866755/handsomeYe.SearchView
BUG修改
之前说有几个传资源时的BUG,我先在这写出来,gayhub的过后我改了再传。
1.背景资源的类型写错了
seaBackgroup = typedArray.getResourceId(R.styleable.kylin_search_style_search_backgroup,R.drawable.bg_search_default);
这里我之前用getInteger用错了。
2.设置字体大小
我之前说怎么设置字体大小总是出问题,然后发现是setTextSize方法默认是sp的,然后我用getDimension传进来的会转成px,这样就造成了px套到sp上,尺寸就错了,应该把类型再做一次转换
edtSearch.setTextSize(TypedValue.COMPLEX_UNIT_PX,edt_size);
这个问题我之前也碰到过,只是忘了做笔记,所以不记得了。详细的原因可以看下这篇博客,讲得很不错。
https://www.jianshu.com/p/7f2941dbfb17
3.设置自适应大小
我发现用getDimension如果设默认大小的话就扩展性太差了,所以想改成自适应,但是我又不知道这个方法要怎么设置默认值为自适应,所以我就投机用正负值来判断,默认为负数就是自适应的情况。
imgSize = typedArray.getDimension(R.styleable.kylin_search_style_img_size, -1);
// 初始化图片
ViewGroup.LayoutParams lp = ivSearch.getLayoutParams();
if (imgSize >= 0) {
lp.width = (int) imgSize;
lp.height = (int) imgSize;
}else {
lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
}
ivSearch.setLayoutParams(lp);
补充
我想做个功能的补充是关于软键盘的,有很多时候,我们需要在软键盘弹出的情况下,点击空白处的话就隐藏软键盘。
而这个操作我这边不好一起封装到searchView里面,我觉得可以直接在页面中用拦截事件来实现这功能。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN){
// 收软键盘
InputMethodManager imm = (InputMethodManager) getSystemService(this.INPUT_METHOD_SERVICE);
if (imm.isActive()){
imm.hideSoftInputFromWindow(searchView.getSearchEditText().getWindowToken(), 0); //隐藏软键盘
}
}
return super.dispatchTouchEvent(ev);
}
这样写就能实现点击页面时隐藏软键盘的操作,比如美团的搜索就是点页面能隐藏软键盘。
但是这里有个问题,dispatchTouchEvent方法会频繁的调用,只要一碰到这个页面就会调用这个方法,而我在这里面创建InputMethodManager 的话是不是会一直创建对象。
我看Monitors来测试,其实我不是很会用Monitors,然后用我单身20年的手速不断搓屏幕,发现内存一直在增长,所以我觉得InputMethodManager 在dispatchTouchEvent中创建不是很好,把它抽出去,在全局中创建。