├── .gitignore ├── CODE_OF_CONDUCT.md ├── COPYRIGHT ├── LICENSE ├── README.md ├── bintray.gradle ├── build.gradle ├── demo.gif ├── demo ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── net │ │ └── opacapp │ │ └── multilinecollapsingtoolbar │ │ └── demo │ │ └── DemoActivity.java │ └── res │ ├── layout │ └── activity_demo.xml │ ├── menu │ └── menu_demo.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-v21 │ └── styles.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── multiline-collapsingtoolbar ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── net │ │ └── opacapp │ │ └── multilinecollapsingtoolbar │ │ ├── AnimationUtils.java │ │ ├── CollapsingTextHelper.java │ │ ├── CollapsingToolbarLayout.java │ │ ├── ThemeUtils.java │ │ └── ViewOffsetHelper.java │ └── res │ └── values │ ├── collapsing_toolbar_attrs.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | .idea/ 30 | *.iml 31 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info@opacapp.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Original copyright (C) 2015 The Android Open Source Project 2 | Modifications copyright (C) 2015 Johan von Forstner, Raphael Michel 3 | See LICENSE file for details 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multiline-collapsingtoolbar [ ![Download](https://api.bintray.com/packages/opacapp/libs/multiline-collapsingtoolbar/images/download.svg) ](https://bintray.com/opacapp/libs/multiline-collapsingtoolbar/_latestVersion) 2 | _multiline-collapsingtoolbar_ is a replacement for `CollapsingToolbarLayout` from the [Android 3 | Design Support Library](https://github.com/android/platform_frameworks_support/tree/master/design) 4 | which can deal with multiline titles (with a customizable maximum number of lines) in the 5 | expanded state. When collapsing the toolbar, the lower lines of the title fade away to leave 6 | only the top line visible. 7 | 8 | ## Information about compatibility with AndroidX 9 | 10 | We are currently not planning to update this library for support of the Android Support Library version 28 or the new AndroidX libraries, as has been discussed in [#62](https://github.com/opacapp/multiline-collapsingtoolbar/issues/62) and other places. Instead, we are trying to get our modifications merged into the official Material Components Android library. Please see [the PR](https://github.com/material-components/material-components-android/pull/413) for more details. 11 | 12 | ## Example 13 | Here you can see the library in action in the included demo app: 14 | 15 | ![Demo image](https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/master/demo.gif) 16 | 17 | ## Installation 18 | 19 | If you are using Gradle and the JCenter Maven Repository, installing the library is as simple as 20 | adding a new dependency statement. 21 | 22 | ```gradle 23 | dependencies { 24 | compile 'net.opacapp:multiline-collapsingtoolbar:27.1.1' 25 | } 26 | ``` 27 | 28 | The current version 27.1.1 of the library is based on the code and tested with the **Design Support 29 | Library version 27.1.1**. 30 | We'll try to keep up to date with new support library versions as soon as possible, but please **do not expect this 31 | library to run with support versions other than that.** 32 | 33 | ## Usage 34 | The library's public API is nearly identical to the version from the support library, so you can use it as a drop-in replacement. We only added a `maxLines` attribute and corresponding getter and setter functions to the `CollapsingToolbarLayout` to make it possible to change the maximum number of lines, which is set to 3 by default. 35 | 36 | As the Design Support Library, it should be compatible with API 14 (Android 4.0) and above. 37 | 38 | XML layout example: 39 | ```xml 40 | 43 | 48 | 52 | 53 | 54 | ``` 55 | 56 | You can find a simple demo application in the `demo` module. 57 | 58 | ## Implementation details 59 | Most of the code is copied from the original Support Library classes, the only changes (apart from the package name, imports and automatic code reformatting) are in the `CollapsingTextHelper` and `CollapsingToolbarLayout` classes. The changes there are marked with comments. 60 | 61 | ## Contributing 62 | 63 | This library is probably not complete and might contain bugs that only occur in constellations we did not 64 | yet test. Please do not hesitate to create an issue on GitHub for any problems that cross your way. Please 65 | understand that we cannot afford to spend time fixing problems that do not affect our products, but we'll 66 | be happy to merge pull requests if you or someone else is able to improve this library. 67 | 68 | If you get stuck anywhere in the process, please do not hestitate to ask us anytime at info@opacapp.de. 69 | 70 | Please note that we have a [Code of Conduct](https://github.com/opacapp/multiline-collapsingtoolbar/blob/master/CODE_OF_CONDUCT.md) 71 | in place that applies to all project-related communication. 72 | 73 | ## Contributors 74 | - [@johan12345](https://github.com/johan12345) 75 | - [@raphaelm](https://github.com/raphaelm) 76 | - [@jsotuyod](https://github.com/jsotuyod) 77 | - [@zsolt-szecsi](https://github.com/zsolt-szecsi) 78 | -------------------------------------------------------------------------------- /bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | 5 | version = libraryVersion 6 | group = 'net.opacapp' 7 | 8 | task sourcesJar(type: Jar) { 9 | classifier = 'sources' 10 | from android.sourceSets.main.java.srcDirs 11 | } 12 | 13 | artifacts { 14 | archives sourcesJar 15 | } 16 | 17 | 18 | install { 19 | repositories.mavenInstaller { 20 | // This generates POM.xml with proper parameters 21 | pom { 22 | project { 23 | packaging 'aar' 24 | groupId publishedGroupId 25 | artifactId artifact 26 | 27 | // Add your description here 28 | name libraryName 29 | description libraryDescription 30 | url siteUrl 31 | 32 | // Set your license 33 | licenses { 34 | license { 35 | name licenseName 36 | url licenseUrl 37 | } 38 | } 39 | developers { 40 | developer { 41 | id developerId 42 | name developerName 43 | email developerEmail 44 | } 45 | } 46 | scm { 47 | connection gitUrl 48 | developerConnection gitUrl 49 | url siteUrl 50 | 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | // Bintray 58 | Properties properties = new Properties() 59 | if (project.rootProject.file('local.properties').exists()) { 60 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 61 | } 62 | 63 | bintray { 64 | user = properties.getProperty("bintray.user") 65 | key = properties.getProperty("bintray.apikey") 66 | 67 | configurations = ['archives'] 68 | pkg { 69 | userOrg = bintrayOrga 70 | repo = bintrayRepo 71 | name = bintrayName 72 | desc = libraryDescription 73 | websiteUrl = siteUrl 74 | vcsUrl = gitUrl 75 | licenses = allLicenses 76 | publish = true 77 | publicDownloadNumbers = true 78 | version { 79 | desc = libraryDescription 80 | name = libraryVersion 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | google() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:3.1.1' 8 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0' 9 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4.1' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | jcenter() 16 | google() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/demo.gif -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | 6 | defaultConfig { 7 | applicationId "net.opacapp.multilinecollapsingtoolbar.demo" 8 | minSdkVersion 14 9 | targetSdkVersion 27 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | } 14 | 15 | dependencies { 16 | compile 'com.android.support:appcompat-v7:27.1.1' 17 | compile 'com.android.support:design:27.1.1' 18 | compile project(':multiline-collapsingtoolbar') 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /demo/src/main/java/net/opacapp/multilinecollapsingtoolbar/demo/DemoActivity.java: -------------------------------------------------------------------------------- 1 | package net.opacapp.multilinecollapsingtoolbar.demo; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.Toolbar; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | 9 | public class DemoActivity extends AppCompatActivity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_demo); 15 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 16 | setSupportActionBar(toolbar); 17 | } 18 | 19 | @Override 20 | public boolean onCreateOptionsMenu(Menu menu) { 21 | getMenuInflater().inflate(R.menu.menu_demo, menu); 22 | return true; 23 | } 24 | 25 | @Override 26 | public boolean onOptionsItemSelected(MenuItem item) { 27 | int id = item.getItemId(); 28 | 29 | //noinspection SimplifiableIfStatement 30 | if (id == R.id.action_settings) { 31 | return true; 32 | } 33 | 34 | return super.onOptionsItemSelected(item); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demo/src/main/res/layout/activity_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/src/main/res/menu/menu_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/demo/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/demo/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/demo/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /demo/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /demo/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Demo 3 | This is a long title text displayed in multiple lines. 4 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam 5 | nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 6 | Settings 7 | 8 | -------------------------------------------------------------------------------- /demo/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opacapp/multiline-collapsingtoolbar/dca374575654f741194d0f037cf89edf2daeb967/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Mar 27 20:13:09 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 27 5 | 6 | defaultConfig { 7 | minSdkVersion 14 8 | targetSdkVersion 27 9 | versionCode 11 10 | versionName "27.1.1" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | } 16 | } 17 | lintOptions { 18 | abortOnError false 19 | } 20 | } 21 | 22 | ext { 23 | bintrayOrga = 'opacapp' 24 | bintrayRepo = 'libs' 25 | bintrayName = 'multiline-collapsingtoolbar' 26 | 27 | publishedGroupId = 'net.opacapp' 28 | libraryName = 'Multiline Collapsing Toolbar' 29 | artifact = 'multiline-collapsingtoolbar' 30 | 31 | libraryDescription = "A modified collapsingtoolbar that can work with multi-line titles" 32 | 33 | siteUrl = 'https://github.com/opacapp/multiline-collapsingtoolbar' 34 | gitUrl = 'https://github.com/opacapp/multiline-collapsingtoolbar.git' 35 | 36 | libraryVersion = '27.1.1' 37 | 38 | developerId = 'opacapp' 39 | developerName = 'Web Opac App' 40 | developerEmail = 'info@opacapp.net' 41 | 42 | licenseName = 'The Apache Software License, Version 2.0' 43 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 44 | allLicenses = ["Apache-2.0"] 45 | } 46 | 47 | dependencies { 48 | compile 'com.android.support:design:27.1.1' 49 | compile 'com.android.support:support-v4:27.1.1' 50 | compile 'com.android.support:appcompat-v7:27.1.1' 51 | } 52 | 53 | 54 | apply from: '../bintray.gradle' 55 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/java/net/opacapp/multilinecollapsingtoolbar/AnimationUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.opacapp.multilinecollapsingtoolbar; 18 | 19 | import android.support.v4.view.animation.FastOutLinearInInterpolator; 20 | import android.support.v4.view.animation.FastOutSlowInInterpolator; 21 | import android.support.v4.view.animation.LinearOutSlowInInterpolator; 22 | import android.view.animation.DecelerateInterpolator; 23 | import android.view.animation.Interpolator; 24 | import android.view.animation.LinearInterpolator; 25 | 26 | class AnimationUtils { 27 | 28 | static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 29 | static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator(); 30 | static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator(); 31 | static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator(); 32 | static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(); 33 | 34 | /** 35 | * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. 36 | */ 37 | static float lerp(float startValue, float endValue, float fraction) { 38 | return startValue + (fraction * (endValue - startValue)); 39 | } 40 | 41 | static int lerp(int startValue, int endValue, float fraction) { 42 | return startValue + Math.round(fraction * (endValue - startValue)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/java/net/opacapp/multilinecollapsingtoolbar/CollapsingTextHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * Modified 2015 by Johan v. Forstner (modifications are marked with comments) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package net.opacapp.multilinecollapsingtoolbar; 18 | 19 | import android.content.res.ColorStateList; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Bitmap; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.Paint; 25 | import android.graphics.Rect; 26 | import android.graphics.RectF; 27 | import android.graphics.Typeface; 28 | import android.os.Build; 29 | import android.support.annotation.ColorInt; 30 | import android.support.v4.math.MathUtils; 31 | import android.support.v4.text.TextDirectionHeuristicsCompat; 32 | import android.support.v4.view.GravityCompat; 33 | import android.support.v4.view.ViewCompat; 34 | import android.text.Layout; 35 | import android.text.StaticLayout; 36 | import android.text.TextPaint; 37 | import android.text.TextUtils; 38 | import android.view.Gravity; 39 | import android.view.View; 40 | import android.view.animation.Interpolator; 41 | 42 | // BEGIN MODIFICATION: Added imports 43 | // END MODIFICATION 44 | 45 | final class CollapsingTextHelper { 46 | 47 | // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it 48 | // by using our own texture 49 | private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18; 50 | 51 | private static final boolean DEBUG_DRAW = false; 52 | private static final Paint DEBUG_DRAW_PAINT; 53 | 54 | static { 55 | DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null; 56 | if (DEBUG_DRAW_PAINT != null) { 57 | DEBUG_DRAW_PAINT.setAntiAlias(true); 58 | DEBUG_DRAW_PAINT.setColor(Color.MAGENTA); 59 | } 60 | } 61 | 62 | private final View mView; 63 | 64 | private boolean mDrawTitle; 65 | private float mExpandedFraction; 66 | 67 | private final Rect mExpandedBounds; 68 | private final Rect mCollapsedBounds; 69 | private final RectF mCurrentBounds; 70 | private int mExpandedTextGravity = Gravity.CENTER_VERTICAL; 71 | private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL; 72 | private float mExpandedTextSize = 15; 73 | private float mCollapsedTextSize = 15; 74 | private ColorStateList mExpandedTextColor; 75 | private ColorStateList mCollapsedTextColor; 76 | 77 | private float mExpandedDrawY; 78 | private float mCollapsedDrawY; 79 | private float mExpandedDrawX; 80 | private float mCollapsedDrawX; 81 | private float mCurrentDrawX; 82 | private float mCurrentDrawY; 83 | private Typeface mCollapsedTypeface; 84 | private Typeface mExpandedTypeface; 85 | private Typeface mCurrentTypeface; 86 | 87 | private CharSequence mText; 88 | private CharSequence mTextToDraw; 89 | private boolean mIsRtl; 90 | 91 | private boolean mUseTexture; 92 | private Bitmap mExpandedTitleTexture; 93 | private Paint mTexturePaint; 94 | // MODIFICATION: Removed now unused fields mTextureAscent and mTextureDescent 95 | 96 | private float mScale; 97 | private float mCurrentTextSize; 98 | 99 | private int[] mState; 100 | 101 | private boolean mBoundsChanged; 102 | 103 | private final TextPaint mTextPaint; 104 | 105 | private Interpolator mPositionInterpolator; 106 | private Interpolator mTextSizeInterpolator; 107 | 108 | private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy; 109 | private int mCollapsedShadowColor; 110 | 111 | private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy; 112 | private int mExpandedShadowColor; 113 | 114 | // BEGIN MODIFICATION: Added fields 115 | private CharSequence mTextToDrawCollapsed; 116 | private Bitmap mCollapsedTitleTexture; 117 | private Bitmap mCrossSectionTitleTexture; 118 | private StaticLayout mTextLayout; 119 | private float mCollapsedTextBlend; 120 | private float mExpandedTextBlend; 121 | private float mExpandedFirstLineDrawX; 122 | private int maxLines = 3; 123 | private float lineSpacingExtra = 0; 124 | private float lineSpacingMultiplier = 1; 125 | // END MODIFICATION 126 | 127 | public CollapsingTextHelper(View view) { 128 | mView = view; 129 | 130 | mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); 131 | 132 | mCollapsedBounds = new Rect(); 133 | mExpandedBounds = new Rect(); 134 | mCurrentBounds = new RectF(); 135 | } 136 | 137 | void setTextSizeInterpolator(Interpolator interpolator) { 138 | mTextSizeInterpolator = interpolator; 139 | recalculate(); 140 | } 141 | 142 | void setPositionInterpolator(Interpolator interpolator) { 143 | mPositionInterpolator = interpolator; 144 | recalculate(); 145 | } 146 | 147 | void setExpandedTextSize(float textSize) { 148 | if (mExpandedTextSize != textSize) { 149 | mExpandedTextSize = textSize; 150 | recalculate(); 151 | } 152 | } 153 | 154 | void setCollapsedTextSize(float textSize) { 155 | if (mCollapsedTextSize != textSize) { 156 | mCollapsedTextSize = textSize; 157 | recalculate(); 158 | } 159 | } 160 | 161 | void setCollapsedTextColor(ColorStateList textColor) { 162 | if (mCollapsedTextColor != textColor) { 163 | mCollapsedTextColor = textColor; 164 | recalculate(); 165 | } 166 | } 167 | 168 | void setExpandedTextColor(ColorStateList textColor) { 169 | if (mExpandedTextColor != textColor) { 170 | mExpandedTextColor = textColor; 171 | recalculate(); 172 | } 173 | } 174 | 175 | void setExpandedBounds(int left, int top, int right, int bottom) { 176 | if (!rectEquals(mExpandedBounds, left, top, right, bottom)) { 177 | mExpandedBounds.set(left, top, right, bottom); 178 | mBoundsChanged = true; 179 | onBoundsChanged(); 180 | } 181 | } 182 | 183 | void setCollapsedBounds(int left, int top, int right, int bottom) { 184 | if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) { 185 | mCollapsedBounds.set(left, top, right, bottom); 186 | mBoundsChanged = true; 187 | onBoundsChanged(); 188 | } 189 | } 190 | 191 | void onBoundsChanged() { 192 | mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0 193 | && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0; 194 | } 195 | 196 | void setExpandedTextGravity(int gravity) { 197 | if (mExpandedTextGravity != gravity) { 198 | mExpandedTextGravity = gravity; 199 | recalculate(); 200 | } 201 | } 202 | 203 | int getExpandedTextGravity() { 204 | return mExpandedTextGravity; 205 | } 206 | 207 | void setCollapsedTextGravity(int gravity) { 208 | if (mCollapsedTextGravity != gravity) { 209 | mCollapsedTextGravity = gravity; 210 | recalculate(); 211 | } 212 | } 213 | 214 | int getCollapsedTextGravity() { 215 | return mCollapsedTextGravity; 216 | } 217 | 218 | void setCollapsedTextAppearance(int resId) { 219 | TypedArray a = mView.getContext().obtainStyledAttributes(resId, 220 | android.support.v7.appcompat.R.styleable.TextAppearance); 221 | if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) { 222 | mCollapsedTextColor = a.getColorStateList( 223 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); 224 | } 225 | if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) { 226 | mCollapsedTextSize = a.getDimensionPixelSize( 227 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 228 | (int) mCollapsedTextSize); 229 | } 230 | mCollapsedShadowColor = a.getInt( 231 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0); 232 | mCollapsedShadowDx = a.getFloat( 233 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0); 234 | mCollapsedShadowDy = a.getFloat( 235 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0); 236 | mCollapsedShadowRadius = a.getFloat( 237 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0); 238 | a.recycle(); 239 | 240 | if (Build.VERSION.SDK_INT >= 16) { 241 | mCollapsedTypeface = readFontFamilyTypeface(resId); 242 | } 243 | 244 | recalculate(); 245 | } 246 | 247 | void setExpandedTextAppearance(int resId) { 248 | TypedArray a = mView.getContext().obtainStyledAttributes(resId, 249 | android.support.v7.appcompat.R.styleable.TextAppearance); 250 | if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) { 251 | mExpandedTextColor = a.getColorStateList( 252 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor); 253 | } 254 | if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) { 255 | mExpandedTextSize = a.getDimensionPixelSize( 256 | android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 257 | (int) mExpandedTextSize); 258 | } 259 | mExpandedShadowColor = a.getInt( 260 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0); 261 | mExpandedShadowDx = a.getFloat( 262 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0); 263 | mExpandedShadowDy = a.getFloat( 264 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0); 265 | mExpandedShadowRadius = a.getFloat( 266 | android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0); 267 | a.recycle(); 268 | 269 | if (Build.VERSION.SDK_INT >= 16) { 270 | mExpandedTypeface = readFontFamilyTypeface(resId); 271 | } 272 | 273 | recalculate(); 274 | } 275 | 276 | // BEGIN MODIFICATION: getter and setter method for number of max lines 277 | void setMaxLines(int maxLines) { 278 | if (maxLines != this.maxLines) { 279 | this.maxLines = maxLines; 280 | clearTexture(); 281 | recalculate(); 282 | } 283 | } 284 | 285 | int getMaxLines() { 286 | return maxLines; 287 | } 288 | // END MODIFICATION 289 | 290 | // BEGIN MODIFICATION: getter and setter methods for line spacing 291 | void setLineSpacingExtra(float lineSpacingExtra) { 292 | if (lineSpacingExtra != this.lineSpacingExtra) { 293 | this.lineSpacingExtra = lineSpacingExtra; 294 | clearTexture(); 295 | recalculate(); 296 | } 297 | } 298 | 299 | float getLineSpacingExtra() { 300 | return lineSpacingExtra; 301 | } 302 | 303 | void setLineSpacingMultiplier(float lineSpacingMultiplier) { 304 | if (lineSpacingMultiplier != this.lineSpacingMultiplier) { 305 | this.lineSpacingMultiplier = lineSpacingMultiplier; 306 | clearTexture(); 307 | recalculate(); 308 | } 309 | } 310 | 311 | float getLineSpacingMultiplier() { 312 | return lineSpacingMultiplier; 313 | } 314 | // END MODIFICATION 315 | 316 | private Typeface readFontFamilyTypeface(int resId) { 317 | final TypedArray a = mView.getContext().obtainStyledAttributes(resId, 318 | new int[]{android.R.attr.fontFamily}); 319 | try { 320 | final String family = a.getString(0); 321 | if (family != null) { 322 | return Typeface.create(family, Typeface.NORMAL); 323 | } 324 | } finally { 325 | a.recycle(); 326 | } 327 | return null; 328 | } 329 | 330 | void setCollapsedTypeface(Typeface typeface) { 331 | if (areTypefacesDifferent(mCollapsedTypeface, typeface)) { 332 | mCollapsedTypeface = typeface; 333 | recalculate(); 334 | } 335 | } 336 | 337 | void setExpandedTypeface(Typeface typeface) { 338 | if (areTypefacesDifferent(mExpandedTypeface, typeface)) { 339 | mExpandedTypeface = typeface; 340 | recalculate(); 341 | } 342 | } 343 | 344 | void setTypefaces(Typeface typeface) { 345 | mCollapsedTypeface = mExpandedTypeface = typeface; 346 | recalculate(); 347 | } 348 | 349 | Typeface getCollapsedTypeface() { 350 | return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT; 351 | } 352 | 353 | Typeface getExpandedTypeface() { 354 | return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT; 355 | } 356 | 357 | /** 358 | * Set the value indicating the current scroll value. This decides how much of the 359 | * background will be displayed, as well as the title metrics/positioning. 360 | * 361 | * A value of {@code 0.0} indicates that the layout is fully expanded. 362 | * A value of {@code 1.0} indicates that the layout is fully collapsed. 363 | */ 364 | void setExpansionFraction(float fraction) { 365 | fraction = MathUtils.clamp(fraction, 0f, 1f); 366 | 367 | if (fraction != mExpandedFraction) { 368 | mExpandedFraction = fraction; 369 | calculateCurrentOffsets(); 370 | } 371 | } 372 | 373 | final boolean setState(final int[] state) { 374 | mState = state; 375 | 376 | if (isStateful()) { 377 | recalculate(); 378 | return true; 379 | } 380 | 381 | return false; 382 | } 383 | 384 | final boolean isStateful() { 385 | return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful()) || 386 | (mExpandedTextColor != null && mExpandedTextColor.isStateful()); 387 | } 388 | 389 | 390 | float getExpansionFraction() { 391 | return mExpandedFraction; 392 | } 393 | 394 | float getCollapsedTextSize() { 395 | return mCollapsedTextSize; 396 | } 397 | 398 | float getExpandedTextSize() { 399 | return mExpandedTextSize; 400 | } 401 | 402 | private void calculateCurrentOffsets() { 403 | calculateOffsets(mExpandedFraction); 404 | } 405 | 406 | private void calculateOffsets(final float fraction) { 407 | interpolateBounds(fraction); 408 | mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction, 409 | mPositionInterpolator); 410 | mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction, 411 | mPositionInterpolator); 412 | 413 | setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize, 414 | fraction, mTextSizeInterpolator)); 415 | 416 | // BEGIN MODIFICATION: set text blending 417 | setCollapsedTextBlend(1 - lerp(0, 1, 1 - fraction, AnimationUtils 418 | .FAST_OUT_SLOW_IN_INTERPOLATOR)); 419 | setExpandedTextBlend(lerp(1, 0, fraction, AnimationUtils 420 | .FAST_OUT_SLOW_IN_INTERPOLATOR)); 421 | // END MODIFICATION 422 | 423 | if (mCollapsedTextColor != mExpandedTextColor) { 424 | // If the collapsed and expanded text colors are different, blend them based on the 425 | // fraction 426 | mTextPaint.setColor(blendColors( 427 | getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction)); 428 | } else { 429 | mTextPaint.setColor(getCurrentCollapsedTextColor()); 430 | } 431 | 432 | mTextPaint.setShadowLayer( 433 | lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null), 434 | lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null), 435 | lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null), 436 | blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction)); 437 | 438 | ViewCompat.postInvalidateOnAnimation(mView); 439 | } 440 | 441 | @ColorInt 442 | private int getCurrentExpandedTextColor() { 443 | if (mState != null) { 444 | return mExpandedTextColor.getColorForState(mState, 0); 445 | } else { 446 | return mExpandedTextColor.getDefaultColor(); 447 | } 448 | } 449 | 450 | @ColorInt 451 | private int getCurrentCollapsedTextColor() { 452 | if (mState != null) { 453 | return mCollapsedTextColor.getColorForState(mState, 0); 454 | } else { 455 | return mCollapsedTextColor.getDefaultColor(); 456 | } 457 | } 458 | 459 | private void calculateBaseOffsets() { 460 | final float currentTextSize = mCurrentTextSize; 461 | // We then calculate the collapsed text size, using the same logic 462 | calculateUsingTextSize(mCollapsedTextSize); 463 | 464 | // BEGIN MODIFICATION: set mTextToDrawCollapsed and calculate width using it 465 | mTextToDrawCollapsed = mTextToDraw; 466 | float width = mTextToDrawCollapsed != null ? 467 | mTextPaint.measureText(mTextToDrawCollapsed, 0, mTextToDrawCollapsed.length()) : 0; 468 | // END MODIFICATION 469 | 470 | final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity, 471 | mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); 472 | 473 | // BEGIN MODIFICATION: calculate height and Y position using mTextLayout 474 | float textHeight = mTextLayout != null ? mTextLayout.getHeight() : 0; 475 | 476 | switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { 477 | case Gravity.BOTTOM: 478 | mCollapsedDrawY = mCollapsedBounds.bottom - textHeight; 479 | break; 480 | case Gravity.TOP: 481 | mCollapsedDrawY = mCollapsedBounds.top; 482 | break; 483 | case Gravity.CENTER_VERTICAL: 484 | default: 485 | float textOffset = (textHeight / 2); 486 | mCollapsedDrawY = mCollapsedBounds.centerY() - textOffset; 487 | break; 488 | } 489 | // END MODIFICATION 490 | 491 | switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { 492 | case Gravity.CENTER_HORIZONTAL: 493 | mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2); 494 | break; 495 | case Gravity.RIGHT: 496 | mCollapsedDrawX = mCollapsedBounds.right - width; 497 | break; 498 | case Gravity.LEFT: 499 | default: 500 | mCollapsedDrawX = mCollapsedBounds.left; 501 | break; 502 | } 503 | 504 | calculateUsingTextSize(mExpandedTextSize); 505 | 506 | // BEGIN MODIFICATION: calculate width using mTextLayout based on first line and store that padding 507 | width = mTextLayout != null ? mTextLayout.getLineWidth(0) : 0; 508 | mExpandedFirstLineDrawX = mTextLayout != null ? mTextLayout.getLineLeft(0) : 0; 509 | // END MODIFICATION 510 | 511 | final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity, 512 | mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); 513 | 514 | // BEGIN MODIFICATION: calculate height and Y position using mTextLayout 515 | textHeight = mTextLayout != null ? mTextLayout.getHeight() : 0; 516 | switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { 517 | case Gravity.BOTTOM: 518 | mExpandedDrawY = mExpandedBounds.bottom - textHeight; 519 | break; 520 | case Gravity.TOP: 521 | mExpandedDrawY = mExpandedBounds.top; 522 | break; 523 | case Gravity.CENTER_VERTICAL: 524 | default: 525 | float textOffset = (textHeight / 2); 526 | mExpandedDrawY = mExpandedBounds.centerY() - textOffset; 527 | break; 528 | } 529 | // END MODIFICATION 530 | 531 | switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { 532 | case Gravity.CENTER_HORIZONTAL: 533 | mExpandedDrawX = mExpandedBounds.centerX() - (width / 2); 534 | break; 535 | case Gravity.RIGHT: 536 | mExpandedDrawX = mExpandedBounds.right - width; 537 | break; 538 | case Gravity.LEFT: 539 | default: 540 | mExpandedDrawX = mExpandedBounds.left; 541 | break; 542 | } 543 | 544 | // The bounds have changed so we need to clear the texture 545 | clearTexture(); 546 | // Now reset the text size back to the original 547 | setInterpolatedTextSize(currentTextSize); 548 | } 549 | 550 | private void interpolateBounds(float fraction) { 551 | mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left, 552 | fraction, mPositionInterpolator); 553 | mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY, 554 | fraction, mPositionInterpolator); 555 | mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right, 556 | fraction, mPositionInterpolator); 557 | mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom, 558 | fraction, mPositionInterpolator); 559 | } 560 | 561 | public void draw(Canvas canvas) { 562 | final int saveCount = canvas.save(); 563 | 564 | if (mTextToDraw != null && mDrawTitle) { 565 | float x = mCurrentDrawX; 566 | float y = mCurrentDrawY; 567 | 568 | final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null; 569 | final float ascent; 570 | // MODIFICATION: removed now unused "descent" variable declaration 571 | 572 | // Update the TextPaint to the current text size 573 | mTextPaint.setTextSize(mCurrentTextSize); 574 | 575 | // BEGIN MODIFICATION: new drawing code 576 | if (drawTexture) { 577 | ascent = 0; 578 | } else { 579 | ascent = mTextPaint.ascent() * mScale; 580 | } 581 | 582 | if (DEBUG_DRAW) { 583 | // Just a debug tool, which drawn a magenta rect in the text bounds 584 | canvas.drawRect(mCurrentBounds.left, y, mCurrentBounds.right, 585 | y + mTextLayout.getHeight() * mScale, 586 | DEBUG_DRAW_PAINT); 587 | } 588 | if (mScale != 1f) { 589 | canvas.scale(mScale, mScale, x, y); 590 | } 591 | 592 | // Compute where to draw mTextLayout for this frame 593 | final float currentExpandedX = mCurrentDrawX + mTextLayout.getLineLeft(0) - mExpandedFirstLineDrawX * 2; 594 | if (drawTexture) { 595 | // If we should use a texture, draw it instead of text 596 | // Expanded text 597 | mTexturePaint.setAlpha((int) (mExpandedTextBlend * 255)); 598 | canvas.drawBitmap(mExpandedTitleTexture, currentExpandedX, y, mTexturePaint); 599 | // Collapsed text 600 | mTexturePaint.setAlpha((int) (mCollapsedTextBlend * 255)); 601 | canvas.drawBitmap(mCollapsedTitleTexture, x, y, mTexturePaint); 602 | // Cross-section between both texts (should stay at alpha = 255) 603 | mTexturePaint.setAlpha(255); 604 | canvas.drawBitmap(mCrossSectionTitleTexture, x, y, mTexturePaint); 605 | } else { 606 | // positon expanded text appropriately 607 | canvas.translate(currentExpandedX, y); 608 | // Expanded text 609 | mTextPaint.setAlpha((int) (mExpandedTextBlend * 255)); 610 | mTextLayout.draw(canvas); 611 | 612 | // position the overlays 613 | canvas.translate(x - currentExpandedX, 0); 614 | 615 | // Collapsed text 616 | mTextPaint.setAlpha((int) (mCollapsedTextBlend * 255)); 617 | canvas.drawText(mTextToDrawCollapsed, 0, mTextToDrawCollapsed.length(), 0, 618 | -ascent / mScale, mTextPaint); 619 | // BEGIN MODIFICATION 620 | // Remove ellipsis for Cross-section animation 621 | String tmp = mTextToDrawCollapsed.toString().trim(); 622 | if (tmp.endsWith("\u2026")) { 623 | tmp = tmp.substring(0, tmp.length() - 1); 624 | } 625 | // Cross-section between both texts (should stay at alpha = 255) 626 | mTextPaint.setAlpha(255); 627 | canvas.drawText(tmp, 0, mTextLayout.getLineEnd(0) <= tmp.length() ? mTextLayout.getLineEnd(0) : tmp.length(), 0, -ascent / mScale, mTextPaint); 628 | // END MODIFICATION 629 | } 630 | // END MODIFICATION 631 | } 632 | canvas.restoreToCount(saveCount); 633 | } 634 | 635 | private boolean calculateIsRtl(CharSequence text) { 636 | final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView) 637 | == ViewCompat.LAYOUT_DIRECTION_RTL; 638 | return (defaultIsRtl 639 | ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL 640 | : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length()); 641 | } 642 | 643 | private void setInterpolatedTextSize(float textSize) { 644 | calculateUsingTextSize(textSize); 645 | // Use our texture if the scale isn't 1.0 646 | mUseTexture = USE_SCALING_TEXTURE && mScale != 1f; 647 | if (mUseTexture) { 648 | // Make sure we have an expanded texture if needed 649 | ensureExpandedTexture(); 650 | // BEGIN MODIFICATION: added collapsed and cross section textures 651 | ensureCollapsedTexture(); 652 | ensureCrossSectionTexture(); 653 | } 654 | ViewCompat.postInvalidateOnAnimation(mView); 655 | // END MODIFICATION 656 | } 657 | 658 | // BEGIN MODIFICATION: new setCollapsedTextBlend and setExpandedTextBlend methods 659 | private void setCollapsedTextBlend(float blend) { 660 | mCollapsedTextBlend = blend; 661 | ViewCompat.postInvalidateOnAnimation(mView); 662 | } 663 | 664 | private void setExpandedTextBlend(float blend) { 665 | mExpandedTextBlend = blend; 666 | ViewCompat.postInvalidateOnAnimation(mView); 667 | } 668 | // END MODIFICATION 669 | 670 | private boolean areTypefacesDifferent(Typeface first, Typeface second) { 671 | return (first != null && !first.equals(second)) || (first == null && second != null); 672 | } 673 | 674 | private void calculateUsingTextSize(final float textSize) { 675 | if (mText == null) return; 676 | 677 | final float collapsedWidth = mCollapsedBounds.width(); 678 | final float expandedWidth = mExpandedBounds.width(); 679 | 680 | final float availableWidth; 681 | final float newTextSize; 682 | boolean updateDrawText = false; 683 | // BEGIN MODIFICATION: Add maxLines variable 684 | int maxLines; 685 | // END MODIFICATION 686 | if (isClose(textSize, mCollapsedTextSize)) { 687 | newTextSize = mCollapsedTextSize; 688 | mScale = 1f; 689 | if (areTypefacesDifferent(mCurrentTypeface, mCollapsedTypeface)) { 690 | mCurrentTypeface = mCollapsedTypeface; 691 | updateDrawText = true; 692 | } 693 | availableWidth = collapsedWidth; 694 | // BEGIN MODIFICATION: Set maxLines variable 695 | maxLines = 1; 696 | // END MODIFICATION 697 | } else { 698 | newTextSize = mExpandedTextSize; 699 | if (areTypefacesDifferent(mCurrentTypeface, mExpandedTypeface)) { 700 | mCurrentTypeface = mExpandedTypeface; 701 | updateDrawText = true; 702 | } 703 | if (isClose(textSize, mExpandedTextSize)) { 704 | // If we're close to the expanded text size, snap to it and use a scale of 1 705 | mScale = 1f; 706 | } else { 707 | // Else, we'll scale down from the expanded text size 708 | mScale = textSize / mExpandedTextSize; 709 | } 710 | final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize; 711 | // This is the size of the expanded bounds when it is scaled to match the 712 | // collapsed text size 713 | final float scaledDownWidth = expandedWidth * textSizeRatio; 714 | 715 | if (scaledDownWidth > collapsedWidth) { 716 | // If the scaled down size is larger than the actual collapsed width, we need to 717 | // cap the available width so that when the expanded text scales down, it matches 718 | // the collapsed width 719 | 720 | // BEGIN MODIFICATION: 721 | availableWidth = expandedWidth; 722 | // END MODIFICATION 723 | } else { 724 | // Otherwise we'll just use the expanded width 725 | availableWidth = expandedWidth; 726 | } 727 | 728 | // BEGIN MODIFICATION: Set maxLines variable 729 | maxLines = this.maxLines; 730 | // END MODIFICATION 731 | } 732 | if (availableWidth > 0) { 733 | updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText; 734 | mCurrentTextSize = newTextSize; 735 | mBoundsChanged = false; 736 | } 737 | if (mTextToDraw == null || updateDrawText) { 738 | mTextPaint.setTextSize(mCurrentTextSize); 739 | mTextPaint.setTypeface(mCurrentTypeface); 740 | 741 | // BEGIN MODIFICATION: Text layout creation and text truncation 742 | StaticLayout layout = new StaticLayout(mText, mTextPaint, (int) availableWidth, 743 | Layout.Alignment.ALIGN_NORMAL, lineSpacingMultiplier, lineSpacingExtra, false); 744 | CharSequence truncatedText; 745 | if (layout.getLineCount() > maxLines) { 746 | int lastLine = maxLines - 1; 747 | CharSequence textBefore = 748 | lastLine > 0 ? mText.subSequence(0, layout.getLineEnd(lastLine - 1)) : ""; 749 | CharSequence lineText = mText.subSequence(layout.getLineStart(lastLine), 750 | layout.getLineEnd(lastLine)); 751 | // if last char in line is space, move it behind the ellipsis 752 | CharSequence lineEnd = ""; 753 | if (lineText.charAt(lineText.length() - 1) == ' ') { 754 | lineEnd = lineText.subSequence(lineText.length() - 1, lineText.length()); 755 | lineText = lineText.subSequence(0, lineText.length() - 1); 756 | } 757 | // insert ellipsis character 758 | lineText = TextUtils.concat(lineText, "\u2026", lineEnd); 759 | // if the text is too long, truncate it 760 | CharSequence truncatedLineText = TextUtils.ellipsize(lineText, mTextPaint, 761 | availableWidth, TextUtils.TruncateAt.END); 762 | truncatedText = TextUtils.concat(textBefore, truncatedLineText); 763 | 764 | } else { 765 | truncatedText = mText; 766 | } 767 | if (!TextUtils.equals(truncatedText, mTextToDraw)) { 768 | mTextToDraw = truncatedText; 769 | mIsRtl = calculateIsRtl(mTextToDraw); 770 | } 771 | 772 | final Layout.Alignment alignment; 773 | 774 | // Don't rectify gravity for RTL languages, Layout.Alignment does it already. 775 | switch (mExpandedTextGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { 776 | case Gravity.CENTER_HORIZONTAL: 777 | alignment = Layout.Alignment.ALIGN_CENTER; 778 | break; 779 | case Gravity.RIGHT: 780 | case Gravity.END: 781 | alignment = Layout.Alignment.ALIGN_OPPOSITE; 782 | break; 783 | case Gravity.LEFT: 784 | case Gravity.START: 785 | default: 786 | alignment = Layout.Alignment.ALIGN_NORMAL; 787 | break; 788 | } 789 | 790 | mTextLayout = new StaticLayout(mTextToDraw, mTextPaint, (int) availableWidth, 791 | alignment, lineSpacingMultiplier, lineSpacingExtra, false); 792 | // END MODIFICATION 793 | } 794 | } 795 | 796 | private void ensureExpandedTexture() { 797 | if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty() 798 | || TextUtils.isEmpty(mTextToDraw)) { 799 | return; 800 | } 801 | calculateOffsets(0f); 802 | 803 | // BEGIN MODIFICATION: Calculate width and height using mTextLayout and remove 804 | // mTextureAscent and mTextureDescent assignment 805 | final int w = mTextLayout.getWidth(); 806 | final int h = mTextLayout.getHeight(); 807 | // END MODIFICATION 808 | 809 | if (w <= 0 || h <= 0) { 810 | return; // If the width or height are 0, return 811 | } 812 | mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 813 | 814 | // BEGIN MODIFICATION: Draw text using mTextLayout 815 | Canvas c = new Canvas(mExpandedTitleTexture); 816 | mTextLayout.draw(c); 817 | // END MODIFICATION 818 | if (mTexturePaint == null) { 819 | // Make sure we have a paint 820 | mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 821 | } 822 | } 823 | 824 | // BEGIN MODIFICATION: new ensureCollapsedTexture and ensureCrossSectionTexture methods 825 | private void ensureCollapsedTexture() { 826 | if (mCollapsedTitleTexture != null || mCollapsedBounds.isEmpty() 827 | || TextUtils.isEmpty(mTextToDraw)) { 828 | return; 829 | } 830 | calculateOffsets(0f); 831 | final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length())); 832 | final int h = Math.round(mTextPaint.descent() - mTextPaint.ascent()); 833 | if (w <= 0 && h <= 0) { 834 | return; // If the width or height are 0, return 835 | } 836 | mCollapsedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 837 | Canvas c = new Canvas(mCollapsedTitleTexture); 838 | c.drawText(mTextToDrawCollapsed, 0, mTextToDrawCollapsed.length(), 0, 839 | -mTextPaint.ascent() / mScale, mTextPaint); 840 | if (mTexturePaint == null) { 841 | // Make sure we have a paint 842 | mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 843 | } 844 | } 845 | 846 | private void ensureCrossSectionTexture() { 847 | if (mCrossSectionTitleTexture != null || mCollapsedBounds.isEmpty() 848 | || TextUtils.isEmpty(mTextToDraw)) { 849 | return; 850 | } 851 | calculateOffsets(0f); 852 | final int w = Math.round(mTextPaint.measureText(mTextToDraw, mTextLayout.getLineStart(0), 853 | mTextLayout.getLineEnd(0))); 854 | final int h = Math.round(mTextPaint.descent() - mTextPaint.ascent()); 855 | if (w <= 0 && h <= 0) { 856 | return; // If the width or height are 0, return 857 | } 858 | mCrossSectionTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 859 | Canvas c = new Canvas(mCrossSectionTitleTexture); 860 | String tmp = mTextToDrawCollapsed.toString().trim(); 861 | if (tmp.endsWith("\u2026")) { 862 | tmp = tmp.substring(0, tmp.length() - 1); 863 | } 864 | c.drawText(tmp, 0, 865 | mTextLayout.getLineEnd(0) <= tmp.length() ? mTextLayout.getLineEnd(0) : tmp.length(), 0, -mTextPaint.ascent() / mScale, mTextPaint); 866 | if (mTexturePaint == null) { 867 | // Make sure we have a paint 868 | mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 869 | } 870 | } 871 | // END MODIFICATION 872 | 873 | public void recalculate() { 874 | if (mView.getHeight() > 0 && mView.getWidth() > 0) { 875 | // If we've already been laid out, calculate everything now otherwise we'll wait 876 | // until a layout 877 | calculateBaseOffsets(); 878 | calculateCurrentOffsets(); 879 | } 880 | } 881 | 882 | /** 883 | * Set the title to display 884 | * 885 | * @param text 886 | */ 887 | void setText(CharSequence text) { 888 | if (text == null || !text.equals(mText)) { 889 | mText = text; 890 | mTextToDraw = null; 891 | clearTexture(); 892 | recalculate(); 893 | } 894 | } 895 | 896 | CharSequence getText() { 897 | return mText; 898 | } 899 | 900 | private void clearTexture() { 901 | if (mExpandedTitleTexture != null) { 902 | mExpandedTitleTexture.recycle(); 903 | mExpandedTitleTexture = null; 904 | } 905 | // BEGIN MODIFICATION: clear other textures 906 | if (mCollapsedTitleTexture != null) { 907 | mCollapsedTitleTexture.recycle(); 908 | mCollapsedTitleTexture = null; 909 | } 910 | if (mCrossSectionTitleTexture != null) { 911 | mCrossSectionTitleTexture.recycle(); 912 | mCrossSectionTitleTexture = null; 913 | } 914 | // END MODIFICATION 915 | } 916 | 917 | /** 918 | * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently 919 | * defined as it's difference being < 0.001. 920 | */ 921 | private static boolean isClose(float value, float targetValue) { 922 | return Math.abs(value - targetValue) < 0.001f; 923 | } 924 | 925 | ColorStateList getExpandedTextColor() { 926 | return mExpandedTextColor; 927 | } 928 | 929 | ColorStateList getCollapsedTextColor() { 930 | return mCollapsedTextColor; 931 | } 932 | 933 | /** 934 | * Blend {@code color1} and {@code color2} using the given ratio. 935 | * 936 | * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend, 937 | * 1.0 will return {@code color2}. 938 | */ 939 | private static int blendColors(int color1, int color2, float ratio) { 940 | final float inverseRatio = 1f - ratio; 941 | float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio); 942 | float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio); 943 | float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio); 944 | float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio); 945 | return Color.argb((int) a, (int) r, (int) g, (int) b); 946 | } 947 | 948 | private static float lerp(float startValue, float endValue, float fraction, 949 | Interpolator interpolator) { 950 | if (interpolator != null) { 951 | fraction = interpolator.getInterpolation(fraction); 952 | } 953 | return AnimationUtils.lerp(startValue, endValue, fraction); 954 | } 955 | 956 | private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) { 957 | return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom); 958 | } 959 | } 960 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/java/net/opacapp/multilinecollapsingtoolbar/CollapsingToolbarLayout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.opacapp.multilinecollapsingtoolbar; 18 | 19 | import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 | 21 | import android.animation.ValueAnimator; 22 | import android.content.Context; 23 | import android.content.res.ColorStateList; 24 | import android.content.res.TypedArray; 25 | import android.graphics.Canvas; 26 | import android.graphics.Rect; 27 | import android.graphics.Typeface; 28 | import android.graphics.drawable.ColorDrawable; 29 | import android.graphics.drawable.Drawable; 30 | import android.support.annotation.ColorInt; 31 | import android.support.annotation.DrawableRes; 32 | import android.support.annotation.IntDef; 33 | import android.support.annotation.IntRange; 34 | import android.support.annotation.NonNull; 35 | import android.support.annotation.Nullable; 36 | import android.support.annotation.RequiresApi; 37 | import android.support.annotation.RestrictTo; 38 | import android.support.annotation.StyleRes; 39 | import android.support.design.R; 40 | import android.support.design.widget.AppBarLayout; 41 | import android.support.v4.content.ContextCompat; 42 | import android.support.v4.graphics.drawable.DrawableCompat; 43 | import android.support.v4.math.MathUtils; 44 | import android.support.v4.util.ObjectsCompat; 45 | import android.support.v4.view.GravityCompat; 46 | import android.support.v4.view.ViewCompat; 47 | import android.support.v4.view.WindowInsetsCompat; 48 | import android.support.v4.widget.ViewGroupUtils; 49 | import android.support.v7.widget.Toolbar; 50 | import android.text.TextUtils; 51 | import android.util.AttributeSet; 52 | import android.view.Gravity; 53 | import android.view.View; 54 | import android.view.ViewGroup; 55 | import android.view.ViewParent; 56 | import android.widget.FrameLayout; 57 | 58 | import java.lang.annotation.Retention; 59 | import java.lang.annotation.RetentionPolicy; 60 | 61 | /** 62 | * CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar. 63 | * It is designed to be used as a direct child of a {@link AppBarLayout}. 64 | * CollapsingToolbarLayout contains the following features: 65 | * 66 | *

Collapsing title

67 | * A title which is larger when the layout is fully visible but collapses and becomes smaller as 68 | * the layout is scrolled off screen. You can set the title to display via 69 | * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the 70 | * {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes. 71 | * 72 | *

Content scrim

73 | * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold. 74 | * You can change this via {@link #setContentScrim(Drawable)}. 75 | * 76 | *

Status bar scrim

77 | * A scrim which is show or hidden behind the status bar when the scroll position has hit a certain 78 | * threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works 79 | * on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system 80 | * windows. 81 | * 82 | *

Parallax scrolling children

83 | * Child views can opt to be scrolled within this layout in a parallax fashion. 84 | * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and 85 | * {@link LayoutParams#setParallaxMultiplier(float)}. 86 | * 87 | *

Pinned position children

88 | * Child views can opt to be pinned in space globally. This is useful when implementing a 89 | * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is 90 | * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}. 91 | * 92 | *

Do not manually add views to the Toolbar at run time. 93 | * We will add a 'dummy view' to the Toolbar which allows us to work out the available space 94 | * for the title. This can interfere with any views which you add.

95 | * 96 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance 97 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance 98 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_contentScrim 99 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin 100 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart 101 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd 102 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom 103 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_statusBarScrim 104 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_toolbarId 105 | */ 106 | public class CollapsingToolbarLayout extends FrameLayout { 107 | 108 | private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600; 109 | 110 | private boolean mRefreshToolbar = true; 111 | private int mToolbarId; 112 | private Toolbar mToolbar; 113 | private View mToolbarDirectChild; 114 | private View mDummyView; 115 | 116 | private int mExpandedMarginStart; 117 | private int mExpandedMarginTop; 118 | private int mExpandedMarginEnd; 119 | private int mExpandedMarginBottom; 120 | 121 | private final Rect mTmpRect = new Rect(); 122 | private final CollapsingTextHelper mCollapsingTextHelper; 123 | private boolean mCollapsingTitleEnabled; 124 | private boolean mDrawCollapsingTitle; 125 | 126 | private Drawable mContentScrim; 127 | Drawable mStatusBarScrim; 128 | private int mScrimAlpha; 129 | private boolean mScrimsAreShown; 130 | private ValueAnimator mScrimAnimator; 131 | private long mScrimAnimationDuration; 132 | private int mScrimVisibleHeightTrigger = -1; 133 | 134 | private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener; 135 | 136 | int mCurrentOffset; 137 | 138 | WindowInsetsCompat mLastInsets; 139 | 140 | public CollapsingToolbarLayout(Context context) { 141 | this(context, null); 142 | } 143 | 144 | public CollapsingToolbarLayout(Context context, AttributeSet attrs) { 145 | this(context, attrs, 0); 146 | } 147 | 148 | public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) { 149 | super(context, attrs, defStyleAttr); 150 | 151 | ThemeUtils.checkAppCompatTheme(context); 152 | 153 | mCollapsingTextHelper = new CollapsingTextHelper(this); 154 | mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR); 155 | 156 | // BEGIN MODIFICATION: use own default style 157 | TypedArray a = context.obtainStyledAttributes(attrs, 158 | R.styleable.CollapsingToolbarLayout, defStyleAttr, 159 | net.opacapp.multilinecollapsingtoolbar.R.style.Widget_Design_MultilineCollapsingToolbar); 160 | // END MODIFICATION 161 | 162 | mCollapsingTextHelper.setExpandedTextGravity( 163 | a.getInt(R.styleable.CollapsingToolbarLayout_expandedTitleGravity, 164 | GravityCompat.START | Gravity.BOTTOM)); 165 | mCollapsingTextHelper.setCollapsedTextGravity( 166 | a.getInt(R.styleable.CollapsingToolbarLayout_collapsedTitleGravity, 167 | GravityCompat.START | Gravity.CENTER_VERTICAL)); 168 | 169 | mExpandedMarginStart = mExpandedMarginTop = mExpandedMarginEnd = mExpandedMarginBottom = 170 | a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_expandedTitleMargin, 0); 171 | 172 | if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) { 173 | mExpandedMarginStart = a.getDimensionPixelSize( 174 | R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart, 0); 175 | } 176 | if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) { 177 | mExpandedMarginEnd = a.getDimensionPixelSize( 178 | R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd, 0); 179 | } 180 | if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop)) { 181 | mExpandedMarginTop = a.getDimensionPixelSize( 182 | R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop, 0); 183 | } 184 | if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) { 185 | mExpandedMarginBottom = a.getDimensionPixelSize( 186 | R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom, 0); 187 | } 188 | 189 | mCollapsingTitleEnabled = a.getBoolean( 190 | R.styleable.CollapsingToolbarLayout_titleEnabled, true); 191 | setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title)); 192 | 193 | // First load the default text appearances 194 | mCollapsingTextHelper.setExpandedTextAppearance( 195 | R.style.TextAppearance_Design_CollapsingToolbar_Expanded); 196 | // BEGIN MODIFICATION: use own default style 197 | mCollapsingTextHelper.setCollapsedTextAppearance( 198 | net.opacapp.multilinecollapsingtoolbar.R.style.ActionBar_Title); 199 | // END MODIFICATION 200 | 201 | // Now overlay any custom text appearances 202 | if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance)) { 203 | mCollapsingTextHelper.setExpandedTextAppearance( 204 | a.getResourceId( 205 | R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance, 0)); 206 | } 207 | if (a.hasValue(R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance)) { 208 | mCollapsingTextHelper.setCollapsedTextAppearance( 209 | a.getResourceId( 210 | R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance, 0)); 211 | } 212 | 213 | mScrimVisibleHeightTrigger = a.getDimensionPixelSize( 214 | R.styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger, -1); 215 | 216 | mScrimAnimationDuration = a.getInt( 217 | R.styleable.CollapsingToolbarLayout_scrimAnimationDuration, 218 | DEFAULT_SCRIM_ANIMATION_DURATION); 219 | 220 | setContentScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim)); 221 | setStatusBarScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim)); 222 | 223 | mToolbarId = a.getResourceId(R.styleable.CollapsingToolbarLayout_toolbarId, -1); 224 | 225 | a.recycle(); 226 | 227 | setWillNotDraw(false); 228 | 229 | ViewCompat.setOnApplyWindowInsetsListener(this, 230 | new android.support.v4.view.OnApplyWindowInsetsListener() { 231 | @Override 232 | public WindowInsetsCompat onApplyWindowInsets(View v, 233 | WindowInsetsCompat insets) { 234 | return onWindowInsetChanged(insets); 235 | } 236 | }); 237 | 238 | // BEGIN MODIFICATION: set the value of maxNumberOfLines attribute to the mCollapsingTextHelper 239 | TypedArray typedArray = context.obtainStyledAttributes(attrs, net.opacapp.multilinecollapsingtoolbar.R.styleable.CollapsingToolbarLayoutExtension, defStyleAttr, 0); 240 | mCollapsingTextHelper.setMaxLines(typedArray.getInteger(net.opacapp.multilinecollapsingtoolbar.R.styleable.CollapsingToolbarLayoutExtension_maxLines, 3)); 241 | mCollapsingTextHelper.setLineSpacingExtra(typedArray.getFloat(net.opacapp.multilinecollapsingtoolbar.R.styleable.CollapsingToolbarLayoutExtension_lineSpacingExtra, 0)); 242 | mCollapsingTextHelper.setLineSpacingMultiplier(typedArray.getFloat(net.opacapp.multilinecollapsingtoolbar.R.styleable.CollapsingToolbarLayoutExtension_lineSpacingMultiplier, 1)); 243 | typedArray.recycle(); 244 | // END MODIFICATION 245 | } 246 | 247 | // BEGIN MODIFICATION: add setMaxLines and getMaxLines 248 | /** 249 | * Sets the maximum number of lines to display in the expanded state 250 | */ 251 | public void setMaxLines(int maxLines) { 252 | mCollapsingTextHelper.setMaxLines(maxLines); 253 | } 254 | 255 | /** 256 | * Gets the maximum number of lines to display in the expanded state 257 | */ 258 | public int getMaxLines() { 259 | return mCollapsingTextHelper.getMaxLines(); 260 | } 261 | // END MODIFICATION 262 | 263 | // BEGIN MODIFICATION: add setLineSpacingExtra and getLineSpacingExtra 264 | /** 265 | * Set line spacing extra. The default is 0.0f 266 | */ 267 | void setLineSpacingExtra(float lineSpacingExtra) { 268 | mCollapsingTextHelper.setLineSpacingExtra(lineSpacingExtra); 269 | } 270 | 271 | /** 272 | * Gets the line spacing extra applied to each line in the expanded state 273 | */ 274 | float getLineSpacingExtra() { 275 | return mCollapsingTextHelper.getLineSpacingExtra(); 276 | } 277 | // END MODIFICATION 278 | 279 | // BEGIN MODIFICATION: add setLineSpacingExtra and getLineSpacingExtra 280 | 281 | /** 282 | * Set line spacing multiplier. The default is 1.0f 283 | */ 284 | void setLineSpacingMultiplier(float lineSpacingMultiplier) { 285 | mCollapsingTextHelper.setLineSpacingMultiplier(lineSpacingMultiplier); 286 | } 287 | 288 | /** 289 | * Gets the line spacing multiplier applied to each line in the expanded state 290 | */ 291 | float getLineSpacingMultiplier() { 292 | return mCollapsingTextHelper.getLineSpacingMultiplier(); 293 | } 294 | // END MODIFICATION 295 | 296 | @Override 297 | protected void onAttachedToWindow() { 298 | super.onAttachedToWindow(); 299 | 300 | // Add an OnOffsetChangedListener if possible 301 | final ViewParent parent = getParent(); 302 | if (parent instanceof AppBarLayout) { 303 | // Copy over from the ABL whether we should fit system windows 304 | ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent)); 305 | 306 | if (mOnOffsetChangedListener == null) { 307 | mOnOffsetChangedListener = new OffsetUpdateListener(); 308 | } 309 | ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener); 310 | 311 | // We're attached, so lets request an inset dispatch 312 | ViewCompat.requestApplyInsets(this); 313 | } 314 | } 315 | 316 | @Override 317 | protected void onDetachedFromWindow() { 318 | // Remove our OnOffsetChangedListener if possible and it exists 319 | final ViewParent parent = getParent(); 320 | if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) { 321 | ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener); 322 | } 323 | 324 | super.onDetachedFromWindow(); 325 | } 326 | 327 | WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) { 328 | WindowInsetsCompat newInsets = null; 329 | 330 | if (ViewCompat.getFitsSystemWindows(this)) { 331 | // If we're set to fit system windows, keep the insets 332 | newInsets = insets; 333 | } 334 | 335 | // If our insets have changed, keep them and invalidate the scroll ranges... 336 | if (!ObjectsCompat.equals(mLastInsets, newInsets)) { 337 | mLastInsets = newInsets; 338 | requestLayout(); 339 | } 340 | 341 | // Consume the insets. This is done so that child views with fitSystemWindows=true do not 342 | // get the default padding functionality from View 343 | return insets.consumeSystemWindowInsets(); 344 | } 345 | 346 | @Override 347 | public void draw(Canvas canvas) { 348 | super.draw(canvas); 349 | 350 | // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below. 351 | // Instead, we draw it here, before our collapsing text. 352 | ensureToolbar(); 353 | if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) { 354 | mContentScrim.mutate().setAlpha(mScrimAlpha); 355 | mContentScrim.draw(canvas); 356 | } 357 | 358 | // Let the collapsing text helper draw its text 359 | if (mCollapsingTitleEnabled && mDrawCollapsingTitle) { 360 | mCollapsingTextHelper.draw(canvas); 361 | } 362 | 363 | // Now draw the status bar scrim 364 | if (mStatusBarScrim != null && mScrimAlpha > 0) { 365 | final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 366 | if (topInset > 0) { 367 | mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(), 368 | topInset - mCurrentOffset); 369 | mStatusBarScrim.mutate().setAlpha(mScrimAlpha); 370 | mStatusBarScrim.draw(canvas); 371 | } 372 | } 373 | } 374 | 375 | @Override 376 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 377 | // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present), 378 | // but in front of any other children which are behind it. To do this we intercept the 379 | // drawChild() call, and draw our scrim just before the Toolbar is drawn 380 | boolean invalidated = false; 381 | if (mContentScrim != null && mScrimAlpha > 0 && isToolbarChild(child)) { 382 | mContentScrim.mutate().setAlpha(mScrimAlpha); 383 | mContentScrim.draw(canvas); 384 | invalidated = true; 385 | } 386 | return super.drawChild(canvas, child, drawingTime) || invalidated; 387 | } 388 | 389 | @Override 390 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 391 | super.onSizeChanged(w, h, oldw, oldh); 392 | if (mContentScrim != null) { 393 | mContentScrim.setBounds(0, 0, w, h); 394 | } 395 | } 396 | 397 | private void ensureToolbar() { 398 | if (!mRefreshToolbar) { 399 | return; 400 | } 401 | 402 | // First clear out the current Toolbar 403 | mToolbar = null; 404 | mToolbarDirectChild = null; 405 | 406 | if (mToolbarId != -1) { 407 | // If we have an ID set, try and find it and it's direct parent to us 408 | mToolbar = findViewById(mToolbarId); 409 | if (mToolbar != null) { 410 | mToolbarDirectChild = findDirectChild(mToolbar); 411 | } 412 | } 413 | 414 | if (mToolbar == null) { 415 | // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find 416 | // one from our direct children 417 | Toolbar toolbar = null; 418 | for (int i = 0, count = getChildCount(); i < count; i++) { 419 | final View child = getChildAt(i); 420 | if (child instanceof Toolbar) { 421 | toolbar = (Toolbar) child; 422 | break; 423 | } 424 | } 425 | mToolbar = toolbar; 426 | } 427 | 428 | updateDummyView(); 429 | mRefreshToolbar = false; 430 | } 431 | 432 | private boolean isToolbarChild(View child) { 433 | return (mToolbarDirectChild == null || mToolbarDirectChild == this) 434 | ? child == mToolbar 435 | : child == mToolbarDirectChild; 436 | } 437 | 438 | /** 439 | * Returns the direct child of this layout, which itself is the ancestor of the 440 | * given view. 441 | */ 442 | private View findDirectChild(final View descendant) { 443 | View directChild = descendant; 444 | for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) { 445 | if (p instanceof View) { 446 | directChild = (View) p; 447 | } 448 | } 449 | return directChild; 450 | } 451 | 452 | private void updateDummyView() { 453 | if (!mCollapsingTitleEnabled && mDummyView != null) { 454 | // If we have a dummy view and we have our title disabled, remove it from its parent 455 | final ViewParent parent = mDummyView.getParent(); 456 | if (parent instanceof ViewGroup) { 457 | ((ViewGroup) parent).removeView(mDummyView); 458 | } 459 | } 460 | if (mCollapsingTitleEnabled && mToolbar != null) { 461 | if (mDummyView == null) { 462 | mDummyView = new View(getContext()); 463 | } 464 | if (mDummyView.getParent() == null) { 465 | mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 466 | } 467 | } 468 | } 469 | 470 | @Override 471 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 472 | ensureToolbar(); 473 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 474 | 475 | final int mode = MeasureSpec.getMode(heightMeasureSpec); 476 | final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 477 | if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) { 478 | // If we have a top inset and we're set to wrap_content height we need to make sure 479 | // we add the top inset to our height, therefore we re-measure 480 | heightMeasureSpec = MeasureSpec.makeMeasureSpec( 481 | getMeasuredHeight() + topInset, MeasureSpec.EXACTLY); 482 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 483 | } 484 | } 485 | 486 | @Override 487 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 488 | super.onLayout(changed, left, top, right, bottom); 489 | 490 | if (mLastInsets != null) { 491 | // Shift down any views which are not set to fit system windows 492 | final int insetTop = mLastInsets.getSystemWindowInsetTop(); 493 | for (int i = 0, z = getChildCount(); i < z; i++) { 494 | final View child = getChildAt(i); 495 | if (!ViewCompat.getFitsSystemWindows(child)) { 496 | if (child.getTop() < insetTop) { 497 | // If the child isn't set to fit system windows but is drawing within 498 | // the inset offset it down 499 | ViewCompat.offsetTopAndBottom(child, insetTop); 500 | } 501 | } 502 | } 503 | } 504 | 505 | // Update the collapsed bounds by getting it's transformed bounds 506 | if (mCollapsingTitleEnabled && mDummyView != null) { 507 | // We only draw the title if the dummy view is being displayed (Toolbar removes 508 | // views if there is no space) 509 | mDrawCollapsingTitle = ViewCompat.isAttachedToWindow(mDummyView) 510 | && mDummyView.getVisibility() == VISIBLE; 511 | 512 | if (mDrawCollapsingTitle) { 513 | final boolean isRtl = ViewCompat.getLayoutDirection(this) 514 | == ViewCompat.LAYOUT_DIRECTION_RTL; 515 | 516 | // Update the collapsed bounds 517 | final int maxOffset = getMaxOffsetForPinChild( 518 | mToolbarDirectChild != null ? mToolbarDirectChild : mToolbar); 519 | ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect); 520 | mCollapsingTextHelper.setCollapsedBounds( 521 | mTmpRect.left + (isRtl 522 | ? mToolbar.getTitleMarginEnd() 523 | : mToolbar.getTitleMarginStart()), 524 | mTmpRect.top + maxOffset + mToolbar.getTitleMarginTop(), 525 | mTmpRect.right + (isRtl 526 | ? mToolbar.getTitleMarginStart() 527 | : mToolbar.getTitleMarginEnd()), 528 | mTmpRect.bottom + maxOffset - mToolbar.getTitleMarginBottom()); 529 | 530 | // Update the expanded bounds 531 | mCollapsingTextHelper.setExpandedBounds( 532 | isRtl ? mExpandedMarginEnd : mExpandedMarginStart, 533 | mTmpRect.top + mExpandedMarginTop, 534 | right - left - (isRtl ? mExpandedMarginStart : mExpandedMarginEnd), 535 | bottom - top - mExpandedMarginBottom); 536 | // Now recalculate using the new bounds 537 | mCollapsingTextHelper.recalculate(); 538 | } 539 | } 540 | 541 | // Update our child view offset helpers. This needs to be done after the title has been 542 | // setup, so that any Toolbars are in their original position 543 | for (int i = 0, z = getChildCount(); i < z; i++) { 544 | getViewOffsetHelper(getChildAt(i)).onViewLayout(); 545 | } 546 | 547 | // Finally, set our minimum height to enable proper AppBarLayout collapsing 548 | if (mToolbar != null) { 549 | if (mCollapsingTitleEnabled && TextUtils.isEmpty(mCollapsingTextHelper.getText())) { 550 | // If we do not currently have a title, try and grab it from the Toolbar 551 | mCollapsingTextHelper.setText(mToolbar.getTitle()); 552 | } 553 | if (mToolbarDirectChild == null || mToolbarDirectChild == this) { 554 | setMinimumHeight(getHeightWithMargins(mToolbar)); 555 | } else { 556 | setMinimumHeight(getHeightWithMargins(mToolbarDirectChild)); 557 | } 558 | } 559 | 560 | updateScrimVisibility(); 561 | } 562 | 563 | private static int getHeightWithMargins(@NonNull final View view) { 564 | final ViewGroup.LayoutParams lp = view.getLayoutParams(); 565 | if (lp instanceof MarginLayoutParams) { 566 | final MarginLayoutParams mlp = (MarginLayoutParams) lp; 567 | return view.getHeight() + mlp.topMargin + mlp.bottomMargin; 568 | } 569 | return view.getHeight(); 570 | } 571 | 572 | static ViewOffsetHelper getViewOffsetHelper(View view) { 573 | ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper); 574 | if (offsetHelper == null) { 575 | offsetHelper = new ViewOffsetHelper(view); 576 | view.setTag(R.id.view_offset_helper, offsetHelper); 577 | } 578 | return offsetHelper; 579 | } 580 | 581 | /** 582 | * Sets the title to be displayed by this view, if enabled. 583 | * 584 | * @see #setTitleEnabled(boolean) 585 | * @see #getTitle() 586 | * 587 | * @attr ref R.styleable#CollapsingToolbarLayout_title 588 | */ 589 | public void setTitle(@Nullable CharSequence title) { 590 | mCollapsingTextHelper.setText(title); 591 | } 592 | 593 | /** 594 | * Returns the title currently being displayed by this view. If the title is not enabled, then 595 | * this will return {@code null}. 596 | * 597 | * @attr ref R.styleable#CollapsingToolbarLayout_title 598 | */ 599 | @Nullable 600 | public CharSequence getTitle() { 601 | return mCollapsingTitleEnabled ? mCollapsingTextHelper.getText() : null; 602 | } 603 | 604 | /** 605 | * Sets whether this view should display its own title. 606 | * 607 | *

The title displayed by this view will shrink and grow based on the scroll offset.

608 | * 609 | * @see #setTitle(CharSequence) 610 | * @see #isTitleEnabled() 611 | * 612 | * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled 613 | */ 614 | public void setTitleEnabled(boolean enabled) { 615 | if (enabled != mCollapsingTitleEnabled) { 616 | mCollapsingTitleEnabled = enabled; 617 | updateDummyView(); 618 | requestLayout(); 619 | } 620 | } 621 | 622 | /** 623 | * Returns whether this view is currently displaying its own title. 624 | * 625 | * @see #setTitleEnabled(boolean) 626 | * 627 | * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled 628 | */ 629 | public boolean isTitleEnabled() { 630 | return mCollapsingTitleEnabled; 631 | } 632 | 633 | /** 634 | * Set whether the content scrim and/or status bar scrim should be shown or not. Any change 635 | * in the vertical scroll may overwrite this value. Any visibility change will be animated if 636 | * this view has already been laid out. 637 | * 638 | * @param shown whether the scrims should be shown 639 | * 640 | * @see #getStatusBarScrim() 641 | * @see #getContentScrim() 642 | */ 643 | public void setScrimsShown(boolean shown) { 644 | setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode()); 645 | } 646 | 647 | /** 648 | * Set whether the content scrim and/or status bar scrim should be shown or not. Any change 649 | * in the vertical scroll may overwrite this value. 650 | * 651 | * @param shown whether the scrims should be shown 652 | * @param animate whether to animate the visibility change 653 | * 654 | * @see #getStatusBarScrim() 655 | * @see #getContentScrim() 656 | */ 657 | public void setScrimsShown(boolean shown, boolean animate) { 658 | if (mScrimsAreShown != shown) { 659 | if (animate) { 660 | animateScrim(shown ? 0xFF : 0x0); 661 | } else { 662 | setScrimAlpha(shown ? 0xFF : 0x0); 663 | } 664 | mScrimsAreShown = shown; 665 | } 666 | } 667 | 668 | private void animateScrim(int targetAlpha) { 669 | ensureToolbar(); 670 | if (mScrimAnimator == null) { 671 | mScrimAnimator = new ValueAnimator(); 672 | mScrimAnimator.setDuration(mScrimAnimationDuration); 673 | mScrimAnimator.setInterpolator( 674 | targetAlpha > mScrimAlpha 675 | ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR 676 | : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); 677 | mScrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 678 | @Override 679 | public void onAnimationUpdate(ValueAnimator animator) { 680 | setScrimAlpha((int) animator.getAnimatedValue()); 681 | } 682 | }); 683 | } else if (mScrimAnimator.isRunning()) { 684 | mScrimAnimator.cancel(); 685 | } 686 | 687 | mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha); 688 | mScrimAnimator.start(); 689 | } 690 | 691 | void setScrimAlpha(int alpha) { 692 | if (alpha != mScrimAlpha) { 693 | final Drawable contentScrim = mContentScrim; 694 | if (contentScrim != null && mToolbar != null) { 695 | ViewCompat.postInvalidateOnAnimation(mToolbar); 696 | } 697 | mScrimAlpha = alpha; 698 | ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this); 699 | } 700 | } 701 | 702 | int getScrimAlpha() { 703 | return mScrimAlpha; 704 | } 705 | 706 | /** 707 | * Set the drawable to use for the content scrim from resources. Providing null will disable 708 | * the scrim functionality. 709 | * 710 | * @param drawable the drawable to display 711 | * 712 | * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 713 | * @see #getContentScrim() 714 | */ 715 | public void setContentScrim(@Nullable Drawable drawable) { 716 | if (mContentScrim != drawable) { 717 | if (mContentScrim != null) { 718 | mContentScrim.setCallback(null); 719 | } 720 | mContentScrim = drawable != null ? drawable.mutate() : null; 721 | if (mContentScrim != null) { 722 | mContentScrim.setBounds(0, 0, getWidth(), getHeight()); 723 | mContentScrim.setCallback(this); 724 | mContentScrim.setAlpha(mScrimAlpha); 725 | } 726 | ViewCompat.postInvalidateOnAnimation(this); 727 | } 728 | } 729 | 730 | /** 731 | * Set the color to use for the content scrim. 732 | * 733 | * @param color the color to display 734 | * 735 | * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 736 | * @see #getContentScrim() 737 | */ 738 | public void setContentScrimColor(@ColorInt int color) { 739 | setContentScrim(new ColorDrawable(color)); 740 | } 741 | 742 | /** 743 | * Set the drawable to use for the content scrim from resources. 744 | * 745 | * @param resId drawable resource id 746 | * 747 | * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 748 | * @see #getContentScrim() 749 | */ 750 | public void setContentScrimResource(@DrawableRes int resId) { 751 | setContentScrim(ContextCompat.getDrawable(getContext(), resId)); 752 | 753 | } 754 | 755 | /** 756 | * Returns the drawable which is used for the foreground scrim. 757 | * 758 | * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim 759 | * @see #setContentScrim(Drawable) 760 | */ 761 | @Nullable 762 | public Drawable getContentScrim() { 763 | return mContentScrim; 764 | } 765 | 766 | /** 767 | * Set the drawable to use for the status bar scrim from resources. 768 | * Providing null will disable the scrim functionality. 769 | * 770 | *

This scrim is only shown when we have been given a top system inset.

771 | * 772 | * @param drawable the drawable to display 773 | * 774 | * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 775 | * @see #getStatusBarScrim() 776 | */ 777 | public void setStatusBarScrim(@Nullable Drawable drawable) { 778 | if (mStatusBarScrim != drawable) { 779 | if (mStatusBarScrim != null) { 780 | mStatusBarScrim.setCallback(null); 781 | } 782 | mStatusBarScrim = drawable != null ? drawable.mutate() : null; 783 | if (mStatusBarScrim != null) { 784 | if (mStatusBarScrim.isStateful()) { 785 | mStatusBarScrim.setState(getDrawableState()); 786 | } 787 | DrawableCompat.setLayoutDirection(mStatusBarScrim, 788 | ViewCompat.getLayoutDirection(this)); 789 | mStatusBarScrim.setVisible(getVisibility() == VISIBLE, false); 790 | mStatusBarScrim.setCallback(this); 791 | mStatusBarScrim.setAlpha(mScrimAlpha); 792 | } 793 | ViewCompat.postInvalidateOnAnimation(this); 794 | } 795 | } 796 | 797 | @Override 798 | protected void drawableStateChanged() { 799 | super.drawableStateChanged(); 800 | 801 | final int[] state = getDrawableState(); 802 | boolean changed = false; 803 | 804 | Drawable d = mStatusBarScrim; 805 | if (d != null && d.isStateful()) { 806 | changed |= d.setState(state); 807 | } 808 | d = mContentScrim; 809 | if (d != null && d.isStateful()) { 810 | changed |= d.setState(state); 811 | } 812 | if (mCollapsingTextHelper != null) { 813 | changed |= mCollapsingTextHelper.setState(state); 814 | } 815 | 816 | if (changed) { 817 | invalidate(); 818 | } 819 | } 820 | 821 | @Override 822 | protected boolean verifyDrawable(Drawable who) { 823 | return super.verifyDrawable(who) || who == mContentScrim || who == mStatusBarScrim; 824 | } 825 | 826 | @Override 827 | public void setVisibility(int visibility) { 828 | super.setVisibility(visibility); 829 | 830 | final boolean visible = visibility == VISIBLE; 831 | if (mStatusBarScrim != null && mStatusBarScrim.isVisible() != visible) { 832 | mStatusBarScrim.setVisible(visible, false); 833 | } 834 | if (mContentScrim != null && mContentScrim.isVisible() != visible) { 835 | mContentScrim.setVisible(visible, false); 836 | } 837 | } 838 | 839 | /** 840 | * Set the color to use for the status bar scrim. 841 | * 842 | *

This scrim is only shown when we have been given a top system inset.

843 | * 844 | * @param color the color to display 845 | * 846 | * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 847 | * @see #getStatusBarScrim() 848 | */ 849 | public void setStatusBarScrimColor(@ColorInt int color) { 850 | setStatusBarScrim(new ColorDrawable(color)); 851 | } 852 | 853 | /** 854 | * Set the drawable to use for the content scrim from resources. 855 | * 856 | * @param resId drawable resource id 857 | * 858 | * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 859 | * @see #getStatusBarScrim() 860 | */ 861 | public void setStatusBarScrimResource(@DrawableRes int resId) { 862 | setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId)); 863 | } 864 | 865 | /** 866 | * Returns the drawable which is used for the status bar scrim. 867 | * 868 | * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim 869 | * @see #setStatusBarScrim(Drawable) 870 | */ 871 | @Nullable 872 | public Drawable getStatusBarScrim() { 873 | return mStatusBarScrim; 874 | } 875 | 876 | /** 877 | * Sets the text color and size for the collapsed title from the specified 878 | * TextAppearance resource. 879 | * 880 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance 881 | */ 882 | public void setCollapsedTitleTextAppearance(@StyleRes int resId) { 883 | mCollapsingTextHelper.setCollapsedTextAppearance(resId); 884 | } 885 | 886 | /** 887 | * Sets the text color of the collapsed title. 888 | * 889 | * @param color The new text color in ARGB format 890 | */ 891 | public void setCollapsedTitleTextColor(@ColorInt int color) { 892 | setCollapsedTitleTextColor(ColorStateList.valueOf(color)); 893 | } 894 | 895 | /** 896 | * Sets the text colors of the collapsed title. 897 | * 898 | * @param colors ColorStateList containing the new text colors 899 | */ 900 | public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) { 901 | mCollapsingTextHelper.setCollapsedTextColor(colors); 902 | } 903 | 904 | /** 905 | * Sets the horizontal alignment of the collapsed title and the vertical gravity that will 906 | * be used when there is extra space in the collapsed bounds beyond what is required for 907 | * the title itself. 908 | * 909 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity 910 | */ 911 | public void setCollapsedTitleGravity(int gravity) { 912 | mCollapsingTextHelper.setCollapsedTextGravity(gravity); 913 | } 914 | 915 | /** 916 | * Returns the horizontal and vertical alignment for title when collapsed. 917 | * 918 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity 919 | */ 920 | public int getCollapsedTitleGravity() { 921 | return mCollapsingTextHelper.getCollapsedTextGravity(); 922 | } 923 | 924 | /** 925 | * Sets the text color and size for the expanded title from the specified 926 | * TextAppearance resource. 927 | * 928 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance 929 | */ 930 | public void setExpandedTitleTextAppearance(@StyleRes int resId) { 931 | mCollapsingTextHelper.setExpandedTextAppearance(resId); 932 | } 933 | 934 | /** 935 | * Sets the text color of the expanded title. 936 | * 937 | * @param color The new text color in ARGB format 938 | */ 939 | public void setExpandedTitleColor(@ColorInt int color) { 940 | setExpandedTitleTextColor(ColorStateList.valueOf(color)); 941 | } 942 | 943 | /** 944 | * Sets the text colors of the expanded title. 945 | * 946 | * @param colors ColorStateList containing the new text colors 947 | */ 948 | public void setExpandedTitleTextColor(@NonNull ColorStateList colors) { 949 | mCollapsingTextHelper.setExpandedTextColor(colors); 950 | } 951 | 952 | /** 953 | * Sets the horizontal alignment of the expanded title and the vertical gravity that will 954 | * be used when there is extra space in the expanded bounds beyond what is required for 955 | * the title itself. 956 | * 957 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity 958 | */ 959 | public void setExpandedTitleGravity(int gravity) { 960 | mCollapsingTextHelper.setExpandedTextGravity(gravity); 961 | } 962 | 963 | /** 964 | * Returns the horizontal and vertical alignment for title when expanded. 965 | * 966 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity 967 | */ 968 | public int getExpandedTitleGravity() { 969 | return mCollapsingTextHelper.getExpandedTextGravity(); 970 | } 971 | 972 | /** 973 | * Set the typeface to use for the collapsed title. 974 | * 975 | * @param typeface typeface to use, or {@code null} to use the default. 976 | */ 977 | public void setCollapsedTitleTypeface(@Nullable Typeface typeface) { 978 | mCollapsingTextHelper.setCollapsedTypeface(typeface); 979 | } 980 | 981 | /** 982 | * Returns the typeface used for the collapsed title. 983 | */ 984 | @NonNull 985 | public Typeface getCollapsedTitleTypeface() { 986 | return mCollapsingTextHelper.getCollapsedTypeface(); 987 | } 988 | 989 | /** 990 | * Set the typeface to use for the expanded title. 991 | * 992 | * @param typeface typeface to use, or {@code null} to use the default. 993 | */ 994 | public void setExpandedTitleTypeface(@Nullable Typeface typeface) { 995 | mCollapsingTextHelper.setExpandedTypeface(typeface); 996 | } 997 | 998 | /** 999 | * Returns the typeface used for the expanded title. 1000 | */ 1001 | @NonNull 1002 | public Typeface getExpandedTitleTypeface() { 1003 | return mCollapsingTextHelper.getExpandedTypeface(); 1004 | } 1005 | 1006 | /** 1007 | * Sets the expanded title margins. 1008 | * 1009 | * @param start the starting title margin in pixels 1010 | * @param top the top title margin in pixels 1011 | * @param end the ending title margin in pixels 1012 | * @param bottom the bottom title margin in pixels 1013 | * 1014 | * @see #getExpandedTitleMarginStart() 1015 | * @see #getExpandedTitleMarginTop() 1016 | * @see #getExpandedTitleMarginEnd() 1017 | * @see #getExpandedTitleMarginBottom() 1018 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin 1019 | */ 1020 | public void setExpandedTitleMargin(int start, int top, int end, int bottom) { 1021 | mExpandedMarginStart = start; 1022 | mExpandedMarginTop = top; 1023 | mExpandedMarginEnd = end; 1024 | mExpandedMarginBottom = bottom; 1025 | requestLayout(); 1026 | } 1027 | 1028 | /** 1029 | * @return the starting expanded title margin in pixels 1030 | * 1031 | * @see #setExpandedTitleMarginStart(int) 1032 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart 1033 | */ 1034 | public int getExpandedTitleMarginStart() { 1035 | return mExpandedMarginStart; 1036 | } 1037 | 1038 | /** 1039 | * Sets the starting expanded title margin in pixels. 1040 | * 1041 | * @param margin the starting title margin in pixels 1042 | * @see #getExpandedTitleMarginStart() 1043 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart 1044 | */ 1045 | public void setExpandedTitleMarginStart(int margin) { 1046 | mExpandedMarginStart = margin; 1047 | requestLayout(); 1048 | } 1049 | 1050 | /** 1051 | * @return the top expanded title margin in pixels 1052 | * @see #setExpandedTitleMarginTop(int) 1053 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop 1054 | */ 1055 | public int getExpandedTitleMarginTop() { 1056 | return mExpandedMarginTop; 1057 | } 1058 | 1059 | /** 1060 | * Sets the top expanded title margin in pixels. 1061 | * 1062 | * @param margin the top title margin in pixels 1063 | * @see #getExpandedTitleMarginTop() 1064 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop 1065 | */ 1066 | public void setExpandedTitleMarginTop(int margin) { 1067 | mExpandedMarginTop = margin; 1068 | requestLayout(); 1069 | } 1070 | 1071 | /** 1072 | * @return the ending expanded title margin in pixels 1073 | * @see #setExpandedTitleMarginEnd(int) 1074 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd 1075 | */ 1076 | public int getExpandedTitleMarginEnd() { 1077 | return mExpandedMarginEnd; 1078 | } 1079 | 1080 | /** 1081 | * Sets the ending expanded title margin in pixels. 1082 | * 1083 | * @param margin the ending title margin in pixels 1084 | * @see #getExpandedTitleMarginEnd() 1085 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd 1086 | */ 1087 | public void setExpandedTitleMarginEnd(int margin) { 1088 | mExpandedMarginEnd = margin; 1089 | requestLayout(); 1090 | } 1091 | 1092 | /** 1093 | * @return the bottom expanded title margin in pixels 1094 | * @see #setExpandedTitleMarginBottom(int) 1095 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom 1096 | */ 1097 | public int getExpandedTitleMarginBottom() { 1098 | return mExpandedMarginBottom; 1099 | } 1100 | 1101 | /** 1102 | * Sets the bottom expanded title margin in pixels. 1103 | * 1104 | * @param margin the bottom title margin in pixels 1105 | * @see #getExpandedTitleMarginBottom() 1106 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom 1107 | */ 1108 | public void setExpandedTitleMarginBottom(int margin) { 1109 | mExpandedMarginBottom = margin; 1110 | requestLayout(); 1111 | } 1112 | 1113 | /** 1114 | * Set the amount of visible height in pixels used to define when to trigger a scrim 1115 | * visibility change. 1116 | * 1117 | *

If the visible height of this view is less than the given value, the scrims will be 1118 | * made visible, otherwise they are hidden.

1119 | * 1120 | * @param height value in pixels used to define when to trigger a scrim visibility change 1121 | * 1122 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimVisibleHeightTrigger 1123 | */ 1124 | public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) { 1125 | if (mScrimVisibleHeightTrigger != height) { 1126 | mScrimVisibleHeightTrigger = height; 1127 | // Update the scrim visibility 1128 | updateScrimVisibility(); 1129 | } 1130 | } 1131 | 1132 | /** 1133 | * Returns the amount of visible height in pixels used to define when to trigger a scrim 1134 | * visibility change. 1135 | * 1136 | * @see #setScrimVisibleHeightTrigger(int) 1137 | */ 1138 | public int getScrimVisibleHeightTrigger() { 1139 | if (mScrimVisibleHeightTrigger >= 0) { 1140 | // If we have one explicitly set, return it 1141 | return mScrimVisibleHeightTrigger; 1142 | } 1143 | 1144 | // Otherwise we'll use the default computed value 1145 | final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 1146 | 1147 | final int minHeight = ViewCompat.getMinimumHeight(this); 1148 | if (minHeight > 0) { 1149 | // If we have a minHeight set, lets use 2 * minHeight (capped at our height) 1150 | return Math.min((minHeight * 2) + insetTop, getHeight()); 1151 | } 1152 | 1153 | // If we reach here then we don't have a min height set. Instead we'll take a 1154 | // guess at 1/3 of our height being visible 1155 | return getHeight() / 3; 1156 | } 1157 | 1158 | /** 1159 | * Set the duration used for scrim visibility animations. 1160 | * 1161 | * @param duration the duration to use in milliseconds 1162 | * 1163 | * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimAnimationDuration 1164 | */ 1165 | public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) { 1166 | mScrimAnimationDuration = duration; 1167 | } 1168 | 1169 | /** 1170 | * Returns the duration in milliseconds used for scrim visibility animations. 1171 | */ 1172 | public long getScrimAnimationDuration() { 1173 | return mScrimAnimationDuration; 1174 | } 1175 | 1176 | @Override 1177 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1178 | return p instanceof LayoutParams; 1179 | } 1180 | 1181 | @Override 1182 | protected LayoutParams generateDefaultLayoutParams() { 1183 | return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 1184 | } 1185 | 1186 | @Override 1187 | public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { 1188 | return new LayoutParams(getContext(), attrs); 1189 | } 1190 | 1191 | @Override 1192 | protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1193 | return new LayoutParams(p); 1194 | } 1195 | 1196 | public static class LayoutParams extends FrameLayout.LayoutParams { 1197 | 1198 | private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f; 1199 | 1200 | /** @hide */ 1201 | @RestrictTo(LIBRARY_GROUP) 1202 | @IntDef({ 1203 | COLLAPSE_MODE_OFF, 1204 | COLLAPSE_MODE_PIN, 1205 | COLLAPSE_MODE_PARALLAX 1206 | }) 1207 | @Retention(RetentionPolicy.SOURCE) 1208 | @interface CollapseMode {} 1209 | 1210 | /** 1211 | * The view will act as normal with no collapsing behavior. 1212 | */ 1213 | public static final int COLLAPSE_MODE_OFF = 0; 1214 | 1215 | /** 1216 | * The view will pin in place until it reaches the bottom of the 1217 | * {@link CollapsingToolbarLayout}. 1218 | */ 1219 | public static final int COLLAPSE_MODE_PIN = 1; 1220 | 1221 | /** 1222 | * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)} 1223 | * to change the multiplier used. 1224 | */ 1225 | public static final int COLLAPSE_MODE_PARALLAX = 2; 1226 | 1227 | int mCollapseMode = COLLAPSE_MODE_OFF; 1228 | float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER; 1229 | 1230 | public LayoutParams(Context c, AttributeSet attrs) { 1231 | super(c, attrs); 1232 | 1233 | TypedArray a = c.obtainStyledAttributes(attrs, 1234 | R.styleable.CollapsingToolbarLayout_Layout); 1235 | mCollapseMode = a.getInt( 1236 | R.styleable.CollapsingToolbarLayout_Layout_layout_collapseMode, 1237 | COLLAPSE_MODE_OFF); 1238 | setParallaxMultiplier(a.getFloat( 1239 | R.styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier, 1240 | DEFAULT_PARALLAX_MULTIPLIER)); 1241 | a.recycle(); 1242 | } 1243 | 1244 | public LayoutParams(int width, int height) { 1245 | super(width, height); 1246 | } 1247 | 1248 | public LayoutParams(int width, int height, int gravity) { 1249 | super(width, height, gravity); 1250 | } 1251 | 1252 | public LayoutParams(ViewGroup.LayoutParams p) { 1253 | super(p); 1254 | } 1255 | 1256 | public LayoutParams(MarginLayoutParams source) { 1257 | super(source); 1258 | } 1259 | 1260 | @RequiresApi(19) 1261 | public LayoutParams(FrameLayout.LayoutParams source) { 1262 | // The copy constructor called here only exists on API 19+. 1263 | super(source); 1264 | } 1265 | 1266 | /** 1267 | * Set the collapse mode. 1268 | * 1269 | * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} 1270 | * or {@link #COLLAPSE_MODE_PARALLAX}. 1271 | */ 1272 | public void setCollapseMode(@CollapseMode int collapseMode) { 1273 | mCollapseMode = collapseMode; 1274 | } 1275 | 1276 | /** 1277 | * Returns the requested collapse mode. 1278 | * 1279 | * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN} 1280 | * or {@link #COLLAPSE_MODE_PARALLAX}. 1281 | */ 1282 | @CollapseMode 1283 | public int getCollapseMode() { 1284 | return mCollapseMode; 1285 | } 1286 | 1287 | /** 1288 | * Set the parallax scroll multiplier used in conjunction with 1289 | * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all, 1290 | * {@code 1.0f} indicates normal scroll movement. 1291 | * 1292 | * @param multiplier the multiplier. 1293 | * 1294 | * @see #getParallaxMultiplier() 1295 | */ 1296 | public void setParallaxMultiplier(float multiplier) { 1297 | mParallaxMult = multiplier; 1298 | } 1299 | 1300 | /** 1301 | * Returns the parallax scroll multiplier used in conjunction with 1302 | * {@link #COLLAPSE_MODE_PARALLAX}. 1303 | * 1304 | * @see #setParallaxMultiplier(float) 1305 | */ 1306 | public float getParallaxMultiplier() { 1307 | return mParallaxMult; 1308 | } 1309 | } 1310 | 1311 | /** 1312 | * Show or hide the scrims if needed 1313 | */ 1314 | final void updateScrimVisibility() { 1315 | if (mContentScrim != null || mStatusBarScrim != null) { 1316 | setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger()); 1317 | } 1318 | } 1319 | 1320 | final int getMaxOffsetForPinChild(View child) { 1321 | final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child); 1322 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1323 | return getHeight() 1324 | - offsetHelper.getLayoutTop() 1325 | - child.getHeight() 1326 | - lp.bottomMargin; 1327 | } 1328 | 1329 | private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener { 1330 | OffsetUpdateListener() { 1331 | } 1332 | 1333 | @Override 1334 | public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { 1335 | mCurrentOffset = verticalOffset; 1336 | 1337 | final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0; 1338 | 1339 | for (int i = 0, z = getChildCount(); i < z; i++) { 1340 | final View child = getChildAt(i); 1341 | final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1342 | final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child); 1343 | 1344 | switch (lp.mCollapseMode) { 1345 | case LayoutParams.COLLAPSE_MODE_PIN: 1346 | offsetHelper.setTopAndBottomOffset(MathUtils.clamp( 1347 | -verticalOffset, 0, getMaxOffsetForPinChild(child))); 1348 | break; 1349 | case LayoutParams.COLLAPSE_MODE_PARALLAX: 1350 | offsetHelper.setTopAndBottomOffset( 1351 | Math.round(-verticalOffset * lp.mParallaxMult)); 1352 | break; 1353 | } 1354 | } 1355 | 1356 | // Show or hide the scrims if needed 1357 | updateScrimVisibility(); 1358 | 1359 | if (mStatusBarScrim != null && insetTop > 0) { 1360 | ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this); 1361 | } 1362 | 1363 | // Update the collapsing text's fraction 1364 | final int expandRange = getHeight() - ViewCompat.getMinimumHeight( 1365 | CollapsingToolbarLayout.this) - insetTop; 1366 | mCollapsingTextHelper.setExpansionFraction( 1367 | Math.abs(verticalOffset) / (float) expandRange); 1368 | } 1369 | } 1370 | } 1371 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/java/net/opacapp/multilinecollapsingtoolbar/ThemeUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.opacapp.multilinecollapsingtoolbar; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | 22 | class ThemeUtils { 23 | 24 | private static final int[] APPCOMPAT_CHECK_ATTRS = { 25 | R.attr.colorPrimary 26 | }; 27 | 28 | static void checkAppCompatTheme(Context context) { 29 | TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS); 30 | final boolean failed = !a.hasValue(0); 31 | a.recycle(); 32 | if (failed) { 33 | throw new IllegalArgumentException("You need to use a Theme.AppCompat theme " 34 | + "(or descendant) with the design library."); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/java/net/opacapp/multilinecollapsingtoolbar/ViewOffsetHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.opacapp.multilinecollapsingtoolbar; 18 | 19 | import android.support.v4.view.ViewCompat; 20 | import android.view.View; 21 | 22 | /** 23 | * Utility helper for moving a {@link android.view.View} around using 24 | * {@link android.view.View#offsetLeftAndRight(int)} and 25 | * {@link android.view.View#offsetTopAndBottom(int)}. 26 | *

27 | * Also the setting of absolute offsets (similar to translationX/Y), rather than additive 28 | * offsets. 29 | */ 30 | class ViewOffsetHelper { 31 | 32 | private final View mView; 33 | 34 | private int mLayoutTop; 35 | private int mLayoutLeft; 36 | private int mOffsetTop; 37 | private int mOffsetLeft; 38 | 39 | public ViewOffsetHelper(View view) { 40 | mView = view; 41 | } 42 | 43 | public void onViewLayout() { 44 | // Now grab the intended top 45 | mLayoutTop = mView.getTop(); 46 | mLayoutLeft = mView.getLeft(); 47 | 48 | // And offset it as needed 49 | updateOffsets(); 50 | } 51 | 52 | private void updateOffsets() { 53 | ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop)); 54 | ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft)); 55 | } 56 | 57 | /** 58 | * Set the top and bottom offset for this {@link ViewOffsetHelper}'s view. 59 | * 60 | * @param offset the offset in px. 61 | * @return true if the offset has changed 62 | */ 63 | public boolean setTopAndBottomOffset(int offset) { 64 | if (mOffsetTop != offset) { 65 | mOffsetTop = offset; 66 | updateOffsets(); 67 | return true; 68 | } 69 | return false; 70 | } 71 | 72 | /** 73 | * Set the left and right offset for this {@link ViewOffsetHelper}'s view. 74 | * 75 | * @param offset the offset in px. 76 | * @return true if the offset has changed 77 | */ 78 | public boolean setLeftAndRightOffset(int offset) { 79 | if (mOffsetLeft != offset) { 80 | mOffsetLeft = offset; 81 | updateOffsets(); 82 | return true; 83 | } 84 | return false; 85 | } 86 | 87 | public int getTopAndBottomOffset() { 88 | return mOffsetTop; 89 | } 90 | 91 | public int getLeftAndRightOffset() { 92 | return mOffsetLeft; 93 | } 94 | 95 | public int getLayoutTop() { 96 | return mLayoutTop; 97 | } 98 | 99 | public int getLayoutLeft() { 100 | return mLayoutLeft; 101 | } 102 | } -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/res/values/collapsing_toolbar_attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /multiline-collapsingtoolbar/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |