TextView
getTextSize()和setTextSize()的单位设置
我们都已经知道getTextView取的值是像素值,看过源码就会知道它实际上是调用Paint类的native方法。在这一点上无可争议。但是在setTextSize()处,就会有个小说法,这里我先把结论说下:
根据已有sp值直接设置setTextSize(),请这样设置:
tv.setTextSize(17);
而针对想要把sp转化成px进行传入,或者使用dimens文件中已有值进行设置的话:
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, ScreenUtils.dp2px(this, 17));
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.font_size_sp));
好,下面说下原理:
setTextSize()源码分析
我们知道,TextView的单位有很多,什么sp啊,dp啊,px等等。那么google在android.util包下就专门设置了一个类用来管理这些文字单位:TypedValue。并且设置了几个静态变量来规定类型:
/** {@link #TYPE_DIMENSION} complex unit: Value is raw pixels. */
public static final int COMPLEX_UNIT_PX = 0;
/** {@link #TYPE_DIMENSION} complex unit: Value is Device Independent
* Pixels. */
public static final int COMPLEX_UNIT_DIP = 1;
/** {@link #TYPE_DIMENSION} complex unit: Value is a scaled pixel. */
public static final int COMPLEX_UNIT_SP = 2;
/** {@link #TYPE_DIMENSION} complex unit: Value is in points. */
public static final int COMPLEX_UNIT_PT = 3;
/** {@link #TYPE_DIMENSION} complex unit: Value is in inches. */
public static final int COMPLEX_UNIT_IN = 4;
/** {@link #TYPE_DIMENSION} complex unit: Value is in millimeters. */
public static final int COMPLEX_UNIT_MM = 5;
而我们调用setTextSize()方法时,上面的类型是作为一个参数传入的。
/**
* Set the default text size to a given unit and value. See {@link
* TypedValue} for the possible dimension units.
*
* @param unit The desired dimension unit.
* @param size The desired size in the given units.
*
* @attr ref android.R.styleable#TextView_textSize
*/
public void setTextSize(int unit, float size) {
Context c = getContext();
Resources r;
if (c == null)
r = Resources.getSystem();
else
r = c.getResources();
setRawTextSize(TypedValue.applyDimension(
unit, size, r.getDisplayMetrics()));
}
而实际上我们平时使用的setTextSize()方法就是上面方法的重载,而且它也为我们设置好了默认类型
/**
* Set the default text size to the given value, interpreted as "scaled
* pixel" units. This size is adjusted based on the current density and
* user font size preference.
*
* @param size The scaled pixel size.
*
* @attr ref android.R.styleable#TextView_textSize
*/
@android.view.RemotableViewMethod
public void setTextSize(float size) {
setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
}
这也就是为什么setTextSize(float size)的传入值是sp类型的原因。而将字体单位传入也不是没有用的,我们看到此方法中将单位值传给了TypedValue.applyDimension(unit, size, r.getDisplayMetrics())。我们可以跟进去看看:
/**
* Converts an unpacked complex data value holding a dimension to its final floating
* point value. The two parameters <var>unit</var> and <var>value</var>
* are as in {@link #TYPE_DIMENSION}.
*
* @param unit The unit to convert from.
* @param value The value to apply the unit to.
* @param metrics Current display metrics to use in the conversion --
* supplies display density and scaling information.
*
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
可以看到,系统根据文字单位的不同对字号大小进行了屏幕适配。sp类型会乘以scaledDensity值,dp类型会乘以density,像素则不作处理。
看来,系统对于字体大小没有进行过多的处理,仅仅是根据文字单位作出了适配处理。那么就和我们传入的值有很大关系了。
setTextSize()传入参数比较
1.直接调用setTextSize(),只需要传入我们已经计算好的sp值就可以
2.通过dp2px将sp值转化为像素值,我们在setTextSize()时,把传入单位设置成TypedValue.COMPLEX_UNIT_PX也可达到相同效果。
3.但当我们使用dimens文件进行适配时,就要小心,因为系统会针对文件中dp或sp乘上density或scaledDensity,使得getResources().getDimension(R.dimen.font_size_dp)取值就是适配好的像素值,如果我们这时再去调用文字单位为TypedValue.COMPLEX_UNIT_DIP或者TypedValue.COMPLEX_UNIT_SP,文字最终效果将会翻倍变大。所以在使用该文件时应该TypedValue.COMPLEX_UNIT_PX
以小米红米Note 1 LTE机型为例,屏幕分辨率在1280×720,屏幕尺寸在5.5寸,density为2.
<dimen name="font_size_sp">17sp</dimen>
<dimen name="font_size_dp">17dp</dimen>
<dimen name="font_size_px">34px</dimen>
int i = (int)getResources().getDimension(R.dimen.font_size_sp);
int x = (int)getResources().getDimension(R.dimen.font_size_dp);
int y = (int)getResources().getDimension(R.dimen.font_size_px);
debug取值后:
跑马灯效果实现
xml文件代码:
android:singleLine="true"
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
仅仅设置这几行代码就可以实现一个永久滑动的TextView的跑马灯效果。但需要注意几点:
1.上面前四个属性是必需的,最后一个属性是可添加的,没有这条属性,跑马灯效果依然可以实现,只不过仅仅显示一次而已。
2.界面上有多个TextView想要显示跑马灯的效果,仅仅在xml中设置是不够的,需要对每个TextView在java代码中设置:
tv.setSelected(true);
3.文本长度应该超出屏幕宽度,否则无效果。
android:ellipsize
属性是对TextView文本进行省略设置, 值有五个:none代表不省略(默认值)start在开始处省略,middle代表在中间处省略,end代表在结尾处省略,marquee代表以跑马灯的形式展示。
对应的java代码是:
setEllipsize(TextUtils.TruncateAt)
android:marqueeRepeatLimit
该属性是对TextView跑马灯效果次数的限制,单位为一个int值,例如android:marqueeRepeatLimit="marquee_forever"就是跑马灯效果展示100次。值marquee_forever为永远展示。对应java代码:setMarqueeRepeatLimit(int)
android:singleLine
该属性是对TextView的文本内容进行单行显示的限制,使用时默认会自带android:ellipsize="end"的属性,所以显示效果会是“XXXXXXXXXXXX...”。但是自从api23时谷歌就不推荐使用这个属性了,在官方网站上也没有找到推荐替换的属性。一直不明白这样一个很方便的属性为什么会被弃掉。当然,不推荐不代表不能使用,而且可替换的属性有maxLines,lines。java代码中对应setSingleLine ()
阴影效果
使用下面四个属性,可以使TextView具有阴影效果:
android:shadowDx="5"
android:shadowDy="5"
android:shadowRadius="5"
android:shadowColor="@color/colorAccent"
- android:shadowDx——设置阴影横向坐标开始位置(相对于文本内容)
- android:shadowDy——设置阴影纵向坐标开始位置(相对于文本内容)
- android:shadowRadius——设置阴影的半径(以真正打印的单个字形的边界为基准点,向外辐射的半径)
- android:shadowColor——指定文本阴影的颜色
对应java代码为setShadowLayer(float radius, float dx, float dy, int color)
注意:xml属性中的值不需要加单位,x和y值可以为负值。
同时改变TextView中部分文字的显示字号和字色
ColorStateList redColors = ColorStateList.valueOf(0xffff0000);
SpannableStringBuilder spanBuilder = new SpannableStringBuilder("这是一个测试");
//style 为0 即是正常的,还有Typeface.BOLD(粗体) Typeface.ITALIC(斜体)等
//size 为0 即采用原始的正常的 size大小
spanBuilder.setSpan(new TextAppearanceSpan(null, 0, 60, redColors, null), 0, 3, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
TextView tv_test = (TextView) findViewById(R.id.tv_test);
tv_test.setText(spanBuilder);
改变TextView中部分文字的显示字色以及可点击
SpannableString builder = new SpannableString(commentText);
builder.setSpan(new NoUnderLineSpan() {
@Override
public void onClick(View widget) {
UserInfoActivityFactory.start(activity, articleComment.getQuote().getUserId(), false);
}
}, start, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
holder.textUserComment.setText(builder);
holder.textUserComment.setMovementMethod(LinkMovementMethod.getInstance());
class NoUnderLineSpan extends ClickableSpan {
@Override
public void onClick(View widget) {
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
ds.setColor(activity.getResources().getColor(R.color.iOS7_d__district));
}
}
可是这样会带来两个隐藏问题:
1.只有build设置过的部分可点击,build之外的地方不可点击,即使点击事件设在了view上。
2.如果文本过长,textView没有显示完全,那么默认的LinkMovementMethod在点击完成后会将未显示完的文本内容滑动出来。
解决:1.给build分段设置Spannable。2自定义LinkMovementMethod,覆写其中逻辑。
commentText = "这是一段测试文本";
int start = commentText.indexOf(atUserName);
int end = start + atUserName.length();
SpannableString builder = new SpannableString(commentText);
builder.setSpan(new NoUnderLineSpan() {
@Override
public void onClick(View widget) {
UserInfoActivityFactory.start(activity, articleComment.getQuote().getUserId(), false);
}
}, start, end, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
builder.setSpan(new NoUnderLineNormalSpan(){
@Override
public void onClick(View view) {
}
},end, commentText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.textUserComment.setText(builder);
holder.textUserComment.setMovementMethod(new NoScrollLinkMovementMethod());
NoScrollLinkMovementMethod的逻辑:
public class NoScrollLinkMovementMethod extends LinkMovementMethod {
public NoScrollLinkMovementMethod() {
}
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return Touch.onTouchEvent(widget, buffer, event);
}
}
代码中直接设置资源图片
.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.ico_btn_retracted, 0, 0);
设置字体
android:fontFamily="monospace"