├── .gitignore ├── .idea └── copyright │ └── profiles_settings.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── freecats │ │ └── demo │ │ └── view │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── freecats │ │ │ └── demo │ │ │ ├── activity │ │ │ ├── MainActivity.java │ │ │ ├── NormalModeActivity.java │ │ │ └── RecyclerViewModeActivity.java │ │ │ ├── adapter │ │ │ └── RecyclerViewAdapter.java │ │ │ ├── model │ │ │ └── ContentModel.java │ │ │ └── view │ │ │ └── TextViewExpandableAnimation.java │ └── res │ │ ├── drawable-xhdpi │ │ ├── icon_green_arrow_down.png │ │ └── icon_green_arrow_up.png │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_normal_mode.xml │ │ ├── activity_recycler_view.xml │ │ ├── item_text_view.xml │ │ └── layout_textview_expand_animation.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── freecats │ └── demo │ └── view │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── preview.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TextViewExpandableAnimation# 2 | Expandable TextView With Smooth Transition Animation 3 | 4 | ## Preview ## 5 | ![preview](https://github.com/freecats/TextViewExpandableAnimation/blob/master/preview.gif) 6 | ## How to get it work? ## 7 | * Clone or download my project 8 | 9 | * Copy TextViewExpandableAnimation.java and some resource files such as attrs.xml into your own project 10 | 11 | * in xml layout file 12 | 13 | ```java 14 | 26 | ``` 27 | in java 28 | ```java 29 | TextViewExpandableAnimation tvExpand = (TextViewExpandableAnimation) findViewById(R.id.tv_expand); 30 | tvExpand.setText(text); 31 | ``` 32 | All Done! 33 | 34 | If you use it as an item in `RecyclerView`, you may have to make sure your `RecyclerView Adapter's onBindViewHolder` looks like this to avoid `Unexpected Shrink/Expand State`. 35 | ```java 36 | @Override 37 | public void onBindViewHolder(RecyclerViewAdapter.ViewHolder holder, final int position) { 38 | //record its state 39 | holder.tvExpand.setOnStateChangeListener(new TextViewExpandableAnimation.OnStateChangeListener() { 40 | @Override 41 | public void onStateChange(boolean isShrink) { 42 | ContentModel cm = mLists.get(position); 43 | cm.setShrink(isShrink); 44 | mLists.set(position, cm); 45 | } 46 | }); 47 | holder.tvExpand.setText(mLists.get(position).getText()); 48 | //important! reset its state as where it left 49 | holder.tvExpand.resetState(mLists.get(position).isShrink()); 50 | 51 | } 52 | 53 | ``` 54 |
Attributes Support as below 55 | 56 | | Attributes |format| 57 | | -------------|------------- | 58 | | tvea_expandLines | integer | 59 | | tvea_expandBitmap | reference | 60 | | tvea_shrinkBitmap |reference | 61 | | tvea_textContentColor | color| 62 | | tvea_textContentSize |dimension | 63 | | tvea_textExpand |string | 64 | | tvea_textShrink |string | 65 | | tvea_textStateColor |color | 66 | 67 | # License 68 | 69 | 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); 72 | you may not use this file except in compliance with the License. 73 | You may obtain a copy of the License at 74 | 75 | http://www.apache.org/licenses/LICENSE-2.0 76 | 77 | Unless required by applicable law or agreed to in writing, software 78 | distributed under the License is distributed on an "AS IS" BASIS, 79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | See the License for the specific language governing permissions and 81 | limitations under the License. 82 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 24 5 | buildToolsVersion "24.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.freecats.demo.textviewexpandable" 9 | minSdkVersion 8 10 | targetSdkVersion 24 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:24.0.0-beta1' 26 | compile 'com.android.support:recyclerview-v7:24.0.0' 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\program files\as1.5\as sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/freecats/demo/view/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.view; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/freecats/demo/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.freecats.demo.view.R; 9 | 10 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 11 | 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | findViewById(R.id.btn_normal_mode).setOnClickListener(this); 18 | findViewById(R.id.btn_recycler_view_mode).setOnClickListener(this); 19 | 20 | } 21 | 22 | @Override 23 | public void onClick(View view) { 24 | if (view.getId() == R.id.btn_normal_mode) { 25 | Intent intent = new Intent(this, NormalModeActivity.class); 26 | startActivity(intent); 27 | } else if (view.getId() == R.id.btn_recycler_view_mode) { 28 | Intent intent = new Intent(this, RecyclerViewModeActivity.class); 29 | startActivity(intent); 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/freecats/demo/activity/NormalModeActivity.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.freecats.demo.view.R; 7 | import com.freecats.demo.view.TextViewExpandableAnimation; 8 | 9 | /** 10 | *
Created by wbj on 2016/12/30. 11 | */ 12 | 13 | public class NormalModeActivity extends AppCompatActivity { 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_normal_mode); 18 | String text = getString(R.string.tips); 19 | 20 | TextViewExpandableAnimation tvExpand = (TextViewExpandableAnimation) findViewById(R.id.tv_expand); 21 | tvExpand.setText(text + text + text + text); 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/freecats/demo/activity/RecyclerViewModeActivity.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.LinearLayoutManager; 6 | import android.support.v7.widget.RecyclerView; 7 | 8 | import com.freecats.demo.adapter.RecyclerViewAdapter; 9 | import com.freecats.demo.model.ContentModel; 10 | import com.freecats.demo.view.R; 11 | 12 | import java.util.ArrayList; 13 | 14 | public class RecyclerViewModeActivity extends AppCompatActivity { 15 | 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_recycler_view); 21 | 22 | String text = getString(R.string.tips); 23 | String textShort = getString(R.string.tips_short); 24 | ArrayList lists = new ArrayList<>(); 25 | for (int i = 0; i < 20; i++) { 26 | ContentModel cm = new ContentModel(); 27 | cm.setText(i + " " + text); 28 | if (i == 10) cm.setText(i + " " + textShort); 29 | cm.setShrink(true); 30 | lists.add(cm); 31 | } 32 | 33 | RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 34 | RecyclerViewAdapter adapter = new RecyclerViewAdapter(lists); 35 | recyclerView.setAdapter(adapter); 36 | recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/freecats/demo/adapter/RecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.freecats.demo.model.ContentModel; 9 | import com.freecats.demo.view.R; 10 | import com.freecats.demo.view.TextViewExpandableAnimation; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | *
Created by wbj on 2016/12/30. 16 | */ 17 | 18 | public class RecyclerViewAdapter extends RecyclerView.Adapter { 19 | private List mLists; 20 | 21 | public RecyclerViewAdapter(List lists) { 22 | this.mLists = lists; 23 | } 24 | 25 | @Override 26 | public RecyclerViewAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 27 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_text_view, null)); 28 | } 29 | 30 | @Override 31 | public void onBindViewHolder(RecyclerViewAdapter.ViewHolder holder, final int position) { 32 | //record its state 33 | holder.tvExpand.setOnStateChangeListener(new TextViewExpandableAnimation.OnStateChangeListener() { 34 | @Override 35 | public void onStateChange(boolean isShrink) { 36 | ContentModel cm = mLists.get(position); 37 | cm.setShrink(isShrink); 38 | mLists.set(position, cm); 39 | } 40 | }); 41 | holder.tvExpand.setText(mLists.get(position).getText()); 42 | //important! reset its state as where it left 43 | holder.tvExpand.resetState(mLists.get(position).isShrink()); 44 | 45 | } 46 | 47 | @Override 48 | public int getItemCount() { 49 | return mLists.size(); 50 | } 51 | 52 | class ViewHolder extends RecyclerView.ViewHolder { 53 | TextViewExpandableAnimation tvExpand; 54 | 55 | public ViewHolder(View itemView) { 56 | super(itemView); 57 | tvExpand = (TextViewExpandableAnimation) itemView.findViewById(R.id.tv_expand); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/freecats/demo/model/ContentModel.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.model; 2 | 3 | /** 4 | *
Created by wbj on 2016/12/30. 5 | */ 6 | 7 | public class ContentModel { 8 | private String text; 9 | private boolean isShrink; 10 | 11 | public String getText() { 12 | return text; 13 | } 14 | 15 | public void setText(String text) { 16 | this.text = text; 17 | } 18 | 19 | public boolean isShrink() { 20 | return isShrink; 21 | } 22 | 23 | public void setShrink(boolean shrink) { 24 | isShrink = shrink; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/freecats/demo/view/TextViewExpandableAnimation.java: -------------------------------------------------------------------------------- 1 | package com.freecats.demo.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.drawable.Drawable; 7 | import android.os.Handler; 8 | import android.os.Message; 9 | import android.support.v4.content.ContextCompat; 10 | import android.text.TextUtils; 11 | import android.util.AttributeSet; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.View.OnClickListener; 15 | import android.view.ViewTreeObserver; 16 | import android.view.ViewTreeObserver.OnPreDrawListener; 17 | import android.widget.ImageView; 18 | import android.widget.LinearLayout; 19 | import android.widget.RelativeLayout; 20 | import android.widget.TextView; 21 | 22 | 23 | /** 24 | * 按行数进行折叠带过渡动画的TextView 25 | *
custom TextView that can be expanded with a smooth transition animation 26 | */ 27 | public class TextViewExpandableAnimation extends LinearLayout 28 | implements 29 | OnClickListener { 30 | 31 | /** 32 | * TextView 33 | */ 34 | private TextView textView; 35 | 36 | /** 37 | * 收起/全部TextView 38 | *
shrink/expand TextView 39 | */ 40 | private TextView tvState; 41 | 42 | /** 43 | * 点击进行折叠/展开的图片 44 | *
shrink/expand icon 45 | */ 46 | private ImageView ivExpandOrShrink; 47 | 48 | /** 49 | * 底部是否折叠/收起的父类布局 50 | *
shrink/expand layout parent 51 | */ 52 | private RelativeLayout rlToggleLayout; 53 | 54 | /** 55 | * 提示折叠的图片资源 56 | *
shrink drawable 57 | */ 58 | private Drawable drawableShrink; 59 | /** 60 | * 提示显示全部的图片资源 61 | *
expand drawable 62 | */ 63 | private Drawable drawableExpand; 64 | 65 | /** 66 | * 全部/收起文本的字体颜色 67 | *
color of shrink/expand text 68 | */ 69 | private int textViewStateColor; 70 | /** 71 | * 展开提示文本 72 | *
expand text 73 | */ 74 | private String textExpand; 75 | /** 76 | * 收缩提示文本 77 | *
shrink text 78 | */ 79 | private String textShrink; 80 | 81 | /** 82 | * 是否折叠显示的标示 83 | *
flag of shrink/expand 84 | */ 85 | private boolean isShrink = false; 86 | 87 | /** 88 | * 是否需要折叠的标示 89 | *
flag of expand needed 90 | */ 91 | private boolean isExpandNeeded = false; 92 | 93 | /** 94 | * 是否初始化TextView 95 | *
flag of TextView Initialization 96 | */ 97 | private boolean isInitTextView = true; 98 | 99 | /** 100 | * 折叠显示的行数 101 | *
number of lines to expand 102 | */ 103 | private int expandLines; 104 | 105 | /** 106 | * 文本的行数 107 | *
Original number of lines 108 | */ 109 | private int textLines; 110 | 111 | /** 112 | * 显示的文本 113 | *
content text 114 | */ 115 | private CharSequence textContent; 116 | 117 | /** 118 | * 显示的文本颜色 119 | *
content color 120 | */ 121 | private int textContentColor; 122 | 123 | /** 124 | * 显示的文本字体大小 125 | *
content text size 126 | */ 127 | private float textContentSize; 128 | 129 | /** 130 | * 动画线程 131 | *
thread 132 | */ 133 | private Thread thread; 134 | 135 | /** 136 | * 动画过度间隔 137 | *
animation interval 138 | */ 139 | private int sleepTime = 22; 140 | 141 | public OnStateChangeListener onStateChangeListener; 142 | 143 | /** 144 | * handler信号 145 | *
handler signal 146 | */ 147 | private final int WHAT = 2; 148 | /** 149 | * 动画结束信号 150 | *
animation end signal of handler 151 | */ 152 | private final int WHAT_ANIMATION_END = 3; 153 | 154 | /** 155 | * 动画结束,只是改变图标,并不隐藏 156 | *
animation end and expand only,but not disappear 157 | */ 158 | private final int WHAT_EXPAND_ONLY = 4; 159 | 160 | public TextViewExpandableAnimation(Context context, AttributeSet attrs) { 161 | super(context, attrs); 162 | initValue(context, attrs); 163 | initView(context); 164 | initClick(); 165 | } 166 | 167 | private void initValue(Context context, AttributeSet attrs) { 168 | TypedArray ta = context.obtainStyledAttributes(attrs, 169 | R.styleable.TextViewExpandableAnimation); 170 | 171 | expandLines = ta.getInteger( 172 | R.styleable.TextViewExpandableAnimation_tvea_expandLines, 5); 173 | 174 | drawableShrink = ta 175 | .getDrawable(R.styleable.TextViewExpandableAnimation_tvea_shrinkBitmap); 176 | drawableExpand = ta 177 | .getDrawable(R.styleable.TextViewExpandableAnimation_tvea_expandBitmap); 178 | 179 | textViewStateColor = ta.getColor(R.styleable.TextViewExpandableAnimation_tvea_textStateColor, ContextCompat.getColor(context, R.color.colorPrimary)); 180 | 181 | textShrink = ta.getString(R.styleable.TextViewExpandableAnimation_tvea_textShrink); 182 | textExpand = ta.getString(R.styleable.TextViewExpandableAnimation_tvea_textExpand); 183 | 184 | if (null == drawableShrink) { 185 | drawableShrink = ContextCompat.getDrawable(context, R.drawable.icon_green_arrow_up); 186 | } 187 | 188 | if (null == drawableExpand) { 189 | drawableExpand = ContextCompat.getDrawable(context, R.drawable.icon_green_arrow_down); 190 | } 191 | 192 | if (TextUtils.isEmpty(textShrink)) { 193 | textShrink = context.getString(R.string.shrink); 194 | } 195 | 196 | if (TextUtils.isEmpty(textExpand)) { 197 | textExpand = context.getString(R.string.expand); 198 | } 199 | 200 | 201 | textContentColor = ta.getColor(R.styleable.TextViewExpandableAnimation_tvea_textContentColor, ContextCompat.getColor(context, R.color.color_gray_light_content_text)); 202 | textContentSize = ta.getDimension(R.styleable.TextViewExpandableAnimation_tvea_textContentSize, 14); 203 | 204 | ta.recycle(); 205 | } 206 | 207 | private void initView(Context context) { 208 | 209 | LayoutInflater inflater = (LayoutInflater) context 210 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 211 | inflater.inflate(R.layout.layout_textview_expand_animation, this); 212 | 213 | rlToggleLayout = (RelativeLayout) findViewById(R.id.rl_expand_text_view_animation_toggle_layout); 214 | 215 | textView = (TextView) findViewById(R.id.tv_expand_text_view_animation); 216 | textView.setTextColor(textContentColor); 217 | textView.getPaint().setTextSize(textContentSize); 218 | 219 | ivExpandOrShrink = (ImageView) findViewById(R.id.iv_expand_text_view_animation_toggle); 220 | 221 | tvState = (TextView) findViewById(R.id.tv_expand_text_view_animation_hint); 222 | tvState.setTextColor(textViewStateColor); 223 | 224 | } 225 | 226 | private void initClick() { 227 | textView.setOnClickListener(this); 228 | rlToggleLayout.setOnClickListener(this); 229 | } 230 | 231 | public void setText(CharSequence charSequence) { 232 | 233 | textContent = charSequence; 234 | 235 | textView.setText(charSequence.toString()); 236 | 237 | ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver(); 238 | viewTreeObserver.addOnPreDrawListener(new OnPreDrawListener() { 239 | 240 | @Override 241 | public boolean onPreDraw() { 242 | if (!isInitTextView) { 243 | return true; 244 | } 245 | textLines = textView.getLineCount(); 246 | isExpandNeeded = textLines > expandLines; 247 | isInitTextView = false; 248 | if (isExpandNeeded) { 249 | isShrink = true; 250 | doAnimation(expandLines, expandLines, WHAT_ANIMATION_END); 251 | } else { 252 | isShrink = false; 253 | doNotExpand(); 254 | } 255 | return true; 256 | } 257 | }); 258 | 259 | if (!isInitTextView) { 260 | textLines = textView.getLineCount(); 261 | } 262 | 263 | } 264 | 265 | @SuppressLint("HandlerLeak") 266 | private Handler handler = new Handler() { 267 | 268 | @Override 269 | public void handleMessage(Message msg) { 270 | if (WHAT == msg.what) { 271 | textView.setMaxLines(msg.arg1); 272 | textView.invalidate(); 273 | } else if (WHAT_ANIMATION_END == msg.what) { 274 | setExpandState(msg.arg1); 275 | } else if (WHAT_EXPAND_ONLY == msg.what) { 276 | changeExpandState(msg.arg1); 277 | } 278 | super.handleMessage(msg); 279 | } 280 | 281 | }; 282 | 283 | /** 284 | * @param startIndex 开始动画的起点行数
start index of animation 285 | * @param endIndex 结束动画的终点行数
end index of animation 286 | * @param what 动画结束后的handler信号标示
signal of animation end 287 | */ 288 | private void doAnimation(final int startIndex, final int endIndex, 289 | final int what) { 290 | 291 | thread = new Thread(new Runnable() { 292 | 293 | @Override 294 | public void run() { 295 | 296 | if (startIndex < endIndex) { 297 | // 如果起止行数小于结束行数,那么往下展开至结束行数 298 | // if start index smaller than end index ,do expand action 299 | int count = startIndex; 300 | while (count++ < endIndex) { 301 | Message msg = handler.obtainMessage(WHAT, count, 0); 302 | 303 | try { 304 | Thread.sleep(sleepTime); 305 | } catch (InterruptedException e) { 306 | e.printStackTrace(); 307 | } 308 | 309 | handler.sendMessage(msg); 310 | } 311 | } else if (startIndex > endIndex) { 312 | // 如果起止行数大于结束行数,那么往上折叠至结束行数 313 | // if start index bigger than end index ,do shrink action 314 | int count = startIndex; 315 | while (count-- > endIndex) { 316 | Message msg = handler.obtainMessage(WHAT, count, 0); 317 | try { 318 | Thread.sleep(sleepTime); 319 | } catch (InterruptedException e) { 320 | e.printStackTrace(); 321 | } 322 | 323 | handler.sendMessage(msg); 324 | } 325 | } 326 | 327 | // 动画结束后发送结束的信号 328 | // animation end,send signal 329 | Message msg = handler.obtainMessage(what, endIndex, 0); 330 | handler.sendMessage(msg); 331 | 332 | } 333 | 334 | }); 335 | 336 | thread.start(); 337 | 338 | } 339 | 340 | /** 341 | * 改变折叠状态(仅仅改变折叠与展开状态,不会隐藏折叠/展开图片布局) 342 | * change shrink/expand state(only change state,but not hide shrink/expand icon) 343 | * 344 | * @param endIndex 345 | */ 346 | @SuppressWarnings("deprecation") 347 | private void changeExpandState(int endIndex) { 348 | rlToggleLayout.setVisibility(View.VISIBLE); 349 | if (endIndex < textLines) { 350 | ivExpandOrShrink.setBackgroundDrawable(drawableExpand); 351 | tvState.setText(textExpand); 352 | } else { 353 | ivExpandOrShrink.setBackgroundDrawable(drawableShrink); 354 | tvState.setText(textShrink); 355 | } 356 | 357 | } 358 | 359 | /** 360 | * 设置折叠状态(如果折叠行数设定大于文本行数,那么折叠/展开图片布局将会隐藏,文本将一直处于展开状态) 361 | * change shrink/expand state(if number of expand lines bigger than original text lines,hide shrink/expand icon,and TextView will always be at expand state) 362 | * 363 | * @param endIndex 364 | */ 365 | @SuppressWarnings("deprecation") 366 | private void setExpandState(int endIndex) { 367 | 368 | if (endIndex < textLines) { 369 | isShrink = true; 370 | rlToggleLayout.setVisibility(View.VISIBLE); 371 | ivExpandOrShrink.setBackgroundDrawable(drawableExpand); 372 | textView.setOnClickListener(this); 373 | tvState.setText(textExpand); 374 | } else { 375 | isShrink = false; 376 | rlToggleLayout.setVisibility(View.GONE); 377 | ivExpandOrShrink.setBackgroundDrawable(drawableShrink); 378 | textView.setOnClickListener(null); 379 | tvState.setText(textShrink); 380 | } 381 | 382 | } 383 | 384 | /** 385 | * 无需折叠 386 | * do not expand 387 | */ 388 | private void doNotExpand() { 389 | textView.setMaxLines(expandLines); 390 | rlToggleLayout.setVisibility(View.GONE); 391 | textView.setOnClickListener(null); 392 | } 393 | 394 | @Override 395 | public void onClick(View v) { 396 | if (v.getId() == R.id.rl_expand_text_view_animation_toggle_layout || v.getId() == R.id.tv_expand_text_view_animation) { 397 | clickImageToggle(); 398 | if (null != onStateChangeListener) onStateChangeListener.onStateChange(isShrink); 399 | } 400 | 401 | } 402 | 403 | private void clickImageToggle() { 404 | if (isShrink) { 405 | // 如果是已经折叠,那么进行非折叠处理 406 | // do shrink action 407 | doAnimation(expandLines, textLines, WHAT_EXPAND_ONLY); 408 | } else { 409 | // 如果是非折叠,那么进行折叠处理 410 | // do expand action 411 | doAnimation(textLines, expandLines, WHAT_EXPAND_ONLY); 412 | } 413 | 414 | // 切换状态 415 | // set flag 416 | isShrink = !isShrink; 417 | } 418 | 419 | public void resetState(boolean isShrink) { 420 | 421 | this.isShrink = isShrink; 422 | if (textLines > expandLines) { 423 | if (isShrink) { 424 | rlToggleLayout.setVisibility(View.VISIBLE); 425 | ivExpandOrShrink.setBackgroundDrawable(drawableExpand); 426 | textView.setOnClickListener(this); 427 | textView.setMaxLines(expandLines); 428 | tvState.setText(textExpand); 429 | } else { 430 | rlToggleLayout.setVisibility(View.VISIBLE); 431 | ivExpandOrShrink.setBackgroundDrawable(drawableShrink); 432 | textView.setOnClickListener(this); 433 | textView.setMaxLines(textLines); 434 | tvState.setText(textShrink); 435 | } 436 | } else { 437 | doNotExpand(); 438 | } 439 | } 440 | 441 | public Drawable getDrawableShrink() { 442 | return drawableShrink; 443 | } 444 | 445 | public void setDrawableShrink(Drawable drawableShrink) { 446 | this.drawableShrink = drawableShrink; 447 | } 448 | 449 | public Drawable getDrawableExpand() { 450 | return drawableExpand; 451 | } 452 | 453 | public void setDrawableExpand(Drawable drawableExpand) { 454 | this.drawableExpand = drawableExpand; 455 | } 456 | 457 | public int getExpandLines() { 458 | return expandLines; 459 | } 460 | 461 | public void setExpandLines(int newExpandLines) { 462 | int start = isShrink ? this.expandLines : textLines; 463 | int end = textLines < newExpandLines ? textLines : newExpandLines; 464 | doAnimation(start, end, WHAT_ANIMATION_END); 465 | this.expandLines = newExpandLines; 466 | } 467 | 468 | /** 469 | * 取得显示的文本内容 470 | * get content text 471 | * 472 | * @return content text 473 | */ 474 | public CharSequence getTextContent() { 475 | return textContent; 476 | } 477 | 478 | public int getSleepTime() { 479 | return sleepTime; 480 | } 481 | 482 | public void setSleepTime(int sleepTime) { 483 | this.sleepTime = sleepTime; 484 | } 485 | 486 | public interface OnStateChangeListener { 487 | void onStateChange(boolean isShrink); 488 | } 489 | 490 | public void setOnStateChangeListener(OnStateChangeListener onStateChangeListener) { 491 | this.onStateChangeListener = onStateChangeListener; 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_green_arrow_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freecats/TextViewExpandableAnimation/7bd219432c781602ce7b663126a3df8b23048416/app/src/main/res/drawable-xhdpi/icon_green_arrow_down.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_green_arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freecats/TextViewExpandableAnimation/7bd219432c781602ce7b663126a3df8b23048416/app/src/main/res/drawable-xhdpi/icon_green_arrow_up.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |