UI-TextView相关

TextView

TV内容过长,显示省略号

TextView中有个ellipsize属性,作用是当文字过长时,该控件该如何显示,解释如下:

  1. android:ellipsize="start"—–省略号显示在开头
  2. android:ellipsize="end"——省略号显示在结尾
  3. android:ellipsize="middle"—-省略号显示在中间
  4. android:ellipsize="marquee"–以跑马灯的方式显示(动画横向移动)

以上需要搭配 android:maxLines="1" 属性才可生效(android:singleLine="true"已经不推荐使用)

TV的省略号不起效

解析:设置了属性

1
2
android:ellipsize="end"
android:maxLines="1"

但在代码中使用了以下的代码,导致省略号不起效

1
StringHelpUtils.setSpannableString(tv, color, name, matchStr);

其中setSpannableString的代码是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @param textView
* @param clickColor 不是id是颜色
* @param alltext
* @param clickText
*/
public static void setSpannableString(@NonNull TextView textView, int clickColor, String alltext, String clickText) {
if (textView == null){
return;
}
if (TextUtils.isEmpty(alltext)) {
textView.setText("");
DebugLog.log("请输入非空字段");
return;
}
if (TextUtils.isEmpty(clickText)) {
textView.setText(alltext);
DebugLog.log("clickText不能为空");
return;
}
if (!alltext.contains(clickText)) {
textView.setText(alltext);
DebugLog.log("没有需要点击事件或者改变颜色的字段");
return;
}
avoidHintColor(textView);
int start = alltext.indexOf(clickText);
SpannableString spannableString = new SpannableString(alltext);
spannableString.setSpan(new ForegroundColorSpan(clickColor), start, start + clickText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
textView.setMovementMethod(LinkMovementMethod.getInstance()); //为TextView设置完Span后,别忘了setMovementMethod
}

是因为textView.setMovementMethod影响到的

解决:

  1. 方法一:使用android:singleLine="true"可以解决上述问题,但是这个属性不推荐了

  2. 方法二:自己拼接...

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    import android.text.TextUtils;
    import android.view.ViewTreeObserver;
    import android.widget.TextView;

    public class OnGlobalLayoutListenerByEllipSize implements ViewTreeObserver.OnGlobalLayoutListener {
    private TextView mTextView;
    private int mMaxLines;

    public OnGlobalLayoutListenerByEllipSize(TextView textView, int maxLines) {
    if (maxLines <= 0) {
    throw new IllegalArgumentException("maxLines不能小于等于0");
    }
    this.mTextView = textView;
    this.mMaxLines = maxLines;
    this.mTextView.setMaxLines(mMaxLines + 1);
    this.mTextView.setSingleLine(false);
    }

    @Override
    public void onGlobalLayout() {
    if (mTextView.getLineCount() > mMaxLines) {
    int line = mTextView.getLayout().getLineEnd(mMaxLines - 1);
    //定义成CharSequence类型,是为了兼容emoji表情,如果是String类型会造成emoji无法显示
    CharSequence truncate = "...";
    CharSequence text = mTextView.getText();
    try {
    text = text.subSequence(0, line - 3);
    } catch (Exception e) {
    truncate = "";
    text = mTextView.getText();
    }
    TextUtils.TruncateAt at = mTextView.getEllipsize();
    if (at == TextUtils.TruncateAt.START) {
    mTextView.setText(truncate);
    mTextView.append(text);
    } else if (at == TextUtils.TruncateAt.MIDDLE) {
    mTextView.setText(text.subSequence(0, text.length() / 2));
    mTextView.append(truncate);
    mTextView.append(text.subSequence(text.length() / 2, text.length()));
    } else {
    mTextView.setText(text);
    mTextView.append(truncate);
    }
    }
    }
    }

    调用:tv.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListenerByEllipSize(tv,1));

    但此种做法会导致tv后面会多一些空白,使用控件位置检测看到有个空白的控件(未解决)

  3. 方法三:去掉tv.setMovementMethod(LinkMovementMethod.getInstance());(会导致span不能点击了),然后自己添加点击事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // textview点击监听
    linkText.setOnTouchListener(new View.OnTouchListener() {

    long startTime = 0;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
    startTime = System.currentTimeMillis();
    }
    TextView tv = (TextView) v;
    CharSequence text = tv.getText();
    if (text instanceof SpannableString) {
    if (action == MotionEvent.ACTION_UP) {
    // 避免长按和点击冲突,如果超过300毫秒,认为是在长按,不执行点击操作
    if (System.currentTimeMillis() - startTime > 300) {
    return false;
    }
    int x = (int) event.getX();
    int y = (int) event.getY();

    x -= tv.getTotalPaddingLeft();
    y -= tv.getTotalPaddingTop();

    x += tv.getScrollX();
    y += tv.getScrollY();

    Layout layout = tv.getLayout();
    int line = layout.getLineForVertical(y);
    int off = layout.getOffsetForHorizontal(line, x);

    ClickableSpan[] link = ((SpannableString) text).getSpans(off, off, ClickableSpan.class);
    if (link.length != 0) {
    link[0].onClick(tv);
    return true;
    }
    }
    }

    return false;
    }

    });

TV部分关键字带下划线可点击的

1
2
3
4
5
<declare-styleable name="AutoLinkStyleTextView">
<attr name="AutoLinkStyleTextView_text_value" format="string|reference"/>//key word with color and underline, and split with ','(en)
<attr name="AutoLinkStyleTextView_default_color" format="color|reference"/>//word and underline's color
<attr name="AutoLinkStyleTextView_has_under_line" format="boolean"/>//underline with true and false
</declare-styleable>

使用

1
2
3
4
5
6
7
8
<xx.AutoLinkStyleTextView
android:id="@+id/tv_clause"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="我已核对付款金额,仔细阅读并同意“购买须知”及约克论坛团购“用户条款”"
android:textSize="16sp"
app:AutoLinkStyleTextView_text_value="“购买须知”,“用户条款”"
/>
1
2
3
4
5
6
7
8
9
10
autoLinkStyleTextView.setOnClickCallBack(new AutoLinkStyleTextView.ClickCallBack() {
@Override
public void onClick(int position) {
if (position == 0) {
Toast.makeText(MainActivity.this, "购买须知", Toast.LENGTH_SHORT).show();
} else if (position == 1) {
Toast.makeText(MainActivity.this, "用户条款", Toast.LENGTH_SHORT).show();
}
}
});

image-20210218102959106

TV部分文字响应点击事件

  1. 给TextView设置设置文字背景为透明色,不然会有点击选中效果
  2. SpannableString拼接字符串,给8到11的字符串用Clickable对象去处理
    1).Clickable对象继承ClickableSpan 实现了onclick()跟updateDrawState()方法
    2).onclick()处理点击事件,updateDrawState()中可以改变字体颜色,控件背景色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class MainActivity extends Activity {  
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView textView=(TextView) findViewById(R.id.hello_world);

//ds.setColor()设定的是span超链接的文本颜色,而不是点击后的颜色,
//点击后的背景颜色(HighLightColor)属于TextView的属性,
//Android4.0以上默认是淡绿色,低版本的是黄色。解决方法就是通过重新设置文字背景为透明色
textView.setHighlightColor(getResources().getColor(android.R.color.transparent));

SpannableString spanableInfo = new SpannableString("这是一个测试"+": "+"点击我");
spanableInfo.setSpan(new Clickable(clickListener),8,11,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spanableInfo);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}

private OnClickListener clickListener=new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "点击成功....",Toast.LENGTH_SHORT).show();
}
};

class Clickable extends ClickableSpan{
private final View.OnClickListener mListener;

public Clickable(View.OnClickListener l) {
mListener = l;
}

/**
* 重写父类点击事件
*/
@Override
public void onClick(View v) {
mListener.onClick(v);
}

/**
* 重写父类updateDrawState方法 我们可以给TextView设置字体颜色,背景颜色等等...
*/
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(getResources().getColor(R.color.video_comment_like_number));
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/hello_world"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp" />
</LinearLayout>

image-20210218103755471

TV部分文字响应点击事件(法2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;

/**
* 说明:
*
* @author shenbh
* time 2021/2/6 22:07
*/
public class RecordClickSpan extends ClickableSpan {
@Override
public void onClick(View widget) {

}

@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(DemoApplication.getInstance().getResources().getColor(R.color.main_color));
ds.setUnderlineText(false);
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//感谢使用爱学拼音!使用前请仔细阅读《用户协议》和《隐私协议》了解您的合法权益。我们将严格遵守承诺,保护个人信息。
String protocolName = "《用户协议》";
String privacyName = "《隐私协议》";
SpannableString protocolSpannable = new SpannableString(protocolName);
SpannableString privacySpannable = new SpannableString(privacyName);
RecordClickSpan protocolSpan = new RecordClickSpan() {
@Override
public void onClick(View widget) {
context.startActivity(WebViewActivity.createIntent(context, "用户协议", HttpRequest.getH5Url("userAgreement" )));
}
};
RecordClickSpan privacySpan = new RecordClickSpan() {
@Override
public void onClick(View widget) {
context.startActivity(WebViewActivity.createIntent(context, "隐私协议", HttpRequest.getH5Url("privacyPolicy" )));
}
};
protocolSpannable.setSpan(protocolSpan, 0, protocolName.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
privacySpannable.setSpan(privacySpan, 0, privacyName.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
//append需要分开一个个来,否则会有奇怪的问题
contentTv.append("感谢使用爱学拼音!使用前请仔细阅读");
contentTv.append(protocolSpannable);
contentTv.append("和");
contentTv.append(privacySpannable);
contentTv.append("了解您的合法权益。我们将严格遵守承诺,保护个人信息。");
contentTv.setMovementMethod(LinkMovementMethod.getInstance());

TV加中划线、下划线

加中划线

1
2
tv.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG);//设置中划线
tv.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG|Paint.ANTI_ALIAS_FLAG); // 设置中划线并加清晰

加下划线(5种做法)

  1. 将要处理的文字写到一个资源文件,如string.xml(使用html用法格式化)

    1
    2
    3
    4
    <resources>
    <string name="hello"><u>phone:0123456</u></string>
    <string name="app_name">MyLink</string>
    </resources>
  2. 当文字中出现URL、E-mail、电话号码等的时候,可以将TextView的android:autoLink属性设置为相应的的值,如果是所有的类型都出来就是android:autoLink=”all”,当然也可以在java代码里 做,textView01.setAutoLinkMask(Linkify.ALL);

    1
    2
    3
    4
    5
    6
    <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
    android:id="@+id/text1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:autoLink="all"
    android:text="@string/link_text_auto" />
  3. Html类的fromHtml()方法格式化要放到TextView里的文字 ,与第1种一样,只是是用代码动态设置

    1
    2
    TextView textView = (TextView)findViewById(R.id.tv_test); 
    textView.setText(Html.fromHtml("<u>"+"0123456"+"</u>"));
  4. 设置TextView的Paint属性:tvTest.getPaint().setFlags(Paint. UNDERLINE_TEXT_FLAG ); //下划线

    1
    2
    tvTest.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG); //下划线
    tvTest.getPaint().setAntiAlias(true);//抗锯齿
  5. 用Spannable或实现它的类,如SpannableString来格式部分字符串。

    1
    2
    SpannableString content = new SpannableString(str);
    content.setSpan(new UnderLineSpan, 0, str.length(), 0);

    另外附上一篇博客介绍:Android TextView中文字通过SpannableString来设置超链接、颜色、字体等属性

取消设置的线

1
tv.getPaint().setFlags(0); // 取消设置的的划线 

自定义超链接样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TextView tv=new TextView(this);
tv.setText(Html.fromHtml("<a href=\"http://blog.csdn.net/CAIYUNFREEDOM\">自定义的超链接样式</a>"));
// 在单击链接时凡是有要执行的动作,都必须设置MovementMethod对象
tv.setMovementMethod(LinkMovementMethod.getInstance());
CharSequence text = tv.getText();
if (text instanceof Spannable){
int end = text.length();
Spannable sp = (Spannable)tv.getText();
URLSpan[] urls = sp.getSpans( 0 , end, URLSpan.class );

SpannableStringBuilder style = new SpannableStringBuilder(text);
style.clearSpans(); // should clear old spans
for (URLSpan url : urls){
URLSpan myURLSpan= new URLSpan(url.getURL());
style.setSpan(myURLSpan,sp.getSpanStart(url),sp.getSpanEnd(url),Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
style.setSpan(new//设置前景色为红色
}
tv.setText(style);
}

Tv跑马灯

有时文字过长,又只想单行显示来节省空间,走马灯式显示是个不错的选择。(饿了么顶部地址显示就是这种情况)

其实只要在布局文件的Textview中添加属性android:ellipsize="marquee"

但是要在获得焦点的情况下该属性才会起作用。

那么怎么让控件一直获得焦点呢?

系统判断TextView是否获得焦点是通过 isFocused()这个方法的返回值来判断

这样的话就可以新创建一个类,继承Textveiw,重写isFoused()方法,让它返回true就ko了。

文字左右滚动三个属性:

1
2
3
android:singleLine="true" 
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"

Tv动态添加drawableLeft

1
2
3
4
5
Drawable drawable = mContext.getResources().getDrawable(R.drawable.icon_location_4);
drawable.setBounds(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight());
tv.setCompoundDrawables(drawable, null, null, null);
//设置drawablePadding
.setCompoundDrawablePadding(4);

判断是否所有TextView都不为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.xm597.common.manager;

import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

/**
* <pre>
* desc : 判断是否所有TextView(EditText)都不为空
* EditText是继承于TextView的
* author : shenbh
* e-mail : shenbh@qq.com
* time : 2021-12-30 15:28
* </pre>
*/
public class TvIsEmptyManager {
private boolean isAllTvNotEmpty = false;
private TextWatcher watcher;
private TextWatcher mWatcher;
private ArrayList<TextView> mTextViews = new ArrayList<>();

public TvIsEmptyManager() {
watcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override
public void afterTextChanged(Editable s) {
if (getWatcher() != null) {
getWatcher().afterTextChanged(s);
}
}
};
}


public void setEditTexts(ArrayList<TextView> textViews) {
mTextViews.clear();
if (textViews != null) {
mTextViews.addAll(textViews);
for (TextView tv : textViews) {
if (tv != null) {
tv.addTextChangedListener(watcher);
}
}
}
}

public boolean isAllTvNotEmpty() {
List<CharSequence> builder = new ArrayList<>();
for (TextView textView : mTextViews) {
if (!TextUtils.isEmpty(textView.getText())) {
builder.add(textView.getText());
}
}
return builder.size() == mTextViews.size();
}

public TextWatcher getWatcher() {
return mWatcher;
}

public void setWatcher(TextWatcher mWatcher) {
this.mWatcher = mWatcher;
}
}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ArrayList<TextView> textViews = new ArrayList<>();
textViews.add(mBinding.nameMicroResume.getRightEdt());
textViews.add(mBinding.birthMicroResume.getRightEdt());
textViews.add(mBinding.cityMicroResume.getRightEdt());
textViews.add(mBinding.jobIntentionMicroResume.getRightEdt());
textViews.add(mBinding.salaryMicroResume.getRightEdt());
TvIsEmptyManager tvIsEmptyManager = new TvIsEmptyManager();
tvIsEmptyManager.setWatcher(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {

}

@Override
public void afterTextChanged(Editable s) {
if (tvIsEmptyManager.isAllTvNotEmpty()){
mBinding.btnConfirmMicroResume.setBackground(getResources().getDrawable(R.drawable.shape_corner_5_color_f5f5f6));
}
}
});
tvIsEmptyManager.setEditTexts(textViews);

判断页面所有控件是否有为空的

注:

  1. extraIds是表示不参与判断的控件的id,可以对这些id在页面中单独判断

  2. 底下的AllWidgetHasEmpty目前只添加了TextView的处理(EditText是继承于TextView),要对页面中其他种类的控件进行判空的话要在这个类中添加对应的类型以及它的判空逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;

import java.util.ArrayList;

/**
* <pre>
* desc : 判断页面所有控件是否有为空的
* 注:extraIds是表示不参与判断的控件的id,可以对这些id在页面中单独判断
* author : shenbh
* e-mail : shenbh@qq.com
* time : 2021-12-30 17:07
* </pre>
*/
public class AllWidgetHasEmpty {

private static final String TAG = "AllWidgetNotEmpty";

private AllWidgetHasEmpty() {

}

public static AllWidgetHasEmpty getInstance() {
return AllWidgetNotEmptyHolder.singleton;
}

/**
* 单例模式->静态内部类<br/>
* 多线程情况下,使用合理一些,推荐
*/
static class AllWidgetNotEmptyHolder {
private static AllWidgetHasEmpty singleton = new AllWidgetHasEmpty();
}


/**
* 用for遍历添加观察者对象
* 可以重写
*
* @param view
* @param ids 不参与判断的控件
*/
public boolean isEmpty(View view, ArrayList<Integer> ids) {
if (view == null) {
throw new IllegalArgumentException("view参数不能为空");
}

if (view != null) {
if (view.getVisibility() == View.VISIBLE) {
if (view instanceof EditText) {
if (TextUtils.isEmpty(((EditText) view).getText())) {
return true;
}
} else if (view instanceof TextView) {
if (TextUtils.isEmpty(((TextView) view).getText())) {
return true;
}
} else if (view instanceof ViewGroup) {
boolean observerviewgroup = observerviewgroup((ViewGroup) view, ids);
if (observerviewgroup) {
return true;
}
}
}
}
return false;
}

/**
* 如果是集合就这样遍历
* 这里采用递归
*
* @param view
* @param ids 不参与判断的控件
*/
private boolean observerviewgroup(ViewGroup view, ArrayList<Integer> ids) {
int childCount = view.getChildCount();
if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View child = view.getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
if (ids != null && !ids.isEmpty() && ids.contains(view.getId())) {
continue;
} else if (child instanceof TextView) {//这里添加你需要的view
if (TextUtils.isEmpty(((TextView) child).getText())) {
return true;
}
} else {
if (isEmpty(child, ids)) {
return true;
}
}
}

}
}
return false;
}
}

使用:

先在基类Activity中写,方便它的子类直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//BaseActivity.java

private ArrayList<Integer> extraIds;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
allViewHasEmpty();
onCreated(savedInstanceState);
}

/**
* 页面所有控件是否有一个为空
*/
protected void allViewHasEmpty() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
//getWindow().getDecorView()页面所有的控件元素
ViewTreeObserver viewTreeObserver = getWindow().getDecorView().getViewTreeObserver();
viewTreeObserver.addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
//控件在内容、焦点变化的时候都会走这个绘制方法
@Override
public void onDraw() {
boolean hasEmpty =
AllWidgetHasEmpty.getInstance().isEmpty(getWindow().getDecorView(),
extraIds);
onViewDrawChange(hasEmpty);
}
});
}
}

/**
* 设置需要页面单独处理的控件id
*/
protected void setExtraIds(ArrayList<Integer> extraIds){
this.extraIds = extraIds;
}

/**
* 子类重写这个方法来实现自己的判空后的逻辑
*/
protected void onViewDrawChange(boolean hasEmpty){

}

子类Activity中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
protected void onCreated(Bundle savedInstanceState) {
ArrayList<Integer> extraIds = new ArrayList<>();
extraIds.add(R.id.gender_female_micro_resume);
extraIds.add(R.id.gender_male_micro_resume);
setExtraIds(extraIds);
allViewHasEmpty();
}

/**
* 子类的判空后的逻辑处理
*/
@Override
protected void onViewDrawChange(boolean hasEmpty) {
//hasEmpty是页面ViewGroup中所有控件元素是否有为空的判断。“||”后面的是extraIds中的单独判空逻辑
if (hasEmpty || (!mBinding.genderFemaleMicroResume.isSelected() && !mBinding.genderMaleMicroResume.isSelected())){
//添加开关,避免 setBackground 后会调用 onDraw,进入到死循环
if (mBinding.btnConfirmMicroResume.isClickable()) {
mBinding.btnConfirmMicroResume.setClickable(false);
mBinding.btnConfirmMicroResume.setBackground(getResources().getDrawable(R.drawable.shape_corner_5_color_2b4b9));
}
} else {
//添加开关,避免 setBackground 后会调用 onDraw,进入到死循环
if (!mBinding.btnConfirmMicroResume.isClickable()) {
mBinding.btnConfirmMicroResume.setClickable(true);
mBinding.btnConfirmMicroResume.setBackground(getResources().getDrawable(R.drawable.shape_corner_5_color_ff5c5b));
}
}
}

进一步封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

/**
* <pre>
* desc : 判断页面所有控件是否有为空的
* 注:extraIds是表示不参与判断的控件的id,可以对这些id在页面中单独判断
* author : shenbh
* e-mail : shenbh@qq.com
* time : 2021-12-30 17:07
* </pre>
*/
public class AllWidgetHasEmpty {

private AllWidgetHasEmpty() {

}

public static AllWidgetHasEmpty getInstance() {
return AllWidgetNotEmptyHolder.singleton;
}

/**
* 单例模式->静态内部类<br/>
* 多线程情况下,使用合理一些,推荐
*/
static class AllWidgetNotEmptyHolder {
private static AllWidgetHasEmpty singleton = new AllWidgetHasEmpty();
}


/**
* 用for遍历添加观察者对象
* 可以重写
*
* @param view
* @param listener 把判空处理下发出去
*/
public boolean isEmpty(View view, ViewsEmptyListener listener) {
if (view == null) {
throw new IllegalArgumentException("view参数不能为空");
}

if (view != null) {
if (view.getVisibility() == View.VISIBLE) {
if (listener != null && listener.hasEmpty(view)) {
return true;
} else if (view instanceof ViewGroup) {
boolean observerviewgroup = observerviewgroup((ViewGroup) view, listener);
if (observerviewgroup) {
return true;
}
}
}
}
return false;
}


/**
* 如果是集合就这样遍历
* 这里采用递归
*
* @param view
* @param listener 把判空处理下发出去
*/
private boolean observerviewgroup(ViewGroup view, ViewsEmptyListener listener) {
int childCount = view.getChildCount();
if (childCount > 0) {
for (int i = 0; i < childCount; i++) {
View child = view.getChildAt(i);
if (isEmpty(child, listener)) {
return true;
}
}
}
return false;
}

private ViewsEmptyListener listener;

public ViewsEmptyListener getListener() {
return listener;
}

public void setListener(ViewsEmptyListener listener) {
this.listener = listener;
}

public interface ViewsEmptyListener {
boolean hasEmpty(View view);
}
}

使用:

先在基类中写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//BaseActivity.java中

private AllWidgetHasEmpty.ViewsEmptyListener viewsEmptyListener;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
initViewsEmptyListener();
allViewHasEmpty();
onCreated(savedInstanceState);
}

/**
* 一些公共控件的判空处理
*/
private void initViewsEmptyListener() {
if (viewsEmptyListener == null) {
viewsEmptyListener = new AllWidgetHasEmpty.ViewsEmptyListener() {
@Override
public boolean hasEmpty(View view) {
if (specialViewHasEmpty(view)) {
return true;
} else if (view instanceof EditText) {
if (TextUtils.isEmpty(((EditText) view).getText())) {
return true;
}
} else if (view instanceof TextView) {
if (TextUtils.isEmpty(((TextView) view).getText())) {
return true;
}
}
return false;
}
};
}
}

/**
* 所有控件是否有空值
*/
private void allViewHasEmpty() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ViewTreeObserver viewTreeObserver = getWindow().getDecorView().getViewTreeObserver();
viewTreeObserver.addOnDrawListener(() -> {
boolean hasEmpty =
AllWidgetHasEmpty.getInstance().isEmpty(getWindow().getDecorView(),
viewsEmptyListener);
onViewDrawChange(hasEmpty);
});
}
}

/**
* 特殊控件是否有空值
* 子类有特殊控件需要判空的需要重写这个方法
*/
protected boolean specialViewHasEmpty(View view) {
return false;
}

/**
* 子类重写这个方法来实现自己的判空后的逻辑
*/
protected void onViewDrawChange(boolean hasEmpty) {

}

在具体子类中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//重写两个方法即可

/**
* 特殊控件是否有空值
* 子类有特殊控件需要判空的重写这个方法,加上子类自己特殊控件的判空逻辑
*/
@Override
protected boolean specialViewHasEmpty() {
return !mBinding.genderFemaleMicroResume.isSelected() && !mBinding.genderMaleMicroResume.isSelected();
}

/**
* 子类的判空后的逻辑处理
*/
@Override
protected void onViewDrawChange(boolean hasEmpty) {
if (hasEmpty != mBinding.btnConfirmMicroResume.isSelected()) {
mBinding.btnConfirmMicroResume.setSelected(hasEmpty);
if (hasEmpty) {
mBinding.btnConfirmMicroResume.setBackground(getResources().getDrawable(R.drawable.shape_corner_5_color_2b4b9));
} else {
mBinding.btnConfirmMicroResume.setBackground(getResources().getDrawable(R.drawable.shape_corner_5_color_ff5c5b));
}
}
}

标题右侧带个标签,标题长度是包裹内容且能换行(标签不能顶出屏幕)

方法一:

安卓/标题长度动态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="收到Ladd利夫卡是领导看了发生的反馈爱上了肯定是法兰克福的十六客服的是客服" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="全职" />
</LinearLayout>

方法二:

另一个思路:

在代码中动态测量,动态设置左侧标题的宽度

1
2
3
4
5
6
7
8
9
10
titleTv.setText(jobRow.getJname());
tagTv.setText(jobRow.getJobTypeStr());
KViewMeasurer.measureView(titleTv);
KViewMeasurer.measureView(tagTv);
int titleWidth = titleTv.getMeasuredWidth();
int useableWidth = ScreenUtils.getScreenWidth() - tagTv.getMeasuredWidth() - 64;//64是 各种margin的和
int minWidth = Math.min(titleWidth, useableWidth);
ViewGroup.LayoutParams lp = titleTv.getLayoutParams();
lp.width = minWidth;
titleTv.setLayoutParams(lp);

Tv英文的换行问题

思路:动态测量,手动切割字符串

设置可滚动

1
2
3
4
5
6
7
<TextView
android:id="@+id/txt_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:scrollbarStyle="outsideInset"
tools:text="&lt;p&gt;因工作需要,厦门市人力资源和社会保障局拟面向社会公开招聘非在编驾驶员。现将简章公布如下:" />

设置scrollbars属性(有三个属性 horizontal、vertical、none)

系统默认scrolbar是渐隐效果,如果需要常显设置 android:fadeScrollbars=”false”

1
2
3
4
5
6
7
8
class MainActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val text = findViewById<TextView>(R.id.txt_scroll)
text.movementMethod = ScrollingMovementMethod.getInstance()
}
}

调用 public final void setMovementMethod(MovementMethod movement){},设置 MovementMethod

显示html

1
tv.setText(Html.fromHtml(htmlStr));

支持外链跳转

1
2
3
4
5
6
7
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_margin="16dp"
android:text="123"
android:autoLink="phone"/>

添加 android:autoLink 属性

TextView相关问题

tv.setTextColor(color)显示出来的颜色值不对

解决:tv.setTextColor(R.color.red);//不报错,但是显示效果不是红色

改成 tv.setTextColor(getResource().getColor(R.color.red));//即可

显示不出省略号…的问题

1
2
3
4
5
6
7
8
9
10
11
<TextView
android:id="@+id/tv_storeName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:maxLength="10"
android:textColor="@color/dark_text_color"
android:textSize="14sp"
tools:text="门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2门店2" />

解决:网上查tv有android:inputType="text"属性要去掉。此处的问题是android:maxLength="10"属性,改成android:maxEms="10"即可

TextView跑马灯无效

1
2
3
4
5
6
7
<TextView
style="@style/fillX"
android:text="内容需要超过tv的宽度才能有跑马灯的效果"
android:ellipsize="marquee"
android:focusableInTouchMode="true"
android:focusable="true"
android:marqueeRepeatLimit="marquee_forever"/>

android:marqueeRepeatLimit=”marquee_forever”重复次数

默认是需要获取到焦点才能滚动,所以多个tv一起跑马灯需要自定义TV,重写isFocused()方法返回true

android:ellipsize=”start”—–省略号显示在开头 “…pedia”
android:ellipsize=”end”——省略号显示在结尾 “encyc…”
android:ellipsize=”middle”—-省略号显示在中间 “en…dia”
android:ellipsize=”marquee”–以横向滚动方式显示(需获得当前焦点时)

TextView有值但是width或height为0,导致不显示

(又一城)发生在SBC2.0的燕谷坊的首页的店名,发现用as第一次运行出现这个问题“(debug模式取到值)TextView有值但是width或height为0,导致不显示”。而通过as的Tools-Layout Inspector却得到此TextView的值为“”且width和height为0

尝试解决:textview调用invalidate()、requestLayout()、measure(500,50)、post(Runnable)都不行

后来借鉴stackoverflow相似的问题在xml中在对应TextView中加上

1
android:animateLayoutChanges="false"

运行了几次是可以的,但是具体是否解决了这个问题还需待验证。

android:animateLayoutChanges=”false” 的作用是关闭系统带的动画效果

布局动画实之LayoutTransition

前面我们说过ViewGroup在设置android:animateLayoutChanges="true"后在添加或者删除子view时可以启用系统带着的动画效果,但这种效果无法通过自定义动画去替换。不过还好android官方为我们提供了LayoutTransition类,通过LayoutTransition就可以很容易为ViewGroup在添加或者删除子view设置自定义动画的过渡效果了。