├── .gitignore
├── LICENSE
├── README.md
├── build.gradle
├── changelog.md
├── example
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── klinker
│ │ └── android
│ │ └── link_builder_example
│ │ ├── HtmlLinkExampleActivity.kt
│ │ ├── JavaMainActivity.java
│ │ ├── MainActivity.kt
│ │ └── list_view_example
│ │ ├── ListActivity.kt
│ │ └── SampleAdapter.kt
│ └── res
│ ├── layout
│ ├── activity_html_example.xml
│ ├── activity_list.xml
│ ├── activity_main.xml
│ └── list_item.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
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── klinker
│ │ └── android
│ │ └── link_builder
│ │ ├── Extensions.kt
│ │ ├── Link.kt
│ │ ├── LinkBuilder.kt
│ │ ├── LinkConsumableTextView.kt
│ │ ├── TouchableBaseSpan.kt
│ │ ├── TouchableMovementMethod.kt
│ │ └── TouchableSpan.kt
│ └── res
│ └── values
│ └── attrs.xml
├── preview.png
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | # built application files
4 | *.apk
5 | *.ap_
6 |
7 | # files for the dex VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # generated files
14 | bin/
15 | gen/
16 |
17 | # Local configuration file (sdk path, etc)
18 | local.properties
19 |
20 | # Eclipse project files
21 | .classpath
22 | .project
23 |
24 | *.a
25 | *.dylib
26 | *.log
27 | *.o
28 | *.pot
29 | *.pyc
30 | *.pydevproject
31 | *.so
32 | *.suo
33 | *.xcworkspace
34 | *_ReSharper*
35 |
36 | .DS_Store
37 | ._.DS_Store
38 |
39 | project.properties
40 |
41 | .settings
42 | build/
43 | .gradle/
44 | .idea
45 | *.iml
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2012 by Satya Narayan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android TextView-LinkBuilder [](https://android-arsenal.com/details/1/2049)
2 |
3 | 
4 |
5 | Insanely easy way to create clickable links within a `TextView`.
6 |
7 | While creating [Talon for Twitter](https://github.com/klinker24/Talon-for-Twitter), one of the most difficult things I encountered was creating these clickable links based on specific text. Luckily, I have made it easy for anyone to apply this type of style to their `TextView`'s.
8 |
9 | ## Features
10 |
11 | Similar to how all the big players do it (Google+, Twitter, *cough* Talon *cough*), this library allows you to create clickable links for any combination of `String`s within a `TextView`.
12 |
13 | - Specify long and short click actions of a specific word within your `TextView`
14 | - Provide user feedback by highlighting the text when the user touches it
15 | - Match single `String`s or use a regular expression to set clickable links to any text conforming to that pattern
16 | - Change the color of the linked text
17 | - Change the color of the linked text when the user touches it
18 | - Modify the transparency of the text's highlighting when the user touches it
19 | - Set whether or not you want the text underlined
20 | - Set whether or not you want the text bold
21 | - Default link color from an activity theme
22 |
23 | The main advantage to using this library over `TextView`'s autolink functionality is that you can link anything, not just web address, emails, and phone numbers. It also provides color customization and touch feedback.
24 |
25 | ## Installation
26 |
27 | There are two ways to use this library:
28 |
29 | #### As a Gradle dependency
30 |
31 | This is the preferred way. Simply add:
32 |
33 | ```groovy
34 | dependencies {
35 | compile 'com.klinkerapps:link_builder:2.0.5'
36 | }
37 | ```
38 |
39 | to your project dependencies and run `gradle build` or `gradle assemble`.
40 |
41 | #### As a library project
42 |
43 | Download the source code and import it as a library project in Eclipse. The project is available in the folder **library**. For more information on how to do this, read [here](http://developer.android.com/tools/projects/index.html#LibraryProjects).
44 |
45 | ## Example Usage
46 |
47 | Functionality can be found in the Kotlin example's [MainActivity](https://github.com/klinker24/Android-TextView-LinkBuilder/blob/master/example/src/main/java/com/klinker/android/link_builder_example/MainActivity.kt). For Java check [JavaMainActivity](https://github.com/klinker24/Android-TextView-LinkBuilder/blob/master/example/src/main/java/com/klinker/android/link_builder_example/JavaMainActivity.java).
48 |
49 | For a list of regular expressions that I use in Talon, you can go [here](https://github.com/klinker24/Talon-for-Twitter/blob/master/app/src/main/java/com/klinker/android/twitter/utils/text/Regex.java)
50 |
51 | ```java
52 | // Create the link rule to set what text should be linked.
53 | // can use a specific string or a regex pattern
54 | Link link = new Link("click here")
55 | .setTextColor(Color.parseColor("#259B24")) // optional, defaults to holo blue
56 | .setTextColorOfHighlightedLink(Color.parseColor("#0D3D0C")) // optional, defaults to holo blue
57 | .setHighlightAlpha(.4f) // optional, defaults to .15f
58 | .setUnderlined(false) // optional, defaults to true
59 | .setBold(true) // optional, defaults to false
60 | .setOnLongClickListener(new Link.OnLongClickListener() {
61 | @Override
62 | public void onLongClick(String clickedText) {
63 | // long clicked
64 | }
65 | })
66 | .setOnClickListener(new Link.OnClickListener() {
67 | @Override
68 | public void onClick(String clickedText) {
69 | // single clicked
70 | }
71 | });
72 |
73 | TextView demoText = (TextView) findViewById(R.id.test_text);
74 |
75 | // create the link builder object add the link rule
76 | LinkBuilder.on(demoText)
77 | .addLink(link)
78 | .build(); // create the clickable links
79 | ```
80 |
81 | You can also create a `CharSequence` instead of creating and applying the links directly to the `TextView`. Do not forget to set the movement method on your `TextView`'s after you have applied the `CharSequence`, or else the links will not be clickable.
82 |
83 | ```java
84 | // find the text view. Used to create the link builder
85 | TextView demoText = (TextView) findViewById(R.id.test_text);
86 |
87 | // Add the links and make the links clickable
88 | CharSequence sequence = LinkBuilder.from(this, demoText.getText().toString())
89 | .addLinks(getExampleLinks())
90 | .build();
91 |
92 | demoText.setText(sequence);
93 |
94 | // if you forget to set the movement method, then your text will not be clickable!
95 | demoText.setMovementMethod(TouchableMovementMethod.getInstance());
96 | ```
97 |
98 | If you would like to set the default text color for links without inputting it manually on each `Link` object, it can be set from the activity theme.
99 |
100 | ```xml
101 |
104 |
108 | ```
109 |
110 | ## Kotlin Support
111 |
112 | The library is built on Kotlin, so you get some extension methods that you can use to apply the links to the `TextView`, instead of creating the builder.
113 |
114 | ```kotlin
115 | val demo = findViewById(R.id.demo_text)
116 | demo.applyLinks(link1, link2, ...)
117 | demo.applyLinks(listOfLinks)
118 | ```
119 |
120 | ## Usage with ListView.OnItemClickListener
121 |
122 | By default, `LinkBuilder` will consume all the touch events on your `TextView`. This means that `ListView.OnItemClickListener` will never get called if you try to implement it. The fix for this is to implement the `LinkConsumableTextView` rather than the normal TextView in your layouts.
123 |
124 | My `LinkConsumableTextView` will only consume touch events if you have clicked the link within the `TextView`. Otherwise, it will defer the touch event to the parent, which allows you to use `ListView.OnItemClickListener` method.
125 |
126 | ## Contributing
127 |
128 | Please fork this repository and contribute back using [pull requests](https://github.com/klinker24/Android-TextView-LinkBuilder/pulls). Features can be requested using [issues](https://github.com/klinker24/Android-TextView-LinkBuilder/issues). All code, comments, and critiques are greatly appreciated.
129 |
130 | ## Changelog
131 |
132 | The full changelog for the library can be found [here](https://github.com/klinker24/Android-TextView-LinkBuilder/blob/master/changelog.md).
133 |
134 |
135 | ## License
136 |
137 | Copyright 2015 Luke Klinker
138 |
139 | Licensed under the Apache License, Version 2.0 (the "License");
140 | you may not use this file except in compliance with the License.
141 | You may obtain a copy of the License at
142 |
143 | http://www.apache.org/licenses/LICENSE-2.0
144 |
145 | Unless required by applicable law or agreed to in writing, software
146 | distributed under the License is distributed on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
148 | See the License for the specific language governing permissions and
149 | limitations under the License.
150 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | buildscript {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | dependencies {
21 | classpath "com.android.tools.build:gradle:$project.ANDROID_GRADLE_VERSION"
22 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
23 | }
24 | }
25 |
26 | allprojects {
27 | repositories {
28 | google()
29 | jcenter()
30 | mavenCentral()
31 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
32 | }
33 | }
34 |
35 | subprojects {
36 | tasks.withType(Javadoc).all { enabled = false }
37 | }
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | ### Version 2.0.5:
4 | - Crash fix
5 |
6 | ### Version 2.0.4:
7 | - Support `CharSequence` when building links
8 |
9 | ### Version 2.0.3:
10 | - Improvements for Java interop support
11 |
12 | ### Version 2.0.2:
13 | - Kotlin cleanup
14 |
15 | ### Version 2.0.0:
16 | - Built on Kotlin
17 | - Extension method support for `TextView#applyLinks` to avoid boilerplate of creating the `LinkBuilder`
18 |
19 | ### Version 1.6.0:
20 | - Remove `Spannable` that would be consumed by other links so they are fully clickable
21 |
22 | ### Version 1.6.0:
23 | - Improvements around support for regular expression matching
24 |
25 | ### Version 1.5.2:
26 | - Don't default the text color of highlighted links to light blue
27 |
28 | ### Version 1.5.1:
29 | - If a link already exists on a block of text, don't try to put another link over the top of it
30 |
31 | ### Version 1.5.0:
32 | - Add ability to only search for the first occurrence of a pattern
33 |
34 | ### Version 1.4.0:
35 | - Add the ability to customize the text color of links when they are being touched/highlighted
36 |
37 | ### Version 1.3.3:
38 | - Update example with Material Design
39 | - Update dependencies and build tools for SDK 25
40 |
41 | ### Version 1.3.2:
42 | - Convert to using `CharSequence`
43 |
44 | ### Version 1.3.1:
45 | - Add a method to make links bold
46 |
47 | ### Version 1.3.0:
48 | - Added ability to work with `ListView.OnItemClickListener` through the use of `LinkConsumableTextView`
49 |
50 | ### Version 1.2.0:
51 | - Add a set typeface method to `Link`
52 |
53 | ### Version 1.1.0:
54 | - Cut to `LinkBuilder.from(String)` and `LinkBuilder.on(TextView)` to create an instance of `LinkBuilder`
55 | - Allow `LinkBuilder` to return a `CharSequence` for the links without setting them to a `TextView` automatically. (Use `LinkBuilder.from(String).build()`)
56 |
57 | ### Version 1.0.6:
58 | - Allow for styling the default color of the links from activity theme
59 | - Don't allow null links when building the `SpannableString`
60 | - Find all links of the same text when using a regular expression
61 |
62 | ### Version 1.0.5:
63 | - If the link contained the last character of the line, clicking the empty space at the end of the line would also click the link. This removes that bug.
64 |
65 | ### Version 1.0.4:
66 | - Add prepended and appended text to links
67 | - Fix: if no matches are found when linking regular expressions, the text would not be shown at all
68 | - Convert line endings from CRLF to LF for git repository
69 | - Add changelog
70 | - Add release tags
71 |
72 | ### Version 1.0.3:
73 | - Use the `TextView` to preform haptic feedback instead of `Vibrator` class.
74 | - Remove vibrate permission
75 |
76 | ### Version 1.0.2:
77 | - Chain the `LinkBuilder` class for easier and more structured implementation
78 |
79 | ### Version 1.0.1:
80 | - Fix incorrect package name
81 | - Upload to maven central
82 |
83 | ### Version 1.0.0:
84 | - Initial release
85 |
--------------------------------------------------------------------------------
/example/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | apply plugin: 'com.android.application'
16 | apply plugin: 'kotlin-android'
17 |
18 | android {
19 | compileSdkVersion Integer.parseInt(project.COMPILE_SDK)
20 |
21 | defaultConfig {
22 | applicationId project.APPLICATION_ID
23 | minSdkVersion Integer.parseInt(project.MIN_SDK)
24 | targetSdkVersion Integer.parseInt(project.TARGET_SDK)
25 | versionCode Integer.parseInt(project.VERSION_CODE)
26 | versionName project.VERSION_NAME
27 | }
28 |
29 | buildTypes {
30 | release {
31 | minifyEnabled false
32 | }
33 | }
34 |
35 | lintOptions {
36 | abortOnError false
37 | }
38 | }
39 |
40 | dependencies {
41 | implementation project (':library')
42 |
43 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
44 |
45 | implementation "com.android.support:appcompat-v7:${ANDROID_SUPPORT_VERSION}"
46 | implementation "com.android.support:design:${ANDROID_SUPPORT_VERSION}"
47 | implementation "com.android.support:support-v4:${ANDROID_SUPPORT_VERSION}"
48 | }
49 |
--------------------------------------------------------------------------------
/example/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/example/src/main/java/com/klinker/android/link_builder_example/HtmlLinkExampleActivity.kt:
--------------------------------------------------------------------------------
1 | package com.klinker.android.link_builder_example
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.graphics.Color
6 | import android.graphics.Typeface
7 | import android.net.Uri
8 | import android.os.Bundle
9 | import android.support.v7.app.AppCompatActivity
10 | import android.support.v7.widget.Toolbar
11 | import android.text.Html
12 | import android.view.View
13 | import android.widget.TextView
14 | import android.widget.Toast
15 |
16 | import com.klinker.android.link_builder.Link
17 | import com.klinker.android.link_builder.LinkBuilder
18 | import com.klinker.android.link_builder.applyLinks
19 |
20 | import java.util.ArrayList
21 | import java.util.regex.Pattern
22 |
23 | class HtmlLinkExampleActivity : AppCompatActivity() {
24 |
25 | public override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 |
28 | setContentView(R.layout.activity_html_example)
29 |
30 | val toolbar = findViewById(R.id.toolbar) as Toolbar
31 | setSupportActionBar(toolbar)
32 | toolbar.setTitle(R.string.app_name)
33 |
34 | val demo1 = findViewById(R.id.demo1) as TextView
35 | demo1.text = Html.fromHtml(TEXT)
36 | demo1.applyLinks(getLinks())
37 |
38 | val demo2 = findViewById(R.id.demo2) as TextView
39 | demo2.text = Html.fromHtml(TEXT.replace("?a[^>]*>".toRegex(), ""))
40 | demo2.applyLinks(getLinks())
41 | }
42 |
43 | private fun getLinks(): List {
44 | val google = Link("www.google.com")
45 | .setTextColor(Color.parseColor("#00BCD4"))
46 | .setHighlightAlpha(.4f)
47 | .setOnClickListener { showToast("clicked: $it") }
48 |
49 | val exampleText = Link("this")
50 | .setTextColor(Color.parseColor("#00BCD4"))
51 | .setHighlightAlpha(.4f)
52 | .setOnClickListener { showToast("clicked the example text") }
53 |
54 | return listOf(google, exampleText)
55 | }
56 |
57 | private fun showToast(text: String) {
58 | Toast.makeText(this@HtmlLinkExampleActivity, text, Toast.LENGTH_SHORT).show()
59 | }
60 |
61 | companion object {
62 | private const val TEXT =
63 | "Here is an example link www.google.com." +
64 | "To show it alongside other LinkBuilder functionality, lets highlight this."
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/example/src/main/java/com/klinker/android/link_builder_example/JavaMainActivity.java:
--------------------------------------------------------------------------------
1 | package com.klinker.android.link_builder_example;
2 |
3 | import android.content.Intent;
4 | import android.graphics.Color;
5 | import android.graphics.Typeface;
6 | import android.net.Uri;
7 | import android.os.Bundle;
8 | import android.support.v7.app.AppCompatActivity;
9 | import android.support.v7.widget.Toolbar;
10 | import android.widget.TextView;
11 | import android.widget.Toast;
12 |
13 | import com.klinker.android.link_builder.Link;
14 | import com.klinker.android.link_builder.LinkBuilder;
15 |
16 | import java.util.ArrayList;
17 | import java.util.List;
18 | import java.util.regex.Pattern;
19 |
20 | public class JavaMainActivity extends AppCompatActivity {
21 |
22 | private static final String GITHUB_LINK = "https://github.com/klinker24";
23 | private static final String TWITTER_PROFILE = "https://twitter.com/";
24 | private static final String PLAY_STORE = "https://play.google.com/store/apps/developer?id=Klinker+Apps&hl=en";
25 |
26 | @Override
27 | public void onCreate(Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 |
30 | // set the content view. Contains a scrollview with a text view inside
31 | setContentView(R.layout.activity_main);
32 |
33 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
34 | setSupportActionBar(toolbar);
35 |
36 | toolbar.setTitle(R.string.app_name);
37 |
38 | // find the text view. Used to create the link builder
39 | TextView demoText = (TextView) findViewById(R.id.test_text);
40 |
41 | // Add the links and make the links clickable
42 | LinkBuilder.on(demoText)
43 | .addLinks(getExampleLinks())
44 | .build();
45 | }
46 |
47 | private List getExampleLinks() {
48 | List links = new ArrayList<>();
49 |
50 | // create a single click link to the github page
51 | Link github = new Link("TextView-LinkBuilder");
52 | github.setTypeface(Typeface.DEFAULT_BOLD)
53 | .setOnClickListener(new Link.OnClickListener() {
54 | @Override
55 | public void onClick(String clickedText) {
56 | openLink(GITHUB_LINK);
57 | }
58 | });
59 |
60 | // create a single click link to the matched twitter profiles
61 | Link mentions = new Link(Pattern.compile("@\\w{1,15}"));
62 | mentions.setTextColor(Color.parseColor("#00BCD4"));
63 | mentions.setHighlightAlpha(.4f);
64 | mentions.setOnClickListener(new Link.OnClickListener() {
65 | @Override
66 | public void onClick(String clickedText) {
67 | openLink(TWITTER_PROFILE + clickedText.replace("@", ""));
68 | }
69 | });
70 |
71 | // match the numbers that I created
72 | Link numbers = new Link(Pattern.compile("[0-9]+"));
73 | numbers.setTextColor(Color.parseColor("#FF9800"));
74 | numbers.setOnClickListener(new Link.OnClickListener() {
75 | @Override
76 | public void onClick(String clickedText) {
77 | showToast("Clicked: " + clickedText);
78 | }
79 | });
80 |
81 | // action on a long click instead of a short click
82 | Link longClickHere = new Link("here");
83 | longClickHere.setTextColor(Color.parseColor("#259B24"));
84 | longClickHere.setOnLongClickListener(new Link.OnLongClickListener() {
85 | @Override
86 | public void onLongClick(String clickedText) {
87 | showToast("You long clicked. Nice job.");
88 | }
89 | });
90 |
91 | // underlined
92 | Link yes = new Link("Yes");
93 | yes.setUnderlined(true);
94 | yes.setTextColor(Color.parseColor("#FFEB3B"));
95 |
96 | // not underlined
97 | Link no = new Link("No");
98 | no.setUnderlined(false);
99 | no.setTextColor(Color.parseColor("#FFEB3B"));
100 |
101 | // bold
102 | Link bold = new Link("bold");
103 | bold.setBold(true);
104 | bold.setTextColor(Color.parseColor("#FF0000"));
105 |
106 | // prepended text
107 | Link prepend = new Link("prepended");
108 | prepend.setPrependedText("(!)");
109 |
110 | Link appended = new Link("appended");
111 | appended.setAppendedText("(!)");
112 |
113 | // link to our play store page
114 | Link playStore = new Link("Play Store");
115 | playStore.setTextColor(Color.parseColor("#FF9800"));
116 | playStore.setTextColorOfHighlightedLink(Color.parseColor("#FF6600"));
117 | playStore.setHighlightAlpha(0f);
118 | playStore.setOnClickListener(new Link.OnClickListener() {
119 | @Override
120 | public void onClick(String clickedText) {
121 | openLink(PLAY_STORE);
122 | }
123 | });
124 |
125 | // add the links to the list
126 | links.add(github);
127 | links.add(mentions);
128 | links.add(numbers);
129 | links.add(longClickHere);
130 | links.add(yes);
131 | links.add(no);
132 | links.add(bold);
133 | links.add(prepend);
134 | links.add(appended);
135 | links.add(playStore);
136 |
137 | return links;
138 | }
139 |
140 | private void openLink(String link) {
141 | Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
142 | startActivity(browserIntent);
143 | }
144 |
145 | private void showToast(String text) {
146 | Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
147 | }
148 | }
--------------------------------------------------------------------------------
/example/src/main/java/com/klinker/android/link_builder_example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | package com.klinker.android.link_builder_example
16 |
17 | import android.content.Intent
18 | import android.graphics.Color
19 | import android.graphics.Typeface
20 | import android.net.Uri
21 | import android.os.Bundle
22 | import android.support.v7.app.AppCompatActivity
23 | import android.support.v7.widget.Toolbar
24 | import android.view.View
25 | import android.widget.TextView
26 | import android.widget.Toast
27 |
28 | import com.klinker.android.link_builder.Link
29 | import com.klinker.android.link_builder.applyLinks
30 |
31 | import java.util.regex.Pattern
32 |
33 | class MainActivity : AppCompatActivity() {
34 |
35 | public override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 |
38 | setContentView(R.layout.activity_main)
39 |
40 | val toolbar = findViewById(R.id.toolbar) as Toolbar
41 | setSupportActionBar(toolbar)
42 | toolbar.setTitle(R.string.app_name)
43 |
44 | val demoText = findViewById(R.id.test_text) as TextView
45 | demoText.applyLinks(getLinks())
46 | }
47 |
48 | private fun getLinks(): List {
49 | val github = Link("TextView-LinkBuilder")
50 | .setTypeface(Typeface.DEFAULT_BOLD)
51 | .setOnClickListener { openLink(GITHUB_LINK) }
52 |
53 | val mentions = Link(Pattern.compile("@\\w{1,15}"))
54 | .setTextColor(Color.parseColor("#00BCD4"))
55 | .setHighlightAlpha(.4f)
56 | .setOnClickListener { clickedText ->
57 | openLink(TWITTER_PROFILE + clickedText.replace("@", ""))
58 | }
59 |
60 | val numbers = Link(Pattern.compile("[0-9]+"))
61 | .setTextColor(Color.parseColor("#FF9800"))
62 | .setOnClickListener { showToast("Clicked: $it") }
63 |
64 | val longClickHere = Link("here")
65 | .setTextColor(Color.parseColor("#259B24"))
66 | .setOnLongClickListener { showToast("You long clicked. Nice job.") }
67 |
68 | val yes = Link("Yes").setUnderlined(true)
69 | .setTextColor(Color.parseColor("#FFEB3B"))
70 |
71 | val no = Link("No").setUnderlined(false)
72 | .setTextColor(Color.parseColor("#FFEB3B"))
73 |
74 | val bold = Link("bold").setBold(true)
75 | .setTextColor(Color.parseColor("#FF0000"))
76 |
77 | val prepend = Link("prepended").setPrependedText("(!)")
78 | val appended = Link("appended").setAppendedText("(!)")
79 |
80 | val playStore = Link("Play Store")
81 | .setTextColor(Color.parseColor("#FF9800"))
82 | .setTextColorOfHighlightedLink(Color.parseColor("#FF6600"))
83 | .setHighlightAlpha(0f)
84 | .setOnClickListener { openLink(PLAY_STORE) }
85 |
86 | return listOf(github, mentions, numbers, longClickHere, yes, no, bold, prepend, appended, playStore)
87 | }
88 |
89 | private fun openLink(link: String) {
90 | val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
91 | startActivity(browserIntent)
92 | }
93 |
94 | private fun showToast(text: String) {
95 | Toast.makeText(this@MainActivity, text, Toast.LENGTH_SHORT).show()
96 | }
97 |
98 | companion object {
99 |
100 | private const val GITHUB_LINK = "https://github.com/klinker24"
101 | private const val TWITTER_PROFILE = "https://twitter.com/"
102 | private const val PLAY_STORE = "https://play.google.com/store/apps/developer?id=Klinker+Apps&hl=en"
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/example/src/main/java/com/klinker/android/link_builder_example/list_view_example/ListActivity.kt:
--------------------------------------------------------------------------------
1 | package com.klinker.android.link_builder_example.list_view_example
2 |
3 | import android.os.Bundle
4 | import android.support.v7.app.AppCompatActivity
5 | import android.support.v7.widget.Toolbar
6 | import android.util.Log
7 | import android.view.View
8 | import android.widget.AdapterView
9 | import android.widget.ListView
10 |
11 | import com.klinker.android.link_builder_example.R
12 |
13 | /**
14 | * This sample illustrates how to use LinkBuilder along with a ListView.OnItemClickListener method.
15 | *
16 | * It is pretty simple, but requires you to implement the LinkConsumableTextView rather than a normal
17 | * TextView in your layout.
18 | *
19 | * By doing this, the LinkConsumableTextView will only consume the touch event if the link was actually clicked.
20 | * If the link was not clicked, then it will defer to your ListView.OnItemClickListener method instead.
21 | *
22 | * The SampleAdapter contains the LinkBuilder code for the list items.
23 | */
24 | class ListActivity : AppCompatActivity() {
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | setContentView(R.layout.activity_list)
29 |
30 | val toolbar = findViewById(R.id.toolbar) as Toolbar
31 | setSupportActionBar(toolbar)
32 | toolbar.setTitle(R.string.app_name)
33 |
34 | val listView = findViewById(R.id.list) as ListView
35 | listView.adapter = SampleAdapter(this)
36 | listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, i, _ ->
37 | Log.d(TAG, "onListItemClick position=$i")
38 | }
39 | }
40 |
41 | companion object {
42 | private val TAG = ListActivity::class.java.simpleName
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/example/src/main/java/com/klinker/android/link_builder_example/list_view_example/SampleAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.klinker.android.link_builder_example.list_view_example
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.BaseAdapter
9 |
10 | import com.klinker.android.link_builder.Link
11 | import com.klinker.android.link_builder.LinkBuilder
12 | import com.klinker.android.link_builder.LinkConsumableTextView
13 | import com.klinker.android.link_builder.applyLinks
14 | import com.klinker.android.link_builder_example.R
15 |
16 | class SampleAdapter(private val mContext: Context) : BaseAdapter() {
17 |
18 | override fun getCount(): Int {
19 | return SIZE
20 | }
21 |
22 | override fun getItem(position: Int): Any {
23 | return position
24 | }
25 |
26 | override fun getItemId(position: Int): Long {
27 | return position.toLong()
28 | }
29 |
30 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
31 | var convertView = convertView
32 | if (convertView == null) {
33 | convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false)
34 | }
35 |
36 | val textView = convertView as LinkConsumableTextView?
37 | textView!!.text = String.format(TEXT, position)
38 |
39 | val link1 = Link(LINK1).setOnClickListener { Log.d(TAG, LINK1) }
40 | val link2 = Link(LINK2).setOnClickListener { Log.d(TAG, LINK2) }
41 |
42 | textView.applyLinks(link1, link2)
43 | return convertView
44 | }
45 |
46 | companion object {
47 |
48 | private val TAG = SampleAdapter::class.java.simpleName
49 | private const val SIZE = 42
50 | private const val LINK1 = "First link"
51 | private const val LINK2 = "Second link"
52 | private const val TEXT = "This is item %d. $LINK1 $LINK2"
53 |
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_html_example.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
50 |
51 |
56 |
57 |
62 |
63 |
68 |
69 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
36 |
37 |
38 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 |
25 |
28 |
29 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/example/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/example/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/example/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2196F3
4 | #1565C0
5 | #FFAB40
6 | #FF9100
7 |
--------------------------------------------------------------------------------
/example/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 | Link Builder
19 |
20 | Testing the TextView-LinkBuilder library I made.\n\n
21 |
22 | Follow @lukeklinker on Twitter for more info (Also to check out the regex matching.)\n\n
23 |
24 | Matching numbers (more regex): 1 25 66 1220\n\n
25 |
26 | Long click here for something special.\n\n
27 |
28 | Should this be underlined? Yes No\n\n
29 |
30 | This word is bold\n\n
31 |
32 | This link has some prepended text.\n\n
33 |
34 | While this link has some appended text.\n\n
35 |
36 | Hope you enjoyed the demo :)\n\n
37 |
38 | Check out our real apps on the Play Store
39 |
40 |
41 |
--------------------------------------------------------------------------------
/example/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2015 Luke Klinker
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 | VERSION_NAME=2.0.5
18 | VERSION_CODE=22
19 |
20 | MIN_SDK=14
21 | TARGET_SDK=29
22 | COMPILE_SDK=29
23 | KOTLIN_VERSION=1.3.61
24 |
25 | ANDROID_SUPPORT_VERSION=28.0.0
26 |
27 | ANDROID_GRADLE_VERSION=3.5.3
28 |
29 | APPLICATION_ID=com.klinker.android.link_builder_example
30 | GROUP=com.klinkerapps
31 |
32 | POM_DESCRIPTION=Easily create clickable text within any TextView
33 | POM_URL=https://github.com/klinker24/Android-TextView-LinkBuilder
34 | POM_SCM_URL=https://github.com/klinker24/Android-TextView-LinkBuilder
35 | POM_SCM_CONNECTION=scm:git@github.com:klinker24/Android-TextView-LinkBuilder.git
36 | POM_SCM_DEV_CONNECTION=scm:git@github.com:klinker24/Android-TextView-LinkBuilder.git
37 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
38 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
39 | POM_LICENCE_DIST=repo
40 | POM_DEVELOPER_ID=klinker24
41 | POM_DEVELOPER_NAME=Luke Klinker
42 | POM_NAME=Android TextView LinkBuilder
43 | POM_ARTIFACT_ID=link_builder
44 | POM_PACKAGING=aar
45 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | apply plugin: 'com.android.library'
16 | apply plugin: 'kotlin-android'
17 |
18 | android {
19 | compileSdkVersion Integer.parseInt(project.COMPILE_SDK)
20 |
21 | defaultConfig {
22 | minSdkVersion Integer.parseInt(project.MIN_SDK)
23 | targetSdkVersion Integer.parseInt(project.TARGET_SDK)
24 | versionCode Integer.parseInt(project.VERSION_CODE)
25 | versionName project.VERSION_NAME
26 | }
27 |
28 | lintOptions {
29 | abortOnError false
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
35 | }
36 |
37 | apply from: 'https://raw.github.com/klinker24/gradle-mvn-push/master/gradle-mvn-push.gradle'
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
19 |
20 |
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.klinker.android.link_builder
2 |
3 | import android.content.Context
4 | import android.widget.TextView
5 |
6 | fun TextView.applyLinks(vararg links: Link) {
7 | val builder = LinkBuilder.on(this)
8 | links.forEach { builder.addLink(it) }
9 |
10 | builder.build()
11 | }
12 |
13 |
14 | fun TextView.applyLinks(links: List) {
15 | LinkBuilder.on(this)
16 | .addLinks(links)
17 | .build()
18 | }
19 |
20 | fun TextView.applyLinkedText(text: CharSequence) {
21 | this.text = text
22 | this.movementMethod = TouchableMovementMethod.instance
23 | }
24 |
25 | fun CharSequence.createLinks(context: Context, vararg links: Link) {
26 | val builder = LinkBuilder.from(context, this.toString())
27 | links.forEach { builder.addLink(it) }
28 |
29 | builder.build()
30 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/Link.kt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | package com.klinker.android.link_builder
16 |
17 | import android.graphics.Color
18 | import android.graphics.Typeface
19 |
20 | import java.util.regex.Pattern
21 |
22 | @Suppress("MemberVisibilityCanBePrivate")
23 | class Link {
24 |
25 | @JvmField var text: String? = null
26 | @JvmField var pattern: Pattern? = null
27 | @JvmField var prependedText: String? = null
28 | @JvmField var appendedText: String? = null
29 | @JvmField var textColor = 0
30 | @JvmField var textColorOfHighlightedLink = 0
31 | @JvmField var highlightAlpha = DEFAULT_ALPHA
32 | @JvmField var underlined = true
33 | @JvmField var bold = false
34 | @JvmField var typeface: Typeface? = null
35 | @JvmField var clickListener: OnClickListener? = null
36 | @JvmField var longClickListener: OnLongClickListener? = null
37 |
38 | /**
39 | * Copy Constructor.
40 | * @param link what you want to base the new link off of.
41 | */
42 | constructor(link: Link) {
43 | this.text = link.text
44 | this.prependedText = link.prependedText
45 | this.appendedText = link.appendedText
46 | this.pattern = link.pattern
47 | this.clickListener = link.clickListener
48 | this.longClickListener = link.longClickListener
49 | this.textColor = link.textColor
50 | this.textColorOfHighlightedLink = link.textColorOfHighlightedLink
51 | this.highlightAlpha = link.highlightAlpha
52 | this.underlined = link.underlined
53 | this.bold = link.bold
54 | this.typeface = link.typeface
55 | }
56 |
57 | /**
58 | * Construct a new Link rule to match the text.
59 | * @param text Text you want to highlight.
60 | */
61 | constructor(text: String) {
62 | this.text = text
63 | this.pattern = null
64 | }
65 |
66 | /**
67 | * Construct a new Link rule to match the pattern.
68 | * @param pattern pattern of the different texts you want to highlight.
69 | */
70 | constructor(pattern: Pattern) {
71 | this.pattern = pattern
72 | this.text = null
73 | }
74 |
75 | /**
76 | * Specify the text you want to match.
77 | * @param text to match.
78 | * @return the current link object.
79 | */
80 | fun setText(text: String): Link {
81 | this.text = text
82 | this.pattern = null
83 | return this
84 | }
85 |
86 | /**
87 | * Specify the pattern you want to match.
88 | * @param pattern to match.
89 | * @return the current link object.
90 | */
91 | fun setPattern(pattern: Pattern): Link {
92 | this.pattern = pattern
93 | this.text = null
94 | return this
95 | }
96 |
97 | /**
98 | * This text will be added *before* any matches.
99 | * @param text to place before the link's text.
100 | * @return the current link object.
101 | */
102 | fun setPrependedText(text: String): Link {
103 | this.prependedText = text
104 | return this
105 | }
106 |
107 | /**
108 | * This text will be added *after* any matches.
109 | * @param text to place after the link's text.
110 | * @return the current link object.
111 | */
112 | fun setAppendedText(text: String): Link {
113 | this.appendedText = text
114 | return this
115 | }
116 |
117 | /**
118 | * Specify what happens with a short click.
119 | * @param clickListener action for the short click.
120 | * @return the current link object.
121 | */
122 | fun setOnClickListener(clickListener: OnClickListener): Link {
123 | this.clickListener = clickListener
124 | return this
125 | }
126 |
127 | fun setOnClickListener(listener: (String) -> Unit): Link {
128 | this.clickListener = object : OnClickListener {
129 | override fun onClick(clickedText: String) {
130 | listener(clickedText)
131 | }
132 | }
133 |
134 | return this
135 | }
136 |
137 | /**
138 | * Specify what happens with a long click.
139 | * @param longClickListener action for the long click.
140 | * @return the current link object.
141 | */
142 | fun setOnLongClickListener(longClickListener: OnLongClickListener): Link {
143 | this.longClickListener = longClickListener
144 | return this
145 | }
146 |
147 | fun setOnLongClickListener(listener: (String) -> Unit): Link {
148 | this.longClickListener = object : OnLongClickListener {
149 | override fun onLongClick(clickedText: String) {
150 | listener(clickedText)
151 | }
152 | }
153 |
154 | return this
155 | }
156 |
157 | /**
158 | * Specify the text color for the linked text.
159 | * @param color as an integer (not resource).
160 | * @return the current link object.
161 | */
162 | fun setTextColor(color: Int): Link {
163 | this.textColor = color
164 | return this
165 | }
166 |
167 | /**
168 | * Specify the text color for the linked text when the link is pressed.
169 | * @param colorOfHighlightedLink as an integer (not resource).
170 | * @return the current link object.
171 | */
172 | fun setTextColorOfHighlightedLink(colorOfHighlightedLink: Int): Link {
173 | this.textColorOfHighlightedLink = colorOfHighlightedLink
174 | return this
175 | }
176 |
177 | /**
178 | * Specify whether you want it underlined or not.
179 | * @param underlined
180 | * @return the current link object.
181 | */
182 | fun setUnderlined(underlined: Boolean): Link {
183 | this.underlined = underlined
184 | return this
185 | }
186 |
187 | /**
188 | * Specify whether you want it bold or not.
189 | * @param bold
190 | * @return the current link object.
191 | */
192 | fun setBold(bold: Boolean): Link {
193 | this.bold = bold
194 | return this
195 | }
196 |
197 | /**
198 | * Specify the alpha of the links background when the user clicks it.
199 | * @param alpha
200 | * @return the current link object.
201 | */
202 | fun setHighlightAlpha(alpha: Float): Link {
203 | this.highlightAlpha = alpha
204 | return this
205 | }
206 |
207 | /**
208 | * Specify the typeface for the link.
209 | * @param typeface
210 | * @return the current link object
211 | */
212 | fun setTypeface(typeface: Typeface): Link {
213 | this.typeface = typeface
214 | return this
215 | }
216 |
217 | /**
218 | * Interface to manage the single clicks.
219 | */
220 | @FunctionalInterface
221 | interface OnClickListener {
222 | fun onClick(clickedText: String)
223 | }
224 |
225 | /**
226 | * Interface to manage the long clicks.
227 | */
228 | @FunctionalInterface
229 | interface OnLongClickListener {
230 | fun onLongClick(clickedText: String)
231 | }
232 |
233 | companion object {
234 | val DEFAULT_COLOR = Color.parseColor("#33B5E5")
235 | private const val DEFAULT_ALPHA = .20f
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/LinkBuilder.kt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | package com.klinker.android.link_builder
16 |
17 | import android.content.Context
18 | import android.text.Spannable
19 | import android.text.SpannableString
20 | import android.text.Spanned
21 | import android.text.TextUtils
22 | import android.widget.TextView
23 |
24 | import java.util.ArrayList
25 | import java.util.regex.Pattern
26 |
27 |
28 | class LinkBuilder {
29 |
30 | private var context: Context? = null
31 |
32 | private val type: Int
33 | private val links = ArrayList()
34 |
35 | private var textView: TextView? = null
36 | private var text: CharSequence? = null
37 | private var findOnlyFirstMatch = false
38 | private var spannable: SpannableString? = null
39 |
40 | /**
41 | * Construct a LinkBuilder object.
42 | *
43 | * @param type TYPE_TEXT or TYPE_TEXT_VIEW
44 | */
45 | private constructor(type: Int) {
46 | this.type = type
47 | }
48 |
49 | @Deprecated("")
50 | constructor(textView: TextView?) {
51 | if (textView == null) {
52 | throw IllegalArgumentException("textView is null")
53 | }
54 |
55 | this.textView = textView
56 | this.type = -1
57 |
58 | setText(textView.text.toString())
59 | }
60 |
61 | fun setTextView(textView: TextView): LinkBuilder {
62 | this.textView = textView
63 | return setText(textView.text)
64 | }
65 |
66 | fun setText(text: CharSequence): LinkBuilder {
67 | this.text = text
68 | return this
69 | }
70 |
71 | fun setContext(context: Context): LinkBuilder {
72 | this.context = context
73 | return this
74 | }
75 |
76 | fun setFindOnlyFirstMatchesForAnyLink(findOnlyFirst: Boolean): LinkBuilder {
77 | this.findOnlyFirstMatch = findOnlyFirst
78 | return this
79 | }
80 |
81 | /**
82 | * Add a single link to the builder.
83 | *
84 | * @param link the rule that you want to link with.
85 | */
86 | fun addLink(link: Link): LinkBuilder {
87 | this.links.add(link)
88 | return this
89 | }
90 |
91 | /**
92 | * Add a list of links to the builder.
93 | *
94 | * @param links list of rules you want to link with.
95 | */
96 | fun addLinks(links: List): LinkBuilder {
97 | if (links.isEmpty()) {
98 | throw IllegalArgumentException("link list is empty")
99 | }
100 |
101 | this.links.addAll(links)
102 | return this
103 | }
104 |
105 | /**
106 | * Execute the rules to create the linked text.
107 | */
108 | fun build(): CharSequence? {
109 | // we extract individual links from the patterns
110 | turnPatternsToLinks()
111 |
112 | // exit if there are no links
113 | if (links.size == 0) {
114 | return null
115 | }
116 |
117 | // we need to apply this text before the links are created
118 | applyAppendedAndPrependedText()
119 |
120 |
121 | // add those links to our spannable text so they can be clicked
122 | for (link in links) {
123 | addLinkToSpan(link)
124 | }
125 |
126 | if (type == TYPE_TEXT_VIEW) {
127 | // set the spannable text
128 | textView!!.text = spannable
129 |
130 | // add the movement method so we know what actions to perform on the clicks
131 | addLinkMovementMethod()
132 | }
133 |
134 | return spannable
135 | }
136 |
137 | /**
138 | * Add the link rule and check if spannable text is created.
139 | *
140 | * @param link rule to add to the text.
141 | */
142 | private fun addLinkToSpan(link: Link) {
143 | // create a new spannable string if none exists
144 | if (spannable == null) {
145 | spannable = SpannableString.valueOf(text)
146 | }
147 |
148 | // add the rule to the spannable string
149 | addLinkToSpan(spannable!!, link)
150 | }
151 |
152 | /**
153 | * Find the link within the spannable text
154 | *
155 | * @param s spannable text that we are adding the rule to.
156 | * @param link rule to add to the text.
157 | */
158 | private fun addLinkToSpan(s: Spannable, link: Link) {
159 | // get the current text
160 | val pattern = Pattern.compile(Pattern.quote(link.text))
161 | val matcher = pattern.matcher(text!!)
162 |
163 | // find one or more links inside the text
164 | while (matcher.find()) {
165 |
166 | // find the start and end point of the linked text within the TextView
167 | val start = matcher.start()
168 |
169 | //int start = text.indexOf(link.getText());
170 | if (start >= 0 && link.text != null) {
171 | val end = start + link.text!!.length
172 |
173 | // add link to the spannable text
174 | applyLink(link, Range(start, end), s)
175 | }
176 |
177 | // if we are only looking for the first occurrence of this pattern,
178 | // then quit now and don't look any further
179 | if (findOnlyFirstMatch) {
180 | break
181 | }
182 | }
183 | }
184 |
185 | /**
186 | * Add the movement method to handle the clicks.
187 | */
188 | private fun addLinkMovementMethod() {
189 | if (textView == null) {
190 | return
191 | }
192 |
193 | val m = textView!!.movementMethod
194 | if (m == null || m !is TouchableMovementMethod) {
195 | if (textView!!.linksClickable) {
196 | textView!!.movementMethod = TouchableMovementMethod.instance
197 | }
198 | }
199 | }
200 |
201 | /**
202 | * Set the link rule to the spannable text.
203 | *
204 | * @param link rule we are applying.
205 | * @param range the start and end point of the link within the text.
206 | * @param text the spannable text to add the link to.
207 | */
208 | private fun applyLink(link: Link, range: Range, text: Spannable) {
209 | val existingSpans = text.getSpans(range.start, range.end, TouchableSpan::class.java)
210 | if (existingSpans.isEmpty()) {
211 | val span = TouchableSpan(context!!, link)
212 | text.setSpan(span, range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
213 | } else {
214 | var newSpanConsumesAllOld = true
215 | for (span in existingSpans) {
216 | val start = spannable!!.getSpanStart(span)
217 | val end = spannable!!.getSpanEnd(span)
218 | if (range.start > start || range.end < end) {
219 | newSpanConsumesAllOld = false
220 | break
221 | } else {
222 | text.removeSpan(span)
223 | }
224 | }
225 |
226 | if (newSpanConsumesAllOld) {
227 | val span = TouchableSpan(context!!, link)
228 | text.setSpan(span, range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
229 | }
230 | }
231 | }
232 |
233 | /**
234 | * Find the links that contain patterns and convert them to individual links.
235 | */
236 | private fun turnPatternsToLinks() {
237 | var size = links.size
238 | var i = 0
239 |
240 | while (i < size) {
241 | if (links[i].pattern != null) {
242 | addLinksFromPattern(links[i])
243 |
244 | links.removeAt(i)
245 | size--
246 | } else {
247 | i++
248 | }
249 | }
250 | }
251 |
252 | /**
253 | * Add the appended and prepended text to the links and apply it to the TextView
254 | * so that we can create the SpannableString.
255 | */
256 | private fun applyAppendedAndPrependedText() {
257 | for (i in links.indices) {
258 | val link = links[i]
259 |
260 | if (link.prependedText != null) {
261 | val totalText = link.prependedText + " " + link.text
262 |
263 | text = TextUtils.replace(text, arrayOf(link.text), arrayOf(totalText))
264 | links[i].setText(totalText)
265 | }
266 |
267 | if (link.appendedText != null) {
268 | val totalText = link.text + " " + link.text
269 |
270 | text = TextUtils.replace(text, arrayOf(link.text), arrayOf(totalText))
271 | links[i].setText(totalText)
272 | }
273 | }
274 | }
275 |
276 | /**
277 | * Convert the pattern to individual links.
278 | *
279 | * @param linkWithPattern pattern we want to match.
280 | */
281 | private fun addLinksFromPattern(linkWithPattern: Link) {
282 | val pattern = linkWithPattern.pattern
283 | val m = pattern?.matcher(text!!) ?: return
284 |
285 | while (m.find()) {
286 | links.add(Link(linkWithPattern).setText(text!!.subSequence(m.start(), m.end()).toString()))
287 |
288 | // if we are only looking for the first occurrence of this pattern,
289 | // then quit now and don't look any further
290 | if (findOnlyFirstMatch) {
291 | break
292 | }
293 | }
294 | }
295 |
296 | /**
297 | * Manages the start and end points of the linked text.
298 | */
299 | private class Range(var start: Int, var end: Int)
300 |
301 | companion object {
302 |
303 | private const val TYPE_TEXT = 1
304 | private const val TYPE_TEXT_VIEW = 2
305 |
306 | @JvmStatic fun from(context: Context, text: CharSequence): LinkBuilder {
307 | return LinkBuilder(TYPE_TEXT)
308 | .setContext(context)
309 | .setText(text)
310 | }
311 |
312 | @JvmStatic fun on(tv: TextView): LinkBuilder {
313 | return LinkBuilder(TYPE_TEXT_VIEW)
314 | .setContext(tv.context)
315 | .setTextView(tv)
316 | }
317 | }
318 | }
319 |
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/LinkConsumableTextView.kt:
--------------------------------------------------------------------------------
1 | package com.klinker.android.link_builder
2 |
3 | import android.content.Context
4 | import android.text.method.MovementMethod
5 | import android.util.AttributeSet
6 | import android.view.MotionEvent
7 | import android.widget.TextView
8 |
9 | class LinkConsumableTextView : TextView {
10 |
11 | constructor(context: Context) : super(context)
12 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
13 | constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
14 |
15 | /**
16 | * Overriding the methods below is required if you want to use ListView's OnItemClickListener.
17 | *
18 | * These methods will force the list view to only consume the touch event if the TouchableSpan
19 | * has been clicked. Otherwise, it will not consume the touch event and it will defer to your
20 | * ListView.OnItemClickListener method.
21 | */
22 |
23 | override fun hasFocusable() = false
24 | override fun onTouchEvent(event: MotionEvent): Boolean {
25 | super.onTouchEvent(event)
26 |
27 | val movementMethod = movementMethod
28 |
29 | if (movementMethod is TouchableMovementMethod) {
30 | val span = movementMethod.pressedSpan
31 |
32 | if (span != null) {
33 | return true
34 | }
35 | }
36 |
37 | return false
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/TouchableBaseSpan.kt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | package com.klinker.android.link_builder
16 |
17 | import android.os.Handler
18 | import android.text.style.ClickableSpan
19 | import android.view.View
20 |
21 | abstract class TouchableBaseSpan : ClickableSpan() {
22 |
23 | var isTouched = false
24 |
25 | /**
26 | * This TouchableSpan has been clicked.
27 | * @param widget TextView containing the touchable span
28 | */
29 | override fun onClick(widget: View) {
30 | Handler().postDelayed({ TouchableMovementMethod.touched = false }, 500)
31 | }
32 |
33 | /**
34 | * This TouchableSpan has been long clicked.
35 | * @param widget TextView containing the touchable span
36 | */
37 | open fun onLongClick(widget: View) {
38 | Handler().postDelayed({ TouchableMovementMethod.touched = false }, 500)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/TouchableMovementMethod.kt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | package com.klinker.android.link_builder
16 |
17 | import android.os.Handler
18 | import android.text.Layout
19 | import android.text.Selection
20 | import android.text.Spannable
21 | import android.text.method.LinkMovementMethod
22 | import android.text.method.MovementMethod
23 | import android.view.HapticFeedbackConstants
24 | import android.view.MotionEvent
25 | import android.widget.TextView
26 | import java.lang.IndexOutOfBoundsException
27 |
28 | class TouchableMovementMethod : LinkMovementMethod() {
29 |
30 | var pressedSpan: TouchableBaseSpan? = null
31 | private set
32 |
33 | /**
34 | * Manages the touches to find the link that was clicked and highlight it
35 | * @param textView view the user clicked
36 | * @param spannable spannable string inside the clicked view
37 | * @param event motion event that occurred
38 | * @return
39 | */
40 | override fun onTouchEvent(textView: TextView, spannable: Spannable, event: MotionEvent): Boolean {
41 | if (event.action == MotionEvent.ACTION_DOWN) {
42 | pressedSpan = getPressedSpan(textView, spannable, event)
43 |
44 | if (pressedSpan != null) {
45 | pressedSpan!!.isTouched = true
46 | touched = true
47 |
48 | Handler().postDelayed({
49 | if (touched && pressedSpan != null) {
50 | if (textView.isHapticFeedbackEnabled)
51 | textView.isHapticFeedbackEnabled = true
52 | textView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
53 |
54 | pressedSpan!!.onLongClick(textView)
55 | pressedSpan!!.isTouched = false
56 | pressedSpan = null
57 |
58 | Selection.removeSelection(spannable)
59 | }
60 | }, 500)
61 |
62 | Selection.setSelection(spannable, spannable.getSpanStart(pressedSpan),
63 | spannable.getSpanEnd(pressedSpan))
64 | }
65 | } else if (event.action == MotionEvent.ACTION_MOVE) {
66 | val touchedSpan = getPressedSpan(textView, spannable, event)
67 |
68 | if (pressedSpan != null && pressedSpan != touchedSpan) {
69 | pressedSpan!!.isTouched = false
70 | pressedSpan = null
71 | touched = false
72 |
73 | Selection.removeSelection(spannable)
74 | }
75 | } else if (event.action == MotionEvent.ACTION_UP) {
76 | if (pressedSpan != null) {
77 | pressedSpan!!.onClick(textView)
78 | pressedSpan!!.isTouched = false
79 | pressedSpan = null
80 |
81 | Selection.removeSelection(spannable)
82 | }
83 | } else {
84 | if (pressedSpan != null) {
85 | pressedSpan!!.isTouched = false
86 | touched = false
87 |
88 | super.onTouchEvent(textView, spannable, event)
89 | }
90 |
91 | pressedSpan = null
92 |
93 | Selection.removeSelection(spannable)
94 | }
95 |
96 | return true
97 | }
98 |
99 | /**
100 | * Find the span that was clicked
101 | * @param widget view the user clicked
102 | * @param spannable spannable string inside the clicked view
103 | * @param event motion event that occurred
104 | * @return the touchable span that was pressed
105 | */
106 | private fun getPressedSpan(widget: TextView, spannable: Spannable, event: MotionEvent): TouchableBaseSpan? {
107 |
108 | var x = event.x.toInt()
109 | var y = event.y.toInt()
110 |
111 | x -= widget.totalPaddingLeft
112 | y -= widget.totalPaddingTop
113 |
114 | x += widget.scrollX
115 | y += widget.scrollY
116 |
117 | val layout = widget.layout
118 | val line = layout.getLineForVertical(y)
119 |
120 | val off = try {
121 | layout.getOffsetForHorizontal(line, x.toFloat())
122 | } catch (e: IndexOutOfBoundsException) {
123 | return null
124 | }
125 |
126 | val end = layout.getLineEnd(line)
127 |
128 | // offset seems like it can be one off in some cases
129 | // Could be what was causing issue 7 in the first place:
130 | // https://github.com/klinker24/Android-TextView-LinkBuilder/issues/7
131 | if (off != end && off != end - 1) {
132 | val link = spannable.getSpans(off, off, TouchableBaseSpan::class.java)
133 |
134 | if (link.isNotEmpty())
135 | return link[0]
136 | }
137 |
138 | return null
139 | }
140 |
141 | companion object {
142 |
143 | private var sInstance: TouchableMovementMethod? = null
144 | var touched = false
145 |
146 | /**
147 | * Don't need to create a new instance for every text view. We can re-use them
148 | * @return Instance of the movement method
149 | */
150 | val instance: MovementMethod
151 | get() {
152 | if (sInstance == null)
153 | sInstance = TouchableMovementMethod()
154 |
155 | return sInstance!!
156 | }
157 | }
158 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/klinker/android/link_builder/TouchableSpan.kt:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | package com.klinker.android.link_builder
16 |
17 | import android.content.Context
18 | import android.content.res.TypedArray
19 | import android.graphics.Color
20 | import android.os.Handler
21 | import android.text.TextPaint
22 | import android.text.style.ClickableSpan
23 | import android.util.TypedValue
24 | import android.view.View
25 |
26 | class TouchableSpan(context: Context, private val link: Link) : TouchableBaseSpan() {
27 |
28 | private var textColor: Int = 0
29 | private var textColorOfHighlightedLink: Int = 0
30 |
31 | init {
32 | if (link.textColor == 0) {
33 | this.textColor = getDefaultColor(context, R.styleable.LinkBuilder_defaultLinkColor)
34 | } else {
35 | this.textColor = link.textColor
36 | }
37 |
38 | if (link.textColorOfHighlightedLink == 0) {
39 | this.textColorOfHighlightedLink = getDefaultColor(context, R.styleable.LinkBuilder_defaultTextColorOfHighlightedLink)
40 |
41 | // don't use the default of light blue for this color
42 | if (this.textColorOfHighlightedLink == Link.DEFAULT_COLOR) {
43 | this.textColorOfHighlightedLink = textColor
44 | }
45 | } else {
46 | this.textColorOfHighlightedLink = link.textColorOfHighlightedLink
47 | }
48 | }
49 |
50 | /**
51 | * Finds the default color for links based on the current theme.
52 | * @param context activity
53 | * @param index index of attribute to retrieve based on current theme
54 | * @return color as an integer
55 | */
56 | private fun getDefaultColor(context: Context, index: Int): Int {
57 | val array = obtainStyledAttrsFromThemeAttr(context, R.attr.linkBuilderStyle, R.styleable.LinkBuilder)
58 | val color = array.getColor(index, Link.DEFAULT_COLOR)
59 | array.recycle()
60 |
61 | return color
62 | }
63 |
64 | /**
65 | * This TouchableSpan has been clicked.
66 | * @param widget TextView containing the touchable span
67 | */
68 | override fun onClick(widget: View) {
69 | if (link.text != null) {
70 | link.clickListener?.onClick(link.text!!)
71 | }
72 |
73 | super.onClick(widget)
74 | }
75 |
76 | /**
77 | * This TouchableSpan has been long clicked.
78 | * @param widget TextView containing the touchable span
79 | */
80 | override fun onLongClick(widget: View) {
81 | if (link.text != null) {
82 | link.longClickListener?.onLongClick(link.text!!)
83 | }
84 |
85 | super.onLongClick(widget)
86 | }
87 |
88 | /**
89 | * Set the alpha for the color based on the alpha factor
90 | * @param color original color
91 | * @param factor how much we want to scale the alpha to
92 | * @return new color with scaled alpha
93 | */
94 | fun adjustAlpha(color: Int, factor: Float): Int {
95 | val alpha = Math.round(Color.alpha(color) * factor)
96 | val red = Color.red(color)
97 | val green = Color.green(color)
98 | val blue = Color.blue(color)
99 |
100 | return Color.argb(alpha, red, green, blue)
101 | }
102 |
103 | /**
104 | * Draw the links background and set whether or not we want it to be underlined or bold
105 | * @param ds the link
106 | */
107 | override fun updateDrawState(ds: TextPaint) {
108 | super.updateDrawState(ds)
109 |
110 | ds.isUnderlineText = link.underlined
111 | ds.isFakeBoldText = link.bold
112 | ds.color = if (isTouched) textColorOfHighlightedLink else textColor
113 | ds.bgColor = if (isTouched) adjustAlpha(textColor, link.highlightAlpha) else Color.TRANSPARENT
114 |
115 | if (link.typeface != null) {
116 | ds.typeface = link.typeface
117 | }
118 | }
119 |
120 | companion object {
121 |
122 | private fun obtainStyledAttrsFromThemeAttr(context: Context, themeAttr: Int, styleAttrs: IntArray): TypedArray {
123 | // Need to get resource id of style pointed to from the theme attr
124 | val outValue = TypedValue()
125 | context.theme.resolveAttribute(themeAttr, outValue, true)
126 | val styleResId = outValue.resourceId
127 |
128 | // Now return the values (from styleAttrs) from the style
129 | return context.obtainStyledAttributes(styleResId, styleAttrs)
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/klinker24/Android-TextView-LinkBuilder/f2040d0c90d718004a4fa03a0276504cc5d8636b/preview.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed under the Apache License, Version 2.0 (the "License");
3 | you may not use this file except in compliance with the License.
4 | You may obtain a copy of the License at
5 |
6 | http://www.apache.org/licenses/LICENSE-2.0
7 |
8 | Unless required by applicable law or agreed to in writing, software
9 | distributed under the License is distributed on an "AS IS" BASIS,
10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 | See the License for the specific language governing permissions and
12 | limitations under the License.
13 | */
14 |
15 | include ':library'
16 | include ':example'
17 |
--------------------------------------------------------------------------------