├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_add.png │ │ │ │ ├── ic_bad.png │ │ │ │ ├── ic_cry.png │ │ │ │ ├── ic_delete.png │ │ │ │ ├── ic_error.png │ │ │ │ ├── ic_good.png │ │ │ │ ├── ic_right.png │ │ │ │ ├── ic_smile.png │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ └── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── activity_test.xml │ │ │ │ ├── activity_tag_shape.xml │ │ │ │ ├── activity_tag_reverse.xml │ │ │ │ ├── activity_tag_choice.xml │ │ │ │ ├── activity_tag_change.xml │ │ │ │ ├── activity_tag_edit.xml │ │ │ │ └── activity_tag_view.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── dl7 │ │ │ │ └── taglayout │ │ │ │ ├── MyApplication.java │ │ │ │ ├── TestActivity.java │ │ │ │ ├── utils │ │ │ │ ├── TagWordFactory.java │ │ │ │ ├── RxHelper.java │ │ │ │ └── ToastUtils.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── TagShapeActivity.java │ │ │ │ ├── TagReverseActivity.java │ │ │ │ ├── TagChoiceActivity.java │ │ │ │ ├── TagEditActivity.java │ │ │ │ ├── drawable │ │ │ │ ├── MultiCircleDrawable.java │ │ │ │ └── CircleDrawable.java │ │ │ │ ├── TagChangeActivity.java │ │ │ │ └── TagViewActivity.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── dl7 │ │ │ └── taglayout │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── dl7 │ │ └── taglayout │ │ └── ApplicationTest.java ├── proguard-rules.pro └── build.gradle ├── taglayout ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ └── attrs.xml │ │ │ └── drawable │ │ │ │ └── ic_change.png │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── dl7 │ │ │ └── tag │ │ │ ├── drawable │ │ │ ├── SimpleCallBack.java │ │ │ └── RotateDrawable.java │ │ │ ├── utils │ │ │ ├── BitmapUtils.java │ │ │ ├── ColorsFactory.java │ │ │ └── MeasureUtils.java │ │ │ ├── TagEditView.java │ │ │ ├── TagLayout.java │ │ │ └── TagView.java │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── dl7 │ │ │ └── tag │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── dl7 │ │ └── tag │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── gradlew └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /taglayout/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':taglayout' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TagLayout 3 | 4 | -------------------------------------------------------------------------------- /taglayout/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TagLibrary 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_add.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_bad.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_cry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_cry.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_delete.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_error.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_good.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_right.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_smile.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /taglayout/src/main/res/drawable/ic_change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/taglayout/src/main/res/drawable/ic_change.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rukey7/TagLayout/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /taglayout/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/test/java/com/dl7/taglayout/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.app.Application; 4 | 5 | import com.dl7.taglayout.utils.ToastUtils; 6 | 7 | /** 8 | * Created by long on 2016/12/9. 9 | */ 10 | 11 | public class MyApplication extends Application { 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | ToastUtils.init(this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/dl7/taglayout/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #88F44336 8 | #33F44336 9 | #FF666666 10 | #66333333 11 | 12 | -------------------------------------------------------------------------------- /taglayout/src/test/java/com/dl7/tag/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/drawable/SimpleCallBack.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag.drawable; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | /** 6 | * Created by long on 2017/4/21. 7 | */ 8 | 9 | public abstract class SimpleCallBack implements Drawable.Callback { 10 | 11 | @Override 12 | public void scheduleDrawable(Drawable drawable, Runnable runnable, long l) { 13 | } 14 | 15 | @Override 16 | public void unscheduleDrawable(Drawable drawable, Runnable runnable) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in F:\WorkTools\Android\SDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /taglayout/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in F:\WorkTools\Android\SDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /taglayout/src/androidTest/java/com/dl7/tag/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.dl7.tag.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.dl7.taglayout" 9 | minSdkVersion 14 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:25.0.1' 26 | compile project(':taglayout') 27 | compile 'io.reactivex:rxjava:1.2.5' 28 | compile 'io.reactivex:rxandroid:1.2.1' 29 | compile 'com.trello:rxlifecycle:1.0' 30 | compile 'com.trello:rxlifecycle-components:1.0' 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.dl7.tag.TagView; 7 | import com.dl7.taglayout.drawable.CircleDrawable; 8 | import com.dl7.taglayout.drawable.MultiCircleDrawable; 9 | 10 | public class TestActivity extends AppCompatActivity { 11 | 12 | private TagView mTagView; 13 | private TagView mTagMultiCircle; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_test); 19 | initView(); 20 | } 21 | 22 | private void initView() { 23 | mTagView = (TagView) findViewById(R.id.tag_view); 24 | mTagView.setDecorateIcon(new CircleDrawable()); 25 | mTagMultiCircle = (TagView) findViewById(R.id.tag_multi_circle); 26 | mTagMultiCircle.setDecorateIcon(new MultiCircleDrawable()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/utils/BitmapUtils.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag.utils; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Matrix; 5 | 6 | /** 7 | * Created by Rukey7 on 2016/12/5. 8 | */ 9 | 10 | public final class BitmapUtils { 11 | 12 | private BitmapUtils() { 13 | throw new AssertionError(); 14 | } 15 | 16 | /** 17 | * 放大缩小图片 18 | * 19 | * @param bitmap 源Bitmap 20 | * @param w 宽 21 | * @param h 高 22 | * @return 目标Bitmap 23 | */ 24 | public static Bitmap zoom(Bitmap bitmap, int w, int h) { 25 | int width = bitmap.getWidth(); 26 | int height = bitmap.getHeight(); 27 | Matrix matrix = new Matrix(); 28 | float scaleWidth = ((float) w / width); 29 | float scaleHeight = ((float) h / height); 30 | matrix.postScale(scaleWidth, scaleHeight); 31 | Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, 32 | matrix, true); 33 | return newBitmap; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /taglayout/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | group='com.github.Rukey7' 4 | 5 | android { 6 | compileSdkVersion 25 7 | buildToolsVersion "25.0.1" 8 | 9 | defaultConfig { 10 | minSdkVersion 14 11 | targetSdkVersion 25 12 | versionCode 106 13 | versionName "1.0.6" 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(dir: 'libs', include: ['*.jar']) 28 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 29 | exclude group: 'com.android.support', module: 'support-annotations' 30 | }) 31 | compile 'com.android.support:appcompat-v7:25.0.1' 32 | testCompile 'junit:junit:4.12' 33 | } 34 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/utils/ColorsFactory.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag.utils; 2 | 3 | import android.support.annotation.ColorInt; 4 | 5 | import java.util.Random; 6 | 7 | /** 8 | * Created by long on 2016/12/5. 9 | * 随机颜色生成器 10 | */ 11 | public final class ColorsFactory { 12 | 13 | private static final 14 | @ColorInt 15 | int[] COLORS = new int[]{ 16 | 0xe51c23, 0xe91e63, 0x9c27b0, 0x673ab7, 17 | 0x3f51b5, 0x5677fc, 0x03a9f4, 0x00bcd4, 18 | 0x009688, 0x259b24, 0x8bc34a, 0x607d8b, 19 | 0xffc107, 0xff9800, 0xff5722, 0x795548, 0x9e9e9e, 20 | }; 21 | private static Random sRandom = new Random(); 22 | 23 | private ColorsFactory() { 24 | throw new AssertionError(); 25 | } 26 | 27 | @ColorInt 28 | public static int[] provideColor() { 29 | int index = sRandom.nextInt(COLORS.length); 30 | int[] colors = new int[2]; 31 | colors[0] = COLORS[index] + 0xff000000; 32 | colors[1] = COLORS[index] + 0x88000000; 33 | return colors; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/utils/TagWordFactory.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout.utils; 2 | 3 | import java.util.Random; 4 | 5 | /** 6 | * Created by long on 2016/12/9. 7 | */ 8 | 9 | public final class TagWordFactory { 10 | 11 | public static final String[] TAG_WORD = new String[] { 12 | "Hello", "Android", "Java", "我是TagView", "Hello World", 13 | "This is a long string, This is a long string, This is a long string", 14 | "这是长字符串,这是长字符串,这是长字符串,这是长字符串", "它在我的机器上可以很好运行T_T", 15 | "Learn not and know not.", "Life is but a span.", "Never say die.", "知识改变命运,英语成就未来" 16 | }; 17 | 18 | public static final String[] TAG_WORD_2 = new String[] { 19 | "Success", "Failure", "美女", "影视", "豆瓣Top250", 20 | "Have you given any thought to your future?", 21 | "我猜着了开头,但我猜不中这结局" 22 | }; 23 | 24 | private static Random sRandom = new Random(); 25 | 26 | private TagWordFactory() { 27 | throw new AssertionError(); 28 | } 29 | 30 | 31 | public static String provideTagWord() { 32 | return TAG_WORD[sRandom.nextInt(TAG_WORD.length)]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/utils/RxHelper.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout.utils; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import rx.Observable; 6 | import rx.android.schedulers.AndroidSchedulers; 7 | import rx.functions.Func1; 8 | import rx.schedulers.Schedulers; 9 | 10 | /** 11 | * Created by Rukey7 on 2017/1/22. 12 | */ 13 | 14 | public final class RxHelper { 15 | 16 | public static Observable countdown(int time) { 17 | if (time < 0) { 18 | time = 0; 19 | } 20 | final int countTime = time; 21 | 22 | return Observable.interval(0, 1, TimeUnit.SECONDS) 23 | .map(new Func1() { 24 | @Override 25 | public Integer call(Long increaseTime) { 26 | return countTime - increaseTime.intValue(); 27 | } 28 | }) 29 | .take(countTime + 1) 30 | .subscribeOn(Schedulers.io()) 31 | .unsubscribeOn(Schedulers.io()) 32 | .subscribeOn(AndroidSchedulers.mainThread()) 33 | .observeOn(AndroidSchedulers.mainThread()); 34 | 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/utils/MeasureUtils.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag.utils; 2 | 3 | import android.content.Context; 4 | import android.graphics.Paint; 5 | import android.util.TypedValue; 6 | 7 | /** 8 | * Created by Rukey7 on 2016/7/18. 9 | * 测量帮助类 10 | */ 11 | public class MeasureUtils { 12 | 13 | private MeasureUtils() { 14 | throw new AssertionError(); 15 | } 16 | 17 | 18 | public static float dp2px(Context context, float dp) { 19 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, 20 | context.getResources().getDisplayMetrics()); 21 | } 22 | 23 | public static float sp2px(Context context, float sp) { 24 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, 25 | context.getResources().getDisplayMetrics()); 26 | } 27 | 28 | public static float px2sp(Context context, float pxValue) { 29 | final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; 30 | return pxValue / fontScale; 31 | } 32 | 33 | /** 34 | * 获取字体高度 35 | * @param fontSize 36 | * @return 37 | */ 38 | public static int getFontHeight(float fontSize) { 39 | Paint paint = new Paint(); 40 | paint.setTextSize(fontSize); 41 | Paint.FontMetrics fm = paint.getFontMetrics(); 42 | return (int) Math.ceil(fm.descent - fm.top) + 2; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/utils/ToastUtils.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout.utils; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | /** 7 | * Created by long on 2016/6/6. 8 | * 避免同样的信息多次触发重复弹出的问题 9 | */ 10 | public class ToastUtils { 11 | 12 | private static Context sContext; 13 | private static String oldMsg; 14 | protected static Toast toast = null; 15 | private static long oneTime = 0; 16 | private static long twoTime = 0; 17 | 18 | private ToastUtils() { 19 | throw new RuntimeException("ToastUtils cannot be initialized!"); 20 | } 21 | 22 | public static void init(Context context) { 23 | sContext = context; 24 | } 25 | 26 | public static void showToast(String s) { 27 | if (toast == null) { 28 | toast = Toast.makeText(sContext, s, Toast.LENGTH_SHORT); 29 | toast.show(); 30 | oneTime = System.currentTimeMillis(); 31 | } else { 32 | twoTime = System.currentTimeMillis(); 33 | if (s.equals(oldMsg)) { 34 | if (twoTime - oneTime > Toast.LENGTH_SHORT) { 35 | toast.show(); 36 | } 37 | } else { 38 | oldMsg = s; 39 | toast.setText(s); 40 | toast.show(); 41 | } 42 | oneTime = twoTime; 43 | } 44 | } 45 | 46 | public static void showToast(int resId) { 47 | showToast(sContext.getString(resId)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import com.dl7.tag.TagLayout; 8 | import com.dl7.tag.TagView; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | private final String[] mTagWords = new String[]{ 13 | "不同边框形状的标签", "单选和多选标签", "可编辑的标签", "动画效果的换一换标签", 14 | "TagView的一些其它用途", "水平反向排列(RTL)" 15 | }; 16 | private TagLayout mTagLayout; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | initView(); 23 | } 24 | 25 | private void initView() { 26 | mTagLayout = (TagLayout) findViewById(R.id.tag_layout); 27 | mTagLayout.setTags(mTagWords); 28 | mTagLayout.setTagClickListener(new TagView.OnTagClickListener() { 29 | @Override 30 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 31 | if (mTagWords[0].equals(text)) { 32 | startActivity(new Intent(MainActivity.this, TagShapeActivity.class)); 33 | } else if (mTagWords[1].equals(text)) { 34 | startActivity(new Intent(MainActivity.this, TagChoiceActivity.class)); 35 | } else if (mTagWords[2].equals(text)) { 36 | startActivity(new Intent(MainActivity.this, TagEditActivity.class)); 37 | } else if (mTagWords[3].equals(text)) { 38 | startActivity(new Intent(MainActivity.this, TagChangeActivity.class)); 39 | } else if (mTagWords[4].equals(text)) { 40 | startActivity(new Intent(MainActivity.this, TagViewActivity.class)); 41 | } else if (mTagWords[5].equals(text)) { 42 | startActivity(new Intent(MainActivity.this, TagReverseActivity.class)); 43 | } 44 | } 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 31 | 32 | 49 | 50 | -------------------------------------------------------------------------------- /taglayout/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 24 | 25 | 28 | 29 | 32 | 33 | 36 | 37 | 40 | 41 | 45 | 46 | 49 | 50 | 53 | 54 | 57 | 58 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TagShapeActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.dl7.tag.TagLayout; 7 | import com.dl7.tag.TagView; 8 | import com.dl7.taglayout.utils.TagWordFactory; 9 | import com.dl7.taglayout.utils.ToastUtils; 10 | 11 | public class TagShapeActivity extends AppCompatActivity implements TagView.OnTagClickListener, TagView.OnTagLongClickListener { 12 | 13 | private TagLayout mTagLayout1; 14 | private TagLayout mTagLayout2; 15 | private TagLayout mTagLayout3; 16 | private TagView mTagDel; 17 | private TagView mTagAdd; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_tag_shape); 23 | initView(); 24 | } 25 | 26 | private void initView() { 27 | mTagLayout1 = (TagLayout) findViewById(R.id.tag_layout_1); 28 | mTagLayout2 = (TagLayout) findViewById(R.id.tag_layout_2); 29 | mTagLayout3 = (TagLayout) findViewById(R.id.tag_layout_3); 30 | mTagDel = (TagView) findViewById(R.id.tag_del); 31 | mTagAdd = (TagView) findViewById(R.id.tag_add); 32 | mTagLayout1.setTagClickListener(this); 33 | mTagLayout1.setTagLongClickListener(this); 34 | mTagLayout2.setTagClickListener(this); 35 | mTagLayout2.setTagLongClickListener(this); 36 | mTagLayout3.setTagClickListener(this); 37 | mTagLayout3.setTagLongClickListener(this); 38 | 39 | mTagAdd.setTagClickListener(new TagView.OnTagClickListener() { 40 | @Override 41 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 42 | String word = TagWordFactory.provideTagWord(); 43 | mTagLayout1.addTag(word); 44 | mTagLayout2.addTag(word); 45 | mTagLayout3.addTag(word); 46 | } 47 | }); 48 | mTagDel.setTagClickListener(new TagView.OnTagClickListener() { 49 | @Override 50 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 51 | mTagLayout1.deleteTag(0); 52 | mTagLayout2.deleteTag(0); 53 | mTagLayout3.deleteTag(0); 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 60 | ToastUtils.showToast(text); 61 | } 62 | 63 | @Override 64 | public void onTagLongClick(int position, String text, @TagView.TagMode int tagMode) { 65 | ToastUtils.showToast("长按:" + text); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TagReverseActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.dl7.tag.TagLayout; 7 | import com.dl7.tag.TagView; 8 | import com.dl7.taglayout.utils.TagWordFactory; 9 | import com.dl7.taglayout.utils.ToastUtils; 10 | 11 | public class TagReverseActivity extends AppCompatActivity implements TagView.OnTagClickListener, TagView.OnTagLongClickListener { 12 | 13 | private TagLayout mTagLayout1; 14 | private TagLayout mTagLayout2; 15 | private TagLayout mTagLayout3; 16 | private TagView mTagDel; 17 | private TagView mTagAdd; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_tag_reverse); 23 | initView(); 24 | } 25 | 26 | private void initView() { 27 | mTagLayout1 = (TagLayout) findViewById(R.id.tag_layout_1); 28 | mTagLayout2 = (TagLayout) findViewById(R.id.tag_layout_2); 29 | mTagLayout3 = (TagLayout) findViewById(R.id.tag_layout_3); 30 | mTagDel = (TagView) findViewById(R.id.tag_del); 31 | mTagAdd = (TagView) findViewById(R.id.tag_add); 32 | mTagLayout1.setTagClickListener(this); 33 | mTagLayout1.setTagLongClickListener(this); 34 | mTagLayout2.setTagClickListener(this); 35 | mTagLayout2.setTagLongClickListener(this); 36 | mTagLayout3.setTagClickListener(this); 37 | mTagLayout3.setTagLongClickListener(this); 38 | 39 | mTagAdd.setTagClickListener(new TagView.OnTagClickListener() { 40 | @Override 41 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 42 | String word = TagWordFactory.provideTagWord(); 43 | mTagLayout1.addTag(word); 44 | mTagLayout2.addTag(word); 45 | mTagLayout3.addTag(word); 46 | } 47 | }); 48 | mTagDel.setTagClickListener(new TagView.OnTagClickListener() { 49 | @Override 50 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 51 | mTagLayout1.deleteTag(0); 52 | mTagLayout2.deleteTag(0); 53 | mTagLayout3.deleteTag(0); 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 60 | ToastUtils.showToast(text); 61 | } 62 | 63 | @Override 64 | public void onTagLongClick(int position, String text, @TagView.TagMode int tagMode) { 65 | ToastUtils.showToast("长按:" + text); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TagChoiceActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.dl7.tag.TagLayout; 7 | import com.dl7.tag.TagView; 8 | import com.dl7.taglayout.utils.TagWordFactory; 9 | import com.dl7.taglayout.utils.ToastUtils; 10 | 11 | public class TagChoiceActivity extends AppCompatActivity implements TagView.OnTagClickListener, TagView.OnTagLongClickListener { 12 | 13 | private TagLayout mTagLayout1; 14 | private TagLayout mTagLayout2; 15 | private TagLayout mTagLayout3; 16 | private TagView mTagDel; 17 | private TagView mTagAdd; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_tag_choice); 23 | initView(); 24 | } 25 | 26 | private void initView() { 27 | mTagLayout1 = (TagLayout) findViewById(R.id.tag_layout_1); 28 | mTagLayout2 = (TagLayout) findViewById(R.id.tag_layout_2); 29 | mTagLayout3 = (TagLayout) findViewById(R.id.tag_layout_3); 30 | mTagDel = (TagView) findViewById(R.id.tag_del); 31 | mTagAdd = (TagView) findViewById(R.id.tag_add); 32 | mTagLayout1.setTagClickListener(this); 33 | mTagLayout1.setTagLongClickListener(this); 34 | mTagLayout2.setTagClickListener(this); 35 | mTagLayout2.setTagLongClickListener(this); 36 | mTagLayout3.setTagClickListener(this); 37 | mTagLayout3.setTagLongClickListener(this); 38 | 39 | mTagAdd.setTagClickListener(new TagView.OnTagClickListener() { 40 | @Override 41 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 42 | String word = TagWordFactory.provideTagWord(); 43 | mTagLayout1.addTag(word); 44 | mTagLayout2.addTag(word); 45 | mTagLayout3.addTag(word); 46 | } 47 | }); 48 | mTagDel.setTagClickListener(new TagView.OnTagClickListener() { 49 | @Override 50 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 51 | mTagLayout1.deleteCheckedTags(); 52 | mTagLayout2.deleteCheckedTags(); 53 | mTagLayout3.deleteCheckedTags(); 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 60 | ToastUtils.showToast(text); 61 | } 62 | 63 | @Override 64 | public void onTagLongClick(int position, String text, @TagView.TagMode int tagMode) { 65 | ToastUtils.showToast("长按:" + text); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TagEditActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.dl7.tag.TagLayout; 7 | import com.dl7.tag.TagView; 8 | import com.dl7.taglayout.utils.TagWordFactory; 9 | import com.dl7.taglayout.utils.ToastUtils; 10 | 11 | public class TagEditActivity extends AppCompatActivity implements TagView.OnTagClickListener, TagView.OnTagLongClickListener { 12 | 13 | private TagLayout mTagLayout1; 14 | private TagLayout mTagLayout2; 15 | private TagLayout mTagLayout3; 16 | private TagView mTagDel; 17 | private TagView mTagAdd; 18 | private TagView mTagEditControl; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | setContentView(R.layout.activity_tag_edit); 24 | initView(); 25 | } 26 | 27 | private void initView() { 28 | mTagLayout1 = (TagLayout) findViewById(R.id.tag_layout_1); 29 | mTagLayout2 = (TagLayout) findViewById(R.id.tag_layout_2); 30 | mTagLayout3 = (TagLayout) findViewById(R.id.tag_layout_3); 31 | mTagDel = (TagView) findViewById(R.id.tag_del); 32 | mTagAdd = (TagView) findViewById(R.id.tag_add); 33 | mTagEditControl = (TagView) findViewById(R.id.tag_open_edit); 34 | mTagLayout1.setTagClickListener(this); 35 | mTagLayout1.setTagLongClickListener(this); 36 | mTagLayout2.setTagClickListener(this); 37 | mTagLayout2.setTagLongClickListener(this); 38 | mTagLayout3.setTagClickListener(this); 39 | mTagLayout3.setTagLongClickListener(this); 40 | 41 | mTagAdd.setTagClickListener(new TagView.OnTagClickListener() { 42 | @Override 43 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 44 | String word = TagWordFactory.provideTagWord(); 45 | mTagLayout1.addTag(word); 46 | mTagLayout2.addTag(word); 47 | mTagLayout3.addTag(word); 48 | } 49 | }); 50 | mTagDel.setTagClickListener(new TagView.OnTagClickListener() { 51 | @Override 52 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 53 | mTagLayout1.deleteTag(0); 54 | mTagLayout2.deleteTag(0); 55 | mTagLayout3.deleteTag(0); 56 | } 57 | }); 58 | mTagEditControl.setTagCheckListener(new TagView.OnTagCheckListener() { 59 | @Override 60 | public void onTagCheck(int position, String text, boolean isChecked) { 61 | if (isChecked) { 62 | mTagLayout1.entryEditMode(); 63 | mTagLayout2.entryEditMode(); 64 | mTagLayout3.entryEditMode(); 65 | } else { 66 | mTagLayout1.exitEditMode(); 67 | mTagLayout2.exitEditMode(); 68 | mTagLayout3.exitEditMode(); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | @Override 75 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 76 | ToastUtils.showToast(text); 77 | } 78 | 79 | @Override 80 | public void onTagLongClick(int position, String text, @TagView.TagMode int tagMode) { 81 | ToastUtils.showToast("长按:" + text); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/drawable/MultiCircleDrawable.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout.drawable; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.ColorFilter; 5 | import android.graphics.PixelFormat; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.Animatable; 8 | import android.graphics.drawable.Drawable; 9 | 10 | /** 11 | * Created by long on 2016/7/2. 12 | * 复数Circle的Drawable,需要实现Drawable.Callback接口 13 | */ 14 | public class MultiCircleDrawable extends Drawable implements Animatable, Drawable.Callback { 15 | 16 | // 每个Drawable动画启动的间隔 17 | private static final int EACH_CIRCLE_SPACE = 200; 18 | // CircleDrawable数组 19 | private CircleDrawable[] mCircleDrawables; 20 | 21 | 22 | public MultiCircleDrawable() { 23 | mCircleDrawables = new CircleDrawable[] { 24 | new CircleDrawable(), 25 | new CircleDrawable(), 26 | new CircleDrawable() 27 | }; 28 | for (int i = 0; i < mCircleDrawables.length; i++) { 29 | // 设置动画启动延迟 30 | mCircleDrawables[i].setAnimatorDelay(EACH_CIRCLE_SPACE * i); 31 | // 设置回调监听,当CircleDrawable发生重绘时就会调用 invalidateDrawable(Drawable who) 方法 32 | mCircleDrawables[i].setCallback(this); 33 | } 34 | } 35 | 36 | 37 | @Override 38 | public void draw(Canvas canvas) { 39 | for (CircleDrawable drawable : mCircleDrawables) { 40 | // 分层绘制每个CircleDrawable 41 | int count = canvas.save(); 42 | drawable.draw(canvas); 43 | canvas.restoreToCount(count); 44 | } 45 | } 46 | 47 | @Override 48 | public void setAlpha(int alpha) { 49 | } 50 | 51 | @Override 52 | public void setColorFilter(ColorFilter colorFilter) { 53 | for (CircleDrawable drawable : mCircleDrawables) { 54 | drawable.setColorFilter(colorFilter); 55 | } 56 | } 57 | 58 | @Override 59 | public int getOpacity() { 60 | return PixelFormat.TRANSLUCENT; 61 | } 62 | 63 | @Override 64 | protected void onBoundsChange(Rect bounds) { 65 | super.onBoundsChange(bounds); 66 | for (CircleDrawable drawable : mCircleDrawables) { 67 | drawable.onBoundsChange(bounds); 68 | } 69 | } 70 | /************************************************************/ 71 | 72 | @Override 73 | public void start() { 74 | for (CircleDrawable drawable : mCircleDrawables) { 75 | drawable.start(); 76 | } 77 | } 78 | 79 | @Override 80 | public void stop() { 81 | for (CircleDrawable drawable : mCircleDrawables) { 82 | drawable.stop(); 83 | } 84 | } 85 | 86 | @Override 87 | public boolean isRunning() { 88 | for (CircleDrawable drawable : mCircleDrawables) { 89 | if (drawable.isRunning()) { 90 | return true; 91 | } 92 | } 93 | return false; 94 | } 95 | 96 | @Override 97 | public void invalidateDrawable(Drawable who) { 98 | // 需要重绘,子Drawable发生重绘会调用这个方法通知父Drawable,如果有设置Callback回调监听的话 99 | invalidateSelf(); 100 | } 101 | 102 | @Override 103 | public void scheduleDrawable(Drawable who, Runnable what, long when) { 104 | } 105 | 106 | @Override 107 | public void unscheduleDrawable(Drawable who, Runnable what) { 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /taglayout/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/drawable/RotateDrawable.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag.drawable; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapShader; 6 | import android.graphics.Canvas; 7 | import android.graphics.ColorFilter; 8 | import android.graphics.Paint; 9 | import android.graphics.PixelFormat; 10 | import android.graphics.Rect; 11 | import android.graphics.RectF; 12 | import android.graphics.Shader; 13 | import android.graphics.drawable.Animatable; 14 | import android.graphics.drawable.Drawable; 15 | import android.view.animation.AccelerateDecelerateInterpolator; 16 | 17 | import com.dl7.tag.utils.BitmapUtils; 18 | 19 | /** 20 | * Created by Rukey7 on 2016/12/5. 21 | * 旋转动画 Drawable 22 | */ 23 | public class RotateDrawable extends Drawable implements Animatable { 24 | 25 | private Paint mPaint; 26 | // 绘制的矩形框 27 | private RectF mRect = new RectF(); 28 | // 动画控制 29 | private ValueAnimator mValueAnimator; 30 | // 旋转角度 31 | private float mRotate; 32 | // icon 33 | private Bitmap mBitmap; 34 | // 偏移 35 | private int mTranslationX; 36 | private int mTranslationY; 37 | 38 | 39 | public RotateDrawable(Bitmap bitmap) { 40 | mBitmap = bitmap; 41 | mPaint = new Paint(); 42 | mPaint.setAntiAlias(true); 43 | } 44 | 45 | @Override 46 | public void draw(Canvas canvas) { 47 | canvas.translate(mTranslationX, mTranslationY); 48 | canvas.rotate(mRotate, mRect.width() / 2, mRect.height() / 2); 49 | canvas.drawPaint(mPaint); 50 | } 51 | 52 | @Override 53 | public void setAlpha(int alpha) { 54 | mPaint.setAlpha(alpha); 55 | } 56 | 57 | @Override 58 | public void setColorFilter(ColorFilter colorFilter) { 59 | mPaint.setColorFilter(colorFilter); 60 | } 61 | 62 | @Override 63 | public int getOpacity() { 64 | return PixelFormat.TRANSLUCENT; 65 | } 66 | 67 | @Override 68 | protected void onBoundsChange(Rect bounds) { 69 | super.onBoundsChange(bounds); 70 | mRect.set(_clipSquare(bounds)); 71 | mTranslationX = (int) mRect.left; 72 | mTranslationY = (int) mRect.top; 73 | // 缩放 Bitmap 74 | Bitmap zoom = BitmapUtils.zoom(mBitmap, bounds.width(), bounds.height()); 75 | BitmapShader bitmapShader = new BitmapShader(zoom, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 76 | mPaint.setShader(bitmapShader); 77 | if (isRunning()) { 78 | stop(); 79 | } 80 | // 设置动画 81 | mValueAnimator = ValueAnimator.ofFloat(0, 2880).setDuration(2000); 82 | mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 83 | boolean isOver = false; 84 | 85 | @Override 86 | public void onAnimationUpdate(ValueAnimator animation) { 87 | mRotate = (float) animation.getAnimatedValue(); 88 | if (mRotate <= 2160) { 89 | isOver = false; 90 | } else if (!isOver) { 91 | isOver = true; 92 | mRotate = 2160; 93 | } 94 | invalidateSelf(); 95 | } 96 | }); 97 | // 设置动画无限循环 98 | mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); 99 | mValueAnimator.setRepeatMode(ValueAnimator.RESTART); 100 | mValueAnimator.setRepeatCount(ValueAnimator.INFINITE); 101 | start(); 102 | } 103 | 104 | /** 105 | * 裁剪Rect为正方形 106 | * @param rect 107 | * @return 108 | */ 109 | private Rect _clipSquare(Rect rect) { 110 | int w = rect.width(); 111 | int h = rect.height(); 112 | int min = Math.min(w, h); 113 | int cx = rect.centerX(); 114 | int cy = rect.centerY(); 115 | int r = min / 2; 116 | return new Rect( 117 | cx - r, 118 | cy - r, 119 | cx + r, 120 | cy + r 121 | ); 122 | } 123 | 124 | /** 125 | * ==================================== 显示模式 ==================================== 126 | */ 127 | 128 | @Override 129 | public void start() { 130 | mValueAnimator.start(); 131 | } 132 | 133 | @Override 134 | public void stop() { 135 | mValueAnimator.end(); 136 | } 137 | 138 | @Override 139 | public boolean isRunning() { 140 | return mValueAnimator != null && mValueAnimator.isRunning(); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/drawable/CircleDrawable.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout.drawable; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.animation.PropertyValuesHolder; 5 | import android.animation.ValueAnimator; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.ColorFilter; 9 | import android.graphics.Paint; 10 | import android.graphics.PixelFormat; 11 | import android.graphics.Rect; 12 | import android.graphics.RectF; 13 | import android.graphics.drawable.Animatable; 14 | import android.graphics.drawable.Drawable; 15 | import android.util.Property; 16 | 17 | /** 18 | * Created by long on 2016/7/2. 19 | * 圆圈Drawable 20 | */ 21 | public class CircleDrawable extends Drawable implements Animatable { 22 | 23 | private Paint mPaint; 24 | // 动画控制 25 | private ValueAnimator mValueAnimator; 26 | // 扩散半径 27 | private int mRadius; 28 | // 绘制的矩形框 29 | private RectF mRect = new RectF(); 30 | // 动画启动延迟时间 31 | private int mStartDelay; 32 | 33 | // 自定义一个扩散半径属性 34 | Property mRadiusProperty = new Property(Integer.class, "radius") { 35 | @Override 36 | public void set(CircleDrawable object, Integer value) { 37 | object.setRadius(value); 38 | } 39 | 40 | @Override 41 | public Integer get(CircleDrawable object) { 42 | return object.getRadius(); 43 | } 44 | }; 45 | public int getRadius() { 46 | return mRadius; 47 | } 48 | public void setRadius(int radius) { 49 | mRadius = radius; 50 | } 51 | 52 | 53 | public CircleDrawable() { 54 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 55 | mPaint.setColor(Color.WHITE); 56 | mPaint.setStyle(Paint.Style.STROKE); 57 | mPaint.setStrokeWidth(2); 58 | } 59 | 60 | 61 | @Override 62 | public void draw(Canvas canvas) { 63 | // 绘制圆圈 64 | canvas.drawCircle(mRect.centerX(), mRect.centerY(), mRadius, mPaint); 65 | } 66 | 67 | @Override 68 | public void setAlpha(int alpha) { 69 | mPaint.setAlpha(alpha); 70 | } 71 | 72 | @Override 73 | public void setColorFilter(ColorFilter colorFilter) { 74 | mPaint.setColorFilter(colorFilter); 75 | } 76 | 77 | @Override 78 | public int getOpacity() { 79 | return PixelFormat.TRANSLUCENT; 80 | } 81 | 82 | @Override 83 | protected void onBoundsChange(Rect bounds) { 84 | super.onBoundsChange(bounds); 85 | mRect.set(_clipSquare(bounds)); 86 | if (isRunning()) { 87 | stop(); 88 | } 89 | // 计算最大半径 90 | int maxRadius = (int) ((mRect.right - mRect.left) / 2); 91 | // 控制扩散半径的属性变化 92 | PropertyValuesHolder radiusHolder = PropertyValuesHolder.ofInt(mRadiusProperty, 0, maxRadius); 93 | // 控制透明度的属性变化 94 | PropertyValuesHolder alphaHolder = PropertyValuesHolder.ofInt("alpha", 255, 0); 95 | mValueAnimator = ObjectAnimator.ofPropertyValuesHolder(this, radiusHolder, alphaHolder); 96 | mValueAnimator.setStartDelay(mStartDelay); 97 | mValueAnimator.setDuration(1200); 98 | mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 99 | @Override 100 | public void onAnimationUpdate(ValueAnimator animation) { 101 | // 监听属性动画并进行重绘 102 | invalidateSelf(); 103 | } 104 | }); 105 | // 设置动画无限循环 106 | mValueAnimator.setRepeatMode(ValueAnimator.RESTART); 107 | mValueAnimator.setRepeatCount(ValueAnimator.INFINITE); 108 | start(); 109 | } 110 | 111 | /** 112 | * 裁剪Rect为正方形 113 | * @param rect 114 | * @return 115 | */ 116 | private Rect _clipSquare(Rect rect) { 117 | int w = rect.width(); 118 | int h = rect.height(); 119 | int min = Math.min(w, h); 120 | int cx = rect.centerX(); 121 | int cy = rect.centerY(); 122 | int r = min / 2; 123 | return new Rect( 124 | cx - r, 125 | cy - r, 126 | cx + r, 127 | cy + r 128 | ); 129 | } 130 | 131 | /************************************************************/ 132 | 133 | @Override 134 | public void start() { 135 | mValueAnimator.start(); 136 | } 137 | 138 | @Override 139 | public void stop() { 140 | mValueAnimator.end(); 141 | } 142 | 143 | @Override 144 | public boolean isRunning() { 145 | return mValueAnimator != null && mValueAnimator.isRunning(); 146 | } 147 | 148 | public void setAnimatorDelay(int startDelay) { 149 | mStartDelay = startDelay; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TagChangeActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.dl7.tag.TagLayout; 7 | import com.dl7.tag.TagView; 8 | import com.dl7.taglayout.utils.TagWordFactory; 9 | import com.dl7.taglayout.utils.ToastUtils; 10 | 11 | public class TagChangeActivity extends AppCompatActivity implements TagView.OnTagLongClickListener { 12 | 13 | private TagLayout mTagLayout1; 14 | private TagLayout mTagLayout2; 15 | private TagLayout mTagLayout3; 16 | private TagView mTagDel; 17 | private TagView mTagAdd; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_tag_change); 23 | initView(); 24 | } 25 | 26 | private void initView() { 27 | mTagLayout1 = (TagLayout) findViewById(R.id.tag_layout_1); 28 | mTagLayout2 = (TagLayout) findViewById(R.id.tag_layout_2); 29 | mTagLayout3 = (TagLayout) findViewById(R.id.tag_layout_3); 30 | mTagDel = (TagView) findViewById(R.id.tag_del); 31 | mTagAdd = (TagView) findViewById(R.id.tag_add); 32 | mTagLayout1.setTagClickListener(new TagView.OnTagClickListener() { 33 | boolean isChange = false; 34 | 35 | @Override 36 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 37 | if (tagMode == TagView.MODE_CHANGE) { 38 | if (isChange) { 39 | mTagLayout1.updateTags(TagWordFactory.TAG_WORD); 40 | } else { 41 | mTagLayout1.updateTags(TagWordFactory.TAG_WORD_2); 42 | } 43 | isChange = !isChange; 44 | } else { 45 | ToastUtils.showToast(text); 46 | } 47 | } 48 | }); 49 | mTagLayout2.setTagClickListener(new TagView.OnTagClickListener() { 50 | boolean isChange = false; 51 | 52 | @Override 53 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 54 | if (tagMode == TagView.MODE_CHANGE) { 55 | if (isChange) { 56 | mTagLayout2.updateTags(TagWordFactory.TAG_WORD); 57 | } else { 58 | mTagLayout2.updateTags(TagWordFactory.TAG_WORD_2); 59 | } 60 | isChange = !isChange; 61 | } else { 62 | ToastUtils.showToast(text); 63 | } 64 | } 65 | }); 66 | mTagLayout3.setTagClickListener(new TagView.OnTagClickListener() { 67 | boolean isChange = false; 68 | 69 | @Override 70 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 71 | if (tagMode == TagView.MODE_CHANGE) { 72 | if (isChange) { 73 | mTagLayout3.updateTags(TagWordFactory.TAG_WORD); 74 | } else { 75 | mTagLayout3.updateTags(TagWordFactory.TAG_WORD_2); 76 | } 77 | isChange = !isChange; 78 | } else { 79 | ToastUtils.showToast(text); 80 | } 81 | } 82 | }); 83 | mTagLayout1.setTagLongClickListener(this); 84 | mTagLayout2.setTagLongClickListener(this); 85 | mTagLayout3.setTagLongClickListener(this); 86 | 87 | mTagAdd.setTagClickListener(new TagView.OnTagClickListener() { 88 | @Override 89 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 90 | String word = TagWordFactory.provideTagWord(); 91 | mTagLayout1.addTag(word); 92 | mTagLayout2.addTag(word); 93 | mTagLayout3.addTag(word); 94 | } 95 | }); 96 | mTagDel.setTagClickListener(new TagView.OnTagClickListener() { 97 | @Override 98 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 99 | mTagLayout1.deleteTag(0); 100 | mTagLayout2.deleteTag(0); 101 | mTagLayout3.deleteTag(0); 102 | } 103 | }); 104 | 105 | mTagLayout1.setTags(TagWordFactory.TAG_WORD); 106 | mTagLayout2.setTags(TagWordFactory.TAG_WORD); 107 | mTagLayout3.setTags(TagWordFactory.TAG_WORD); 108 | } 109 | 110 | @Override 111 | public void onTagLongClick(int position, String text, @TagView.TagMode int tagMode) { 112 | ToastUtils.showToast("长按:" + text); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tag_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 20 | 21 | 26 | 27 | 33 | 34 | 40 | 41 | 46 | 47 | 53 | 54 | 60 | 61 | 66 | 67 | 73 | 74 | 80 | 81 | 82 | 83 | 84 | 88 | 89 | 93 | 94 | 108 | 109 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tag_reverse.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 20 | 21 | 26 | 27 | 33 | 34 | 41 | 42 | 47 | 48 | 54 | 55 | 62 | 63 | 68 | 69 | 75 | 76 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 96 | 97 | 111 | 112 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tag_choice.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 20 | 21 | 26 | 27 | 33 | 34 | 40 | 41 | 46 | 47 | 53 | 54 | 61 | 62 | 67 | 68 | 74 | 75 | 82 | 83 | 84 | 85 | 86 | 90 | 91 | 95 | 96 | 110 | 111 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tag_change.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 20 | 21 | 26 | 27 | 33 | 34 | 40 | 41 | 46 | 47 | 53 | 54 | 63 | 64 | 69 | 70 | 76 | 77 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 96 | 97 | 111 | 112 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TagLayout 2 | [![Apache 2.0 License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0.html) [![](https://jitpack.io/v/Rukey7/TagLayout.svg)](https://jitpack.io/#Rukey7/TagLayout) 3 | 4 | ### 多功能的标签流布局 5 | 6 | ## Screenshot 7 | 8 | ##### 不同标签形状: 9 | 10 | ![](https://raw.githubusercontent.com/Rukey7/ScreenShot/master/TagLayout/tag_shape.png) 11 | 12 | ##### 标签单选和多选模式: 13 | 14 | ![](https://raw.githubusercontent.com/Rukey7/ScreenShot/master/TagLayout/tag_choice.png) 15 | 16 | ##### 标签编辑模式: 17 | 18 | ![](https://raw.githubusercontent.com/Rukey7/ScreenShot/master/TagLayout/tag_edit.png) 19 | 20 | ##### 标签换一换模式: 21 | 22 | ![](https://raw.githubusercontent.com/Rukey7/ScreenShot/master/TagLayout/tag_change.gif) 23 | 24 | ##### 单个标签的其它用法: 25 | 26 | ![](https://raw.githubusercontent.com/Rukey7/ScreenShot/master/TagLayout/tag_view.gif) 27 | 28 | ## dependence 29 | 30 | 你需要在项目的根 `build.gradle` 加入如下JitPack仓库链接: 31 | 32 | ```gradle 33 | allprojects { 34 | repositories { 35 | ... 36 | maven { url 'https://jitpack.io' } 37 | } 38 | } 39 | ``` 40 | 41 | 接着在你的需要依赖的Module的`build.gradle`加入依赖: 42 | 43 | ```gradle 44 | compile 'com.github.Rukey7:TagLayout:{lastest-version}' 45 | ``` 46 | 47 | 其中 `{lastest-version}` 为最新的版本,你可以查看上面显示的jitpack版本信息,也可以到[jitpack.io](https://jitpack.io/#Rukey7/IjkPlayerView)仓库查看。 48 | 49 | ## Usage 50 | 51 | 52 | 在布局中直接使用: 53 | 54 | ```xml 55 | 56 | 57 | 63 | 64 | 65 | 74 | ``` 75 | 76 | ### 属性设置 77 | 78 | 标签布局属性(有对应接口): 79 | 80 | |name|format|description| 81 | |:---:|:---:|:---:| 82 | | tag_layout_mode | enum | {normal,edit,change,single_choice,multi_choice}, 分别为正常、编辑、换一换、单选和多选等模式 83 | | tag_layout_shape | enum | {round_rect,arc,rect},标签形状分别为圆角矩形、圆弧形和直角矩形,默认round_rect 84 | | tag_layout_random_color | boolean | 随机颜色 85 | | tag_layout_press_feedback | boolean | 按压反馈效果 86 | | tag_layout_fit_num | integer | 设置一行固定显示几个标签 87 | | tag_layout_bg_color | color | 标签布局背景颜色 88 | | tag_layout_border_color | color | 标签布局边框颜色 89 | | tag_layout_border_radius | dimension | 标签布局边框圆角弧度 90 | | tag_layout_border_width | dimension | 标签布局边框大小 91 | | tag_layout_vertical_interval | dimension | 标签垂直间隔 92 | | tag_layout_horizontal_interval | dimension | 标签水平间隔 93 | | tag_view_bg_color | color | 标签背景颜色 94 | | tag_view_border_color | color | 标签边框颜色 95 | | tag_view_text_color | color | 标签字体颜色 96 | | tag_view_bg_color_check | color | 标签选中背景颜色 97 | | tag_view_border_color_check | color | 标签选中边框颜色 98 | | tag_view_text_color_check | color | 标签选中字体颜色 99 | | tag_view_border_width | dimension | 标签边框大小 100 | | tag_view_border_radius | dimension | 标签边框圆角弧度 101 | | tag_view_vertical_padding | dimension | 标签垂直填充 102 | | tag_view_horizontal_padding | dimension | 标签水平填充 103 | | tag_view_icon_padding | dimension | 标签icon和文字的间隔 104 | | tag_view_text_size | dimension | 标签字体大小(1.0.5由float改为dimension) 105 | | tag_layout_horizontal_reverse | boolean | 水平反向排列(RTL) 106 | 107 | 标签属性: 108 | 109 | |name|format|description| 110 | |:---:|:---:|:---:| 111 | | tag_mode | enum | {normal,check,icon_check_invisible,icon_check_change}, 分别为正常、可选中、选中图标消失和选中换图标等模式 112 | | tag_shape | enum | {round_rect,arc,rect},标签形状分别为圆角矩形、圆弧形和直角矩形,默认round_rect 113 | | tag_auto_check | boolean | 使能自动点击选中操作 114 | | tag_press_feedback | boolean | 按压反馈效果 115 | | tag_checked | boolean | 初始选中状态 116 | | tag_icon | reference | 标签图标 117 | | tag_icon_change | reference | 标签选中时替换的图标(icon_check_change模式) 118 | | tag_text_check | string | 标签选中时替换的字符 119 | | tag_bg_color | color | 标签背景颜色 120 | | tag_border_color | color | 标签边框颜色 121 | | tag_text_color | color | 标签字体颜色 122 | | tag_bg_color_check | color | 标签选中背景颜色 123 | | tag_border_color_check | color | 标签选中边框颜色 124 | | tag_text_color_check | color | 标签选中字体颜色 125 | | tag_border_width | dimension | 标签边框大小 126 | | tag_border_radius | dimension | 标签边框圆角弧度 127 | | tag_vertical_padding | dimension | 标签垂直填充 128 | | tag_horizontal_padding | dimension | 标签水平填充 129 | | tag_text | dimension | 标签icon和文字的间隔 130 | | tag_icon_padding | dimension | 标签icon和文字的间隔 131 | | tag_text | string | 标签字符 132 | | tag_text_size | dimension | 标签字体大小 133 | | tag_gravity | enum | 图标放置位置,只支持left和right 134 | 135 | 136 | ### ChangeLog 137 | 138 | ##### 1.0.4 -> 1.0.5 139 | 140 | 1、重写TagView直接继承View,简化了代码逻辑,不再支持TextView的android:text和android:textSize属性,替换为自定义的tag_text和tag_text_size属性; 141 | 142 | 2、增加了tag_gravity属性来设置Drawable的放置位置,只支持left和right; 143 | 144 | ##### 1.0.5 -> 1.0.6 145 | 146 | 1、添加水平反向排列属性(tag_layout_horizontal_reverse); 147 | 148 | 149 | License 150 | ------- 151 | 152 | Copyright 2017 Rukey7 153 | 154 | Licensed under the Apache License, Version 2.0 (the "License"); 155 | you may not use this file except in compliance with the License. 156 | You may obtain a copy of the License at 157 | 158 | http://www.apache.org/licenses/LICENSE-2.0 159 | 160 | Unless required by applicable law or agreed to in writing, software 161 | distributed under the License is distributed on an "AS IS" BASIS, 162 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 163 | See the License for the specific language governing permissions and 164 | limitations under the License. 165 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tag_edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 20 | 21 | 26 | 27 | 33 | 34 | 41 | 42 | 47 | 48 | 54 | 55 | 64 | 65 | 70 | 71 | 77 | 78 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 96 | 97 | 111 | 112 | 129 | 130 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/dl7/taglayout/TagViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.dl7.taglayout; 2 | 3 | import android.os.Bundle; 4 | import android.os.Handler; 5 | import android.os.Message; 6 | import android.util.SparseIntArray; 7 | 8 | import com.dl7.tag.TagView; 9 | import com.dl7.taglayout.drawable.CircleDrawable; 10 | import com.dl7.taglayout.drawable.MultiCircleDrawable; 11 | import com.dl7.taglayout.utils.RxHelper; 12 | import com.dl7.taglayout.utils.ToastUtils; 13 | import com.trello.rxlifecycle.android.ActivityEvent; 14 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity; 15 | 16 | import rx.Subscriber; 17 | 18 | public class TagViewActivity extends RxAppCompatActivity { 19 | 20 | private TagView mTagGoodOrBad; 21 | private TagView mTagRightOrError; 22 | private TagView mTagSmileOrCry; 23 | private TagView mTagGetCode; 24 | private TagView mTagSkip; 25 | private TagView mTagSkip2; 26 | private TagView mTagSingleCircle; 27 | private TagView mTagMultiCircle; 28 | 29 | private int mCountNum = 5; 30 | private Handler mHandler = new Handler() { 31 | @Override 32 | public void handleMessage(Message msg) { 33 | super.handleMessage(msg); 34 | } 35 | }; 36 | private Runnable mRunnable = new Runnable() { 37 | @Override 38 | public void run() { 39 | mTagSkip2.setText("跳过 " + mCountNum); 40 | mTagSkip.setText((mCountNum--) + " 跳过"); 41 | if (mCountNum == 0) { 42 | mCountNum = 5; 43 | } 44 | mHandler.postDelayed(this, 1000); 45 | } 46 | }; 47 | private SparseIntArray mTimeSparse = new SparseIntArray(); 48 | 49 | @Override 50 | protected void onCreate(Bundle savedInstanceState) { 51 | super.onCreate(savedInstanceState); 52 | setContentView(R.layout.activity_tag_view); 53 | initView(); 54 | } 55 | 56 | private void initView() { 57 | mTagGoodOrBad = (TagView) findViewById(R.id.tag_good_or_bad); 58 | mTagRightOrError = (TagView) findViewById(R.id.tag_right_or_error); 59 | mTagSmileOrCry = (TagView) findViewById(R.id.tag_smile_or_cry); 60 | mTagGetCode = (TagView) findViewById(R.id.tag_get_code); 61 | mTagSkip = (TagView) findViewById(R.id.tag_skip); 62 | mTagSkip2 = (TagView) findViewById(R.id.tag_skip_2); 63 | mTagGoodOrBad.setTagClickListener(new TagView.OnTagClickListener() { 64 | @Override 65 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 66 | if (_isClickedNow(R.id.tag_good_or_bad)) { 67 | return; 68 | } 69 | mHandler.postDelayed(new Runnable() { 70 | @Override 71 | public void run() { 72 | mTagGoodOrBad.setChecked(!mTagGoodOrBad.isChecked()); 73 | } 74 | }, 2000); 75 | } 76 | }); 77 | mTagRightOrError.setTagClickListener(new TagView.OnTagClickListener() { 78 | @Override 79 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 80 | if (_isClickedNow(R.id.tag_right_or_error)) { 81 | return; 82 | } 83 | mHandler.postDelayed(new Runnable() { 84 | @Override 85 | public void run() { 86 | mTagRightOrError.setChecked(!mTagRightOrError.isChecked()); 87 | } 88 | }, 2000); 89 | } 90 | }); 91 | mTagSmileOrCry.setTagClickListener(new TagView.OnTagClickListener() { 92 | @Override 93 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 94 | if (_isClickedNow(R.id.tag_smile_or_cry)) { 95 | return; 96 | } 97 | mHandler.postDelayed(new Runnable() { 98 | @Override 99 | public void run() { 100 | mTagSmileOrCry.setChecked(!mTagSmileOrCry.isChecked()); 101 | } 102 | }, 2000); 103 | } 104 | }); 105 | 106 | mTagGetCode.setTagClickListener(new TagView.OnTagClickListener() { 107 | @Override 108 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 109 | if (!mTagGetCode.isChecked()) { 110 | mTagGetCode.setChecked(true); 111 | RxHelper.countdown(10) 112 | .compose(TagViewActivity.this.bindUntilEvent(ActivityEvent.DESTROY)) 113 | .subscribe(new Subscriber() { 114 | @Override 115 | public void onCompleted() { 116 | mTagGetCode.setChecked(false); 117 | mTagGetCode.setText("重新获取"); 118 | } 119 | 120 | @Override 121 | public void onError(Throwable e) { 122 | mTagGetCode.setChecked(false); 123 | mTagGetCode.setText("重新获取"); 124 | } 125 | 126 | @Override 127 | public void onNext(Integer time) { 128 | mTagGetCode.setTextChecked("已发送(" + time + ")"); 129 | } 130 | }); 131 | } 132 | } 133 | }); 134 | 135 | mTagSkip.setTagClickListener(new TagView.OnTagClickListener() { 136 | @Override 137 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 138 | ToastUtils.showToast(text); 139 | } 140 | }); 141 | mTagSkip2.setTagClickListener(new TagView.OnTagClickListener() { 142 | @Override 143 | public void onTagClick(int position, String text, @TagView.TagMode int tagMode) { 144 | ToastUtils.showToast(text); 145 | } 146 | }); 147 | mHandler.postDelayed(mRunnable, 1000); 148 | // 149 | mTagSingleCircle = (TagView) findViewById(R.id.tag_single_circle); 150 | mTagSingleCircle.setDecorateIcon(new CircleDrawable()); 151 | mTagMultiCircle = (TagView) findViewById(R.id.tag_multi_circle); 152 | mTagMultiCircle.setDecorateIcon(new MultiCircleDrawable()); 153 | } 154 | 155 | private boolean _isClickedNow(int id) { 156 | int lastTime = mTimeSparse.get(id); 157 | boolean isClickedNow = false; 158 | int curTime = (int) (System.currentTimeMillis() % 1000000); 159 | if (curTime - lastTime < 2000) { 160 | isClickedNow = true; 161 | } 162 | mTimeSparse.put(id, curTime); 163 | return isClickedNow; 164 | } 165 | 166 | @Override 167 | protected void onDestroy() { 168 | mHandler.removeCallbacksAndMessages(null); 169 | super.onDestroy(); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/TagEditView.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.DashPathEffect; 7 | import android.graphics.Paint; 8 | import android.graphics.PathEffect; 9 | import android.graphics.RectF; 10 | import android.text.TextUtils; 11 | import android.text.method.ArrowKeyMovementMethod; 12 | import android.util.AttributeSet; 13 | import android.view.Gravity; 14 | import android.view.KeyEvent; 15 | import android.view.ViewParent; 16 | import android.view.inputmethod.EditorInfo; 17 | import android.view.inputmethod.InputMethodManager; 18 | import android.widget.TextView; 19 | 20 | import com.dl7.tag.utils.MeasureUtils; 21 | 22 | import static com.dl7.tag.TagView.INVALID_VALUE; 23 | import static com.dl7.tag.TagView.SHAPE_ARC; 24 | import static com.dl7.tag.TagView.SHAPE_RECT; 25 | import static com.dl7.tag.TagView.SHAPE_ROUND_RECT; 26 | 27 | /** 28 | * Created by long on 2017/4/21. 29 | */ 30 | 31 | public class TagEditView extends TextView { 32 | 33 | private Paint mBorderPaint; 34 | // 虚线路径 35 | private PathEffect mPathEffect = new DashPathEffect(new float[]{10, 5}, 0); 36 | // 边框矩形 37 | private RectF mRect; 38 | // 边框大小 39 | private float mBorderWidth; 40 | // 显示外形 41 | private int mTagShape = SHAPE_ROUND_RECT; 42 | // 边框角半径 43 | private float mRadius; 44 | // 边框颜色 45 | private int mBorderColor; 46 | // 字体水平空隙 47 | private int mHorizontalPadding; 48 | // 字体垂直空隙 49 | private int mVerticalPadding; 50 | 51 | public TagEditView(Context context) { 52 | super(context); 53 | _init(context); 54 | } 55 | 56 | public TagEditView(Context context, AttributeSet attrs) { 57 | super(context, attrs); 58 | _init(context); 59 | } 60 | 61 | private void _init(Context context) { 62 | mHorizontalPadding = (int) MeasureUtils.dp2px(context, 5f); 63 | mVerticalPadding = (int) MeasureUtils.dp2px(context, 5f); 64 | setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding); 65 | mRect = new RectF(); 66 | mBorderColor = Color.parseColor("#ff333333"); 67 | mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 68 | mBorderPaint.setStyle(Paint.Style.STROKE); 69 | mBorderPaint.setColor(mBorderColor); 70 | mBorderWidth = MeasureUtils.dp2px(context, 0.5f); 71 | mRadius = MeasureUtils.dp2px(context, 5f); 72 | // 设置字体占中 73 | setGravity(Gravity.CENTER); 74 | _initEditMode(); 75 | } 76 | 77 | @Override 78 | protected boolean getDefaultEditable() { 79 | return true; 80 | } 81 | 82 | @Override 83 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 84 | super.onSizeChanged(w, h, oldw, oldh); 85 | // 设置矩形边框 86 | mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h - mBorderWidth); 87 | } 88 | 89 | @Override 90 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 91 | ViewParent parent = getParent(); 92 | if (parent instanceof TagLayout) { 93 | int fitTagNum = ((TagLayout) getParent()).getFitTagNum(); 94 | if (fitTagNum != INVALID_VALUE) { 95 | int availableWidth = ((TagLayout) getParent()).getAvailableWidth(); 96 | int horizontalInterval = ((TagLayout) getParent()).getHorizontalInterval(); 97 | int fitWidth = (availableWidth - (fitTagNum - 1) * horizontalInterval) / fitTagNum; 98 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(fitWidth, MeasureSpec.EXACTLY); 99 | } 100 | } 101 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 102 | } 103 | 104 | @Override 105 | protected void onDraw(Canvas canvas) { 106 | float radius = mRadius; 107 | if (mTagShape == SHAPE_ARC) { 108 | radius = mRect.height() / 2; 109 | } else if (mTagShape == SHAPE_RECT) { 110 | radius = 0; 111 | } 112 | canvas.drawRoundRect(mRect, radius, radius, mBorderPaint); 113 | super.onDraw(canvas); 114 | } 115 | 116 | /** 117 | * 初始化编辑模式 118 | */ 119 | private void _initEditMode() { 120 | setClickable(true); 121 | setFocusable(true); 122 | setFocusableInTouchMode(true); 123 | setHint("添加标签"); 124 | mBorderPaint.setPathEffect(mPathEffect); 125 | setHintTextColor(Color.parseColor("#ffaaaaaa")); 126 | setMovementMethod(ArrowKeyMovementMethod.getInstance()); 127 | requestFocus(); 128 | setOnEditorActionListener(new OnEditorActionListener() { 129 | @Override 130 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 131 | if (actionId == EditorInfo.IME_NULL 132 | && (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER 133 | && event.getAction() == KeyEvent.ACTION_DOWN)) { 134 | if (!TextUtils.isEmpty(getText())) { 135 | ((TagLayout) getParent()).addTag(getText().toString()); 136 | setText(""); 137 | _closeSoftInput(); 138 | } 139 | return true; 140 | } 141 | return false; 142 | } 143 | }); 144 | } 145 | 146 | /** 147 | * 离开编辑模式 148 | */ 149 | public void exitEditMode() { 150 | clearFocus(); 151 | setFocusable(false); 152 | setFocusableInTouchMode(false); 153 | setHint(null); 154 | setMovementMethod(null); 155 | _closeSoftInput(); 156 | } 157 | 158 | /** 159 | * 关闭软键盘 160 | */ 161 | private void _closeSoftInput() { 162 | InputMethodManager inputMethodManager = (InputMethodManager) getContext() 163 | .getSystemService(Context.INPUT_METHOD_SERVICE); 164 | //如果软键盘已经开启 165 | if (inputMethodManager.isActive()) { 166 | inputMethodManager.hideSoftInputFromWindow(getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); 167 | } 168 | } 169 | 170 | /** 171 | * ==================================== 开放接口 ==================================== 172 | */ 173 | 174 | public void setBorderColor(int borderColor) { 175 | mBorderColor = borderColor; 176 | mBorderPaint.setColor(mBorderColor); 177 | } 178 | 179 | public void setBorderWidth(float borderWidth) { 180 | mBorderWidth = borderWidth; 181 | } 182 | 183 | public void setRadius(float radius) { 184 | mRadius = radius; 185 | } 186 | 187 | public void setHorizontalPadding(int horizontalPadding) { 188 | mHorizontalPadding = horizontalPadding; 189 | setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding); 190 | } 191 | 192 | public void setVerticalPadding(int verticalPadding) { 193 | mVerticalPadding = verticalPadding; 194 | setPadding(mHorizontalPadding, mVerticalPadding, mHorizontalPadding, mVerticalPadding); 195 | } 196 | 197 | @Override 198 | public void setTextColor(int color) { 199 | super.setTextColor(color); 200 | setHintTextColor(color); 201 | } 202 | 203 | /** 204 | * 调用这些接口进行属性设置如果最后可能会改变按钮的大小的话最后调用一下这个接口,以刷新界面,建议属性直接在布局里设置 205 | * 只需要回调onDraw()的话调用invalidate()就可以了 206 | */ 207 | public void updateView() { 208 | requestLayout(); 209 | invalidate(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tag_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 23 | 24 | 28 | 29 | 42 | 43 | 57 | 58 | 72 | 73 | 74 | 79 | 80 | 86 | 87 | 91 | 92 | 106 | 107 | 122 | 123 | 138 | 139 | 140 | 145 | 146 | 152 | 153 | 157 | 158 | 173 | 174 | 190 | 191 | 204 | 205 | 206 | 211 | 212 | 218 | 219 | 223 | 224 | 234 | 235 | 249 | 250 | 262 | 263 | 264 | 269 | 270 | 276 | 277 | 281 | 282 | 294 | 295 | 311 | 312 | 328 | 329 | 330 | 335 | 336 | 342 | 343 | 347 | 348 | 362 | 363 | 381 | 382 | 400 | 401 | 402 | 407 | 408 | 414 | 415 | 419 | 420 | 436 | 437 | 450 | 451 | 468 | 469 | 470 | 471 | 476 | 477 | 483 | 484 | 488 | 489 | 503 | 504 | 522 | 523 | 524 | 525 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/TagLayout.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.RectF; 9 | import android.support.v4.content.ContextCompat; 10 | import android.util.AttributeSet; 11 | import android.util.SparseBooleanArray; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | 15 | import com.dl7.tag.utils.ColorsFactory; 16 | import com.dl7.tag.utils.MeasureUtils; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | 22 | /** 23 | * Created by long on 2016/7/20. 24 | * TagLayout 25 | */ 26 | public class TagLayout extends ViewGroup { 27 | 28 | private Paint mPaint; 29 | // 背景色 30 | private int mBgColor; 31 | // 边框颜色 32 | private int mBorderColor; 33 | // 边框大小 34 | private float mBorderWidth; 35 | // 边框角半径 36 | private float mRadius; 37 | // Tag之间的垂直间隙 38 | private int mVerticalInterval; 39 | // Tag之间的水平间隙 40 | private int mHorizontalInterval; 41 | // 边框矩形 42 | private RectF mRect; 43 | // 可用的最大宽度 44 | private int mAvailableWidth; 45 | 46 | private int mTagBgColor; 47 | private int mTagBorderColor; 48 | private int mTagTextColor; 49 | private int mTagBgColorCheck; 50 | private int mTagBorderColorCheck; 51 | private int mTagTextColorCheck; 52 | private float mTagBorderWidth; 53 | private float mTagTextSize; 54 | private float mTagRadius; 55 | private int mTagHorizontalPadding; 56 | private int mTagVerticalPadding; 57 | private TagView.OnTagClickListener mTagClickListener; 58 | private TagView.OnTagLongClickListener mTagLongClickListener; 59 | private TagView.OnTagCheckListener mTagCheckListener; 60 | private TagView.OnTagCheckListener mInsideTagCheckListener; 61 | // 这个用来保存设置监听器之前的TagView 62 | private List mTagViews = new ArrayList<>(); 63 | private SparseBooleanArray mCheckSparseArray = new SparseBooleanArray(); 64 | // 显示模式 65 | private int mTagShape; 66 | private int mFitTagNum; 67 | private int mIconPadding; 68 | private boolean mIsPressFeedback; 69 | // 显示类型 70 | private int mTagMode; 71 | // 固定状态的TagView 72 | private TagView mFitTagView; 73 | private TagEditView mFitTagEditView; 74 | // 使能随机颜色 75 | private boolean mEnableRandomColor; 76 | // 是否反转水平布局 77 | private boolean mIsHorizontalReverse; 78 | 79 | 80 | public TagLayout(Context context) { 81 | this(context, null); 82 | } 83 | 84 | public TagLayout(Context context, AttributeSet attrs) { 85 | this(context, attrs, -1); 86 | } 87 | 88 | public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) { 89 | super(context, attrs, defStyleAttr); 90 | _init(context, attrs, defStyleAttr); 91 | } 92 | 93 | private void _init(Context context, AttributeSet attrs, int defStyleAttr) { 94 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 95 | mRect = new RectF(); 96 | mTagTextSize = MeasureUtils.sp2px(context, 13.0f); 97 | 98 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TagLayout); 99 | try { 100 | mTagMode = a.getInteger(R.styleable.TagLayout_tag_layout_mode, TagView.MODE_NORMAL); 101 | mTagShape = a.getInteger(R.styleable.TagLayout_tag_layout_shape, TagView.SHAPE_ROUND_RECT); 102 | mIsPressFeedback = a.getBoolean(R.styleable.TagLayout_tag_layout_press_feedback, false); 103 | mEnableRandomColor = a.getBoolean(R.styleable.TagLayout_tag_layout_random_color, false); 104 | mFitTagNum = a.getInteger(R.styleable.TagLayout_tag_layout_fit_num, TagView.INVALID_VALUE); 105 | mBgColor = a.getColor(R.styleable.TagLayout_tag_layout_bg_color, Color.WHITE); 106 | mBorderColor = a.getColor(R.styleable.TagLayout_tag_layout_border_color, Color.WHITE); 107 | mBorderWidth = a.getDimension(R.styleable.TagLayout_tag_layout_border_width, MeasureUtils.dp2px(context, 0.5f)); 108 | mRadius = a.getDimension(R.styleable.TagLayout_tag_layout_border_radius, MeasureUtils.dp2px(context, 5f)); 109 | mHorizontalInterval = (int) a.getDimension(R.styleable.TagLayout_tag_layout_horizontal_interval, MeasureUtils.dp2px(context, 5f)); 110 | mVerticalInterval = (int) a.getDimension(R.styleable.TagLayout_tag_layout_vertical_interval, MeasureUtils.dp2px(context, 5f)); 111 | 112 | mTagBgColor = a.getColor(R.styleable.TagLayout_tag_view_bg_color, Color.WHITE); 113 | mTagBorderColor = a.getColor(R.styleable.TagLayout_tag_view_border_color, Color.parseColor("#ff333333")); 114 | mTagTextColor = a.getColor(R.styleable.TagLayout_tag_view_text_color, Color.parseColor("#ff666666")); 115 | if (mIsPressFeedback || mTagMode == TagView.MODE_SINGLE_CHOICE || mTagMode == TagView.MODE_MULTI_CHOICE) { 116 | mTagBgColorCheck = a.getColor(R.styleable.TagLayout_tag_view_bg_color_check, mTagTextColor); 117 | mTagBorderColorCheck = a.getColor(R.styleable.TagLayout_tag_view_border_color_check, mTagTextColor); 118 | mTagTextColorCheck = a.getColor(R.styleable.TagLayout_tag_view_text_color_check, Color.WHITE); 119 | } else { 120 | mTagBgColorCheck = a.getColor(R.styleable.TagLayout_tag_view_bg_color_check, mTagBgColor); 121 | mTagBorderColorCheck = a.getColor(R.styleable.TagLayout_tag_view_border_color_check, mTagBorderColor); 122 | mTagTextColorCheck = a.getColor(R.styleable.TagLayout_tag_view_text_color_check, mTagTextColor); 123 | } 124 | mTagBorderWidth = a.getDimension(R.styleable.TagLayout_tag_view_border_width, MeasureUtils.dp2px(context, 0.5f)); 125 | mTagTextSize = a.getDimension(R.styleable.TagLayout_tag_view_text_size, mTagTextSize); 126 | mTagRadius = a.getDimension(R.styleable.TagLayout_tag_view_border_radius, MeasureUtils.dp2px(context, 5f)); 127 | mTagHorizontalPadding = (int) a.getDimension(R.styleable.TagLayout_tag_view_horizontal_padding, MeasureUtils.dp2px(context, 5f)); 128 | mTagVerticalPadding = (int) a.getDimension(R.styleable.TagLayout_tag_view_vertical_padding, MeasureUtils.dp2px(context, 5f)); 129 | mIconPadding = (int) a.getDimension(R.styleable.TagLayout_tag_view_icon_padding, MeasureUtils.dp2px(context, 3f)); 130 | mIsHorizontalReverse = a.getBoolean(R.styleable.TagLayout_tag_layout_horizontal_reverse, false); 131 | } finally { 132 | a.recycle(); 133 | } 134 | // 如果想要自己绘制内容,则必须设置这个标志位为false,否则onDraw()方法不会调用 135 | setWillNotDraw(false); 136 | setPadding(mHorizontalInterval, mVerticalInterval, mHorizontalInterval, mVerticalInterval); 137 | // 设置对应模式 138 | if (mTagMode == TagView.MODE_CHANGE) { 139 | mFitTagView = _initTagView("换一换", TagView.MODE_CHANGE); 140 | addView(mFitTagView); 141 | } else if (mTagMode == TagView.MODE_EDIT) { 142 | _initTagEditView(); 143 | addView(mFitTagEditView); 144 | } else if (mTagMode == TagView.MODE_SINGLE_CHOICE || mTagMode == TagView.MODE_MULTI_CHOICE) { 145 | mIsPressFeedback = true; 146 | mInsideTagCheckListener = new TagView.OnTagCheckListener() { 147 | @Override 148 | public void onTagCheck(int position, String text, boolean isChecked) { 149 | if (mTagCheckListener != null) { 150 | mTagCheckListener.onTagCheck(position, text, isChecked); 151 | } 152 | for (int i = 0; i < mTagViews.size(); i++) { 153 | if (mTagViews.get(i).getText().equals(text)) { 154 | mCheckSparseArray.put(i, isChecked); 155 | } else if (mTagMode == TagView.MODE_SINGLE_CHOICE && mCheckSparseArray.get(i)) { 156 | mTagViews.get(i).cleanTagCheckStatus(); 157 | mCheckSparseArray.put(i, false); 158 | } 159 | } 160 | } 161 | }; 162 | } 163 | } 164 | 165 | @Override 166 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 167 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 168 | int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 169 | int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 170 | int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 171 | // 计算可用宽度,为测量宽度减去左右padding值,这个放在measureChildren前面,在子视图中会用到这个参数 172 | mAvailableWidth = widthSpecSize - getPaddingLeft() - getPaddingRight(); 173 | // 测量子视图 174 | measureChildren(widthMeasureSpec, heightMeasureSpec); 175 | int childCount = getChildCount(); 176 | int tmpWidth = 0; 177 | int measureHeight = 0; 178 | int maxLineHeight = 0; 179 | for (int i = 0; i < childCount; i++) { 180 | View child = getChildAt(i); 181 | // 记录该行的最大高度 182 | if (maxLineHeight == 0) { 183 | maxLineHeight = child.getMeasuredHeight(); 184 | } else { 185 | maxLineHeight = Math.max(maxLineHeight, child.getMeasuredHeight()); 186 | } 187 | // 统计该行TagView的总宽度 188 | tmpWidth += child.getMeasuredWidth() + mHorizontalInterval; 189 | // 如果超过可用宽度则换行 190 | if (tmpWidth - mHorizontalInterval > mAvailableWidth) { 191 | // 统计TagGroup的测量高度,要加上垂直间隙 192 | measureHeight += maxLineHeight + mVerticalInterval; 193 | // 重新赋值 194 | tmpWidth = child.getMeasuredWidth() + mHorizontalInterval; 195 | maxLineHeight = child.getMeasuredHeight(); 196 | } 197 | } 198 | // 统计TagGroup的测量高度,加上最后一行 199 | measureHeight += maxLineHeight; 200 | 201 | // 设置测量宽高,记得算上padding 202 | if (childCount == 0) { 203 | setMeasuredDimension(0, 0); 204 | } else if (heightSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.AT_MOST) { 205 | setMeasuredDimension(widthSpecSize, measureHeight + getPaddingTop() + getPaddingBottom()); 206 | } else { 207 | setMeasuredDimension(widthSpecSize, heightSpecSize); 208 | } 209 | } 210 | 211 | @Override 212 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 213 | int childCount = getChildCount(); 214 | if (childCount <= 0) { 215 | return; 216 | } 217 | 218 | mAvailableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 219 | // 当前布局使用的top坐标 220 | int curTop = getPaddingTop(); 221 | // 当前布局使用的left坐标 222 | int curLeft; 223 | final int maxRight = mAvailableWidth + getPaddingLeft(); 224 | if (mIsHorizontalReverse) { 225 | curLeft = maxRight; 226 | } else { 227 | curLeft = getPaddingLeft(); 228 | } 229 | int maxHeight = 0; 230 | for (int i = 0; i < childCount; i++) { 231 | View child = getChildAt(i); 232 | 233 | if (maxHeight == 0) { 234 | maxHeight = child.getMeasuredHeight(); 235 | } else { 236 | maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); 237 | } 238 | 239 | int width = child.getMeasuredWidth(); 240 | int height = child.getMeasuredHeight(); 241 | // 超过一行做换行操作 242 | if (mIsHorizontalReverse) { 243 | curLeft -= width; 244 | if (getPaddingLeft() > curLeft) { 245 | curLeft = maxRight - width; 246 | curLeft = Math.max(curLeft, getPaddingLeft()); 247 | // 计算top坐标,要加上垂直间隙 248 | curTop += maxHeight + mVerticalInterval; 249 | maxHeight = child.getMeasuredHeight(); 250 | } 251 | // 设置子视图布局 252 | child.layout(curLeft, curTop, curLeft + width, curTop + height); 253 | curLeft -= mHorizontalInterval; 254 | } else { 255 | if (width + curLeft > maxRight) { 256 | curLeft = getPaddingLeft(); 257 | // 计算top坐标,要加上垂直间隙 258 | curTop += maxHeight + mVerticalInterval; 259 | maxHeight = child.getMeasuredHeight(); 260 | } 261 | // 设置子视图布局 262 | child.layout(curLeft, curTop, curLeft + width, curTop + height); 263 | // 计算left坐标,要加上水平间隙 264 | curLeft += width + mHorizontalInterval; 265 | } 266 | } 267 | } 268 | 269 | @Override 270 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 271 | super.onSizeChanged(w, h, oldw, oldh); 272 | mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h - mBorderWidth); 273 | } 274 | 275 | @Override 276 | protected void onDraw(Canvas canvas) { 277 | // super.onDraw(canvas); 278 | // 绘制背景 279 | mPaint.setStyle(Paint.Style.FILL); 280 | mPaint.setColor(mBgColor); 281 | canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint); 282 | // 绘制边框 283 | mPaint.setStyle(Paint.Style.STROKE); 284 | mPaint.setStrokeWidth(mBorderWidth); 285 | mPaint.setColor(mBorderColor); 286 | canvas.drawRoundRect(mRect, mRadius, mRadius, mPaint); 287 | } 288 | 289 | /** 290 | * ==================================== 设置TagGroup ==================================== 291 | */ 292 | 293 | public int getBgColor() { 294 | return mBgColor; 295 | } 296 | 297 | public void setBgColor(int bgColor) { 298 | mBgColor = bgColor; 299 | } 300 | 301 | public int getBorderColor() { 302 | return mBorderColor; 303 | } 304 | 305 | public void setBorderColor(int borderColor) { 306 | mBorderColor = borderColor; 307 | } 308 | 309 | public float getBorderWidth() { 310 | return mBorderWidth; 311 | } 312 | 313 | public void setBorderWidth(float borderWidth) { 314 | mBorderWidth = MeasureUtils.dp2px(getContext(), borderWidth); 315 | } 316 | 317 | public float getRadius() { 318 | return mRadius; 319 | } 320 | 321 | public void setRadius(float radius) { 322 | mRadius = radius; 323 | } 324 | 325 | public int getVerticalInterval() { 326 | return mVerticalInterval; 327 | } 328 | 329 | public void setVerticalInterval(int verticalInterval) { 330 | mVerticalInterval = verticalInterval; 331 | } 332 | 333 | public int getHorizontalInterval() { 334 | return mHorizontalInterval; 335 | } 336 | 337 | public void setHorizontalInterval(int horizontalInterval) { 338 | mHorizontalInterval = horizontalInterval; 339 | } 340 | 341 | protected int getAvailableWidth() { 342 | return mAvailableWidth; 343 | } 344 | 345 | public int getFitTagNum() { 346 | return mFitTagNum; 347 | } 348 | 349 | public void setFitTagNum(int fitTagNum) { 350 | mFitTagNum = fitTagNum; 351 | } 352 | 353 | /** 354 | * ==================================== 设置TagView ==================================== 355 | */ 356 | 357 | private void _initTagEditView() { 358 | mFitTagEditView = new TagEditView(getContext()); 359 | if (mEnableRandomColor) { 360 | int[] color = ColorsFactory.provideColor(); 361 | mFitTagEditView.setBorderColor(color[0]); 362 | mFitTagEditView.setTextColor(color[1]); 363 | } else { 364 | mFitTagEditView = new TagEditView(getContext()); 365 | mFitTagEditView.setBorderColor(mTagBorderColor); 366 | mFitTagEditView.setTextColor(mTagTextColor); 367 | } 368 | mFitTagEditView.setBorderWidth(mTagBorderWidth); 369 | mFitTagEditView.setRadius(mTagRadius); 370 | mFitTagEditView.setHorizontalPadding(mTagHorizontalPadding); 371 | mFitTagEditView.setVerticalPadding(mTagVerticalPadding); 372 | mFitTagEditView.setTextSize(MeasureUtils.px2sp(getContext(), mTagTextSize)); 373 | mFitTagEditView.updateView(); 374 | } 375 | 376 | private TagView _initTagView(String text, @TagView.TagMode int tagMode) { 377 | TagView tagView = new TagView(getContext(), text); 378 | if (mEnableRandomColor) { 379 | _setTagRandomColors(tagView); 380 | } else { 381 | tagView.setBgColorLazy(mTagBgColor); 382 | tagView.setBorderColorLazy(mTagBorderColor); 383 | tagView.setTextColorLazy(mTagTextColor); 384 | tagView.setBgColorCheckedLazy(mTagBgColorCheck); 385 | tagView.setBorderColorCheckedLazy(mTagBorderColorCheck); 386 | tagView.setTextColorCheckedLazy(mTagTextColorCheck); 387 | } 388 | tagView.setBorderWidthLazy(mTagBorderWidth); 389 | tagView.setRadiusLazy(mTagRadius); 390 | tagView.setTextSizeLazy(mTagTextSize); 391 | tagView.setHorizontalPaddingLazy(mTagHorizontalPadding); 392 | tagView.setVerticalPaddingLazy(mTagVerticalPadding); 393 | tagView.setPressFeedback(mIsPressFeedback); 394 | tagView.setTagClickListener(mTagClickListener); 395 | tagView.setTagLongClickListener(mTagLongClickListener); 396 | tagView.setTagCheckListener(mInsideTagCheckListener); 397 | tagView.setTagShapeLazy(mTagShape); 398 | tagView.setTagModeLazy(tagMode); 399 | tagView.setIconPaddingLazy(mIconPadding); 400 | tagView.updateView(); 401 | mTagViews.add(tagView); 402 | tagView.setTag(mTagViews.size() - 1); 403 | return tagView; 404 | } 405 | 406 | private void _setTagRandomColors(TagView tagView) { 407 | int[] color = ColorsFactory.provideColor(); 408 | if (mIsPressFeedback) { 409 | tagView.setTextColorLazy(color[1]); 410 | tagView.setBgColorLazy(Color.WHITE); 411 | tagView.setBgColorCheckedLazy(color[0]); 412 | tagView.setBorderColorCheckedLazy(color[0]); 413 | tagView.setTextColorCheckedLazy(Color.WHITE); 414 | } else { 415 | tagView.setBgColorLazy(color[1]); 416 | tagView.setTextColorLazy(mTagTextColor); 417 | tagView.setBgColorCheckedLazy(color[1]); 418 | tagView.setBorderColorCheckedLazy(color[0]); 419 | tagView.setTextColorCheckedLazy(mTagTextColor); 420 | } 421 | tagView.setBorderColorLazy(color[0]); 422 | } 423 | 424 | public int getTagBgColor() { 425 | return mTagBgColor; 426 | } 427 | 428 | public void setTagBgColor(int tagBgColor) { 429 | mTagBgColor = tagBgColor; 430 | } 431 | 432 | public int getTagBorderColor() { 433 | return mTagBorderColor; 434 | } 435 | 436 | public void setTagBorderColor(int tagBorderColor) { 437 | mTagBorderColor = tagBorderColor; 438 | } 439 | 440 | public int getTagTextColor() { 441 | return mTagTextColor; 442 | } 443 | 444 | public void setTagTextColor(int tagTextColor) { 445 | mTagTextColor = tagTextColor; 446 | } 447 | 448 | public float getTagBorderWidth() { 449 | return mTagBorderWidth; 450 | } 451 | 452 | public void setTagBorderWidth(float tagBorderWidth) { 453 | mTagBorderWidth = MeasureUtils.dp2px(getContext(), tagBorderWidth); 454 | if (mFitTagView != null) { 455 | mFitTagView.setBorderWidth(mTagBorderWidth); 456 | } 457 | } 458 | 459 | public float getTagTextSize() { 460 | return mTagTextSize; 461 | } 462 | 463 | public void setTagTextSize(float tagTextSize) { 464 | mTagTextSize = tagTextSize; 465 | if (mFitTagView != null) { 466 | mFitTagView.setTextSize(tagTextSize); 467 | } 468 | } 469 | 470 | public float getTagRadius() { 471 | return mTagRadius; 472 | } 473 | 474 | public void setTagRadius(float tagRadius) { 475 | mTagRadius = tagRadius; 476 | if (mFitTagView != null) { 477 | mFitTagView.setRadius(mTagRadius); 478 | } 479 | } 480 | 481 | public int getTagHorizontalPadding() { 482 | return mTagHorizontalPadding; 483 | } 484 | 485 | public void setTagHorizontalPadding(int tagHorizontalPadding) { 486 | mTagHorizontalPadding = tagHorizontalPadding; 487 | if (mFitTagView != null) { 488 | mFitTagView.setHorizontalPadding(mTagHorizontalPadding); 489 | } 490 | } 491 | 492 | public int getTagVerticalPadding() { 493 | return mTagVerticalPadding; 494 | } 495 | 496 | public void setTagVerticalPadding(int tagVerticalPadding) { 497 | mTagVerticalPadding = tagVerticalPadding; 498 | if (mFitTagView != null) { 499 | mFitTagView.setVerticalPadding(mTagVerticalPadding); 500 | } 501 | } 502 | 503 | public boolean isPressFeedback() { 504 | return mIsPressFeedback; 505 | } 506 | 507 | public void setPressFeedback(boolean pressFeedback) { 508 | mIsPressFeedback = pressFeedback; 509 | if (mFitTagView != null) { 510 | mFitTagView.setPressFeedback(mIsPressFeedback); 511 | } 512 | } 513 | 514 | public TagView.OnTagClickListener getTagClickListener() { 515 | return mTagClickListener; 516 | } 517 | 518 | public void setTagClickListener(TagView.OnTagClickListener tagClickListener) { 519 | mTagClickListener = tagClickListener; 520 | // 避免先调用设置TagView,后设置监听器导致前面设置的TagView不能响应点击 521 | for (TagView tagView : mTagViews) { 522 | tagView.setTagClickListener(mTagClickListener); 523 | } 524 | } 525 | 526 | public TagView.OnTagLongClickListener getTagLongClickListener() { 527 | return mTagLongClickListener; 528 | } 529 | 530 | public void setTagLongClickListener(TagView.OnTagLongClickListener tagLongClickListener) { 531 | mTagLongClickListener = tagLongClickListener; 532 | for (TagView tagView : mTagViews) { 533 | tagView.setTagLongClickListener(mTagLongClickListener); 534 | } 535 | } 536 | 537 | public TagView.OnTagCheckListener getTagCheckListener() { 538 | return mTagCheckListener; 539 | } 540 | 541 | public void setTagCheckListener(TagView.OnTagCheckListener tagCheckListener) { 542 | mTagCheckListener = tagCheckListener; 543 | } 544 | 545 | public void setTagShape(@TagView.TagShape int tagShape) { 546 | mTagShape = tagShape; 547 | } 548 | 549 | public void setEnableRandomColor(boolean enableRandomColor) { 550 | mEnableRandomColor = enableRandomColor; 551 | } 552 | 553 | public void setIconPadding(int padding) { 554 | mIconPadding = padding; 555 | if (mFitTagView != null) { 556 | mFitTagView.setIconPadding(mIconPadding); 557 | } 558 | } 559 | 560 | /** 561 | * ==================================== 添加/删除TagView ==================================== 562 | */ 563 | 564 | /** 565 | * add Tag 566 | * 567 | * @param text tag content 568 | */ 569 | public void addTag(String text) { 570 | if (mTagMode == TagView.MODE_CHANGE || (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null)) { 571 | addView(_initTagView(text, TagView.MODE_NORMAL), getChildCount() - 1); 572 | } else { 573 | addView(_initTagView(text, mTagMode)); 574 | } 575 | } 576 | 577 | /** 578 | * add Tag 579 | * 580 | * @param text tag content 581 | */ 582 | public void addTagWithIcon(String text, int iconResId) { 583 | TagView tagView; 584 | if (mTagMode == TagView.MODE_CHANGE || (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null)) { 585 | tagView = _initTagView(text, TagView.MODE_NORMAL); 586 | } else { 587 | tagView = _initTagView(text, mTagMode); 588 | } 589 | tagView.setDecorateIcon(ContextCompat.getDrawable(getContext(), iconResId)); 590 | tagView.setIconPadding(mIconPadding); 591 | if (mTagMode == TagView.MODE_CHANGE || (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null)) { 592 | addView(tagView, getChildCount() - 1); 593 | } else { 594 | addView(tagView); 595 | } 596 | } 597 | 598 | 599 | /** 600 | * 设置对应位置Tag 601 | * 602 | * @param startPos 603 | */ 604 | private void _refreshPositionTag(int startPos) { 605 | for (int i = startPos; i < mTagViews.size(); i++) { 606 | mTagViews.get(i).setTag(i); 607 | } 608 | } 609 | 610 | /** 611 | * delete tag 612 | * 613 | * @param position 614 | */ 615 | public void deleteTag(int position) { 616 | if (position < 0 || position >= getChildCount()) { 617 | // Toast.makeText(getContext(), "Invalid position", Toast.LENGTH_SHORT).show(); 618 | return; 619 | } 620 | int pos = mTagMode == TagView.MODE_CHANGE ? position + 1 : position; 621 | if (mTagMode == TagView.MODE_CHANGE || (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null)) { 622 | if (position == getChildCount() - 1) { 623 | // 最后一项为固定项不删除 624 | return; 625 | } 626 | // 第0项为固定项 627 | mTagViews.remove(pos); 628 | } else { 629 | mTagViews.remove(position); 630 | } 631 | removeViewAt(position); 632 | _refreshPositionTag(position); 633 | } 634 | 635 | /** 636 | * add Tags 637 | * 638 | * @param textList tag list 639 | */ 640 | public void addTags(String... textList) { 641 | for (String text : textList) { 642 | addTag(text); 643 | } 644 | } 645 | 646 | /** 647 | * add Tags 648 | * 649 | * @param textList tag list 650 | */ 651 | public void addTags(List textList) { 652 | for (String text : textList) { 653 | addTag(text); 654 | } 655 | } 656 | 657 | /** 658 | * clean Tags 659 | */ 660 | public void cleanTags() { 661 | if (mTagMode == TagView.MODE_CHANGE || (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null)) { 662 | removeViews(0, getChildCount() - 1); 663 | mTagViews.clear(); 664 | mCheckSparseArray.clear(); 665 | mTagViews.add(mFitTagView); 666 | } else { 667 | removeAllViews(); 668 | mTagViews.clear(); 669 | } 670 | postInvalidate(); 671 | } 672 | 673 | /** 674 | * set Tags 675 | * 676 | * @param textList tag list 677 | */ 678 | public void setTags(String... textList) { 679 | cleanTags(); 680 | addTags(textList); 681 | } 682 | 683 | /** 684 | * set Tags 685 | * 686 | * @param textList tag list 687 | */ 688 | public void setTags(List textList) { 689 | cleanTags(); 690 | addTags(textList); 691 | } 692 | 693 | /** 694 | * update Tags 695 | * 696 | * @param textList tag list 697 | */ 698 | public void updateTags(String... textList) { 699 | int startPos = 0; 700 | int minSize; 701 | if (mTagMode == TagView.MODE_CHANGE || (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null)) { 702 | startPos = 1; 703 | minSize = Math.min(textList.length, mTagViews.size() - 1); 704 | } else { 705 | minSize = Math.min(textList.length, mTagViews.size()); 706 | } 707 | for (int i = 0; i < minSize; i++) { 708 | mTagViews.get(i + startPos).setText(textList[i]); 709 | } 710 | if (mEnableRandomColor) { 711 | startPos = 0; 712 | if (mTagMode == TagView.MODE_EDIT) { 713 | startPos = 1; 714 | } 715 | for (int i = startPos; i < mTagViews.size(); i++) { 716 | _setTagRandomColors(mTagViews.get(i)); 717 | } 718 | postInvalidate(); 719 | } 720 | } 721 | 722 | /** 723 | * update Tags 724 | * 725 | * @param textList tag list 726 | */ 727 | public void updateTags(List textList) { 728 | updateTags((String[]) textList.toArray()); 729 | } 730 | 731 | /** 732 | * get checked tags 733 | * 734 | * @return 735 | */ 736 | public List getCheckedTags() { 737 | List checkTags = new ArrayList<>(); 738 | for (int i = 0; i < mCheckSparseArray.size(); i++) { 739 | if (mCheckSparseArray.valueAt(i)) { 740 | checkTags.add(mTagViews.get(mCheckSparseArray.keyAt(i)).getText()); 741 | } 742 | } 743 | return checkTags; 744 | } 745 | 746 | /** 747 | * set tag to be checked 748 | * 749 | * @param text 750 | */ 751 | public void setCheckTag(String text) { 752 | if (mTagMode == TagView.MODE_SINGLE_CHOICE) { 753 | for (TagView tagView : mTagViews) { 754 | if (tagView.getText().equals(text)) { 755 | tagView.setChecked(true); 756 | } 757 | } 758 | } 759 | } 760 | 761 | public void setCheckTag(int... indexs) { 762 | if (mTagMode == TagView.MODE_SINGLE_CHOICE) { 763 | for (int i : indexs) { 764 | if (mTagViews.get(i) != null) { 765 | mTagViews.get(i).setChecked(true); 766 | } 767 | } 768 | } 769 | } 770 | 771 | /** 772 | * delete checked tags 773 | */ 774 | public void deleteCheckedTags() { 775 | for (int i = mCheckSparseArray.size() - 1; i >= 0; i--) { 776 | if (mCheckSparseArray.valueAt(i)) { 777 | deleteTag(mCheckSparseArray.keyAt(i)); 778 | mCheckSparseArray.delete(mCheckSparseArray.keyAt(i)); 779 | } 780 | } 781 | } 782 | 783 | /** 784 | * exit edit mode 785 | */ 786 | public void exitEditMode() { 787 | if (mTagMode == TagView.MODE_EDIT && mFitTagEditView != null) { 788 | mFitTagEditView.exitEditMode(); 789 | removeViewAt(getChildCount() - 1); 790 | mFitTagEditView = null; 791 | } 792 | } 793 | 794 | /** 795 | * entry edit mode 796 | */ 797 | public void entryEditMode() { 798 | if (mTagMode == TagView.MODE_NORMAL || mTagMode == TagView.MODE_EDIT) { 799 | mTagMode = TagView.MODE_EDIT; 800 | _initTagEditView(); 801 | addView(mFitTagEditView); 802 | } 803 | } 804 | } 805 | -------------------------------------------------------------------------------- /taglayout/src/main/java/com/dl7/tag/TagView.java: -------------------------------------------------------------------------------- 1 | package com.dl7.tag; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.PorterDuff; 11 | import android.graphics.RectF; 12 | import android.graphics.drawable.Animatable; 13 | import android.graphics.drawable.Drawable; 14 | import android.os.Parcel; 15 | import android.os.Parcelable; 16 | import android.support.annotation.IntDef; 17 | import android.support.v4.view.MotionEventCompat; 18 | import android.text.TextUtils; 19 | import android.util.AttributeSet; 20 | import android.view.Gravity; 21 | import android.view.MotionEvent; 22 | import android.view.View; 23 | import android.view.ViewParent; 24 | 25 | import com.dl7.tag.drawable.RotateDrawable; 26 | import com.dl7.tag.utils.MeasureUtils; 27 | 28 | import java.lang.annotation.ElementType; 29 | import java.lang.annotation.Retention; 30 | import java.lang.annotation.RetentionPolicy; 31 | import java.lang.annotation.Target; 32 | 33 | 34 | /** 35 | * Created by long on 2016/7/20. 36 | * TagView 37 | */ 38 | public class TagView extends View { 39 | 40 | // 无效数值 41 | public final static int INVALID_VALUE = -1; 42 | 43 | // 3种外形模式:圆角矩形、圆弧、直角矩形 44 | public final static int SHAPE_ROUND_RECT = 101; 45 | public final static int SHAPE_ARC = 102; 46 | public final static int SHAPE_RECT = 103; 47 | 48 | @IntDef({SHAPE_ROUND_RECT, SHAPE_ARC, SHAPE_RECT}) 49 | @Retention(RetentionPolicy.SOURCE) 50 | @Target(ElementType.PARAMETER) 51 | public @interface TagShape { 52 | } 53 | 54 | // 类型模式:正常、编辑、换一换、单选、多选、图标选中消失、图标选中切换 55 | public final static int MODE_NORMAL = 201; 56 | public final static int MODE_EDIT = 202; 57 | public final static int MODE_CHANGE = 203; 58 | public final static int MODE_SINGLE_CHOICE = 204; 59 | public final static int MODE_MULTI_CHOICE = 205; 60 | public final static int MODE_ICON_CHECK_INVISIBLE = 206; 61 | public final static int MODE_ICON_CHECK_CHANGE = 207; 62 | 63 | @IntDef({MODE_NORMAL, MODE_EDIT, MODE_CHANGE, MODE_SINGLE_CHOICE, MODE_MULTI_CHOICE, MODE_ICON_CHECK_INVISIBLE, MODE_ICON_CHECK_CHANGE}) 64 | @Retention(RetentionPolicy.SOURCE) 65 | @Target(ElementType.PARAMETER) 66 | public @interface TagMode { 67 | } 68 | 69 | // 显示外形 70 | private int mTagShape = SHAPE_ROUND_RECT; 71 | // 显示类型 72 | private int mTagMode = MODE_NORMAL; 73 | // 画笔 74 | private Paint mPaint; 75 | // 背景色 76 | private int mBgColor = Color.WHITE; 77 | // 边框颜色 78 | private int mBorderColor = Color.parseColor("#ff333333"); 79 | // 原始标签颜色 80 | private int mTextColor = Color.parseColor("#ff666666"); 81 | // 选中状态背景色 82 | private int mBgColorChecked = Color.WHITE; 83 | // 选中状态边框颜色 84 | private int mBorderColorChecked = Color.parseColor("#ff333333"); 85 | // 选中状态字体颜色 86 | private int mTextColorChecked = Color.parseColor("#ff666666"); 87 | // 遮罩颜色 88 | private int mScrimColor = Color.argb(0x66, 0xc0, 0xc0, 0xc0); 89 | // 字体大小 90 | private float mTextSize; 91 | // 字体宽度和高度 92 | private int mFontLen; 93 | private int mFontH; 94 | private int mFontLenChecked; 95 | // 基线偏移距离 96 | private float mBaseLineDistance; 97 | // 边框大小 98 | private float mBorderWidth; 99 | // 边框角半径 100 | private float mRadius; 101 | // 内容 102 | private String mText; 103 | // 选中时内容 104 | private String mTextChecked; 105 | // 显示的文字 106 | private String mShowText; 107 | // 字体水平空隙 108 | private int mHorizontalPadding; 109 | // 字体垂直空隙 110 | private int mVerticalPadding; 111 | // 边框矩形 112 | private RectF mRect; 113 | // 装饰的icon 114 | private Drawable mDecorateIcon; 115 | // 变化模式下的icon 116 | private Drawable mDecorateIconChange; 117 | // 设置图标的位置,只支持左右两边 118 | private int mIconGravity = Gravity.LEFT; 119 | // icon和文字间距 120 | private int mIconPadding = 0; 121 | // icon大小 122 | private int mIconSize = 0; 123 | // 是否选中 124 | private boolean mIsChecked = false; 125 | // 是否自动切换选中状态,不使能可以灵活地选择切换,通常用于等待网络返回再做切换 126 | private boolean mIsAutoToggleCheck = false; 127 | // 是否被按住 128 | private boolean mIsPressed = false; 129 | // 是否使能按压反馈 130 | private boolean mIsPressFeedback = false; 131 | // 点击监听器 132 | private OnTagClickListener mTagClickListener; 133 | private OnTagLongClickListener mTagLongClickListener; 134 | private OnTagCheckListener mTagCheckListener; 135 | 136 | 137 | public TagView(Context context) { 138 | super(context); 139 | _init(context, null); 140 | } 141 | 142 | public TagView(Context context, String text) { 143 | this(context); 144 | mText = text; 145 | } 146 | 147 | public TagView(Context context, AttributeSet attrs) { 148 | super(context, attrs); 149 | _init(context, attrs); 150 | } 151 | 152 | private void _init(Context context, AttributeSet attrs) { 153 | mBorderWidth = MeasureUtils.dp2px(context, 0.5f); 154 | mRadius = MeasureUtils.dp2px(context, 5f); 155 | mHorizontalPadding = (int) MeasureUtils.dp2px(context, 5f); 156 | mVerticalPadding = (int) MeasureUtils.dp2px(context, 5f); 157 | mIconPadding = (int) MeasureUtils.dp2px(context, 3f); 158 | mTextSize = MeasureUtils.dp2px(context, 14f); 159 | 160 | if (attrs != null) { 161 | final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TagView); 162 | try { 163 | mTagShape = a.getInteger(R.styleable.TagView_tag_shape, TagView.SHAPE_ROUND_RECT); 164 | mTagMode = a.getInteger(R.styleable.TagView_tag_mode, MODE_NORMAL); 165 | if (mTagMode == MODE_SINGLE_CHOICE || mTagMode == MODE_ICON_CHECK_INVISIBLE || mTagMode == MODE_ICON_CHECK_CHANGE) { 166 | mIsAutoToggleCheck = true; 167 | mIsChecked = a.getBoolean(R.styleable.TagView_tag_checked, false); 168 | mDecorateIconChange = a.getDrawable(R.styleable.TagView_tag_icon_change); 169 | } 170 | mIsAutoToggleCheck = a.getBoolean(R.styleable.TagView_tag_auto_check, mIsAutoToggleCheck); 171 | mIsPressFeedback = a.getBoolean(R.styleable.TagView_tag_press_feedback, mIsPressFeedback); 172 | 173 | mText = a.getString(R.styleable.TagView_tag_text); 174 | mTextChecked = a.getString(R.styleable.TagView_tag_text_check); 175 | mTextSize = a.getDimension(R.styleable.TagView_tag_text_size, mTextSize); 176 | mBgColor = a.getColor(R.styleable.TagView_tag_bg_color, Color.WHITE); 177 | mBorderColor = a.getColor(R.styleable.TagView_tag_border_color, Color.parseColor("#ff333333")); 178 | mTextColor = a.getColor(R.styleable.TagView_tag_text_color, Color.parseColor("#ff666666")); 179 | mBgColorChecked = a.getColor(R.styleable.TagView_tag_bg_color_check, mBgColor); 180 | mBorderColorChecked = a.getColor(R.styleable.TagView_tag_border_color_check, mBorderColor); 181 | mTextColorChecked = a.getColor(R.styleable.TagView_tag_text_color_check, mTextColor); 182 | mBorderWidth = a.getDimension(R.styleable.TagView_tag_border_width, mBorderWidth); 183 | mRadius = a.getDimension(R.styleable.TagView_tag_border_radius, mRadius); 184 | mHorizontalPadding = (int) a.getDimension(R.styleable.TagView_tag_horizontal_padding, mHorizontalPadding); 185 | mVerticalPadding = (int) a.getDimension(R.styleable.TagView_tag_vertical_padding, mVerticalPadding); 186 | mIconPadding = (int) a.getDimension(R.styleable.TagView_tag_icon_padding, mIconPadding); 187 | mDecorateIcon = a.getDrawable(R.styleable.TagView_tag_icon); 188 | mIconGravity = a.getInteger(R.styleable.TagView_tag_gravity, Gravity.LEFT); 189 | } finally { 190 | a.recycle(); 191 | } 192 | } 193 | 194 | if (mTagMode == MODE_ICON_CHECK_CHANGE && mDecorateIconChange == null) { 195 | throw new RuntimeException("You must set the drawable by 'tag_icon_change' property in MODE_ICON_CHECK_CHANGE mode"); 196 | } 197 | if (mDecorateIcon != null) { 198 | mDecorateIcon.setCallback(this); 199 | } 200 | if (mDecorateIconChange != null) { 201 | mDecorateIconChange.setColorFilter(mTextColorChecked, PorterDuff.Mode.SRC_IN); 202 | mDecorateIconChange.setCallback(this); 203 | } 204 | mRect = new RectF(); 205 | mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 206 | setClickable(true); 207 | if (!isSaveEnabled()) { 208 | // 使能状态保存 209 | setSaveEnabled(true); 210 | } 211 | 212 | setOnClickListener(new OnClickListener() { 213 | @Override 214 | public void onClick(View v) { 215 | if (mTagClickListener != null) { 216 | mTagClickListener.onTagClick(getTag() == null ? 0 : (int) getTag(), mText, mTagMode); 217 | } 218 | } 219 | }); 220 | setOnLongClickListener(new OnLongClickListener() { 221 | @Override 222 | public boolean onLongClick(View v) { 223 | if (mTagLongClickListener != null) { 224 | mTagLongClickListener.onTagLongClick(getTag() == null ? 0 : (int) getTag(), mText, mTagMode); 225 | } 226 | return true; 227 | } 228 | }); 229 | } 230 | 231 | /** 232 | * 调整显示的字符 233 | * @param maxWidth 234 | * @return 235 | */ 236 | private int _adjustText(int maxWidth) { 237 | if (mPaint.getTextSize() != mTextSize) { 238 | mPaint.setTextSize(mTextSize); 239 | final Paint.FontMetrics fontMetrics = mPaint.getFontMetrics(); 240 | // 文字高度 241 | mFontH = (int) (fontMetrics.descent - fontMetrics.ascent); 242 | // 用来设置基线的偏移量,再加上 getHeight() / 2 就是基线坐标 243 | mBaseLineDistance = (int) Math.ceil((fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent); 244 | } 245 | // 计算文字宽度 246 | if (TextUtils.isEmpty(mText)) { 247 | mText = ""; 248 | } 249 | mFontLen = (int) mPaint.measureText(mText); 250 | if (TextUtils.isEmpty(mTextChecked)) { 251 | mFontLenChecked = mFontLen; 252 | } else { 253 | mFontLenChecked = (int) mPaint.measureText(mTextChecked); 254 | } 255 | // 计算图标大小 256 | if ((mDecorateIcon != null || mDecorateIconChange != null) && mIconSize != mFontH) { 257 | mIconSize = mFontH; 258 | } 259 | // 计算出了文字外所需要占用的宽度 260 | int allPadding; 261 | if (mTagMode == MODE_ICON_CHECK_CHANGE && mIsChecked) { 262 | allPadding = mIconPadding + mIconSize + mHorizontalPadding * 2; 263 | } else if (mDecorateIcon != null) { 264 | allPadding = (mTagMode == MODE_ICON_CHECK_INVISIBLE && mIsChecked) ? mHorizontalPadding * 2 : 265 | mIconPadding + mIconSize + mHorizontalPadding * 2; 266 | } else { 267 | allPadding = mHorizontalPadding * 2; 268 | } 269 | // 设置显示的文字 270 | String showText = (mIsChecked && !TextUtils.isEmpty(mTextChecked)) ? mTextChecked : mText; 271 | if (mIsChecked && mFontLenChecked + allPadding > maxWidth) { 272 | float pointWidth = mPaint.measureText("."); 273 | // 计算能显示的字体长度 274 | float maxTextWidth = maxWidth - allPadding - pointWidth * 3; 275 | mShowText = _clipShowText(showText, mPaint, maxTextWidth); 276 | mFontLenChecked = (int) mPaint.measureText(mShowText); 277 | } else if (!mIsChecked && mFontLen + allPadding > maxWidth) { 278 | float pointWidth = mPaint.measureText("."); 279 | // 计算能显示的字体长度 280 | float maxTextWidth = maxWidth - allPadding - pointWidth * 3; 281 | mShowText = _clipShowText(showText, mPaint, maxTextWidth); 282 | mFontLen = (int) mPaint.measureText(mShowText); 283 | } else { 284 | mShowText = showText; 285 | } 286 | 287 | return allPadding; 288 | } 289 | 290 | @Override 291 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 292 | ViewParent parent = getParent(); 293 | if (parent instanceof TagLayout) { 294 | int fitTagNum = ((TagLayout) getParent()).getFitTagNum(); 295 | if (fitTagNum != INVALID_VALUE) { 296 | int availableWidth = ((TagLayout) getParent()).getAvailableWidth(); 297 | int horizontalInterval = ((TagLayout) getParent()).getHorizontalInterval(); 298 | int fitWidth = (availableWidth - (fitTagNum - 1) * horizontalInterval) / fitTagNum; 299 | widthMeasureSpec = MeasureSpec.makeMeasureSpec(fitWidth, MeasureSpec.EXACTLY); 300 | } 301 | } 302 | 303 | int allPadding = _adjustText(MeasureSpec.getSize(widthMeasureSpec)); 304 | int fontLen = mIsChecked ? mFontLenChecked : mFontLen; 305 | // 如果为精确测量 MeasureSpec.EXACTLY,则直接使用测量的大小,否则让控件实现自适应 306 | // 如果你用了精确测量则 mHorizontalPadding 和 mVerticalPadding 会对最终大小判定无效 307 | int width = (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) ? 308 | MeasureSpec.getSize(widthMeasureSpec) : allPadding + fontLen; 309 | int height = (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) ? 310 | MeasureSpec.getSize(heightMeasureSpec) : mVerticalPadding * 2 + mFontH; 311 | setMeasuredDimension(width, height); 312 | // 计算图标放置位置 313 | if (mDecorateIcon != null || mDecorateIconChange != null) { 314 | int top = (height - mIconSize) / 2; 315 | int left; 316 | if (mIconGravity == Gravity.RIGHT) { 317 | int padding = (width - mIconSize - fontLen - mIconPadding) / 2; 318 | left = width - padding - mIconSize; 319 | } else { 320 | left = (width - mIconSize - fontLen - mIconPadding) / 2; 321 | } 322 | if (mTagMode == MODE_ICON_CHECK_CHANGE && mIsChecked && mDecorateIconChange != null) { 323 | mDecorateIconChange.setBounds(left, top, mIconSize + left, mIconSize + top); 324 | } else if (mDecorateIcon != null) { 325 | mDecorateIcon.setBounds(left, top, mIconSize + left, mIconSize + top); 326 | } 327 | } 328 | } 329 | 330 | @Override 331 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 332 | super.onSizeChanged(w, h, oldw, oldh); 333 | // 设置矩形边框 334 | mRect.set(mBorderWidth, mBorderWidth, w - mBorderWidth, h - mBorderWidth); 335 | } 336 | 337 | @Override 338 | protected void onDraw(Canvas canvas) { 339 | // 圆角 340 | float radius = mRadius; 341 | if (mTagShape == SHAPE_ARC) { 342 | radius = mRect.height() / 2; 343 | } else if (mTagShape == SHAPE_RECT) { 344 | radius = 0; 345 | } 346 | // 判断按压反馈和选中状态 347 | final boolean isChecked = (mIsPressed && mIsPressFeedback) || mIsChecked; 348 | // 绘制背景 349 | mPaint.setStyle(Paint.Style.FILL); 350 | if (isChecked) { 351 | mPaint.setColor(mBgColorChecked); 352 | } else { 353 | mPaint.setColor(mBgColor); 354 | } 355 | canvas.drawRoundRect(mRect, radius, radius, mPaint); 356 | // 绘制边框 357 | mPaint.setStyle(Paint.Style.STROKE); 358 | mPaint.setStrokeWidth(mBorderWidth); 359 | if (isChecked) { 360 | mPaint.setColor(mBorderColorChecked); 361 | } else { 362 | mPaint.setColor(mBorderColor); 363 | } 364 | canvas.drawRoundRect(mRect, radius, radius, mPaint); 365 | // 绘制文字 366 | mPaint.setStyle(Paint.Style.FILL); 367 | if (isChecked) { 368 | mPaint.setColor(mTextColorChecked); 369 | int padding = (mTagMode == MODE_ICON_CHECK_INVISIBLE && mIsChecked) ? 0 : mIconSize + mIconPadding; 370 | int fontLen = mIsChecked ? mFontLenChecked : mFontLen; 371 | canvas.drawText(mShowText, 372 | mIconGravity == Gravity.RIGHT ? (getWidth() - fontLen - padding) / 2 373 | : (getWidth() - fontLen - padding) / 2 + padding, 374 | getHeight() / 2 + mBaseLineDistance, mPaint); 375 | } else { 376 | mPaint.setColor(mTextColor); 377 | int padding = mDecorateIcon == null ? 0 : mIconSize + mIconPadding; 378 | canvas.drawText(mShowText, 379 | mIconGravity == Gravity.RIGHT ? (getWidth() - mFontLen - padding) / 2 380 | : (getWidth() - mFontLen - padding) / 2 + padding, 381 | getHeight() / 2 + mBaseLineDistance, mPaint); 382 | } 383 | // 绘制Icon 384 | if (mTagMode == MODE_ICON_CHECK_CHANGE && mIsChecked && mDecorateIconChange != null) { 385 | mDecorateIconChange.draw(canvas); 386 | } else if (mTagMode == MODE_ICON_CHECK_INVISIBLE && mIsChecked) { 387 | // Don't need to draw 388 | } else if (mDecorateIcon != null) { 389 | mDecorateIcon.setColorFilter(mPaint.getColor(), PorterDuff.Mode.SRC_IN); 390 | mDecorateIcon.draw(canvas); 391 | } 392 | // 绘制半透明遮罩 393 | if (mIsPressed && (mIsChecked || !mIsPressFeedback)) { 394 | mPaint.setColor(mScrimColor); 395 | canvas.drawRoundRect(mRect, radius, radius, mPaint); 396 | } 397 | } 398 | 399 | @Override 400 | protected void onDetachedFromWindow() { 401 | if (mDecorateIcon != null && mDecorateIcon instanceof Animatable) { 402 | ((Animatable)mDecorateIcon).stop(); 403 | mDecorateIcon.setCallback(null); 404 | } 405 | if (mDecorateIconChange != null && mDecorateIconChange instanceof Animatable) { 406 | ((Animatable)mDecorateIconChange).stop(); 407 | mDecorateIconChange.setCallback(null); 408 | } 409 | super.onDetachedFromWindow(); 410 | } 411 | 412 | @Override 413 | public void invalidateDrawable(Drawable drawable) { 414 | if ((mDecorateIcon != null && mDecorateIcon instanceof Animatable) || 415 | (mDecorateIconChange != null && mDecorateIconChange instanceof Animatable)) { 416 | postInvalidate(); 417 | } else { 418 | super.invalidateDrawable(drawable); 419 | } 420 | } 421 | 422 | 423 | /** 424 | * ==================================== 触摸点击控制 ==================================== 425 | */ 426 | 427 | @Override 428 | public boolean onTouchEvent(MotionEvent event) { 429 | switch (MotionEventCompat.getActionMasked(event)) { 430 | case MotionEvent.ACTION_DOWN: 431 | mIsPressed = true; 432 | invalidate(); 433 | break; 434 | case MotionEvent.ACTION_MOVE: 435 | if (mIsPressed && !_isViewUnder(event.getX(), event.getY())) { 436 | mIsPressed = false; 437 | invalidate(); 438 | } 439 | break; 440 | case MotionEvent.ACTION_UP: 441 | if (_isViewUnder(event.getX(), event.getY())) { 442 | _toggleTagCheckStatus(); 443 | } 444 | case MotionEvent.ACTION_CANCEL: 445 | if (mIsPressed) { 446 | mIsPressed = false; 447 | invalidate(); 448 | } 449 | break; 450 | } 451 | return super.onTouchEvent(event); 452 | } 453 | 454 | /** 455 | * 判断是否在 Tag 控件内 456 | * 457 | * @param x 458 | * @param y 459 | * @return 460 | */ 461 | private boolean _isViewUnder(float x, float y) { 462 | return x >= 0 && x < getWidth() && 463 | y >= 0 && y < getHeight(); 464 | } 465 | 466 | /** 467 | * 切换tag选中状态 468 | */ 469 | private void _toggleTagCheckStatus() { 470 | if (mIsAutoToggleCheck) { 471 | setChecked(!mIsChecked); 472 | } 473 | } 474 | 475 | public boolean isChecked() { 476 | return mIsChecked; 477 | } 478 | 479 | /** 480 | * 设置选中状态 481 | * 482 | * @param checked 483 | */ 484 | public void setChecked(boolean checked) { 485 | _setTagCheckStatus(checked); 486 | if (mTagCheckListener != null) { 487 | mTagCheckListener.onTagCheck(getTag() == null ? 0 : (int) getTag(), mText, mIsChecked); 488 | } 489 | } 490 | 491 | /** 492 | * 清除选中状态 493 | */ 494 | public void cleanTagCheckStatus() { 495 | _setTagCheckStatus(false); 496 | } 497 | 498 | private void _setTagCheckStatus(boolean isChecked) { 499 | if (mIsChecked == isChecked) { 500 | return; 501 | } 502 | mIsChecked = isChecked; 503 | // 注意,这里用 requestLayout() 而不是只用 invalidate(),因为 invalidate() 没法回调 onMeasure() 进行测量, 504 | // 如果控件自适应大小的话是有可能改变大小的,所以加上 requestLayout() 505 | // requestLayout(); 506 | // invalidate(); 507 | updateView(); 508 | } 509 | 510 | /** 511 | * 裁剪Text 512 | * 513 | * @param oriText 514 | * @param paint 515 | * @param maxTextWidth 516 | * @return 517 | */ 518 | private String _clipShowText(String oriText, Paint paint, float maxTextWidth) { 519 | float tmpWidth = 0; 520 | StringBuilder strBuilder = new StringBuilder(); 521 | for (int i = 0; i < oriText.length(); i++) { 522 | char c = oriText.charAt(i); 523 | float cWidth = paint.measureText(String.valueOf(c)); 524 | // 计算每个字符的宽度之和,如果超过能显示的长度则退出 525 | if (tmpWidth + cWidth > maxTextWidth) { 526 | break; 527 | } 528 | strBuilder.append(c); 529 | tmpWidth += cWidth; 530 | } 531 | // 末尾添加3个.并设置为显示字符 532 | strBuilder.append("..."); 533 | return strBuilder.toString(); 534 | } 535 | 536 | /** 537 | * ==================================== 接口 ==================================== 538 | * 带有Lazy的后缀统一不调用 requestLayout() 和 invalidate(),避免多次绘制 539 | */ 540 | 541 | public int getTagShape() { 542 | return mTagShape; 543 | } 544 | 545 | public void setTagShape(int tagShape) { 546 | mTagShape = tagShape; 547 | updateView(); 548 | } 549 | 550 | public void setTagShapeLazy(int tagShape) { 551 | mTagShape = tagShape; 552 | } 553 | 554 | public int getTagMode() { 555 | return mTagMode; 556 | } 557 | 558 | public void setTagMode(int tagMode) { 559 | mTagMode = tagMode; 560 | if (mTagMode == MODE_CHANGE) { 561 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_change); 562 | mDecorateIcon = new RotateDrawable(bitmap); 563 | mDecorateIcon.setCallback(this); 564 | } 565 | updateView(); 566 | } 567 | 568 | public void setTagModeLazy(int tagMode) { 569 | mTagMode = tagMode; 570 | if (mTagMode == MODE_SINGLE_CHOICE || mTagMode == MODE_MULTI_CHOICE) { 571 | setPressFeedback(true); 572 | mIsAutoToggleCheck = true; 573 | } else if (mTagMode == MODE_CHANGE) { 574 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_change); 575 | mDecorateIcon = new RotateDrawable(bitmap); 576 | mDecorateIcon.setCallback(this); 577 | } 578 | } 579 | 580 | public int getBgColor() { 581 | return mBgColor; 582 | } 583 | 584 | public void setBgColor(int bgColor) { 585 | mBgColor = bgColor; 586 | // 设置颜色调用这个就行,回调onDraw() 587 | invalidate(); 588 | } 589 | 590 | public void setBgColorLazy(int bgColor) { 591 | mBgColor = bgColor; 592 | } 593 | 594 | public int getBorderColor() { 595 | return mBorderColor; 596 | } 597 | 598 | public void setBorderColor(int borderColor) { 599 | mBorderColor = borderColor; 600 | invalidate(); 601 | } 602 | 603 | public void setBorderColorLazy(int borderColor) { 604 | mBorderColor = borderColor; 605 | } 606 | 607 | public int getTextColor() { 608 | return mTextColor; 609 | } 610 | 611 | public void setTextColor(int textColor) { 612 | mTextColor = textColor; 613 | invalidate(); 614 | } 615 | 616 | public void setTextColorLazy(int textColor) { 617 | mTextColor = textColor; 618 | } 619 | 620 | public int getBgColorChecked() { 621 | return mBgColorChecked; 622 | } 623 | 624 | public void setBgColorChecked(int bgColorChecked) { 625 | mBgColorChecked = bgColorChecked; 626 | invalidate(); 627 | } 628 | 629 | public void setBgColorCheckedLazy(int bgColorChecked) { 630 | mBgColorChecked = bgColorChecked; 631 | } 632 | 633 | public int getBorderColorChecked() { 634 | return mBorderColorChecked; 635 | } 636 | 637 | public void setBorderColorChecked(int borderColorChecked) { 638 | mBorderColorChecked = borderColorChecked; 639 | invalidate(); 640 | } 641 | 642 | public void setBorderColorCheckedLazy(int borderColorChecked) { 643 | mBorderColorChecked = borderColorChecked; 644 | } 645 | 646 | public int getTextColorChecked() { 647 | return mTextColorChecked; 648 | } 649 | 650 | public void setTextColorChecked(int textColorChecked) { 651 | mTextColorChecked = textColorChecked; 652 | if (mDecorateIconChange != null) { 653 | mDecorateIconChange.setColorFilter(mTextColorChecked, PorterDuff.Mode.SRC_IN); 654 | } 655 | invalidate(); 656 | } 657 | 658 | public void setTextColorCheckedLazy(int textColorChecked) { 659 | mTextColorChecked = textColorChecked; 660 | if (mDecorateIconChange != null) { 661 | mDecorateIconChange.setColorFilter(mTextColorChecked, PorterDuff.Mode.SRC_IN); 662 | } 663 | } 664 | 665 | public int getScrimColor() { 666 | return mScrimColor; 667 | } 668 | 669 | public void setScrimColor(int scrimColor) { 670 | mScrimColor = scrimColor; 671 | invalidate(); 672 | } 673 | 674 | public void setScrimColorLazy(int scrimColor) { 675 | mScrimColor = scrimColor; 676 | } 677 | 678 | public float getTextSize() { 679 | return mTextSize; 680 | } 681 | 682 | public void setTextSize(float textSize) { 683 | mTextSize = textSize; 684 | updateView(); 685 | } 686 | 687 | public void setTextSizeLazy(float textSize) { 688 | mTextSize = textSize; 689 | } 690 | 691 | public float getBorderWidth() { 692 | return mBorderWidth; 693 | } 694 | 695 | public void setBorderWidth(float borderWidth) { 696 | mBorderWidth = borderWidth; 697 | invalidate(); 698 | } 699 | 700 | public void setBorderWidthLazy(float borderWidth) { 701 | mBorderWidth = borderWidth; 702 | } 703 | 704 | public float getRadius() { 705 | return mRadius; 706 | } 707 | 708 | public void setRadius(float radius) { 709 | mRadius = radius; 710 | invalidate(); 711 | } 712 | 713 | public void setRadiusLazy(float radius) { 714 | mRadius = radius; 715 | } 716 | 717 | public String getText() { 718 | return mText; 719 | } 720 | 721 | public void setText(String text) { 722 | mText = text; 723 | updateView(); 724 | } 725 | 726 | public void setTextLazy(String text) { 727 | mText = text; 728 | } 729 | 730 | public String getTextChecked() { 731 | return mTextChecked; 732 | } 733 | 734 | public void setTextChecked(String textChecked) { 735 | mTextChecked = textChecked; 736 | updateView(); 737 | } 738 | 739 | public void setTextCheckedLazy(String textChecked) { 740 | mTextChecked = textChecked; 741 | } 742 | 743 | public int getHorizontalPadding() { 744 | return mHorizontalPadding; 745 | } 746 | 747 | public void setHorizontalPaddingLazy(int horizontalPadding) { 748 | mHorizontalPadding = horizontalPadding; 749 | } 750 | 751 | public void setHorizontalPadding(int horizontalPadding) { 752 | mHorizontalPadding = horizontalPadding; 753 | updateView(); 754 | } 755 | 756 | public int getVerticalPadding() { 757 | return mVerticalPadding; 758 | } 759 | 760 | public void setVerticalPaddingLazy(int verticalPadding) { 761 | mVerticalPadding = verticalPadding; 762 | } 763 | 764 | public void setVerticalPadding(int verticalPadding) { 765 | mVerticalPadding = verticalPadding; 766 | updateView(); 767 | } 768 | 769 | public Drawable getDecorateIcon() { 770 | return mDecorateIcon; 771 | } 772 | 773 | public void setDecorateIconLazy(Drawable decorateIcon) { 774 | mDecorateIcon = decorateIcon; 775 | mDecorateIcon.setCallback(this); 776 | } 777 | 778 | public void setDecorateIcon(Drawable decorateIcon) { 779 | mDecorateIcon = decorateIcon; 780 | mDecorateIcon.setCallback(this); 781 | updateView(); 782 | } 783 | 784 | public Drawable getDecorateIconChange() { 785 | return mDecorateIconChange; 786 | } 787 | 788 | public void setDecorateIconChange(Drawable decorateIconChange) { 789 | mDecorateIconChange = decorateIconChange; 790 | mDecorateIconChange.setColorFilter(mTextColorChecked, PorterDuff.Mode.SRC_IN); 791 | mDecorateIconChange.setCallback(this); 792 | updateView(); 793 | } 794 | 795 | public void setDecorateIconChangeLazy(Drawable decorateIconChange) { 796 | mDecorateIconChange = decorateIconChange; 797 | mDecorateIconChange.setColorFilter(mTextColorChecked, PorterDuff.Mode.SRC_IN); 798 | mDecorateIconChange.setCallback(this); 799 | } 800 | 801 | public int getIconPadding() { 802 | return mIconPadding; 803 | } 804 | 805 | public void setIconPadding(int iconPadding) { 806 | mIconPadding = iconPadding; 807 | updateView(); 808 | } 809 | 810 | public void setIconPaddingLazy(int iconPadding) { 811 | mIconPadding = iconPadding; 812 | } 813 | 814 | 815 | public boolean isAutoToggleCheck() { 816 | return mIsAutoToggleCheck; 817 | } 818 | 819 | public void setAutoToggleCheck(boolean autoToggleCheck) { 820 | mIsAutoToggleCheck = autoToggleCheck; 821 | } 822 | 823 | public boolean isPressFeedback() { 824 | return mIsPressFeedback; 825 | } 826 | 827 | public void setPressFeedback(boolean pressFeedback) { 828 | mIsPressFeedback = pressFeedback; 829 | } 830 | 831 | /** 832 | * 调用这些接口进行属性设置如果最后可能会改变按钮的大小的话最后调用一下这个接口,以刷新界面,建议属性直接在布局里设置 833 | * 只需要回调onDraw()的话调用invalidate()就可以了 834 | */ 835 | public void updateView() { 836 | requestLayout(); 837 | invalidate(); 838 | } 839 | 840 | /** 841 | * ==================================== 点击监听 ==================================== 842 | */ 843 | 844 | public OnTagClickListener getTagClickListener() { 845 | return mTagClickListener; 846 | } 847 | 848 | public void setTagClickListener(OnTagClickListener tagClickListener) { 849 | mTagClickListener = tagClickListener; 850 | } 851 | 852 | public OnTagLongClickListener getTagLongClickListener() { 853 | return mTagLongClickListener; 854 | } 855 | 856 | public void setTagLongClickListener(OnTagLongClickListener tagLongClickListener) { 857 | mTagLongClickListener = tagLongClickListener; 858 | } 859 | 860 | public OnTagCheckListener getTagCheckListener() { 861 | return mTagCheckListener; 862 | } 863 | 864 | public void setTagCheckListener(OnTagCheckListener tagCheckListener) { 865 | mTagCheckListener = tagCheckListener; 866 | } 867 | 868 | /** 869 | * ==================================== 状态保存 ==================================== 870 | */ 871 | 872 | @Override 873 | public Parcelable onSaveInstanceState() { 874 | Parcelable superState = super.onSaveInstanceState(); 875 | SavedState state = new SavedState(superState); 876 | state.isChecked = mIsChecked; 877 | return state; 878 | } 879 | 880 | @Override 881 | public void onRestoreInstanceState(Parcelable state) { 882 | if (!(state instanceof SavedState)) { 883 | super.onRestoreInstanceState(state); 884 | return; 885 | } 886 | SavedState ss = (SavedState) state; 887 | super.onRestoreInstanceState(ss.getSuperState()); 888 | mIsChecked = ss.isChecked; 889 | } 890 | 891 | public static class SavedState extends BaseSavedState { 892 | 893 | boolean isChecked; 894 | 895 | public SavedState(Parcelable superState) { 896 | super(superState); 897 | } 898 | 899 | @Override 900 | public void writeToParcel(Parcel out, int flags) { 901 | super.writeToParcel(out, flags); 902 | out.writeInt(isChecked ? 1 : 0); 903 | } 904 | 905 | @SuppressWarnings("hiding") 906 | public static final Creator CREATOR 907 | = new Creator() { 908 | public SavedState createFromParcel(Parcel in) { 909 | return new SavedState(in); 910 | } 911 | 912 | public SavedState[] newArray(int size) { 913 | return new SavedState[size]; 914 | } 915 | }; 916 | 917 | private SavedState(Parcel in) { 918 | super(in); 919 | isChecked = in.readInt() == 1; 920 | } 921 | } 922 | 923 | /** 924 | * ==================================== 监听器 ==================================== 925 | */ 926 | 927 | public interface OnTagClickListener { 928 | void onTagClick(int position, String text, @TagMode int tagMode); 929 | } 930 | 931 | public interface OnTagLongClickListener { 932 | void onTagLongClick(int position, String text, @TagMode int tagMode); 933 | } 934 | 935 | public interface OnTagCheckListener { 936 | void onTagCheck(int position, String text, boolean isChecked); 937 | } 938 | } 939 | --------------------------------------------------------------------------------