├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── keystore.jks ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── glailton │ │ └── expandabletextview │ │ └── demo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── glailton │ │ │ └── expandabletextview │ │ │ └── demo │ │ │ ├── MainActivity.kt │ │ │ └── ui │ │ │ ├── ExamplesFragment.kt │ │ │ ├── ExpandableCardAdapter.kt │ │ │ ├── ListViewFragment.kt │ │ │ └── RecyclerViewFragment.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_examples.xml │ │ ├── fragment_list_view.xml │ │ ├── fragment_recycler_view.xml │ │ ├── item.xml │ │ ├── item_expandable_card.xml │ │ └── nav_header.xml │ │ ├── menu │ │ └── nav_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── navigation │ │ └── nav_graph.xml │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── io │ └── github │ └── glailton │ └── expandabletextview │ └── demo │ └── ExampleUnitTest.kt ├── build.gradle ├── expandable-textview ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── glailton │ │ └── expandabletextview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── glailton │ │ │ └── expandabletextview │ │ │ ├── Constants.kt │ │ │ └── ExpandableTextView.kt │ └── res │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── io │ └── github │ └── glailton │ └── expandabletextview │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── publish-mavencentral.gradle ├── resources └── gif.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/* 42 | .idea/*.xml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | .idea/codeStyles/ 55 | .idea/misc.xml 56 | .idea/compiler.xml 57 | 58 | # Keystore files 59 | # Uncomment the following lines if you do not want to check your keystore files in. 60 | #*.jks 61 | #*.keystore 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | .cxx/ 66 | 67 | # Google Services (e.g. APIs or Firebase) 68 | # google-services.json 69 | 70 | # Freeline 71 | freeline.py 72 | freeline/ 73 | freeline_project_description.json 74 | 75 | # fastlane 76 | fastlane/report.xml 77 | fastlane/Preview.html 78 | fastlane/screenshots 79 | fastlane/test_output 80 | fastlane/readme.md 81 | 82 | # Version control 83 | vcs.xml 84 | 85 | # lint 86 | lint/intermediates/ 87 | lint/generated/ 88 | lint/outputs/ 89 | lint/tmp/ 90 | # lint/reports/ 91 | 92 | *.gpg 93 | 94 | # txt 95 | *.txt 96 | 97 | # mp4 98 | *.mp4 99 | 100 | app/release 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Glailton Costa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpandableTextView 2 | 3 | [![Download](https://img.shields.io/badge/Download-1.0.7-lightgrey)](https://central.sonatype.com/artifact/io.github.glailton/expandabletextview) 4 | [![Preview the app](https://img.shields.io/badge/Preview-Appetize.io-orange.svg)](https://appetize.io/app/b_tebmxa2vwvqnbyxetlafkh725e) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 6 | 7 | An Expandable TextView for Android written in 8 | [Kotlin](https://kotlinlang.org/). The main ideia was study how create a library in Android and deploy 9 | on [Maven Central Repository](https://s01.oss.sonatype.org). 10 | 11 | 12 | ## Table of Contents 13 | 14 | - [Demo project](#demo-project) 15 | - [Getting started](#getting-started) 16 | - [Features](#features) 17 | - [Usage](#usage) 18 | - [XML](#in-xml) 19 | - [Programmatically](#programmatically) 20 | - [Contributing](#contributing) 21 | - [License](#license) 22 | - [Acknowledgments](#acknowledgments) 23 | 24 | ## Demo Project 25 | 26 | `ExpandableTextView` is a custom `TextView` that allows long text to be collapsed or expanded, either inline within the layout or via a popup dialog. 27 | Take a look at the [demo project](app). 28 | 29 | ![Demo](resources/gif.gif) 30 | 31 | ## Getting Started 32 | 33 | The library is included in Maven Central Repository, so just add this dependency to your module level `gradle.build`: 34 | 35 | ```kotlin 36 | dependencies { 37 | implementation 'io.github.glailton.expandabletextview:expandabletextview:$LatestVersion' 38 | } 39 | ``` 40 | Current latest version is: [![Download](https://img.shields.io/badge/Download-1.0.7-lightgrey)](https://search.maven.org/artifact/io.github.glailton.expandabletextview/expandabletextview) 41 | 42 | ## Features 43 | 44 | - Collapsible/expandable text with animation. 45 | - Two expansion modes: layout-based and popup. 46 | - Custom "read more" / "read less" labels. 47 | - Ellipsized text with custom color and underline. 48 | - Optional fade animation on expand/collapse. 49 | - Supports usage in XML, programmatically, or with data binding. 50 | 51 | --- 52 | 53 | ## XML Attributes 54 | 55 | | Attribute | Type | Description | 56 | |------------------------|---------|-----------------------------------------------------------------------------| 57 | | `collapsedLines` | Integer | Number of lines to show when collapsed. Default is 3. | 58 | | `isExpanded` | Boolean | Initial expansion state. Default is `false`. | 59 | | `animDuration` | Integer | Animation duration in milliseconds. Default is 300. | 60 | | `readMoreText` | String | Text shown to expand the view. Default is `"Read more"`. | 61 | | `readLessText` | String | Text shown to collapse the view. Default is `"Read less"`. | 62 | | `expandType` | Enum | Either `layout` or `popup`. Default is `layout`. | 63 | | `ellipsizeTextColor` | Color | Color of the ellipsis and expand label. Default is `Color.BLUE`. | 64 | | `isUnderlined` | Boolean | Whether the expand/collapse label should be underlined. Default is `false`. | 65 | | `fadeAnimationEnabled` | Boolean | Enable or disable fade animation. Default is `true`. | 66 | 67 | --- 68 | 69 | ## Usage 70 | 71 | ### In XML 72 | 73 | Xml snippet: 74 | ```xml 75 | 88 | ``` 89 | ### Programmatically 90 | ``` 91 | binding.expandTvProg 92 | .setAnimationDuration(500) 93 | .setFadeAnimationEnabled(true) 94 | .setReadMoreText("View More") 95 | .setReadLessText("View Less") 96 | .setCollapsedLines(3) 97 | .setIsExpanded(true) 98 | .setIsUnderlined(true) 99 | .setExpandType(EXPAND_TYPE_POPUP) 100 | .setEllipsizedTextColor(ContextCompat.getColor(this, R.color.purple_200)) 101 | 102 | binding.expandTvProg.text = 103 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + 104 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + 105 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + 106 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 107 | 108 | binding.expandTvProg.setOnClickListener { 109 | binding.expandTvProg.toggle() 110 | if (binding.expandTvVeryLong.isExpanded && binding.expandTvVeryLong.expandType == EXPAND_TYPE_LAYOUT) 111 | binding.expandTvVeryLong.toggle() 112 | } 113 | ``` 114 | 115 | ## Contributing 116 | 117 | If you wish to send a pull request, please make sure to checkout from `main` branch and merge with `main` branch as well. 118 | 119 | ## License 120 | 121 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 122 | 123 | ## Acknowledgments 124 | 125 | This library was based on: [viewmore-textview](https://github.com/mike5v/viewmore-textview). 126 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | android { 8 | compileSdk 35 9 | 10 | defaultConfig { 11 | applicationId "br.com.grsoft.expandabletextview.demo" 12 | minSdkVersion 23 13 | targetSdkVersion 35 14 | versionCode 1 15 | versionName "1.0.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_21 28 | targetCompatibility JavaVersion.VERSION_21 29 | } 30 | kotlinOptions { 31 | jvmTarget = '21' 32 | } 33 | buildFeatures { 34 | viewBinding true 35 | } 36 | namespace 'io.github.glailton.expandabletextview.demo' 37 | } 38 | 39 | dependencies { 40 | 41 | implementation 'androidx.core:core-ktx:1.3.2' 42 | implementation 'androidx.appcompat:appcompat:1.2.0' 43 | implementation 'com.google.android.material:material:1.3.0' 44 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 45 | implementation 'androidx.drawerlayout:drawerlayout:1.2.0' 46 | implementation 'androidx.navigation:navigation-fragment-ktx:2.8.9' 47 | implementation 'androidx.navigation:navigation-ui-ktx:2.8.9' 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0" 49 | 50 | implementation project(':expandable-textview') 51 | //implementation("io.github.glailton:expandabletextview:1.0.6") 52 | testImplementation 'junit:junit:4.+' 53 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 55 | } -------------------------------------------------------------------------------- /app/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/keystore.jks -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/io/github/glailton/expandabletextview/demo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("br.com.grsoft.expandabletextview.demo", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/glailton/expandabletextview/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.appcompat.widget.Toolbar 6 | import androidx.drawerlayout.widget.DrawerLayout 7 | import androidx.navigation.NavController 8 | import androidx.navigation.fragment.NavHostFragment 9 | import androidx.navigation.ui.AppBarConfiguration 10 | import androidx.navigation.ui.navigateUp 11 | import androidx.navigation.ui.setupActionBarWithNavController 12 | import androidx.navigation.ui.setupWithNavController 13 | import com.google.android.material.navigation.NavigationView 14 | 15 | class MainActivity : AppCompatActivity() { 16 | 17 | private lateinit var drawerLayout: DrawerLayout 18 | private lateinit var navController: NavController 19 | private lateinit var appBarConfiguration: AppBarConfiguration 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContentView(R.layout.activity_main) 24 | 25 | drawerLayout = findViewById(R.id.drawer_layout) 26 | val navView = findViewById(R.id.navigationView) 27 | val toolbar = findViewById(R.id.toolbar) 28 | setSupportActionBar(toolbar) 29 | 30 | val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment 31 | navController = navHostFragment.navController 32 | appBarConfiguration = AppBarConfiguration( 33 | setOf(R.id.examplesFragment, R.id.listViewFragment), drawerLayout 34 | ) 35 | 36 | setupActionBarWithNavController(navController, appBarConfiguration) 37 | navView.setupWithNavController(navController) 38 | } 39 | 40 | override fun onSupportNavigateUp(): Boolean { 41 | return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/glailton/expandabletextview/demo/ui/ExamplesFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo.ui 2 | 3 | import android.os.Bundle 4 | import android.text.SpannableString 5 | import androidx.fragment.app.Fragment 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.core.content.ContextCompat 11 | import io.github.glailton.expandabletextview.EXPAND_TYPE_LAYOUT 12 | import io.github.glailton.expandabletextview.EXPAND_TYPE_POPUP 13 | import io.github.glailton.expandabletextview.demo.R 14 | import io.github.glailton.expandabletextview.demo.databinding.FragmentExamplesBinding 15 | 16 | class ExamplesFragment : Fragment() { 17 | 18 | private var _binding: FragmentExamplesBinding? = null 19 | private val binding get() = _binding!! 20 | 21 | override fun onCreateView( 22 | inflater: LayoutInflater, container: ViewGroup?, 23 | savedInstanceState: Bundle?, 24 | ): View { 25 | _binding = FragmentExamplesBinding.inflate(inflater, container, false) 26 | return _binding!!.root 27 | } 28 | 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | 32 | binding.expandTvProg 33 | .setAnimationDuration(500) 34 | .setReadMoreText("View More") 35 | .setReadLessText("View Less") 36 | .setCollapsedLines(3) 37 | .setIsExpanded(true) 38 | .setIsUnderlined(true) 39 | .setExpandType(EXPAND_TYPE_POPUP) 40 | .setEllipsizedTextColor(ContextCompat.getColor(requireActivity(), R.color.purple_200)) 41 | 42 | binding.expandTvProg.text = 43 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + 44 | "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + 45 | "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur." + 46 | "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 47 | 48 | binding.expandTvProg.setOnClickListener { 49 | binding.expandTvProg.toggle() 50 | if (binding.expandTvVeryLong.isExpanded && binding.expandTvVeryLong.expandType == EXPAND_TYPE_LAYOUT) 51 | binding.expandTvVeryLong.toggle() 52 | } 53 | 54 | binding.expandTvSpannable 55 | .setAnimationDuration(500) 56 | .setFadeAnimationEnabled(true) 57 | .setReadMoreText("View More") 58 | .setReadLessText("View Less") 59 | .setCollapsedLines(3) 60 | .setIsExpanded(false) 61 | .setIsUnderlined(true) 62 | .setExpandType(EXPAND_TYPE_LAYOUT) 63 | .setEllipsizedTextColor(ContextCompat.getColor(requireActivity(), R.color.purple_200)) 64 | 65 | binding.expandTvSpannable.setText(SpannableString("quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."), TextView.BufferType.SPANNABLE) 66 | 67 | } 68 | 69 | override fun onDestroyView() { 70 | super.onDestroyView() 71 | _binding = null 72 | } 73 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/glailton/expandabletextview/demo/ui/ExpandableCardAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo.ui 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import io.github.glailton.expandabletextview.ExpandableTextView 8 | import io.github.glailton.expandabletextview.demo.R 9 | 10 | class ExpandableCardAdapter(private val items: List) : 11 | RecyclerView.Adapter() { 12 | 13 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExpandableCardViewHolder { 14 | val view = LayoutInflater.from(parent.context) 15 | .inflate(R.layout.item_expandable_card, parent, false) 16 | return ExpandableCardViewHolder(view) 17 | } 18 | 19 | override fun onBindViewHolder(holder: ExpandableCardViewHolder, position: Int) { 20 | holder.bind(items[position]) 21 | } 22 | 23 | override fun getItemCount() = items.size 24 | 25 | class ExpandableCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 26 | private val expandableTextView = 27 | itemView.findViewById(R.id.expandableTextView) 28 | 29 | fun bind(text: String) { 30 | expandableTextView.text = text 31 | expandableTextView 32 | .setReadMoreText(itemView.context.getString(R.string.view_more)) 33 | .setReadLessText(itemView.context.getString(R.string.view_less)) 34 | .setCollapsedLines(3) 35 | .setIsExpanded(false) 36 | .setIsUnderlined(true) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/glailton/expandabletextview/demo/ui/ListViewFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo.ui 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.ArrayAdapter 9 | import android.widget.ListView 10 | import androidx.core.content.ContextCompat 11 | import io.github.glailton.expandabletextview.EXPAND_TYPE_DEFAULT 12 | import io.github.glailton.expandabletextview.ExpandableTextView 13 | import io.github.glailton.expandabletextview.demo.R 14 | import io.github.glailton.expandabletextview.demo.databinding.FragmentListViewBinding 15 | 16 | class ListViewFragment : Fragment() { 17 | 18 | private var _binding: FragmentListViewBinding? = null 19 | val binding get() = _binding 20 | 21 | private val dummyTexts = listOf( 22 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 23 | "Short text.", 24 | "Another very very very very very very very very very very long text that will probably need to be collapsed when displayed! dshgsdfgdfgkjdgçsdjfglpgçsodifghçdlkjb´sdopigj´wpothjw´srpofbjsd~çlbm ´sjb ´pofbj ´dspofb s´pfobgj[sapfogj´dsaopfigjádiogh´-qeir9jgádpofbgj´~dsofpbjdóibj´sdoifbjdosfibjsdfbn´sfioghj´sdfophjsdfplhgsj~sdfpjge" 25 | ) 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, container: ViewGroup?, 29 | savedInstanceState: Bundle?, 30 | ): View { 31 | // Inflate the layout for this fragment 32 | _binding = FragmentListViewBinding.inflate(inflater, container, false) 33 | return _binding!!.root 34 | } 35 | 36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 37 | super.onViewCreated(view, savedInstanceState) 38 | 39 | val listView = view.findViewById(R.id.listView) 40 | listView.adapter = object : ArrayAdapter(requireActivity(), R.layout.item, R.id.listview_review_record_content, dummyTexts) { 41 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 42 | val rowView = super.getView(position, convertView, parent) 43 | 44 | val reviewContent = rowView.findViewById(R.id.listview_review_record_content) 45 | reviewContent.text = getItem(position) 46 | 47 | reviewContent 48 | .setReadMoreText(getString(R.string.view_more)) 49 | .setReadLessText(getString(R.string.view_less)) 50 | .setCollapsedLines(3) 51 | .setIsExpanded(false) 52 | .setIsUnderlined(true) 53 | .setExpandType(EXPAND_TYPE_DEFAULT) 54 | .setEllipsizedTextColor(ContextCompat.getColor(parent.context, R.color.purple_700)) 55 | 56 | return rowView 57 | } 58 | } 59 | } 60 | 61 | override fun onDestroyView() { 62 | super.onDestroyView() 63 | _binding = null 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/io/github/glailton/expandabletextview/demo/ui/RecyclerViewFragment.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo.ui 2 | 3 | import android.os.Bundle 4 | import androidx.fragment.app.Fragment 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView 10 | import io.github.glailton.expandabletextview.demo.R 11 | 12 | class RecyclerViewFragment : Fragment() { 13 | private val dummyTexts = listOf( 14 | "Texto curto.", 15 | "Este é um texto um pouco maior que deve ser truncado ao exibir no card.", 16 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin a justo in sapien laoreet accumsan. Sed fringilla risus non massa ultrices. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin a justo in sapien laoreet accumsan. Sed fringilla risus non massa ultrices" 17 | ) 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, container: ViewGroup?, 21 | savedInstanceState: Bundle?, 22 | ): View? { 23 | // Inflate the layout for this fragment 24 | return inflater.inflate(R.layout.fragment_recycler_view, container, false) 25 | } 26 | 27 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 28 | super.onViewCreated(view, savedInstanceState) 29 | 30 | val recyclerView = view.findViewById(R.id.recyclerView) 31 | recyclerView.layoutManager = LinearLayoutManager(requireContext()) 32 | recyclerView.adapter = ExpandableCardAdapter(dummyTexts) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 25 | 26 | 34 | 35 | 36 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_examples.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 34 | 35 | 53 | 54 | 71 | 72 | 86 | 87 | 99 | 100 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_list_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_recycler_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_expandable_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/nav_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/menu/nav_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 20 | 21 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpandableTextView Demo 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 4 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ornare lacinia magna, sed eleifend dui aliquet sed." 5 | Lorem Ipsum dolor sit amet 6 | 7 | Eu inicio com esse texto que parece com o que eu quero setar mas nao eh Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac orci quis tortor imperdiet venenatis. 8 | Duis elementum auctor accumsan. Aliquam in felis sit amet augue. 9 | 10 | View More 11 | View Less 12 | 13 | Hello blank fragment 14 | ExpandableTextView 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/io/github/glailton/expandabletextview/demo/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview.demo 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:8.10.0' 9 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0' 10 | 11 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.4.10.2' 12 | } 13 | } 14 | 15 | task clean(type: Delete) { 16 | delete rootProject.buildDir 17 | } -------------------------------------------------------------------------------- /expandable-textview/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /expandable-textview/build.gradle: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | id 'com.android.library' 5 | id 'org.jetbrains.kotlin.android' 6 | id 'kotlin-kapt' 7 | id 'com.vanniktech.maven.publish' version '0.30.0' 8 | } 9 | 10 | android { 11 | compileSdk 35 12 | 13 | defaultConfig { 14 | minSdkVersion 23 15 | targetSdkVersion 35 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_11 28 | targetCompatibility JavaVersion.VERSION_11 29 | } 30 | 31 | kotlinOptions { 32 | jvmTarget = '11' 33 | } 34 | 35 | buildFeatures { 36 | dataBinding true 37 | } 38 | 39 | namespace 'io.github.glailton.expandabletextview' 40 | } 41 | 42 | dependencies { 43 | implementation 'androidx.core:core-ktx:1.3.2' 44 | implementation 'androidx.appcompat:appcompat:1.2.0' 45 | implementation 'com.google.android.material:material:1.3.0' 46 | testImplementation 'junit:junit:4.+' 47 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 49 | } 50 | 51 | // Configuração do Maven Publish 52 | mavenPublishing { 53 | // Publicação para Maven Central via OSSRH moderno 54 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, true) // usa https://central.sonatype.com 55 | 56 | signAllPublications() 57 | 58 | coordinates("io.github.glailton", "expandabletextview", "1.0.7") 59 | 60 | pom { 61 | name = "ExpandableTextView" 62 | description = "An expandable Android TextView written in Kotlin." 63 | inceptionYear = "2024" 64 | url = "https://github.com/glailton/ExpandableTextView" 65 | 66 | licenses { 67 | license { 68 | name = "MIT License" 69 | url = "https://opensource.org/licenses/MIT" 70 | } 71 | } 72 | 73 | developers { 74 | developer { 75 | id = "glailton" 76 | name = "Glailton Costa" 77 | email = "glailton.rc@gmail.com" 78 | } 79 | } 80 | 81 | scm { 82 | connection = "scm:git:https://github.com/glailton/ExpandableTextView.git" 83 | developerConnection = "scm:git:https://github.com/glailton/ExpandableTextView.git" 84 | url = "https://github.com/glailton/ExpandableTextView" 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /expandable-textview/gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/expandable-textview/gradle.properties -------------------------------------------------------------------------------- /expandable-textview/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /expandable-textview/src/androidTest/java/io/github/glailton/expandabletextview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("br.com.grsoft.expandabletextview", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /expandable-textview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /expandable-textview/src/main/java/io/github/glailton/expandabletextview/Constants.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview 2 | 3 | class Constants { 4 | companion object { 5 | const val COLLAPSED_MAX_LINES = Int.MAX_VALUE 6 | const val DEFAULT_ANIM_DURATION = 450 7 | const val READ_MORE = "Read more" 8 | const val READ_LESS = "Read less" 9 | const val DEFAULT_ELLIPSIZED_TEXT = "… " 10 | const val EMPTY_SPACE = " " 11 | } 12 | } -------------------------------------------------------------------------------- /expandable-textview/src/main/java/io/github/glailton/expandabletextview/ExpandableTextView.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview 2 | 3 | import android.animation.Animator 4 | import android.animation.ValueAnimator 5 | import android.app.AlertDialog 6 | import android.content.Context 7 | import android.graphics.Color 8 | import android.graphics.Rect 9 | import android.graphics.drawable.GradientDrawable 10 | import android.graphics.drawable.GradientDrawable.Orientation.BOTTOM_TOP 11 | import android.text.Spannable 12 | import android.text.SpannableString 13 | import android.text.SpannableStringBuilder 14 | import android.text.style.ForegroundColorSpan 15 | import android.text.style.UnderlineSpan 16 | import android.util.AttributeSet 17 | import android.view.View 18 | import android.view.ViewGroup 19 | import android.view.animation.DecelerateInterpolator 20 | import androidx.appcompat.widget.AppCompatTextView 21 | import androidx.databinding.BindingAdapter 22 | import io.github.glailton.expandabletextview.Constants.Companion.COLLAPSED_MAX_LINES 23 | import io.github.glailton.expandabletextview.Constants.Companion.DEFAULT_ANIM_DURATION 24 | import io.github.glailton.expandabletextview.Constants.Companion.DEFAULT_ELLIPSIZED_TEXT 25 | import io.github.glailton.expandabletextview.Constants.Companion.EMPTY_SPACE 26 | import io.github.glailton.expandabletextview.Constants.Companion.READ_LESS 27 | import io.github.glailton.expandabletextview.Constants.Companion.READ_MORE 28 | import java.lang.Integer.min 29 | import kotlin.math.max 30 | import kotlin.math.roundToInt 31 | 32 | /** 33 | * Expand the text within layout 34 | */ 35 | const val EXPAND_TYPE_LAYOUT = 0 36 | 37 | /** 38 | * Expand the text as a popup 39 | */ 40 | const val EXPAND_TYPE_POPUP = 1 41 | 42 | /** 43 | * Default expand type which will layout 44 | */ 45 | const val EXPAND_TYPE_DEFAULT = EXPAND_TYPE_LAYOUT 46 | 47 | /** 48 | * This value define how much space should be consumed by ellipsize text to represent itself. 49 | */ 50 | const val ELLIPSIZE_TEXT_LENGTH_MULTIPLIER = 2.0 51 | 52 | class ExpandableTextView @JvmOverloads constructor( 53 | context: Context, 54 | attrs: AttributeSet, 55 | defStyleAttr: Int = R.attr.expandableTextView) : AppCompatTextView(context, attrs, defStyleAttr), 56 | View.OnClickListener { 57 | 58 | private var mOriginalText: CharSequence? = "" 59 | private var mCollapsedLines = 0 60 | private var mReadMoreText: CharSequence = READ_MORE 61 | private var mReadLessText: CharSequence = READ_LESS 62 | var isExpanded: Boolean = false 63 | private set 64 | private var mAnimationDuration: Int? = 0 65 | private var foregroundColor: Int? = 0 66 | private var initialText = "" 67 | private var isUnderlined: Boolean? = false 68 | private var mEllipsizeTextColor: Int? = 0 69 | var expandType = EXPAND_TYPE_DEFAULT 70 | private set 71 | private var fadeAnimationEnabled = true 72 | private lateinit var collapsedVisibleText: String 73 | 74 | override fun onClick(v: View?) { 75 | toggleExpandableTextView() 76 | } 77 | 78 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 79 | super.onLayout(changed, left, top, right, bottom) 80 | if (initialText.isBlank()) { 81 | initialText = text.toString() 82 | post { 83 | collapsedVisibleText = collapsedVisibleText() 84 | //Override expand property in specific scenarios 85 | isExpanded = if (collapsedVisibleText.isAllTextVisible()) { 86 | true 87 | } else when (expandType) { 88 | EXPAND_TYPE_POPUP -> false 89 | else -> isExpanded 90 | } 91 | configureMaxLines() 92 | setEllipsizedText(isExpanded) 93 | setForeground(isExpanded) 94 | } 95 | } 96 | } 97 | 98 | private fun toggleExpandableTextView() { 99 | if (collapsedVisibleText.isAllTextVisible()) return 100 | 101 | when (expandType) { 102 | EXPAND_TYPE_LAYOUT -> animateHeightTransition() 103 | EXPAND_TYPE_POPUP -> showPopupText() 104 | else -> throw UnsupportedOperationException("No toggle operation provided for expand type[$expandType]") 105 | } 106 | } 107 | 108 | 109 | private fun configureMaxLines(){ 110 | if (mCollapsedLines < COLLAPSED_MAX_LINES){ 111 | maxLines = when(expandType) { 112 | EXPAND_TYPE_LAYOUT -> if (isExpanded) COLLAPSED_MAX_LINES else mCollapsedLines 113 | EXPAND_TYPE_POPUP -> mCollapsedLines 114 | else -> maxLines 115 | } 116 | } 117 | } 118 | 119 | private fun animateHeightTransition() { 120 | val expanding = !isExpanded 121 | val startHeight = height 122 | 123 | isExpanded = expanding 124 | configureMaxLines() 125 | setEllipsizedText(expanding) 126 | 127 | measure( 128 | MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 129 | MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) 130 | ) 131 | val endHeight = measuredHeight 132 | 133 | if (startHeight == endHeight) return // 134 | 135 | val duration = mAnimationDuration?.toLong() ?: DEFAULT_ANIM_DURATION.toLong() 136 | 137 | val animator = ValueAnimator.ofInt(startHeight, endHeight).apply { 138 | this.duration = duration 139 | interpolator = DecelerateInterpolator() 140 | 141 | addUpdateListener { animation -> 142 | val value = animation.animatedValue as Int 143 | layoutParams.height = value 144 | requestLayout() 145 | 146 | clipBounds = Rect(0, 0, width, value) 147 | } 148 | 149 | addListener(object : Animator.AnimatorListener { 150 | override fun onAnimationStart(animation: Animator) { 151 | visibility = View.VISIBLE 152 | if (fadeAnimationEnabled) { 153 | alpha = 0f 154 | animate().alpha(1f).setDuration(duration / 2).start() 155 | } else { 156 | alpha = 1f 157 | } 158 | } 159 | 160 | override fun onAnimationEnd(animation: Animator) { 161 | layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT 162 | requestLayout() 163 | clipBounds = null 164 | } 165 | 166 | override fun onAnimationCancel(animation: Animator) { 167 | clipBounds = null 168 | } 169 | 170 | override fun onAnimationRepeat(animation: Animator) {} 171 | }) 172 | } 173 | 174 | animator.start() 175 | } 176 | 177 | private fun showPopupText() { 178 | AlertDialog.Builder(context) 179 | .setTitle("") 180 | .setMessage(initialText) 181 | .setNegativeButton(android.R.string.ok, null) 182 | .show() 183 | } 184 | 185 | override fun setText(text: CharSequence?, type: BufferType?) { 186 | mOriginalText = text 187 | super.setText(text, type) 188 | } 189 | 190 | fun setReadMoreText(readMore: String): ExpandableTextView { 191 | mReadMoreText = readMore 192 | return this 193 | } 194 | 195 | fun setReadLessText(readLess: String): ExpandableTextView { 196 | mReadLessText = readLess 197 | return this 198 | } 199 | 200 | fun setCollapsedLines(collapsedLines: Int): ExpandableTextView { 201 | mCollapsedLines = collapsedLines 202 | return this 203 | } 204 | 205 | fun setIsExpanded(isExpanded: Boolean): ExpandableTextView { 206 | this.isExpanded = isExpanded 207 | return this 208 | } 209 | 210 | fun setAnimationDuration(animationDuration: Int): ExpandableTextView { 211 | mAnimationDuration = animationDuration 212 | return this 213 | } 214 | 215 | fun setIsUnderlined(isUnderlined: Boolean): ExpandableTextView { 216 | this.isUnderlined = isUnderlined 217 | return this 218 | } 219 | 220 | fun setEllipsizedTextColor(ellipsizeTextColor: Int): ExpandableTextView { 221 | mEllipsizeTextColor = ellipsizeTextColor 222 | return this 223 | } 224 | 225 | fun setForegroundColor(foregroundColor: Int): ExpandableTextView { 226 | this.foregroundColor = foregroundColor 227 | return this 228 | } 229 | 230 | fun setExpandType(expandType: Int): ExpandableTextView { 231 | this.expandType = expandType 232 | return this 233 | } 234 | 235 | fun setFadeAnimationEnabled(fadeAnimationEnabled: Boolean): ExpandableTextView { 236 | this.fadeAnimationEnabled = fadeAnimationEnabled 237 | return this 238 | } 239 | 240 | fun toggle() { 241 | toggleExpandableTextView() 242 | } 243 | 244 | init { 245 | context.obtainStyledAttributes(attrs, R.styleable.ExpandableTextView).apply { 246 | try { 247 | mCollapsedLines = getInt(R.styleable.ExpandableTextView_collapsedLines, COLLAPSED_MAX_LINES) 248 | mAnimationDuration = getInt(R.styleable.ExpandableTextView_animDuration, DEFAULT_ANIM_DURATION) 249 | mReadMoreText = getString(R.styleable.ExpandableTextView_readMoreText) ?: READ_MORE 250 | mReadLessText = getString(R.styleable.ExpandableTextView_readLessText) ?: READ_LESS 251 | foregroundColor = getColor(R.styleable.ExpandableTextView_foregroundColor, Color.TRANSPARENT) 252 | isUnderlined = getBoolean(R.styleable.ExpandableTextView_isUnderlined, false) 253 | isExpanded = getBoolean(R.styleable.ExpandableTextView_isExpanded, false) 254 | mEllipsizeTextColor = getColor(R.styleable.ExpandableTextView_ellipsizeTextColor, Color.BLUE) 255 | expandType = getInt(R.styleable.ExpandableTextView_expandType, EXPAND_TYPE_DEFAULT) 256 | fadeAnimationEnabled = getBoolean(R.styleable.ExpandableTextView_fadeAnimationEnabled, true) 257 | 258 | } finally { 259 | this.recycle() 260 | } 261 | } 262 | 263 | configureMaxLines() 264 | setOnClickListener(this) 265 | } 266 | 267 | private fun setEllipsizedText(isExpanded: Boolean) { 268 | if (initialText.isBlank()) 269 | return 270 | 271 | text = if (collapsedVisibleText.isAllTextVisible()){ 272 | initialText 273 | } else{ 274 | when(expandType){ 275 | EXPAND_TYPE_POPUP -> getCollapseText() 276 | EXPAND_TYPE_LAYOUT -> if (isExpanded) getExpandText() else getCollapseText() 277 | else -> throw UnsupportedOperationException("No supported expand mechanism provided for expand type[$expandType]") 278 | } 279 | } 280 | } 281 | 282 | private fun getExpandText(): SpannableStringBuilder { 283 | return SpannableStringBuilder(initialText) 284 | .append(EMPTY_SPACE) 285 | .append(mReadLessText.toString().span()) 286 | } 287 | 288 | private fun getCollapseText(): SpannableStringBuilder { 289 | 290 | val ellipseTextLength = ((mReadMoreText.length + DEFAULT_ELLIPSIZED_TEXT.length) * ELLIPSIZE_TEXT_LENGTH_MULTIPLIER).roundToInt() 291 | val textAvailableLength = max(0, collapsedVisibleText.length - ellipseTextLength) 292 | val ellipsizeAvailableLength = min(collapsedVisibleText.length, DEFAULT_ELLIPSIZED_TEXT.length) 293 | val readMoreAvailableLength = min(collapsedVisibleText.length - ellipsizeAvailableLength, mReadMoreText.length) 294 | 295 | return SpannableStringBuilder(collapsedVisibleText.substring(0, textAvailableLength)) 296 | .append(DEFAULT_ELLIPSIZED_TEXT.substring(0, ellipsizeAvailableLength)) 297 | .append(mReadMoreText.substring(0, readMoreAvailableLength).span()) 298 | } 299 | 300 | private fun collapsedVisibleText(): String { 301 | try { 302 | var finalTextOffset = 0 303 | if (mCollapsedLines < COLLAPSED_MAX_LINES) { 304 | for (i in 0 until mCollapsedLines) { 305 | val textOffset = layout.getLineEnd(i) 306 | if (textOffset == initialText.length) 307 | return initialText 308 | else 309 | finalTextOffset = textOffset 310 | } 311 | return initialText.substring(0, finalTextOffset) 312 | }else { 313 | return initialText 314 | } 315 | }catch (e: Exception){ 316 | e.printStackTrace() 317 | return initialText 318 | } 319 | } 320 | 321 | private fun setForeground(isExpanded: Boolean) { 322 | foreground = GradientDrawable(BOTTOM_TOP, intArrayOf(foregroundColor!!, Color.TRANSPARENT)) 323 | foreground.alpha = if (isExpanded) { 324 | MIN_VALUE_ALPHA 325 | } else { 326 | MAX_VALUE_ALPHA 327 | } 328 | } 329 | 330 | private fun String.isAllTextVisible(): Boolean = this == text.toString() 331 | 332 | private fun String.span(): SpannableString = 333 | SpannableString(this).apply { 334 | setSpan( 335 | ForegroundColorSpan(mEllipsizeTextColor!!), 336 | 0, 337 | this.length, 338 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 339 | ) 340 | if (isUnderlined!!) 341 | setSpan( 342 | UnderlineSpan(), 343 | 0, 344 | this.length, 345 | Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 346 | ) 347 | } 348 | 349 | companion object { 350 | const val MAX_VALUE_ALPHA = 255 351 | const val MIN_VALUE_ALPHA = 0 352 | } 353 | } 354 | 355 | @BindingAdapter("readMoreText") 356 | fun ExpandableTextView.readMoreText(readMore: String) { 357 | this.setReadMoreText(readMore) 358 | } 359 | 360 | @BindingAdapter("readLessText") 361 | fun ExpandableTextView.readLessText(readLess: String) { 362 | this.setReadLessText(readLess) 363 | } 364 | -------------------------------------------------------------------------------- /expandable-textview/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /expandable-textview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /expandable-textview/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /expandable-textview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpandableTextView 3 | -------------------------------------------------------------------------------- /expandable-textview/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /expandable-textview/src/test/java/io/github/glailton/expandabletextview/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.glailton.expandabletextview 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | android.defaults.buildfeatures.buildconfig=true 23 | android.nonTransitiveRClass=false 24 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 22 22:49:52 BRT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /publish-mavencentral.gradle: -------------------------------------------------------------------------------- 1 | mavenPublishing { 2 | publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL) 3 | 4 | coordinates("io.github.glailton", "expandabletextview", "1.0.5") 5 | 6 | pom { 7 | name = "ExpandableTextView" 8 | description = "An expandable Android TextView written in Kotlin." 9 | inceptionYear = "2024" 10 | url = "https://github.com/glailton/ExpandableTextView" 11 | 12 | licenses { 13 | license { 14 | name = "MIT License" 15 | url = "https://opensource.org/licenses/MIT" 16 | } 17 | } 18 | 19 | developers { 20 | developer { 21 | id = "glailton" 22 | name = "Glailton Costa" 23 | email = "glailton.rc@gmail.com" 24 | } 25 | } 26 | 27 | scm { 28 | connection = "scm:git:https://github.com/glailton/ExpandableTextView.git" 29 | developerConnection = "scm:git:https://github.com/glailton/ExpandableTextView.git" 30 | url = "https://github.com/glailton/ExpandableTextView" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glailton/ExpandableTextView/a742011793521cd9af650adf30e3b72308336577/resources/gif.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | plugins { 8 | id "com.vanniktech.maven.publish" version "0.25.3" 9 | id 'org.jetbrains.kotlin.android' version '1.9.20' 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | rootProject.name = "ExpandableTextView Demo" 20 | include ':app' 21 | include ':expandable-textview' 22 | --------------------------------------------------------------------------------