diff --git a/build.gradle b/build.gradle index e0b366a..d9fe302 100644 --- a/build.gradle +++ b/build.gradle @@ -3,9 +3,14 @@ buildscript { repositories { jcenter() + maven { + url 'https://maven.google.com/' + name 'Google' + } + google() } dependencies { - classpath 'com.android.tools.build:gradle:1.5.0' + classpath 'com.android.tools.build:gradle:3.6.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,6 +20,10 @@ buildscript { allprojects { repositories { jcenter() + maven { + url 'https://maven.google.com/' + name 'Google' + } } } diff --git a/gradle.properties b/gradle.properties index 1d3591c..59d5aab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,6 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f23df6e..8a2e104 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Oct 21 11:34:03 PDT 2015 +#Mon Mar 02 16:51:36 CET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/hashtag-helper/build.gradle b/hashtag-helper/build.gradle index e331317..8f2b134 100644 --- a/hashtag-helper/build.gradle +++ b/hashtag-helper/build.gradle @@ -1,12 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "21.1.2" + compileSdkVersion 29 + buildToolsVersion "29.0.2" defaultConfig { minSdkVersion 15 - targetSdkVersion 23 + targetSdkVersion 29 versionCode 1 versionName "1.1" } @@ -19,7 +19,7 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:23.1.1' + implementation fileTree(dir: 'libs', include: ['*.jar']) + testImplementation 'junit:junit:4.12' + implementation 'androidx.appcompat:appcompat:1.1.0' } diff --git a/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/ClickableForegroundColorSpan.java b/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/ClickableForegroundColorSpan.java index cbba84a..cdf9f77 100644 --- a/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/ClickableForegroundColorSpan.java +++ b/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/ClickableForegroundColorSpan.java @@ -1,6 +1,7 @@ package com.volokh.danylo.hashtaghelper; -import android.support.annotation.ColorInt; +import androidx.annotation.ColorInt; + import android.text.Spanned; import android.text.TextPaint; import android.text.style.ClickableSpan; @@ -11,7 +12,7 @@ * Created by danylo.volokh on 12/22/2015. * This class is a combination of {@link android.text.style.ForegroundColorSpan} * and {@link ClickableSpan}. - * + *

* You can set a color of this span plus set a click listener */ public class ClickableForegroundColorSpan extends ClickableSpan { @@ -19,7 +20,11 @@ public class ClickableForegroundColorSpan extends ClickableSpan { private OnHashTagClickListener mOnHashTagClickListener; public interface OnHashTagClickListener { - void onHashTagClicked(String hashTag); + /** + * @param initialChar is a {@link Character} which helps to determine click event + * @param hashTag simple {@link String} after initialChar + */ + void onHashTagClicked(Character initialChar, String hashTag); } private final int mColor; @@ -46,6 +51,10 @@ public void onClick(View widget) { int start = s.getSpanStart(this); int end = s.getSpanEnd(this); - mOnHashTagClickListener.onHashTagClicked(text.subSequence(start + 1/*skip "#" sign*/, end).toString()); + mOnHashTagClickListener + .onHashTagClicked( + text.charAt(start), + text.subSequence(start + 1/*skip "#" sign*/, end).toString() + ); } } diff --git a/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/HashTagHelper.java b/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/HashTagHelper.java index a3062dc..caca06b 100644 --- a/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/HashTagHelper.java +++ b/hashtag-helper/src/main/java/com/volokh/danylo/hashtaghelper/HashTagHelper.java @@ -9,6 +9,9 @@ import android.text.style.ForegroundColorSpan; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -17,46 +20,85 @@ /** * This is a helper class that should be used with {@link android.widget.EditText} or {@link android.widget.TextView} * In order to have hash-tagged words highlighted. It also provides a click listeners for every hashtag - * + *

* Example : * #ThisIsHashTagWord * #ThisIsFirst#ThisIsSecondHashTag * #hashtagendsifitfindsnotletterornotdigitsignlike_thisIsNotHighlithedArea - * */ public final class HashTagHelper implements ClickableForegroundColorSpan.OnHashTagClickListener { + private static final Character NEW_LINE = '\n'; + private static final Character CARRIAGE_RETURN = '\r'; + private static final Character SPACE = ' '; + /** * If this is not null then all of the symbols in the List will be considered as valid symbols of hashtag * For example : * mAdditionalHashTagChars = {'$','_','-'} * it means that hashtag: "#this_is_hashtag-with$dollar-sign" will be highlighted. - * + *

* Note: if mAdditionalHashTagChars would be "null" only "#this" would be highlighted - * */ private final List mAdditionalHashTagChars; + + /** + * If this is not null then all of the symbols in the List will be considered as valid start symbols + * For example: + * mStartChars = {'@','%'} + * it means that all words starting with these symbols will be highlighted. + *

+ * Note: if mStartChars is null, words started with '#' symbol will be highlighted + */ + private final List mStartChars; private TextView mTextView; private int mHashTagWordColor; + /** + * Character style needs to separate different spans in the text + */ + private Class mCharacterStyle; + private OnHashTagClickListener mOnHashTagClickListener; - public static final class Creator{ + private final ArrayList mForbiddenCharacters = new ArrayList<>(); + + public static final class Creator { + + private Creator() { + } - private Creator(){} + public static HashTagHelper create(int color, OnHashTagClickListener listener) { + return new HashTagHelper(color, listener, null, null, null); + } - public static HashTagHelper create(int color, OnHashTagClickListener listener){ - return new HashTagHelper(color, listener, null); + public static HashTagHelper create(int color, OnHashTagClickListener listener, @NonNull List additionalHashTagChars) { + return new HashTagHelper(color, listener, additionalHashTagChars, null, null); } - public static HashTagHelper create(int color, OnHashTagClickListener listener, char... additionalHashTagChars){ - return new HashTagHelper(color, listener, additionalHashTagChars); + public static HashTagHelper create( + int color, + OnHashTagClickListener listener, + List additionalHashTagChars, + @NonNull List startChars + ) { + return new HashTagHelper(color, listener, additionalHashTagChars, startChars, null); + } + + public static HashTagHelper create( + int color, + OnHashTagClickListener listener, + List additionalHashTagChars, + List startChars, + @NonNull Class characterStyle + ) { + return new HashTagHelper(color, listener, additionalHashTagChars, startChars, characterStyle); } } - public interface OnHashTagClickListener{ - void onHashTagClicked(String hashTag); + public interface OnHashTagClickListener { + void onHashTagClicked(Character initialChar, String hashTag); } private final TextWatcher mTextWatcher = new TextWatcher() { @@ -76,27 +118,50 @@ public void afterTextChanged(Editable s) { } }; - private HashTagHelper(int color, OnHashTagClickListener listener, char... additionalHashTagCharacters) { + private HashTagHelper( + int color, + OnHashTagClickListener listener, + @Nullable List additionalHashTagChars, + @Nullable List startChars, + @Nullable Class characterStyle + ) { + + addForbiddenCharactersToList(); + + if (characterStyle == null) { + mCharacterStyle = ClickableForegroundColorSpan.class; + } else { + mCharacterStyle = characterStyle; + } mHashTagWordColor = color; mOnHashTagClickListener = listener; mAdditionalHashTagChars = new ArrayList<>(); + mStartChars = new ArrayList<>(); - if(additionalHashTagCharacters != null){ - for(char additionalChar : additionalHashTagCharacters){ - mAdditionalHashTagChars.add(additionalChar); - } + if (additionalHashTagChars != null) { + mAdditionalHashTagChars.addAll(additionalHashTagChars); } + + if (startChars != null) { + mStartChars.addAll(startChars); + } + } + + private void addForbiddenCharactersToList() { + mForbiddenCharacters.add(NEW_LINE); + mForbiddenCharacters.add(SPACE); + mForbiddenCharacters.add(CARRIAGE_RETURN); } - public void handle(TextView textView){ - if(mTextView == null){ + public void handle(TextView textView) { + if (mTextView == null) { mTextView = textView; mTextView.addTextChangedListener(mTextWatcher); // in order to use spannable we have to set buffer type mTextView.setText(mTextView.getText(), TextView.BufferType.SPANNABLE); - if(mOnHashTagClickListener != null){ + if (mOnHashTagClickListener != null) { // we need to set this in order to get onClick event mTextView.setMovementMethod(LinkMovementMethod.getInstance()); @@ -117,7 +182,7 @@ private void eraseAndColorizeAllText(CharSequence text) { Spannable spannable = ((Spannable) mTextView.getText()); - CharacterStyle[] spans = spannable.getSpans(0, text.length(), CharacterStyle.class); + CharacterStyle[] spans = spannable.getSpans(0, text.length(), mCharacterStyle); for (CharacterStyle span : spans) { spannable.removeSpan(span); } @@ -126,17 +191,18 @@ private void eraseAndColorizeAllText(CharSequence text) { } private void setColorsToAllHashTags(CharSequence text) { - + String trimmedText = text.toString().trim(); int startIndexOfNextHashSign; int index = 0; - while (index < text.length()- 1){ - char sign = text.charAt(index); + while (index < trimmedText.length() - 1) { + char sign = trimmedText.charAt(index); + char nextSign = trimmedText.charAt(index + 1); int nextNotLetterDigitCharIndex = index + 1; // we assume it is next. if if was not changed by findNextValidHashTagChar then index will be incremented by 1 - if(sign == '#'){ + if (mStartChars.contains(sign) && !mStartChars.contains(nextSign) && !mForbiddenCharacters.contains(nextSign)) { startIndexOfNextHashSign = index; - nextNotLetterDigitCharIndex = findNextValidHashTagChar(text, startIndexOfNextHashSign); + nextNotLetterDigitCharIndex = findNextValidHashTagChar(trimmedText, startIndexOfNextHashSign); setColorForHashTagToTheEnd(startIndexOfNextHashSign, nextNotLetterDigitCharIndex); } @@ -152,7 +218,8 @@ private int findNextValidHashTagChar(CharSequence text, int start) { char sign = text.charAt(index); - boolean isValidSign = Character.isLetterOrDigit(sign) || mAdditionalHashTagChars.contains(sign); + boolean isValidSign = (Character.isLetterOrDigit(sign) || mAdditionalHashTagChars.contains(sign)) + && !mStartChars.contains(sign); if (!isValidSign) { nonLetterDigitCharIndex = index; break; @@ -171,7 +238,7 @@ private void setColorForHashTagToTheEnd(int startIndex, int nextNotLetterDigitCh CharacterStyle span; - if(mOnHashTagClickListener != null){ + if (mOnHashTagClickListener != null) { span = new ClickableForegroundColorSpan(mHashTagWordColor, this); } else { // no need for clickable span because it is messing with selection when click @@ -189,11 +256,11 @@ public List getAllHashTags(boolean withHashes) { // use set to exclude duplicates Set hashTags = new LinkedHashSet<>(); - for (CharacterStyle span : spannable.getSpans(0, text.length(), CharacterStyle.class)) { + for (CharacterStyle span : spannable.getSpans(0, text.length(), mCharacterStyle)) { hashTags.add( text.substring(!withHashes ? spannable.getSpanStart(span) + 1/*skip "#" sign*/ - : spannable.getSpanStart(span), - spannable.getSpanEnd(span))); + : spannable.getSpanStart(span), + spannable.getSpanEnd(span))); } return new ArrayList<>(hashTags); @@ -204,7 +271,7 @@ public List getAllHashTags() { } @Override - public void onHashTagClicked(String hashTag) { - mOnHashTagClickListener.onHashTagClicked(hashTag); + public void onHashTagClicked(Character initialChar, String hashTag) { + mOnHashTagClickListener.onHashTagClicked(initialChar, hashTag); } } diff --git a/sample_app/build.gradle b/sample_app/build.gradle index 6d68191..933d82b 100644 --- a/sample_app/build.gradle +++ b/sample_app/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 23 - buildToolsVersion "21.1.2" + compileSdkVersion 29 + buildToolsVersion "29.0.2" defaultConfig { applicationId "com.volokh.danylo.hashtaghelper" minSdkVersion 15 - targetSdkVersion 23 + targetSdkVersion 29 versionCode 1 versionName "1.0" } @@ -20,8 +20,8 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - testCompile 'junit:junit:4.12' - compile 'com.android.support:appcompat-v7:23.1.1' - compile project(':hashtag-helper') + implementation fileTree(dir: 'libs', include: ['*.jar']) + testImplementation 'junit:junit:4.12' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation project(':hashtag-helper') } diff --git a/sample_app/src/main/java/com/volokh/danylo/example/HashTagHelperDemoActivity.java b/sample_app/src/main/java/com/volokh/danylo/example/HashTagHelperDemoActivity.java index f90782f..245d712 100644 --- a/sample_app/src/main/java/com/volokh/danylo/example/HashTagHelperDemoActivity.java +++ b/sample_app/src/main/java/com/volokh/danylo/example/HashTagHelperDemoActivity.java @@ -1,6 +1,5 @@ package com.volokh.danylo.example; -import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -8,8 +7,11 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; + import com.volokh.danylo.hashtaghelper.HashTagHelper; +import java.util.ArrayList; import java.util.List; public class HashTagHelperDemoActivity extends AppCompatActivity implements HashTagHelper.OnHashTagClickListener, View.OnClickListener { @@ -39,24 +41,37 @@ protected void onCreate(Bundle savedInstanceState) { Button getAllHashTagsBtn = (Button) findViewById(R.id.get_all_hashtags_btn); getAllHashTagsBtn.setOnClickListener(this); - char[] additionalSymbols = new char[]{ - '_', - '$' - }; + ArrayList additionalSymbols = new ArrayList<>(); + additionalSymbols.add('_'); + additionalSymbols.add('$'); + + ArrayList startChars = new ArrayList<>(); + startChars.add('@'); + startChars.add('%'); + startChars.add('#'); + // If you set additional symbols not only letters and digits will be a valid symbols for hashtag // Example: "hash_tag_with_underscore_and$dolar$sign$is$also$valid_hashtag" - mTextHashTagHelper = HashTagHelper.Creator.create(getResources().getColor(R.color.colorPrimary), this, additionalSymbols); + mTextHashTagHelper = HashTagHelper.Creator.create( + getResources().getColor(R.color.colorPrimary), + this, + additionalSymbols, + startChars); mTextHashTagHelper.handle(mHashTagText); // Here we don't specify additionalSymbols. It means that in EditText only letters and digits will be valid symbols - mEditTextHashTagHelper = HashTagHelper.Creator.create(getResources().getColor(R.color.colorPrimaryDark), null); + mEditTextHashTagHelper = HashTagHelper.Creator.create( + getResources().getColor(R.color.colorPrimaryDark), + null, + null, + startChars); mEditTextHashTagHelper.handle(mEditTextView); } @Override - public void onHashTagClicked(String hashTag) { + public void onHashTagClicked(Character initialChar, String hashTag) { Log.v(TAG, "onHashTagClicked [" + hashTag + "]"); - if(mToast != null){ + if (mToast != null) { mToast.cancel(); } mToast = Toast.makeText(HashTagHelperDemoActivity.this, hashTag, Toast.LENGTH_SHORT); @@ -65,7 +80,7 @@ public void onHashTagClicked(String hashTag) { @Override public void onClick(View v) { - switch (v.getId()){ + switch (v.getId()) { case R.id.get_entered_text_btn: mHashTagText.setText(mEditTextView.getText()); break; diff --git a/sample_app/src/main/res/values/strings.xml b/sample_app/src/main/res/values/strings.xml index c678ed9..9595fe2 100644 --- a/sample_app/src/main/res/values/strings.xml +++ b/sample_app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ HashTagHelper - enter text with #hashtag here + enter text with # or @ or % here Set text text will be pasted here get all hashtags