提交 383f06fc 编写于 作者: C chaychan

增加expandableTextView

上级 e25a8589
apply plugin: 'com.android.library'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
versionCode 3
versionName "2.3.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
version = "2.3.0"
def siteUrl = 'https://github.com/androiddevelop/AlignTextView'
def gitUrl = 'https://github.com/androiddevelop/AlignTextView.git'
group = "me.codeboy.android"
archivesBaseName = "align-text-view"
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<lint>
<issue id="RtlHardcoded">
<ignore path="src/main/java/cn/androiddevelop/aligntextview/lib/Align.java" />
</issue>
</lint>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest package="me.codeboy.android.aligntextview">
</manifest>
\ No newline at end of file
package me.codeboy.android.aligntextview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
/**
* 两端对齐的text view,可以设置最后一行靠左,靠右,居中对齐
*
* @author YD
*/
public class AlignTextView extends TextView {
private float textHeight; // 单行文字高度
private float textLineSpaceExtra = 0; // 额外的行间距
private int width; // textView宽度
private List<String> lines = new ArrayList<String>(); // 分割后的行
private List<Integer> tailLines = new ArrayList<Integer>(); // 尾行
private Align align = Align.ALIGN_LEFT; // 默认最后一行左对齐
private boolean firstCalc = true; // 初始化计算
private float lineSpacingMultiplier = 1.0f;
private float lineSpacingAdd = 0.0f;
private int originalHeight = 0; //原始高度
private int originalLineCount = 0; //原始行数
private int originalPaddingBottom = 0; //原始paddingBottom
private boolean setPaddingFromMe = false;
// 尾行对齐方式
public enum Align {
ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT // 居中,居左,居右,针对段落最后一行
}
public AlignTextView(Context context) {
super(context);
setTextIsSelectable(false);
}
public AlignTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setTextIsSelectable(false);
lineSpacingMultiplier = attrs.getAttributeFloatValue("http://schemas.android" + "" +
".com/apk/res/android", "lineSpacingMultiplier", 1.0f);
int[] attributes = new int[]{android.R.attr.lineSpacingExtra};
TypedArray arr = context.obtainStyledAttributes(attrs, attributes);
lineSpacingAdd = arr.getDimensionPixelSize(0, 0);
originalPaddingBottom = getPaddingBottom();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.AlignTextView);
int alignStyle = ta.getInt(R.styleable.AlignTextView_align, 0);
switch (alignStyle) {
case 1:
align = Align.ALIGN_CENTER;
break;
case 2:
align = Align.ALIGN_RIGHT;
break;
default:
align = Align.ALIGN_LEFT;
break;
}
ta.recycle();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//首先进行高度调整
if (firstCalc) {
width = getMeasuredWidth();
String text = getText().toString();
TextPaint paint = getPaint();
lines.clear();
tailLines.clear();
// 文本含有换行符时,分割单独处理
String[] items = text.split("\\n");
for (String item : items) {
calc(paint, item);
}
//使用替代textview计算原始高度与行数
measureTextViewHeight(text, paint.getTextSize(), getMeasuredWidth() -
getPaddingLeft() - getPaddingRight());
//获取行高
textHeight = 1.0f * originalHeight / originalLineCount;
textLineSpaceExtra = textHeight * (lineSpacingMultiplier - 1) + lineSpacingAdd;
//计算实际高度,加上多出的行的高度(一般是减少)
int heightGap = (int) ((textLineSpaceExtra + textHeight) * (lines.size() -
originalLineCount));
setPaddingFromMe = true;
//调整textview的paddingBottom来缩小底部空白
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
originalPaddingBottom + heightGap);
firstCalc = false;
}
}
@Override
protected void onDraw(Canvas canvas) {
TextPaint paint = getPaint();
paint.setColor(getCurrentTextColor());
paint.drawableState = getDrawableState();
width = getMeasuredWidth();
Paint.FontMetrics fm = paint.getFontMetrics();
float firstHeight = getTextSize() - (fm.bottom - fm.descent + fm.ascent - fm.top);
int gravity = getGravity();
if ((gravity & 0x1000) == 0) { // 是否垂直居中
firstHeight = firstHeight + (textHeight - firstHeight) / 2;
}
int paddingTop = getPaddingTop();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
width = width - paddingLeft - paddingRight;
for (int i = 0; i < lines.size(); i++) {
float drawY = i * textHeight + firstHeight;
String line = lines.get(i);
// 绘画起始x坐标
float drawSpacingX = paddingLeft;
float gap = (width - paint.measureText(line));
float interval = gap / (line.length() - 1);
// 绘制最后一行
if (tailLines.contains(i)) {
interval = 0;
if (align == Align.ALIGN_CENTER) {
drawSpacingX += gap / 2;
} else if (align == Align.ALIGN_RIGHT) {
drawSpacingX += gap;
}
}
for (int j = 0; j < line.length(); j++) {
float drawX = paint.measureText(line.substring(0, j)) + interval * j;
canvas.drawText(line.substring(j, j + 1), drawX + drawSpacingX, drawY +
paddingTop + textLineSpaceExtra * i, paint);
}
}
}
/**
* 设置尾行对齐方式
*
* @param align 对齐方式
*/
public void setAlign(Align align) {
this.align = align;
invalidate();
}
/**
* 计算每行应显示的文本数
*
* @param text 要计算的文本
*/
private void calc(Paint paint, String text) {
if (text.length() == 0) {
lines.add("\n");
return;
}
int startPosition = 0; // 起始位置
float oneChineseWidth = paint.measureText("中");
int ignoreCalcLength = (int) (width / oneChineseWidth); // 忽略计算的长度
StringBuilder sb = new StringBuilder(text.substring(0, Math.min(ignoreCalcLength + 1,
text.length())));
for (int i = ignoreCalcLength + 1; i < text.length(); i++) {
if (paint.measureText(text.substring(startPosition, i + 1)) > width) {
startPosition = i;
//将之前的字符串加入列表中
lines.add(sb.toString());
sb = new StringBuilder();
//添加开始忽略的字符串,长度不足的话直接结束,否则继续
if ((text.length() - startPosition) > ignoreCalcLength) {
sb.append(text.substring(startPosition, startPosition + ignoreCalcLength));
} else {
lines.add(text.substring(startPosition));
break;
}
i = i + ignoreCalcLength - 1;
} else {
sb.append(text.charAt(i));
}
}
if (sb.length() > 0) {
lines.add(sb.toString());
}
tailLines.add(lines.size() - 1);
}
@Override
public void setText(CharSequence text, BufferType type) {
firstCalc = true;
super.setText(text, type);
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
if (!setPaddingFromMe) {
originalPaddingBottom = bottom;
}
setPaddingFromMe = false;
super.setPadding(left, top, right, bottom);
}
/**
* 获取文本实际所占高度,辅助用于计算行高,行数
*
* @param text 文本
* @param textSize 字体大小
* @param deviceWidth 屏幕宽度
*/
private void measureTextViewHeight(String text, float textSize, int deviceWidth) {
TextView textView = new TextView(getContext());
textView.setText(text);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(deviceWidth, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
textView.measure(widthMeasureSpec, heightMeasureSpec);
originalLineCount = textView.getLineCount();
originalHeight = textView.getMeasuredHeight();
}
}
\ No newline at end of file
package me.codeboy.android.aligntextview;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.widget.TextView;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import me.codeboy.android.aligntextview.util.CBAlignTextViewUtil;
/**
* 对齐的TextView
* <p>
* 为了能够使TextView能够很好的进行排版,同时考虑到原生TextView中以word进行分割排版,
* 那么我们可以将要换行的地方进行添加空格处理,这样就可以在合适的位置换行,同时也不会
* 打乱原生的TextView的排版换行选择复制等问题。为了能够使右端尽可能的对齐,将右侧多出的空隙
* 尽可能的分配到该行的标点后面。达到两段对齐的效果。
* </p>
* Created by yuedong.lyd on 6/28/15.
*/
public class CBAlignTextView extends TextView {
private List<Integer> addCharPosition = new ArrayList<Integer>(); //增加空格的位置
private final char SPACE = ' '; //空格;
private CharSequence oldText = ""; //旧文本,本来应该显示的文本
private CharSequence newText = ""; //新文本,真正显示的文本
private boolean inProcess = false; //旧文本是否已经处理为新文本
private boolean isAddPadding = false; //是否添加过边距
private boolean isConvert = false; //是否转换标点符号
private boolean isAddListener = false; //是否添加监听器
private static List<Character> punctuation = new ArrayList<Character>(); //标点符号
//标点符号用于在textview右侧多出空间时,将空间加到标点符号的后面,以便于右端对齐
static {
punctuation.clear();
punctuation.add(',');
punctuation.add('.');
punctuation.add('?');
punctuation.add('!');
punctuation.add(';');
punctuation.add(',');
punctuation.add('。');
punctuation.add('?');
punctuation.add('!');
punctuation.add(';');
punctuation.add(')');
punctuation.add('】');
punctuation.add(')');
punctuation.add(']');
punctuation.add('}');
}
public CBAlignTextView(Context context) {
super(context);
addLayoutListener();
}
public CBAlignTextView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CBAlignTextView);
isConvert = ta.getBoolean(R.styleable.CBAlignTextView_punctuationConvert, false);
ta.recycle();
addLayoutListener();
}
/**
* 监听文本复制,对于复制的文本进行空格剔除
*
* @param id 操作id(复制,全部选择等)
* @return 是否操作成功
*/
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.copy) {
if (isFocused()) {
final int selStart = getSelectionStart();
final int selEnd = getSelectionEnd();
int min = Math.max(0, Math.min(selStart, selEnd));
int max = Math.max(0, Math.max(selStart, selEnd));
//利用反射获取选择的文本信息,同时关闭操作框
try {
Class cls = getClass().getSuperclass();
Method getSelectTextMethod = cls.getDeclaredMethod("getTransformedText", new
Class[]{int.class, int.class});
getSelectTextMethod.setAccessible(true);
CharSequence selectedText = (CharSequence) getSelectTextMethod.invoke(this,
min, max);
copy(selectedText.toString());
Method closeMenuMethod;
if (Build.VERSION.SDK_INT < 23) {
closeMenuMethod = cls.getDeclaredMethod("stopSelectionActionMode");
} else {
closeMenuMethod = cls.getDeclaredMethod("stopTextActionMode");
}
closeMenuMethod.setAccessible(true);
closeMenuMethod.invoke(this);
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
} else {
return super.onTextContextMenuItem(id);
}
}
/**
* 复制文本到剪切板,去除添加字符
*
* @param text 文本
*/
private void copy(String text) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context
.CLIPBOARD_SERVICE);
int start = newText.toString().indexOf(text);
int end = start + text.length();
StringBuilder sb = new StringBuilder(text);
for (int i = addCharPosition.size() - 1; i >= 0; i--) {
int position = addCharPosition.get(i);
if (position < end && position >= start) {
sb.deleteCharAt(position - start);
}
}
android.content.ClipData clip = android.content.ClipData.newPlainText(null, sb.toString());
clipboard.setPrimaryClip(clip);
}
/**
* 处理多行文本
*
* @param paint 画笔
* @param text 文本
* @param width 最大可用宽度
* @return 处理后的文本
*/
private String processText(Paint paint, String text, int width) {
if (text == null || text.length() == 0) {
return "";
}
String[] lines = text.split("\\n");
StringBuilder newText = new StringBuilder();
for (String line : lines) {
newText.append('\n');
newText.append(processLine(paint, line, width, newText.length() - 1));
}
if (newText.length() > 0) {
newText.deleteCharAt(0);
}
return newText.toString();
}
/**
* 处理单行文本
*
* @param paint 画笔
* @param text 文本
* @param width 最大可用宽度
* @param addCharacterStartPosition 添加文本的起始位置
* @return 处理后的文本
*/
private String processLine(Paint paint, String text, int width, int addCharacterStartPosition) {
if (text == null || text.length() == 0) {
return "";
}
StringBuilder old = new StringBuilder(text);
int startPosition = 0; // 起始位置
float chineseWidth = paint.measureText("中");
float spaceWidth = paint.measureText(SPACE + "");
//最大可容纳的汉字,每一次从此位置向后推进计算
int maxChineseCount = (int) (width / chineseWidth);
//减少一个汉字宽度,保证每一行前后都有一个空格
maxChineseCount--;
//如果不能容纳汉字,直接返回空串
if (maxChineseCount <= 0) {
return "";
}
for (int i = maxChineseCount; i < old.length(); i++) {
if (paint.measureText(old.substring(startPosition, i + 1)) > (width - spaceWidth)) {
//右侧多余空隙宽度
float gap = (width - spaceWidth - paint.measureText(old.substring(startPosition,
i)));
List<Integer> positions = new ArrayList<Integer>();
for (int j = startPosition; j < i; j++) {
char ch = old.charAt(j);
if (punctuation.contains(ch)) {
positions.add(j + 1);
}
}
//空隙最多可以使用几个空格替换
int number = (int) (gap / spaceWidth);
//多增加的空格数量
int use = 0;
if (positions.size() > 0) {
for (int k = 0; k < positions.size() && number > 0; k++) {
int times = number / (positions.size() - k);
int position = positions.get(k / positions.size());
for (int m = 0; m < times; m++) {
old.insert(position + m, SPACE);
addCharPosition.add(position + m + addCharacterStartPosition);
use++;
number--;
}
}
}
//指针移动,将段尾添加空格进行分行处理
i = i + use;
old.insert(i, SPACE);
addCharPosition.add(i + addCharacterStartPosition);
startPosition = i + 1;
i = i + maxChineseCount;
}
}
return old.toString();
}
@Override
public void setText(CharSequence text, BufferType type) {
if (!inProcess && (text != null && !text.equals(newText))) {
oldText = text;
if (!isAddListener) {
addLayoutListener();
}
process(false);
super.setText(newText, type);
} else {
//恢复初始状态
inProcess = false;
super.setText(text, type);
}
}
/**
* 获取真正的text
*
* @return 返回text
*/
public CharSequence getRealText() {
return oldText;
}
/**
* 文本转化
*
* @param setText 是否设置textView的文本
*/
private void process(boolean setText) {
if (!inProcess && !TextUtils.isEmpty(oldText) && getVisibility() == VISIBLE) {
addCharPosition.clear();
//转化字符,5.0系统对字体处理有所变动
if (isConvert) {
oldText = CBAlignTextViewUtil.replacePunctuation(oldText.toString());
}
if (getWidth() == 0) {
return;
}
//添加过边距之后不再次添加
if (!isAddPadding) {
int spaceWidth = (int) (getPaint().measureText(SPACE + ""));
newText = processText(getPaint(), oldText.toString(), getWidth() - getPaddingLeft
() -
getPaddingRight() - spaceWidth);
setPadding(getPaddingLeft() + spaceWidth, getPaddingTop(), getPaddingRight(),
getPaddingBottom());
isAddPadding = true;
} else {
newText = processText(getPaint(), oldText.toString(), getWidth() - getPaddingLeft
() -
getPaddingRight());
}
inProcess = true;
if (setText) {
setText(newText);
}
}
}
/**
* 添加监听器,用于在布局进行改变时重新绘制文本
*/
private void addLayoutListener() {
isAddListener = true;
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (getWidth() == 0) {
return;
}
process(true);
if (Build.VERSION.SDK_INT >= 16) {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
isAddListener = false;
}
});
}
/**
* 是否转化标点符号,将中文标点转化为英文标点
*
* @param convert 是否转化
*/
public void setPunctuationConvert(boolean convert) {
isConvert = convert;
}
}
\ No newline at end of file
package me.codeboy.android.aligntextview.util;
/**
* 文本工具
* Created by yuedong.lyd on 11/30/15.
*/
public class CBAlignTextViewUtil {
/**
* 将中文标点替换为英文标点
*
* @return 替换后的文本
*/
public static String replacePunctuation(String text) {
text = text.replace(',', ',').replace('。', '.').replace('【', '[').replace('】', ']')
.replace('?', '?').replace('!', '!').replace('(', '(').replace(')', ')').replace
('“', '"').replace('”', '"');
return text;
}
}
<resources>
<!--
Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
-->
<style name="AppBaseTheme" parent="android:Theme.Light">
<!--
Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
</style>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 末行对齐方式 -->
<declare-styleable name="AlignTextView">
<attr name="align" format="enum">
<enum name="left" value="0"/>
<enum name="center" value="1"/>
<enum name="right" value="2"/>
</attr>
</declare-styleable>
<!-- 标点转换 -->
<declare-styleable name="CBAlignTextView">
<attr name="punctuationConvert" format="boolean"/>
</declare-styleable>
</resources>
\ No newline at end of file
apply plugin: 'com.android.library'
ext{
PUBLISH_GROUP_ID = 'com.jaydenxiao'
PUBLISH_ARTIFACT_ID = 'ExpandableTextView'
PUBLISH_VERSION = '1.0.0'
}
android {
compileSdkVersion 24
buildToolsVersion "24.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
//打包不检查
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'me.codeboy.android:align-text-view:2.3.0'
compile project(':AliagnTextView')
}
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in H:\android-22\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
package jaydenxiao.com.expandabletextview;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
\ No newline at end of file
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jaydenxiao.com.expandabletextview">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true">
</application>
</manifest>
package jaydenxiao.com.expandabletextview;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.SparseBooleanArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import me.codeboy.android.aligntextview.AlignTextView;
/**
* des:可伸展textview
* Created by xsf
* on 2016.08.24
*/
public class ExpandableTextView extends LinearLayout implements View.OnClickListener {
/* 默认最高行数 */
private static final int MAX_COLLAPSED_LINES = 5;
/* 默认动画执行时间 */
private static final int DEFAULT_ANIM_DURATION = 200;
/*内容textview*/
protected AlignTextView mTvContent;
/*展开收起textview*/
protected TextView mTvExpandCollapse;
/*是否有重新绘制*/
private boolean mRelayout;
/*默认收起*/
private boolean mCollapsed = true;
/*展开图片*/
private Drawable mExpandDrawable;
/*收起图片*/
private Drawable mCollapseDrawable;
/*动画执行时间*/
private int mAnimationDuration;
/*是否正在执行动画*/
private boolean mAnimating;
/* 展开收起状态回调 */
private OnExpandStateChangeListener mListener;
/* listview等列表情况下保存每个item的收起/展开状态 */
private SparseBooleanArray mCollapsedStatus;
/* 列表位置 */
private int mPosition;
/*设置内容最大行数,超过隐藏*/
private int mMaxCollapsedLines;
/*这个linerlayout容器的高度*/
private int mCollapsedHeight;
/*内容tv真实高度(含padding)*/
private int mTextHeightWithMaxLines;
/*内容tvMarginTopAmndBottom高度*/
private int mMarginBetweenTxtAndBottom;
/*内容颜色*/
private int contentTextColor;
/*收起展开颜色*/
private int collapseExpandTextColor;
/*内容字体大小*/
private float contentTextSize;
/*收起展字体大小*/
private float collapseExpandTextSize;
/*收起文字*/
private String textCollapse;
/*展开文字*/
private String textExpand;
/*收起展开位置,默认左边*/
private int grarity;
/*收起展开图标位置,默认在右边*/
private int drawableGrarity;
public ExpandableTextView(Context context) {
this(context, null);
}
public ExpandableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("ExpandableTextView only supports Vertical Orientation.");
}
super.setOrientation(orientation);
}
/**
* 初始化属性
* @param attrs
*/
private void init(AttributeSet attrs) {
mCollapsedStatus=new SparseBooleanArray();
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableTextView);
mMaxCollapsedLines = typedArray.getInt(R.styleable.ExpandableTextView_maxCollapsedLines, MAX_COLLAPSED_LINES);
mAnimationDuration = typedArray.getInt(R.styleable.ExpandableTextView_animDuration, DEFAULT_ANIM_DURATION);
mExpandDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_expandDrawable);
mCollapseDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_collapseDrawable);
textCollapse = typedArray.getString(R.styleable.ExpandableTextView_textCollapse);
textExpand = typedArray.getString(R.styleable.ExpandableTextView_textExpand);
if (mExpandDrawable == null) {
mExpandDrawable = ContextCompat.getDrawable(getContext(),R.drawable.icon_green_arrow_up);
}
if (mCollapseDrawable == null) {
mCollapseDrawable = ContextCompat.getDrawable(getContext(), R.drawable.icon_green_arrow_down);
}
if (TextUtils.isEmpty(textCollapse)) {
textCollapse = getContext().getString(R.string.collapse);
}
if (TextUtils.isEmpty(textExpand)) {
textExpand = getContext().getString(R.string.expand);
}
contentTextColor = typedArray.getColor(R.styleable.ExpandableTextView_contentTextColor, ContextCompat.getColor(getContext(), R.color.gray));
contentTextSize = typedArray.getDimension(R.styleable.ExpandableTextView_contentTextSize, UIUtil.sp2px(getContext(),14));
collapseExpandTextColor = typedArray.getColor(R.styleable.ExpandableTextView_collapseExpandTextColor, ContextCompat.getColor(getContext(), R.color.main_color));
collapseExpandTextSize = typedArray.getDimension(R.styleable.ExpandableTextView_collapseExpandTextSize, UIUtil.sp2px(getContext(),14));
grarity = typedArray.getInt(R.styleable.ExpandableTextView_collapseExpandGrarity, Gravity.LEFT);
drawableGrarity=typedArray.getInt(R.styleable.ExpandableTextView_drawableGrarity, Gravity.RIGHT);
typedArray.recycle();
// enforces vertical orientation
setOrientation(LinearLayout.VERTICAL);
// default visibility is gone
setVisibility(GONE);
}
/**
* 渲染完成时初始化view
*/
@Override
protected void onFinishInflate() {
findViews();
}
/**
* 初始化viwe
*/
private void findViews() {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.item_expand_collapse, this);
mTvContent = (AlignTextView) findViewById(R.id.expandable_text);
mTvContent.setOnClickListener(this);
mTvExpandCollapse = (TextView) findViewById(R.id.expand_collapse);
setDrawbleAndText();
mTvExpandCollapse.setOnClickListener(this);
mTvContent.setTextColor(contentTextColor);
mTvContent.getPaint().setTextSize(contentTextSize);
mTvExpandCollapse.setTextColor(collapseExpandTextColor);
mTvExpandCollapse.getPaint().setTextSize(collapseExpandTextSize);
//设置收起展开位置:左或者右
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
lp.gravity = grarity;
mTvExpandCollapse.setLayoutParams(lp);
}
/**
* 点击事件
* @param view
*/
@Override
public void onClick(View view) {
if (mTvExpandCollapse.getVisibility() != View.VISIBLE) {
return;
}
mCollapsed = !mCollapsed;
//修改收起/展开图标、文字
setDrawbleAndText();
//保存位置状态
if (mCollapsedStatus != null) {
mCollapsedStatus.put(mPosition, mCollapsed);
}
// 执行展开/收起动画
mAnimating = true;
ValueAnimator valueAnimator;
if (mCollapsed) {
// mTvContent.setMaxLines(mMaxCollapsedLines);
valueAnimator = new ValueAnimator().ofInt(getHeight(), mCollapsedHeight);
} else {
valueAnimator = new ValueAnimator().ofInt(getHeight(), getHeight() +
mTextHeightWithMaxLines - mTvContent.getHeight());
}
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int animatedValue = (int) valueAnimator.getAnimatedValue();
mTvContent.setMaxHeight(animatedValue - mMarginBetweenTxtAndBottom);
getLayoutParams().height = animatedValue;
requestLayout();
}
});
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
// 动画结束后发送结束的信号
/// clear the animation flag
mAnimating = false;
// notify the listener
if (mListener != null) {
mListener.onExpandStateChanged(mTvContent, !mCollapsed);
}
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
valueAnimator.setDuration(mAnimationDuration);
valueAnimator.start();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 当动画还在执行状态时,拦截事件,不让child处理
return mAnimating;
}
/**
* 重新测量
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// If no change, measure and return
if (!mRelayout || getVisibility() == View.GONE) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
mRelayout = false;
// Setup with optimistic case
// i.e. Everything fits. No button needed
mTvExpandCollapse.setVisibility(View.GONE);
mTvContent.setMaxLines(Integer.MAX_VALUE);
// Measure
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//如果内容真实行数小于等于最大行数,不处理
if (mTvContent.getLineCount() <= mMaxCollapsedLines) {
return;
}
// 获取内容tv真实高度(含padding)
mTextHeightWithMaxLines = getRealTextViewHeight(mTvContent);
// 如果是收起状态,重新设置最大行数
if (mCollapsed) {
mTvContent.setMaxLines(mMaxCollapsedLines);
}
mTvExpandCollapse.setVisibility(View.VISIBLE);
// Re-measure with new setup
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mCollapsed) {
// Gets the margin between the TextView's bottom and the ViewGroup's bottom
mTvContent.post(new Runnable() {
@Override
public void run() {
mMarginBetweenTxtAndBottom = getHeight() - mTvContent.getHeight();
}
});
// 保存这个容器的测量高度
mCollapsedHeight = getMeasuredHeight();
}
}
/**
* 获取内容tv真实高度(含padding)
* @param textView
* @return
*/
private static int getRealTextViewHeight( TextView textView) {
int textHeight = textView.getLayout().getLineTop(textView.getLineCount());
int padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom();
return textHeight + padding;
}
/**
* 设置收起展开图标位置和文字
*/
private void setDrawbleAndText(){
if(Gravity.LEFT==drawableGrarity){
mTvExpandCollapse.setCompoundDrawablesWithIntrinsicBounds(mCollapsed ? mCollapseDrawable : mExpandDrawable,null,null,null);
}else{
mTvExpandCollapse.setCompoundDrawablesWithIntrinsicBounds(null,null,mCollapsed ? mCollapseDrawable : mExpandDrawable,null);
}
mTvExpandCollapse.setText(mCollapsed ? getResources().getString(R.string.expand) : getResources().getString(R.string.collapse));
}
/*********暴露给外部调用方法***********/
/**
* 设置收起/展开监听
* @param listener
*/
public void setOnExpandStateChangeListener( OnExpandStateChangeListener listener) {
mListener = listener;
}
/**
* 设置内容
* @param text
*/
public void setText( CharSequence text) {
mRelayout = true;
mTvContent.setText(text);
setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
}
/**
* 设置内容,列表情况下,带有保存位置收起/展开状态
* @param text
* @param position
*/
public void setText( CharSequence text,int position) {
mPosition = position;
//获取状态,如无,默认是true:收起
mCollapsed = mCollapsedStatus.get(position, true);
clearAnimation();
//设置收起/展开图标和文字
setDrawbleAndText();
mTvExpandCollapse.setText(mCollapsed ? getResources().getString(R.string.expand) : getResources().getString(R.string.collapse));
setText(text);
getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
requestLayout();
}
/**
* 获取内容
* @return
*/
public CharSequence getText() {
if (mTvContent == null) {
return "";
}
return mTvContent.getText();
}
/**
* 定义状态改变接口
*/
public interface OnExpandStateChangeListener {
/**
* @param textView - TextView being expanded/collapsed
* @param isExpanded - true if the TextView has been expanded
*/
void onExpandStateChanged(TextView textView, boolean isExpanded);
}
}
\ No newline at end of file
package jaydenxiao.com.expandabletextview;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.widget.LinearLayout;
/**
* des:屏幕相关辅助类
* Created by xsf
* on 2016.08.24
*/
public class UIUtil {
private UIUtil() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
/**
* 将px值转换为dip或dp值,保证尺寸大小不变
*
* @param pxValue
* (DisplayMetrics类中属性density)
* @return
*/
public static int px2dip(Context context,float pxValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 将dip或dp值转换为px值,保证尺寸大小不变
*
* @param dipValue
* @return
*/
public static int dip2px( Context context ,float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* 将px值转换为sp值,保证文字大小不变
*
* @param pxValue
* @return
*/
public static int px2sp(Context context,float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
*
* @param spValue
* @return
*/
public static int sp2px(Context context,float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
/**
* 直接获取控件的宽、高
* @param view
* @return int[]
*/
public static int[] getWidgetWH(final View view){
ViewTreeObserver vto2 = view.getViewTreeObserver();
vto2.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
return new int[]{view.getWidth(),view.getHeight()};
}
/**
* 直接获取控件的宽、高
* @param view
* @return int[]
*/
public static int getViewHeight(final View view){
ViewTreeObserver vto2 = view.getViewTreeObserver();
vto2.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
return view.getHeight();
}
/**
* 直接获取控件的宽、高
* @param view
* @return int[]
*/
public static int getViewWidth(final View view){
ViewTreeObserver vto2 = view.getViewTreeObserver();
vto2.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
return view.getWidth();
}
/**
* 获得屏幕宽度
*
* @param context
* @return
*/
public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
/**
* 获得屏幕高度
*
* @param context
* @return
*/
public static int getScreenHeight(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.heightPixels;
}
/**
* 获取控件的宽
* @param view
* @return
*/
public static int getWidgetWidth(View view){
int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(w, h);//先度量
int width = view.getMeasuredWidth();
return width;
}
/**
* 获取控件的高
* @param view
* @return
*/
public static int getWidgetHeight(View view){
int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(w, h);//先度量
int height = view.getMeasuredHeight();
return height;
}
/**
* 设置控件宽
* @param view
* @param width
*/
public static void setWidgetWidth(View view, int width){
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
params.width = width;
view.setLayoutParams(params);
}
/**
* 设置控件高
* @param view
* @param height
*/
public static void setWidgetHeight(View view, int height){
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams();
params.height = height;
view.setLayoutParams(params);
}
//获得状态栏的高度
public static int getStatusBarHeight(Context context) {
int result = 0;
int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
result = context.getResources().getDimensionPixelOffset(resId);
}
return result;
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.codeboy.android.aligntextview.AlignTextView
android:id="@+id/expandable_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/gray"
android:lineSpacingExtra="1dp"
tools:text="@string/test_content"
android:ellipsize="end"
android:textSize="15sp" />
<TextView
android:id="@+id/expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="@android:color/transparent"
tools:text="@string/collapse"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:gravity="center"
android:textColor="@color/main_color"
android:drawablePadding="3dp"
tools:drawableRight="@drawable/icon_green_arrow_up"
android:layout_marginTop="5dp" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--收起展开textview-->
<declare-styleable name="ExpandableTextView">
<attr name="maxCollapsedLines" format="integer"/>
<attr name="animDuration" format="integer"/>
<attr name="expandDrawable" format="reference"/>
<attr name="collapseDrawable" format="reference"/>
<attr name="contentTextColor" format="color" />
<attr name="collapseExpandTextColor" format="color" />
<attr name="contentTextSize" format="dimension" />
<attr name="collapseExpandTextSize" format="dimension" />
<attr name="textCollapse" format="string" />
<attr name="textExpand" format="string" />
<attr name="collapseExpandGrarity" >
<flag name="left" value="3"/>
<flag name="right" value="5"/>
</attr>
<attr name="drawableGrarity" >
<flag name="left" value="3"/>
<flag name="right" value="5"/>
</attr>
</declare-styleable>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="main_color">#FA7C20</color>
<color name="gray">#333333</color>
</resources>
<resources>
<string name="app_name">ExpandableTextView</string>
<string name="expand">展开</string>
<string name="collapse">收起</string>
<string name="test_content">The story of a Chinese couple at Oxford University who first met each other as deskmate in middle school and recently married each other has gone viral, southcn.com reported.
Zhou Si, the groom, and Chen Mengge, the bride, married each other on Aug 18 and will together go to Oxford University to pursue their doctoral degrees later this year.
They were deskmate in middle school and then both were admitted by same high school but not in same class.</string>
</resources>
package jaydenxiao.com.expandabletextview;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* To work on unit tests, switch the Test Artifact in the Build Variants view.
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
include ':app', ':viewlib'
include ':app', ':viewlib', ':ExpandableTextView', ':AliagnTextView'
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册