Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for other than '#' chars #23

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,6 +20,10 @@ buildscript {
allprojects {
repositories {
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
# org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions hashtag-helper/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
Expand All @@ -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'
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,15 +12,19 @@
* Created by danylo.volokh on 12/22/2015.
* This class is a combination of {@link android.text.style.ForegroundColorSpan}
* and {@link ClickableSpan}.
*
* <p>
* You can set a color of this span plus set a click listener
*/
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;
Expand All @@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*
* <p>
* 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.
*
* <p>
* Note: if mAdditionalHashTagChars would be "null" only "#this" would be highlighted
*
*/
private final List<Character> 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.
* <p>
* Note: if mStartChars is null, words started with '#' symbol will be highlighted
*/
private final List<Character> mStartChars;
private TextView mTextView;
private int mHashTagWordColor;

/**
* Character style needs to separate different spans in the text
*/
private Class<? extends CharacterStyle> mCharacterStyle;

private OnHashTagClickListener mOnHashTagClickListener;

public static final class Creator{
private final ArrayList<Character> 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<Character> 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<Character> additionalHashTagChars,
@NonNull List<Character> startChars
) {
return new HashTagHelper(color, listener, additionalHashTagChars, startChars, null);
}

public static HashTagHelper create(
int color,
OnHashTagClickListener listener,
List<Character> additionalHashTagChars,
List<Character> startChars,
@NonNull Class<? extends ClickableForegroundColorSpan> 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() {
Expand All @@ -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<Character> additionalHashTagChars,
@Nullable List<Character> startChars,
@Nullable Class<? extends ClickableForegroundColorSpan> 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());

Expand All @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -189,11 +256,11 @@ public List<String> getAllHashTags(boolean withHashes) {
// use set to exclude duplicates
Set<String> 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);
Expand All @@ -204,7 +271,7 @@ public List<String> getAllHashTags() {
}

@Override
public void onHashTagClicked(String hashTag) {
mOnHashTagClickListener.onHashTagClicked(hashTag);
public void onHashTagClicked(Character initialChar, String hashTag) {
mOnHashTagClickListener.onHashTagClicked(initialChar, hashTag);
}
}
14 changes: 7 additions & 7 deletions sample_app/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
Expand All @@ -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')
}
Loading