diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..4f9005a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4c78841 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e24555f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f364207 --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'me.tatarka:gradle-retrolambda:3.4.0' + classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1' + classpath 'com.jakewharton:butterknife-gradle-plugin:8.5.1' + } +} + +allprojects { + repositories { + jcenter() + maven { url 'https://jitpack.io' } + mavenCentral() + maven { + url "https://maven.google.com" + } + maven { + url 'https://dl.bintray.com/hpdx/maven/' + } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..13372ae Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d166add --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Sep 07 13:57:53 CST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..938b446 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':widget' diff --git a/widget/.gitignore b/widget/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/widget/.gitignore @@ -0,0 +1 @@ +/build diff --git a/widget/build.gradle b/widget/build.gradle new file mode 100644 index 0000000..275a11a --- /dev/null +++ b/widget/build.gradle @@ -0,0 +1,94 @@ +apply plugin: 'com.android.library' +apply plugin: 'me.tatarka.retrolambda' + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + targetCompatibility 1.8 + sourceCompatibility 1.8 + } + + configurations { + all*.exclude module: 'PhotoView' //去除重复依赖库 + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:26.+' + testCompile 'junit:junit:4.12' + + compile 'com.android.support:recyclerview-v7:26.+' + compile 'com.android.support:design:26.+' + + compile 'com.github.Cutta:GifView:1.1' + compile 'com.makeramen:roundedimageview:2.3.0'//圆角 + compile 'com.jude:rollviewpager:1.4.6' + compile 'com.commit451:PhotoView:1.2.4' + compile 'com.davemorrissey.labs:subsampling-scale-image-view:3.6.0' + compile 'com.github.bumptech.glide:glide:4.1.1' + annotationProcessor 'com.github.bumptech.glide:compiler:4.1.1' + //主页底部导航栏 + compile 'com.ashokvarma.android:bottom-navigation-bar:1.3.0' + + //http://www.recyclerview.org/ + compile 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.9' + + compile 'com.contrarywind:Android-PickerView:3.2.4' + + compile 'com.github.johnkil.print:print:1.2.2' + + compile 'me.zhanghai.android.materialprogressbar:library:1.3.0' + + //底部弹出菜单 + //compile 'com.github.Kennyc1012:BottomSheet:2.3.1' + compile 'me.shaohui:bottomdialog:1.1.9' + + compile 'com.jakewharton:butterknife:8.5.1' + //输入框 + compile 'com.mylhyl:circleDialog:2.1.6' + compile 'com.youth.banner:banner:1.4.9' + compile 'com.github.yaozs:ImageShowPicker:1.0.0' + compile 'com.zhihu.android:matisse:0.4.3' + //动图的imageview + compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.7' + compile 'com.rengwuxian.materialedittext:library:2.1.4' + //自动大小textview + compile 'me.grantland:autofittextview:0.2.+' + compile 'com.github.pinguo-zhouwei:CustomPopwindow:2.0.0' + //自增textview + compile 'com.github.chaychan:PowerfulViewLibrary:1.2.1' + //侧滑删除 + compile 'com.github.mcxtzhang:SwipeDelMenuLayout:V1.2.2' + //九宫格 + compile 'cn.bingoogolapple:bga-photopicker:1.2.3@aar' + compile 'cn.bingoogolapple:bga-adapter:1.1.8@aar' + //九宫格 + compile 'com.lqr.ninegridimageview:library:1.0.0' + compile 'pub.devrel:easypermissions:0.1.9' + //可以拖动的rv + compile 'com.yanzhenjie:recyclerview-swipe:1.1.1' + //Android在桌面图标上显示角标 + compile 'com.anbetter:badger-helper:1.0.0' +} diff --git a/widget/proguard-rules.pro b/widget/proguard-rules.pro new file mode 100644 index 0000000..41afecb --- /dev/null +++ b/widget/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in D:\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java b/widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java new file mode 100644 index 0000000..af59bb1 --- /dev/null +++ b/widget/src/androidTest/java/com/hzecool/widget/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.hzecool.widget.test", appContext.getPackageName()); + } +} diff --git a/widget/src/main/AndroidManifest.xml b/widget/src/main/AndroidManifest.xml new file mode 100644 index 0000000..23a3cce --- /dev/null +++ b/widget/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java b/widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java new file mode 100644 index 0000000..788f220 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ActionSheetDialog.java @@ -0,0 +1,510 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.ArrayRes; +import android.support.annotation.StringRes; +import android.support.annotation.StyleRes; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +/** + * 底部弹出选择列表框 + * Created by tutu on 2017/4/21. + */ + +public class ActionSheetDialog extends AlertDialog { + private static final String TAG = "ActionSheetDialog"; + + protected ActionSheetDialog(Context context) { + super(context); + } + + protected ActionSheetDialog(Context context, boolean cancelable, OnCancelListener cancelListener) { + super(context, cancelable, cancelListener); + } + + protected ActionSheetDialog(Context context, @StyleRes int themeResId) { + super(context, themeResId); + + } + + @Override + public Button getButton(int whichButton) { + switch (whichButton) { + + } + return super.getButton(whichButton); + } + + public static class ActionSheetBuilder extends AlertDialog.Builder { + private Context mContext; + private String mTitle; + private String mMessage; + private String mNegativeText; + private String mPositiveText; + private boolean mCancelable; + private List mActionSheetItems; + private OnClickListener mNegativeClickListener; + private OnClickListener mPositiveClickListener; + private ActionSheetDialog mActionSheetDialog; + + //Attributes + //Title attrs + private int mTitleTextColor; + private int mTitleTextSize; + private int mTitleHeight; + private Drawable mTitleDivider; + private int mTitleDividerInset; + private int mTitleDividerHeight; + //Message attrs + private int mMessageTextColor; + private int mMessageTextSize; + private int mMessageHeight; + private Drawable mMessageDivider; + private int mMessageDividerInset; + private int mMessageDividerHeight; + //Item attrs + private int mItemTextColor; + private int mItemTextSize; + private int mItemHeight; + private Drawable mItemDivider; + private int mItemDividerInset; + private int mItemDividerHeight; + //Positive button attrs + private int mPositiveTextColor; + private int mPositiveTextSize; + private int mPositiveHeight; + //Cancel button attrs + private int mCancelTextColor; + private int mCancelTextSize; + private int mCancelHeight; + private int mCancelTopMargin; + private Drawable mCancelBackground; + //Dialog attrs + private int mLayoutMargins; + private int mSheetMargins; + private Drawable mContentBackground; + private int mWindowAnimationId; + private static final int DEFAULT_VALUE = -1; + + + public ActionSheetBuilder setmItemTextColor(int mItemTextColor) { + this.mItemTextColor = mItemTextColor; + + return this; + } + + public ActionSheetBuilder(Context context) { + super(context); + mContext = context; + mActionSheetItems = new ArrayList<>(); + TypedArray defaultTypedArray = context.obtainStyledAttributes(R.style.ActionSheetDialogBase, R.styleable.ActionSheetDialog); + if (null != defaultTypedArray) { + initDefaultAttributes(defaultTypedArray); + defaultTypedArray.recycle(); + } + } + + public ActionSheetBuilder(Context context, int themeResId) { + this(context); + TypedArray typedArray = context.obtainStyledAttributes(themeResId, R.styleable.ActionSheetDialog); + if (null != typedArray) { + initAttributes(typedArray); + typedArray.recycle(); + } + + } + + private void initAttributes(TypedArray typedArray) { + if (null != typedArray) { + mTitleTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_titleTextColor, mTitleTextColor); + mTitleTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleTextSize, mTitleTextSize); + mTitleHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleHeight, mTitleHeight); + mTitleDivider = typedArray.getDrawable(R.styleable.ActionSheetDialog_titleDivider); + mTitleDividerInset = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerInset, mTitleDividerInset); + mTitleDividerHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerHeight, mTitleDividerHeight); + + mMessageTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_messageTextColor, mMessageTextColor); + mMessageTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageTextSize, mMessageTextSize); + mMessageHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageHeight, mMessageHeight); + mMessageDivider = typedArray.getDrawable(R.styleable.ActionSheetDialog_messageDivider); + mMessageDividerInset = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerInset, mMessageDividerInset); + mMessageDividerHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerHeight, mMessageDividerHeight); + + mItemTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_itemTextColor, mItemTextColor); + mItemTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemTextSize, mItemTextSize); + mItemHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemHeight, mItemHeight); + mItemDivider = typedArray.getDrawable(R.styleable.ActionSheetDialog_itemDivider); + mItemDividerInset = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerInset, mItemDividerInset); + mItemDividerHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerHeight, mItemDividerHeight); + + mPositiveTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_positiveTextColor, mPositiveTextColor); + mPositiveTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveTextSize, mPositiveTextSize); + mPositiveHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveHeight, mPositiveHeight); + + mCancelTextColor = typedArray.getColor(R.styleable.ActionSheetDialog_cancelTextColor, mCancelTextColor); + mCancelTextSize = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTextSize, mCancelTextSize); + mCancelHeight = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelHeight, mCancelHeight); + mCancelBackground = typedArray.getDrawable(R.styleable.ActionSheetDialog_cancelBackground); + mCancelTopMargin = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTopMargins, mCancelTopMargin); + + mSheetMargins = typedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_sheetMargins, mSheetMargins); + mContentBackground = typedArray.getDrawable(R.styleable.ActionSheetDialog_contentBackground); + mWindowAnimationId = typedArray.getResourceId(R.styleable.ActionSheetDialog_windowAnimations, mWindowAnimationId); + } + } + + private void initDefaultAttributes(TypedArray defaultTypedArray) { + if (null != defaultTypedArray) { + mTitleTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_titleTextColor, DEFAULT_VALUE); + mTitleTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleTextSize, DEFAULT_VALUE); + mTitleHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleHeight, DEFAULT_VALUE); + mTitleDivider = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_titleDivider); + mTitleDividerInset = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerInset, DEFAULT_VALUE); + mTitleDividerHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_titleDividerHeight, DEFAULT_VALUE); + + mMessageTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_messageTextColor, DEFAULT_VALUE); + mMessageTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageTextSize, DEFAULT_VALUE); + mMessageHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageHeight, DEFAULT_VALUE); + mMessageDivider = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_messageDivider); + mMessageDividerInset = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerInset, DEFAULT_VALUE); + mMessageDividerHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_messageDividerHeight, DEFAULT_VALUE); + + mItemTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_itemTextColor, DEFAULT_VALUE); + mItemTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemTextSize, DEFAULT_VALUE); + mItemHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemHeight, DEFAULT_VALUE); + mItemDivider = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_itemDivider); + mItemDividerInset = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerInset, DEFAULT_VALUE); + mItemDividerHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_itemDividerHeight, DEFAULT_VALUE); + + mPositiveTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_positiveTextColor, DEFAULT_VALUE); + mPositiveTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveTextSize, DEFAULT_VALUE); + mPositiveHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_positiveHeight, DEFAULT_VALUE); + + mCancelTextColor = defaultTypedArray.getColor(R.styleable.ActionSheetDialog_cancelTextColor, DEFAULT_VALUE); + mCancelTextSize = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTextSize, DEFAULT_VALUE); + mCancelHeight = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelHeight, DEFAULT_VALUE); + mCancelBackground = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_cancelBackground); + mCancelTopMargin = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_cancelTopMargins, DEFAULT_VALUE); + + mSheetMargins = defaultTypedArray.getDimensionPixelSize(R.styleable.ActionSheetDialog_sheetMargins, DEFAULT_VALUE); + mContentBackground = defaultTypedArray.getDrawable(R.styleable.ActionSheetDialog_contentBackground); + mWindowAnimationId = defaultTypedArray.getResourceId(R.styleable.ActionSheetDialog_windowAnimations, DEFAULT_VALUE); + + } + } + + + @Override + public ActionSheetBuilder setCancelable(boolean cancelable) { + mCancelable = cancelable; + return this; + } + + @Override + public ActionSheetBuilder setMessage(CharSequence message) { + mMessage = (String) message; + return this; + } + + @Override + public ActionSheetBuilder setMessage(@StringRes int messageId) { + mMessage = mContext.getString(messageId); + return this; + } + + @Override + public ActionSheetBuilder setTitle(CharSequence title) { + mTitle = (String) title; + return this; + } + + @Override + public ActionSheetBuilder setTitle(@StringRes int titleId) { + mTitle = mContext.getString(titleId); + return this; + } + + @Override + public ActionSheetBuilder setNegativeButton(CharSequence text, OnClickListener listener) { + mNegativeText = (String) text; + mNegativeClickListener = listener; + mCancelable = true; + return this; + } + + @Override + public ActionSheetBuilder setPositiveButton(CharSequence text, OnClickListener listener) { + mPositiveText = (String) text; + mPositiveClickListener = listener; + return this; + } + + @Override + public ActionSheetBuilder setItems(CharSequence[] items, OnClickListener listener) { + for (int i = 0; i < items.length; i++) { + ActionSheetItem item = new ActionSheetItem((String) items[i], listener); + mActionSheetItems.add(item); + } + return this; + } + + @Override + public ActionSheetBuilder setItems(@ArrayRes int itemsId, OnClickListener listener) { + this.setItems(mContext.getResources().getStringArray(itemsId), listener); + return this; + } + + @Override + public ActionSheetDialog create() { + mActionSheetDialog = new ActionSheetDialog(mContext); + Window window = mActionSheetDialog.getWindow(); + window.setGravity(Gravity.BOTTOM); + window.setWindowAnimations(mWindowAnimationId); + WindowManager.LayoutParams params = window.getAttributes(); + params.y = dpToPx(mSheetMargins); + params.x = 0; + WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + + int width = windowManager.getDefaultDisplay().getWidth(); + params.width = width - 2 * mSheetMargins; + Drawable drawable = new ColorDrawable(); + drawable.setAlpha(0); + window.setBackgroundDrawable(drawable); + mActionSheetDialog.setCancelable(mCancelable); + if (mCancelable) { + mActionSheetDialog.setCanceledOnTouchOutside(true); + } + initViews(); + return mActionSheetDialog; + } + + TextView mTitleView; + ImageView mTitleDividerView; + TextView mMessageView; + ImageView mMessageDividerView; + LinearLayout mSheetItemContainer; + LinearLayout mContentPanel; + TextView mCancelView; + TextView mPositiveView; + SheetItemOnClickListener mSheetItemOnClickListener = new SheetItemOnClickListener(); + + private void initViews() { + View rootView = LayoutInflater.from(mContext) + .inflate(R.layout.layout_action_sheet_dialog, null); + + mContentPanel = (LinearLayout) rootView.findViewById(R.id.content_panel); + mTitleView = (TextView) rootView.findViewById(R.id.tv_title); + mTitleDividerView = (ImageView) rootView.findViewById(R.id.title_divider); + mMessageView = (TextView) rootView.findViewById(R.id.tv_message); + mMessageDividerView = (ImageView) rootView.findViewById(R.id.message_divider); + mSheetItemContainer = (LinearLayout) rootView.findViewById(R.id.scrollView_sheet_list); + mCancelView = (TextView) rootView.findViewById(R.id.tv_cancel); + if (null != mContentBackground) { + mContentPanel.setBackground(mContentBackground); + } + handleTitle(); + handleMessage(); + handleContent(); + handleCancel(); + handlePositive(); + + mActionSheetDialog.setView(rootView); + + + } + + private ImageView createDivider(Drawable background, int inset) { + ImageView divider = new ImageView(mContext); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + Log.d(TAG, "createDivider: inset = " + inset); + params.setMargins(inset, 0, inset, 0); + divider.setLayoutParams(params); + divider.setBackground(background); + divider.setMinimumHeight(mItemDividerHeight); + return divider; + } + + private void handlePositive() { + if (null != mPositiveText) { + mPositiveView = new TextView(mContext); + mPositiveView.setGravity(Gravity.CENTER); + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + mPositiveView.setLayoutParams(params); + //mPositiveView.setPadding(0, dpToPx(5.0f), 0, dpToPx(5.0f)); + mPositiveView.setText(mPositiveText); + mPositiveView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPositiveTextSize); + mPositiveView.setTextColor(mPositiveTextColor); + mPositiveView.setTag(AlertDialog.BUTTON_POSITIVE); + mPositiveView.setOnClickListener(mSheetItemOnClickListener); + mPositiveView.setMinHeight(mPositiveHeight); + mSheetItemContainer.addView(createDivider(mItemDivider, mItemDividerInset)); + mSheetItemContainer.addView(mPositiveView); + } + + } + + private void handleCancel() { + if (null != mCancelView) { + if (mCancelable) { + mCancelView.setMinHeight(mCancelHeight); + mCancelView.setTextColor(mCancelTextColor); + mCancelView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mCancelTextSize); + if (null != mCancelBackground) { + mCancelView.setBackground(mCancelBackground); + } + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, mCancelTopMargin, 0, 0); + mCancelView.setLayoutParams(params); + if (null != mNegativeText) { + mCancelView.setText(mNegativeText); + } + mCancelView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (null != mNegativeClickListener) { + mNegativeClickListener.onClick(mActionSheetDialog, AlertDialog.BUTTON_NEGATIVE); + } + mActionSheetDialog.dismiss(); + } + }); + } + } + } + + private void handleContent() { + if (null == mActionSheetItems || mActionSheetItems.isEmpty()) { + mSheetItemContainer.setVisibility(View.GONE); + } else { + for (int i = 0, size = mActionSheetItems.size(); i < size; i++) { + ActionSheetItem item = mActionSheetItems.get(i); + TextView sheetItemView = new TextView(mContext); + sheetItemView.setGravity(Gravity.CENTER); + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + sheetItemView.setLayoutParams(params); + //sheetItemView.setPadding(0, dpToPx(5.0f), 0, dpToPx(5.0f)); + sheetItemView.setText(item.text); + sheetItemView.setMinHeight(mItemHeight); + sheetItemView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mItemTextSize); + sheetItemView.setTextColor(mItemTextColor); + sheetItemView.setTag(i); + sheetItemView.setOnClickListener(mSheetItemOnClickListener); + mSheetItemContainer.addView(sheetItemView); + if (i < (size - 1)) { + mSheetItemContainer.addView(createDivider(mItemDivider, mItemDividerInset)); + } + } + + } + } + + private void handleMessage() { + if (null != mMessageView) { + if (null == mMessage) { + mMessageView.setVisibility(View.GONE); + mMessageDividerView.setVisibility(View.GONE); + } else { + mMessageDividerView.setBackground(mMessageDivider); + mMessageDividerView.setMinimumHeight(mMessageDividerHeight); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(mMessageDividerInset, 0, mMessageDividerInset, 0); + mMessageDividerView.setLayoutParams(params); + + mMessageView.setMinHeight(mMessageHeight); + mMessageView.setGravity(Gravity.CENTER); + mMessageView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mMessageTextSize); + mMessageView.setTextColor(mMessageTextColor); + mMessageView.setText(mMessage); + } + } + } + + private void handleTitle() { + if (null != mTitleView) { + if (null == mTitle) { + mTitleView.setVisibility(View.GONE); + mTitleDividerView.setVisibility(View.GONE); + } else { + mTitleDividerView.setBackground(mTitleDivider); + mTitleDividerView.setMinimumHeight(mTitleDividerHeight); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(mTitleDividerInset, 0, mTitleDividerInset, 0); + mTitleDividerView.setLayoutParams(params); + + mTitleView.setMinHeight(mTitleHeight); + mTitleView.setGravity(Gravity.CENTER); + mTitleView.setTextColor(mTitleTextColor); + mTitleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTitleTextSize); + mTitleView.setText(mTitle); + } + } + } + + public int dpToPx(float dp) { + float scale = mContext.getResources().getDisplayMetrics().density; + return (int) (dp * scale + 0.5f); + } + + public int spToPx(float sp) { + float scale = mContext.getResources().getDisplayMetrics().scaledDensity; + return (int) (sp * scale + 0.5f); + } + + + static class ActionSheetItem { + + String text; + OnClickListener listener; + + public ActionSheetItem(String text, OnClickListener listener) { + this.text = text; + this.listener = listener; + } + } + + public interface ActionSheetItemClickListener { + void onClick(int position); + } + + private class SheetItemOnClickListener implements View.OnClickListener { + + @Override + public void onClick(View v) { + + int tag = (int) v.getTag(); + Log.d(TAG, "onClick: tag = " + tag); + if (BUTTON_POSITIVE == tag) { + if (null != mPositiveClickListener) { + mPositiveClickListener.onClick(mActionSheetDialog, BUTTON_POSITIVE); + mActionSheetDialog.dismiss(); + } + mActionSheetDialog.dismiss(); + } else { + mActionSheetItems.get(tag).listener.onClick(mActionSheetDialog, tag); + } + } + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/BalanceEditText.java b/widget/src/main/java/com/hzecool/widget/BalanceEditText.java new file mode 100644 index 0000000..e602404 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/BalanceEditText.java @@ -0,0 +1,80 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.EditText; + +import java.math.BigDecimal; + +/** + * Created by wangzhiguo on 2017/6/8 + */ +public class BalanceEditText extends android.support.v7.widget.AppCompatEditText { + public BalanceEditText(Context context) { + super(context); + } + + public BalanceEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BalanceEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * 字符串转人民币 + * + * @param value + * @return + */ + public static String str2Rmb(String value) { + double money = Double.parseDouble(value); + BigDecimal bigDecimal = BigDecimal.valueOf(money); + return "¥" + bigDecimal.setScale(2,BigDecimal.ROUND_HALF_UP); + } + + /** + * double 类型 转人民币 + * + * @param value + * @return + */ + public static String db2Rmb(double value) { + try { + return "¥" + BigDecimal.valueOf(value).setScale(2,BigDecimal.ROUND_HALF_UP); + } catch (NumberFormatException ex) { + throw new NumberFormatException(); + } + } + + public void setText(String text){ + super.setText(str2Rmb(text)); + } + + public void setText(double text){ + super.setText(db2Rmb(text)); + } + + public String getTextValue(){ + return removeRmbStr(super.getText().toString()); + } + + public double getDoubleValue(){ + if (TextUtils.isEmpty(this.getTextValue())){ + return 0.00; + } + return Double.parseDouble(this.getTextValue()); + } + + private String removeRmbStr(@NonNull String text) { + return text.replace("¥", ""); + } + + + public void setTextZero(){ + this.setText(0.00); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/BalanceTextView.java b/widget/src/main/java/com/hzecool/widget/BalanceTextView.java new file mode 100644 index 0000000..28492d8 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/BalanceTextView.java @@ -0,0 +1,93 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import java.math.BigDecimal; + +/** + * Created by wangzhiguo on 2017/6/8 + */ +public class BalanceTextView extends android.support.v7.widget.AppCompatTextView { + + private double showText; + public BalanceTextView(Context context) { + super(context); + } + + public BalanceTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public BalanceTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * 字符串转人民币 + * + * @param value + * @return + */ + public static String str2Rmb(String value) { + double money = Double.parseDouble(value); + BigDecimal bigDecimal = BigDecimal.valueOf(money); + return "¥" + bigDecimal.setScale(2,BigDecimal.ROUND_HALF_UP); + } + + /** + * double 类型 转人民币 + * + * @param value + * @return + */ + public static String db2Rmb(double value) { + try { + return "¥" + new BigDecimal(value).setScale(2,BigDecimal.ROUND_HALF_UP).toString(); + } catch (NumberFormatException ex) { + throw new NumberFormatException(); + } + } + + public void setText(@NonNull String text){ + super.setText(str2Rmb(text)); + } + + public String getText(){ + return removeRmbStr(super.getText().toString()); + } + + public void setText(@NonNull double text){ + super.setText(db2Rmb(text)); + } + + public double getDoubleValue(){ + return Double.parseDouble(this.getText()); + } + + private String removeRmbStr(@NonNull String text) { + return text.replace("¥", ""); + } + + + public double getShowText(){ + if (isLessZero){ + return new BigDecimal(removeRmbStr(super.getText().toString())).multiply(new BigDecimal(-1)).floatValue(); + }else { + return new BigDecimal(removeRmbStr(super.getText().toString())).multiply(new BigDecimal(1)).floatValue(); + } + } + + private boolean isLessZero = false; + + public void setShowText(double text){ + if (text <= 0){ + isLessZero = true; + }else if (text > 0){ + isLessZero = false; + } + setText(Math.abs(text)); + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/BottomNavigationView.java b/widget/src/main/java/com/hzecool/widget/BottomNavigationView.java new file mode 100644 index 0000000..c115dfd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/BottomNavigationView.java @@ -0,0 +1,877 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.internal.BottomNavigationItemView; +import android.support.design.internal.BottomNavigationMenuView; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; + +/** + * Created by tutu on 2017/5/2. + * 引用自 https://github.com/ittianyu/BottomNavigationViewEx#zh + */ +public class BottomNavigationView extends android.support.design.widget.BottomNavigationView { + // used for animation + private int mShiftAmount; + private float mScaleUpFactor; + private float mScaleDownFactor; + private boolean animationRecord; + private float mLargeLabelSize; + private float mSmallLabelSize; + private boolean visibilityTextSizeRecord; + private boolean visibilityHeightRecord; + private int mItemHeight; + // used for animation end + + // used for setupWithViewPager + private ViewPager mViewPager; + private MyOnNavigationItemSelectedListener mMyOnNavigationItemSelectedListener; + private BottomNavigationViewExOnPageChangeListener mPageChangeListener; + private BottomNavigationMenuView mMenuView; + private BottomNavigationItemView[] mButtons; + // used for setupWithViewPager end + + public BottomNavigationView(Context context) { + super(context); + } + + public BottomNavigationView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * change the visibility of icon + * + * @param visibility + */ + public void setIconVisibility(boolean visibility) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mButtons + private BottomNavigationItemView[] mButtons; + + 3. get mIcon in mButtons + private ImageView mIcon + + 4. set mIcon visibility gone + + 5. change mItemHeight to only text size in mMenuView + */ + // 1. get mMenuView + final BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. get mIcon in mButtons + for (BottomNavigationItemView button : mButtons) { + ImageView mIcon = getField(button.getClass(), button, "mIcon"); + // 4. set mIcon visibility gone + mIcon.setVisibility(visibility ? View.VISIBLE : View.INVISIBLE); + } + + // 5. change mItemHeight to only text size in mMenuView + if (!visibility) { + // if not record mItemHeight + if (!visibilityHeightRecord) { + visibilityHeightRecord = true; + mItemHeight = getItemHeight(); + } + + // change mItemHeight + BottomNavigationItemView button = mButtons[0]; + if (null != button) { + final ImageView mIcon = getField(button.getClass(), button, "mIcon"); +// System.out.println("mIcon.getMeasuredHeight():" + mIcon.getMeasuredHeight()); + if (null != mIcon) { + mIcon.post(new Runnable() { + @Override + public void run() { +// System.out.println("mIcon.getMeasuredHeight():" + mIcon.getMeasuredHeight()); + setItemHeight(mItemHeight - mIcon.getMeasuredHeight()); + } + }); + } + } + } else { + // if not record the mItemHeight, we need do nothing. + if (!visibilityHeightRecord) + return; + + // restore it + setItemHeight(mItemHeight); + } + + mMenuView.updateMenuView(); + } + + /** + * change the visibility of text + * + * @param visibility + */ + public void setTextVisibility(boolean visibility) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mButtons + private BottomNavigationItemView[] mButtons; + + 3. set text size in mButtons + private final TextView mLargeLabel + private final TextView mSmallLabel + + 4. change mItemHeight to only icon size in mMenuView + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. change field mShiftingMode value in mButtons + for (BottomNavigationItemView button : mButtons) { + TextView mLargeLabel = getField(button.getClass(), button, "mLargeLabel"); + TextView mSmallLabel = getField(button.getClass(), button, "mSmallLabel"); + + if (!visibility) { + // if not record the font size, record it + if (!visibilityTextSizeRecord && !animationRecord) { + visibilityTextSizeRecord = true; + mLargeLabelSize = mLargeLabel.getTextSize(); + mSmallLabelSize = mSmallLabel.getTextSize(); + } + + // if not visitable, set font size to 0 + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0); + mSmallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, 0); + + } else { + // if not record the font size, we need do nothing. + if (!visibilityTextSizeRecord) + break; + + // restore it + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLargeLabelSize); + mSmallLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSmallLabelSize); + } + } + + // 4 change mItemHeight to only icon size in mMenuView + if (!visibility) { + // if not record mItemHeight + if (!visibilityHeightRecord) { + visibilityHeightRecord = true; + mItemHeight = getItemHeight(); + } + + // change mItemHeight to only icon size in mMenuView + // private final int mItemHeight; + + // change mItemHeight +// System.out.println("mLargeLabel.getMeasuredHeight():" + getFontHeight(mSmallLabelSize)); + setItemHeight(mItemHeight - getFontHeight(mSmallLabelSize)); + + } else { + // if not record the mItemHeight, we need do nothing. + if (!visibilityHeightRecord) + return; + // restore mItemHeight + setItemHeight(mItemHeight); + } + + mMenuView.updateMenuView(); + } + + /** + * get text height by font size + * + * @param fontSize + * @return + */ + private static int getFontHeight(float fontSize) { + Paint paint = new Paint(); + paint.setTextSize(fontSize); + Paint.FontMetrics fm = paint.getFontMetrics(); + return (int) Math.ceil(fm.descent - fm.top) + 2; + } + + /** + * enable or disable click item animation(text scale and icon move animation in no item shifting mode) + * + * @param enable It means the text won't scale and icon won't move when active it in no item shifting mode if false. + */ + public void enableAnimation(boolean enable) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mButtons + private BottomNavigationItemView[] mButtons; + + 3. chang mShiftAmount to 0 in mButtons + private final int mShiftAmount + + change mScaleUpFactor and mScaleDownFactor to 1f in mButtons + private final float mScaleUpFactor + private final float mScaleDownFactor + + 4. change label font size in mButtons + private final TextView mLargeLabel + private final TextView mSmallLabel + */ + + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. change field mShiftingMode value in mButtons + for (BottomNavigationItemView button : mButtons) { + TextView mLargeLabel = getField(button.getClass(), button, "mLargeLabel"); + TextView mSmallLabel = getField(button.getClass(), button, "mSmallLabel"); + + // if disable animation, need animationRecord the source value + if (!enable) { + if (!animationRecord) { + animationRecord = true; + mShiftAmount = getField(button.getClass(), button, "mShiftAmount"); + mScaleUpFactor = getField(button.getClass(), button, "mScaleUpFactor"); + mScaleDownFactor = getField(button.getClass(), button, "mScaleDownFactor"); + + mLargeLabelSize = mLargeLabel.getTextSize(); + mSmallLabelSize = mSmallLabel.getTextSize(); + +// System.out.println("mShiftAmount:" + mShiftAmount + " mScaleUpFactor:" +// + mScaleUpFactor + " mScaleDownFactor:" + mScaleDownFactor +// + " mLargeLabel:" + mLargeLabelSize + " mSmallLabel:" + mSmallLabelSize); + } + // disable + setField(button.getClass(), button, "mShiftAmount", 0); + setField(button.getClass(), button, "mScaleUpFactor", 1); + setField(button.getClass(), button, "mScaleDownFactor", 1); + + // let the mLargeLabel font size equal to mSmallLabel + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mSmallLabelSize); + + // debug start +// mLargeLabelSize = mLargeLabel.getTextSize(); +// System.out.println("mLargeLabel:" + mLargeLabelSize); + // debug end + + } else { + // haven't change the value. It means it was the first call this method. So nothing need to do. + if (!animationRecord) + return; + // enable animation + setField(button.getClass(), button, "mShiftAmount", mShiftAmount); + setField(button.getClass(), button, "mScaleUpFactor", mScaleUpFactor); + setField(button.getClass(), button, "mScaleDownFactor", mScaleDownFactor); + // restore + mLargeLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLargeLabelSize); + } + } + mMenuView.updateMenuView(); + } + + /** + * enable the shifting mode for navigation + * + * @param enable It will has a shift animation if true. Otherwise all items are the same width. + */ + public void enableShiftingMode(boolean enable) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. change field mShiftingMode value in mMenuView + private boolean mShiftingMode = true; + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. change field mShiftingMode value in mMenuView + setField(mMenuView.getClass(), mMenuView, "mShiftingMode", enable); + + mMenuView.updateMenuView(); + } + + /** + * enable the shifting mode for each item + * + * @param enable It will has a shift animation for item if true. Otherwise the item text always be shown. + */ + public void enableItemShiftingMode(boolean enable) { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in this mMenuView + private BottomNavigationItemView[] mButtons; + + 3. change field mShiftingMode value in mButtons + private boolean mShiftingMode = true; + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. change field mShiftingMode value in mButtons + for (BottomNavigationItemView button : mButtons) { + setField(button.getClass(), button, "mShiftingMode", enable); + } + mMenuView.updateMenuView(); + } + + /** + * get the current checked item position + * + * @return index of item, start from 0. + */ + public int getCurrentItem() { + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mMenuView + private BottomNavigationItemView[] mButtons; + + 3. get menu and traverse it to get the checked one + */ + + // 1. get mMenuView +// BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // 3. get menu and traverse it to get the checked one + Menu menu = getMenu(); + for (int i = 0; i < mButtons.length; i++) { + if (menu.getItem(i).isChecked()) { + return i; + } + } + return 0; + } + + /** + * get menu item position in menu + * + * @param item + * @return position if success, -1 otherwise + */ + public int getMenuItemPosition(MenuItem item) { + // get item id + int itemId = item.getItemId(); + // get meunu + Menu menu = getMenu(); + int size = menu.size(); + for (int i = 0; i < size; i++) { + if (menu.getItem(i).getItemId() == itemId) { + return i; + } + } + return -1; + } + + /** + * set the current checked item + * + * @param item start from 0. + */ + public void setCurrentItem(int item) { + // check bounds + if (item < 0 || item >= getMaxItemCount()) { + throw new ArrayIndexOutOfBoundsException("item is out of bounds, we expected 0 - " + + (getMaxItemCount() - 1) + ". Actually " + item); + } + + /* + 1. get field in this class + private final BottomNavigationMenuView mMenuView; + + 2. get field in mMenuView + private BottomNavigationItemView[] mButtons; + private final OnClickListener mOnClickListener; + + 3. call mOnClickListener.onClick(); + */ + // 1. get mMenuView + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get mButtons + BottomNavigationItemView[] mButtons = getBottomNavigationItemViews(); + // get mOnClickListener + OnClickListener mOnClickListener = getField(mMenuView.getClass(), mMenuView, "mOnClickListener"); + +// System.out.println("mMenuView:" + mMenuView + " mButtons:" + mButtons + " mOnClickListener" + mOnClickListener); + // 3. call mOnClickListener.onClick(); + mOnClickListener.onClick(mButtons[item]); + + } + + /** + * get OnNavigationItemSelectedListener + * + * @return + */ + public OnNavigationItemSelectedListener getOnNavigationItemSelectedListener() { + // private OnNavigationItemSelectedListener mListener; + OnNavigationItemSelectedListener mListener = getField(getClass().getSuperclass(), this, "mListener"); + return mListener; + } + + @Override + public void setOnNavigationItemSelectedListener(@Nullable OnNavigationItemSelectedListener listener) { + // if not set up with view pager, the same with father + if (null == mMyOnNavigationItemSelectedListener) { + super.setOnNavigationItemSelectedListener(listener); + return; + } + + mMyOnNavigationItemSelectedListener.setOnNavigationItemSelectedListener(listener); + } + + /** + * get private mMenuView + * + * @return + */ + private BottomNavigationMenuView getBottomNavigationMenuView() { + if (null == mMenuView) + mMenuView = getField(getClass().getSuperclass(), this, "mMenuView"); + return mMenuView; + } + + /** + * get private mButtons in mMenuView + * + * @return + */ + public BottomNavigationItemView[] getBottomNavigationItemViews() { + if (null != mButtons) + return mButtons; + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + */ + BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + mButtons = getField(mMenuView.getClass(), mMenuView, "mButtons"); + return mButtons; + } + + /** + * get private mButton in mMenuView at position + * + * @param position + * @return + */ + public BottomNavigationItemView getBottomNavigationItemView(int position) { + return getBottomNavigationItemViews()[position]; + } + + /** + * get icon at position + * + * @param position + * @return + */ + public ImageView getIconAt(int position) { + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + * 3 private ImageView mIcon; + */ + BottomNavigationItemView mButtons = getBottomNavigationItemView(position); + ImageView mIcon = getField(BottomNavigationItemView.class, mButtons, "mIcon"); + return mIcon; + } + + /** + * get small label at position + * Each item has tow label, one is large, another is small. + * + * @param position + * @return + */ + public TextView getSmallLabelAt(int position) { + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + * 3 private final TextView mSmallLabel; + */ + BottomNavigationItemView mButtons = getBottomNavigationItemView(position); + TextView mSmallLabel = getField(BottomNavigationItemView.class, mButtons, "mSmallLabel"); + return mSmallLabel; + } + + /** + * get large label at position + * Each item has tow label, one is large, another is small. + * + * @param position + * @return + */ + public TextView getLargeLabelAt(int position) { + /* + * 1 private final BottomNavigationMenuView mMenuView; + * 2 private BottomNavigationItemView[] mButtons; + * 3 private final TextView mLargeLabel; + */ + BottomNavigationItemView mButtons = getBottomNavigationItemView(position); + TextView mLargeLabel = getField(BottomNavigationItemView.class, mButtons, "mLargeLabel"); + return mLargeLabel; + } + + /** + * return item count + * + * @return + */ + public int getItemCount() { + return getBottomNavigationItemViews().length; + } + + /** + * set all item small TextView size + * Each item has tow label, one is large, another is small. + * Small one will be shown when item state is normal + * Large one will be shown when item checked. + * + * @param sp + */ + public void setSmallTextSize(float sp) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getSmallLabelAt(i).setTextSize(sp); + } + mMenuView.updateMenuView(); + } + + /** + * set all item large TextView size + * Each item has tow label, one is large, another is small. + * Small one will be shown when item state is normal. + * Large one will be shown when item checked. + * + * @param sp + */ + public void setLargeTextSize(float sp) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getLargeLabelAt(i).setTextSize(sp); + } + mMenuView.updateMenuView(); + } + + /** + * set all item large and small TextView size + * Each item has tow label, one is large, another is small. + * Small one will be shown when item state is normal + * Large one will be shown when item checked. + * + * @param sp + */ + public void setTextSize(float sp) { + setLargeTextSize(sp); + setSmallTextSize(sp); + } + + /** + * set item ImageView size which at position + * + * @param position position start from 0 + * @param width in dp + * @param height in dp + */ + public void setIconSizeAt(int position, float width, float height) { + ImageView icon = getIconAt(position); + // update size + ViewGroup.LayoutParams layoutParams = icon.getLayoutParams(); + layoutParams.width = dp2px(getContext(), width); + layoutParams.height = dp2px(getContext(), height); + icon.setLayoutParams(layoutParams); + + mMenuView.updateMenuView(); + } + + /** + * set all item ImageView size + * + * @param width in dp + * @param height in dp + */ + public void setIconSize(float width, float height) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + setIconSizeAt(i, width, height); + } + } + + /** + * set menu item height + * + * @param height in px + */ + public void setItemHeight(int height) { + // 1. get mMenuView + final BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. set private final int mItemHeight in mMenuView + setField(mMenuView.getClass(), mMenuView, "mItemHeight", height); + + mMenuView.updateMenuView(); + } + + /** + * get menu item height + * + * @return in px + */ + public int getItemHeight() { + // 1. get mMenuView + final BottomNavigationMenuView mMenuView = getBottomNavigationMenuView(); + // 2. get private final int mItemHeight in mMenuView + return getField(mMenuView.getClass(), mMenuView, "mItemHeight"); + } + + /** + * dp to px + * + * @param context + * @param dpValue dp + * @return px + */ + public static int dp2px(Context context, float dpValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * set Typeface for all item TextView + * + * @attr ref android.R.styleable#TextView_typeface + * @attr ref android.R.styleable#TextView_textStyle + */ + public void setTypeface(Typeface typeface, int style) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getLargeLabelAt(i).setTypeface(typeface, style); + getSmallLabelAt(i).setTypeface(typeface, style); + } + mMenuView.updateMenuView(); + } + + /** + * set Typeface for all item TextView + * + * @attr ref android.R.styleable#TextView_typeface + */ + public void setTypeface(Typeface typeface) { + int count = getItemCount(); + for (int i = 0; i < count; i++) { + getLargeLabelAt(i).setTypeface(typeface); + getSmallLabelAt(i).setTypeface(typeface); + } + mMenuView.updateMenuView(); + } + + /** + * get private filed in this specific object + * + * @param targetClass + * @param instance the filed owner + * @param fieldName + * @param + * @return field if success, null otherwise. + */ + private T getField(Class targetClass, Object instance, String fieldName) { + try { + Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(instance); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + /** + * change the field value + * + * @param targetClass + * @param instance the filed owner + * @param fieldName + * @param value + */ + private void setField(Class targetClass, Object instance, String fieldName, Object value) { + try { + Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, value); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * This method will link the given ViewPager and this BottomNavigationViewEx together so that + * changes in one are automatically reflected in the other. This includes scroll state changes + * and clicks. + * + * @param viewPager + */ + public void setupWithViewPager(@Nullable final ViewPager viewPager) { + setupWithViewPager(viewPager, false); + } + + /** + * This method will link the given ViewPager and this BottomNavigationViewEx together so that + * changes in one are automatically reflected in the other. This includes scroll state changes + * and clicks. + * + * @param viewPager + * @param smoothScroll whether ViewPager changed with smooth scroll animation + */ + public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean smoothScroll) { + if (mViewPager != null) { + // If we've already been setup with a ViewPager, remove us from it + if (mPageChangeListener != null) { + mViewPager.removeOnPageChangeListener(mPageChangeListener); + } + } + + if (null == viewPager) { + mViewPager = null; + super.setOnNavigationItemSelectedListener(null); + return; + } + + mViewPager = viewPager; + + // Add our custom OnPageChangeListener to the ViewPager + if (mPageChangeListener == null) { + mPageChangeListener = new BottomNavigationViewExOnPageChangeListener(this); + } + viewPager.addOnPageChangeListener(mPageChangeListener); + + // Now we'll add a navigation item selected listener to set ViewPager's current item + OnNavigationItemSelectedListener listener = getOnNavigationItemSelectedListener(); + mMyOnNavigationItemSelectedListener = new MyOnNavigationItemSelectedListener(viewPager, this, smoothScroll, listener); + super.setOnNavigationItemSelectedListener(mMyOnNavigationItemSelectedListener); + } + + /** + * A {@link ViewPager.OnPageChangeListener} class which contains the + * necessary calls back to the provided {@link BottomNavigationView} so that the tab position is + * kept in sync. + *

+ *

This class stores the provided BottomNavigationViewEx weakly, meaning that you can use + * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) + * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and + * not cause a leak. + */ + private static class BottomNavigationViewExOnPageChangeListener implements ViewPager.OnPageChangeListener { + private final WeakReference mBnveRef; + + public BottomNavigationViewExOnPageChangeListener(BottomNavigationView bnve) { + mBnveRef = new WeakReference<>(bnve); + } + + @Override + public void onPageScrollStateChanged(final int state) { + } + + @Override + public void onPageScrolled(final int position, final float positionOffset, + final int positionOffsetPixels) { + } + + @Override + public void onPageSelected(final int position) { + final BottomNavigationView bnve = mBnveRef.get(); + if (null != bnve) + bnve.setCurrentItem(position); +// Log.d("onPageSelected", "--------- position " + position + " ------------"); + } + } + + /** + * Decorate OnNavigationItemSelectedListener for setupWithViewPager + */ + private static class MyOnNavigationItemSelectedListener implements OnNavigationItemSelectedListener { + private OnNavigationItemSelectedListener listener; + private final WeakReference viewPagerRef; + private boolean smoothScroll; + private SparseIntArray items;// used for change ViewPager selected item + private int previousPosition = -1; + + + MyOnNavigationItemSelectedListener(ViewPager viewPager, BottomNavigationView bnve, boolean smoothScroll, OnNavigationItemSelectedListener listener) { + this.viewPagerRef = new WeakReference<>(viewPager); + this.listener = listener; + this.smoothScroll = smoothScroll; + + // create items + Menu menu = bnve.getMenu(); + int size = menu.size(); + items = new SparseIntArray(size); + for (int i = 0; i < size; i++) { + int itemId = menu.getItem(i).getItemId(); + items.put(itemId, i); + } + } + + public void setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener listener) { + this.listener = listener; + } + + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int position = items.get(item.getItemId()); + // only set item when item changed + if (previousPosition == position) { + return true; + } + + // user listener + if (null != listener) { + boolean bool = listener.onNavigationItemSelected(item); + // if the selected is invalid, no need change the view pager + if (!bool) + return false; + } + + // change view pager + ViewPager viewPager = viewPagerRef.get(); + if (null == viewPager) + return false; + + viewPager.setCurrentItem(items.get(item.getItemId()), smoothScroll); + + // update previous position + previousPosition = position; + + return true; + } + + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/CheckImageView.java b/widget/src/main/java/com/hzecool/widget/CheckImageView.java new file mode 100644 index 0000000..830067a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/CheckImageView.java @@ -0,0 +1,72 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Checkable; + +/** + * Created by tutu on 2017/5/3. + */ + + +public class CheckImageView extends AppCompatImageView implements Checkable { + private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; + private boolean mChecked; + + public CheckImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CheckImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CheckImageView(Context context) { + super(context); + } + + @Override + public void setChecked(boolean checked) { + if (checked != mChecked) { + mChecked = checked; + refreshDrawableState(); + } + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void toggle() { + mChecked = !mChecked; + refreshDrawableState(); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setCheckable(true); + info.setChecked(mChecked); + } +} + diff --git a/widget/src/main/java/com/hzecool/widget/ClearableEditText.java b/widget/src/main/java/com/hzecool/widget/ClearableEditText.java new file mode 100644 index 0000000..6231a46 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ClearableEditText.java @@ -0,0 +1,120 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +/** + * Created by slh on 2017/3/1. + */ + +public class ClearableEditText extends android.support.v7.widget.AppCompatEditText implements View.OnFocusChangeListener, TextWatcher { + /** + * 删除按钮的引用 + */ + private Drawable mClearDrawable; + /** + * 控件是否有焦点 + */ + private boolean hasFoucs; + + public ClearableEditText(Context context) { + this(context, null); + } + + public ClearableEditText(Context context, AttributeSet attrs) { + // 这里构造方法也很重要,不加这个很多属性不能再XML里面定义 + this(context, attrs, android.R.attr.editTextStyle); + } + + public ClearableEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + // 获取EditText的DrawableRight,假如没有设置我们就使用默认的图片 + mClearDrawable = getCompoundDrawables()[2]; + if (mClearDrawable == null) { + // throw new + // NullPointerException("You can add drawableRight attribute in XML"); + mClearDrawable = getResources().getDrawable(R.mipmap.ic_delete); + } + + mClearDrawable.setBounds(0, 0, mClearDrawable.getIntrinsicWidth(), mClearDrawable.getIntrinsicHeight()); + // 默认设置隐藏图标 + setClearIconVisible(false); + // 设置焦点改变的监听 + setOnFocusChangeListener(this); + // 设置输入框里面内容发生改变的监听 + addTextChangedListener(this); + } + + /** + * 因为我们不能直接给EditText设置点击事件,所以我们用记住我们按下的位置来模拟点击事件 当我们按下的位置 在 EditText的宽度 - + * 图标到控件右边的间距 - 图标的宽度 和 EditText的宽度 - 图标到控件右边的间距之间我们就算点击了图标,竖直方向就没有考虑 + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (getCompoundDrawables()[2] != null) { + + boolean touchable = event.getX() > (getWidth() - getTotalPaddingRight()) && (event.getX() < ((getWidth() - getPaddingRight()))); + + if (touchable) { + this.setText(""); + } + } + } + + return super.onTouchEvent(event); + } + + /** + * 当ClearEditText焦点发生变化的时候,判断里面字符串长度设置清除图标的显示与隐藏 + */ + @Override + public void onFocusChange(View v, boolean hasFocus) { + this.hasFoucs = hasFocus; + if (hasFocus) { + setClearIconVisible(getText().length() > 0); + } else { + setClearIconVisible(false); + } + } + + /** + * 设置清除图标的显示与隐藏,调用setCompoundDrawables为EditText绘制上去 + * + * @param visible + */ + protected void setClearIconVisible(boolean visible) { + Drawable right = visible ? mClearDrawable : null; + setCompoundDrawables(getCompoundDrawables()[0], getCompoundDrawables()[1], right, getCompoundDrawables()[3]); + } + + /** + * 当输入框里面内容发生变化的时候回调的方法 + */ + @Override + public void onTextChanged(CharSequence s, int start, int count, int after) { + if (hasFoucs) { + setClearIconVisible(s.length() > 0); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java b/widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java new file mode 100644 index 0000000..9ede67b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/CountDownButton/CountDownButton.java @@ -0,0 +1,214 @@ +package com.hzecool.widget.CountDownButton; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.CountDownTimer; +import android.support.v7.widget.AppCompatButton; +import android.util.AttributeSet; + +import com.hzecool.widget.R; + +import java.util.Locale; + +/** + * Created by tutu on 2017/3/16. + */ + +public class CountDownButton extends AppCompatButton { + + /** + * 默认时间间隔1000ms + */ + private static final long DEFAULT_INTERVAL = 1000; + /** + * 默认时长60s + */ + private static final long DEFAULT_COUNT = 60 * 1000; + /** + * 默认倒计时文字格式(显示秒数) + */ + private static final String DEFAULT_COUNT_FORMAT = "%d"; + /** + * 默认按钮文字 {@link #getText()} + */ + private String mDefaultText; + /** + * 倒计时时长,单位为毫秒 + */ + private long mCount; + /** + * 时间间隔,单位为毫秒 + */ + private long mInterval; + /** + * 倒计时文字格式 + */ + private String mCountDownFormat = DEFAULT_COUNT_FORMAT; + /** + * 倒计时是否可用 + */ + private boolean mEnableCountDown = true; + /** + * 点击事件监听器 + */ + private OnClickListener onClickListener; + + /** + * 倒计时 + */ + private CountDownTimer mCountDownTimer; + + /** + * 是否正在执行倒计时 + */ + private boolean isCountDownNow; + + private int enableFalseTextColor; + private int enableTextColor; + + public CountDownButton(Context context) { + super(context); + } + + public CountDownButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public CountDownButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + + private void init(Context context, AttributeSet attrs) { + // 获取自定义属性值 + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CountDownButton); + mCountDownFormat = typedArray.getString(R.styleable.CountDownButton_countDownFormat); + enableFalseTextColor = typedArray.getColor(R.styleable.CountDownButton_enableFalseTextColor,getResources().getColor(R.color.white)); + enableTextColor = typedArray.getColor(R.styleable.CountDownButton_enableTextColor,getResources().getColor(R.color.white)); + if (typedArray.hasValue(R.styleable.CountDownButton_countDown)) { + mCount = (int) typedArray.getFloat(R.styleable.CountDownButton_countDown, DEFAULT_COUNT); + } + mInterval = (int) typedArray.getFloat(R.styleable.CountDownButton_countDownInterval, DEFAULT_INTERVAL); + mEnableCountDown = (mCount > mInterval) && typedArray.getBoolean(R.styleable.CountDownButton_enableCountDown, true); + typedArray.recycle(); + // 初始化倒计时Timer + if (mCountDownTimer == null) { + mCountDownTimer = new CountDownTimer(mCount, mInterval) { + @Override + public void onTick(long millisUntilFinished) { + setText(String.format(Locale.CHINA, mCountDownFormat, millisUntilFinished / 1000)); + } + + @Override + public void onFinish() { + isCountDownNow = false; + setEnabled(true); + setClickable(true); + setText(mDefaultText); + setTextColor(enableTextColor); + } + }; + } + } + + + @Override + public void setOnClickListener(OnClickListener onClickListener) { + super.setOnClickListener(onClickListener); + this.onClickListener = onClickListener; + } + + + public void start(){ + if (mEnableCountDown){ + setTextColor(enableFalseTextColor); + mDefaultText = getText().toString(); + // 设置按钮不可点击 + setEnabled(false); + setClickable(false); + // 开始倒计时 + mCountDownTimer.start(); + isCountDownNow = true; + } + } + +// @Override +// public boolean onTouchEvent(MotionEvent event) { +// switch (event.getAction()) { +// case MotionEvent.ACTION_UP: +// Rect rect = new Rect(); +// this.getGlobalVisibleRect(rect); +// if (onClickListener != null && rect.contains((int) event.getRawX(), (int) event.getRawY())) { +// onClickListener.onClick(this); +// } +// if (mEnableCountDown && rect.contains((int) event.getRawX(), (int) event.getRawY())) { +// mDefaultText = getText().toString(); +// // 设置按钮不可点击 +// setEnabled(false); +// setClickable(false); +// // 开始倒计时 +// mCountDownTimer.start(); +// isCountDownNow = true; +// } +// break; +// case MotionEvent.ACTION_MOVE: +// break; +// } +// return super.onTouchEvent(event); +// } + + public void setEnableCountDown(boolean enableCountDown) { + this.mEnableCountDown = (mCount > mInterval) && enableCountDown; + } + + public void setCountDownFormat(String countDownFormat) { + this.mCountDownFormat = countDownFormat; + } + + public void setCount(long count) { + this.mCount = count; + } + + public void setInterval(long interval) { + mInterval = interval; + } + + /** + * 是否正在执行倒计时 + * + * @return 倒计时期间返回true否则返回false + */ + public boolean isCountDownNow() { + return isCountDownNow; + } + + /** + * 设置倒计时数据 + * + * @param count 时长 + * @param interval 间隔 + * @param countDownFormat 文字格式 + */ + public void setCountDown(long count, long interval, String countDownFormat) { + this.mCount = count; + this.mCountDownFormat = countDownFormat; + this.mInterval = interval; + setEnableCountDown(true); + } + + /** + * 移除倒计时 + */ + public void removeCountDown() { + if (mCountDownTimer != null) { + mCountDownTimer.cancel(); + } + setTextColor(enableTextColor); + isCountDownNow = false; + setText(mDefaultText); + setEnabled(true); + setClickable(true); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/FlowLayout.java b/widget/src/main/java/com/hzecool/widget/FlowLayout.java new file mode 100644 index 0000000..be572c1 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/FlowLayout.java @@ -0,0 +1,200 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * 流式布局 + * Created by wangzhiguo on 2017/6/19 + */ +public class FlowLayout extends ViewGroup { + + + public FlowLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public FlowLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FlowLayout(Context context) { + this(context, null); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); + int modeWidth = MeasureSpec.getMode(widthMeasureSpec); + int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); + int modeHeight = MeasureSpec.getMode(heightMeasureSpec); + + // 如果是warp_content情况下,记录宽和高 + int width = 0; + int height = 0; + + // 记录每一行的宽度与高度 + int lineWidth = 0; + int lineHeight = 0; + + // 得到内部元素的个数 + int cCount = getChildCount(); + + for (int i = 0; i < cCount; i++) { + // 通过索引拿到每一个子view + View child = getChildAt(i); + // 测量子View的宽和高,系统提供的measureChild + measureChild(child, widthMeasureSpec, heightMeasureSpec); + // 得到LayoutParams + MarginLayoutParams lp = (MarginLayoutParams) child + .getLayoutParams(); + + // 子View占据的宽度 + int childWidth = child.getMeasuredWidth() + lp.leftMargin + + lp.rightMargin; + // 子View占据的高度 + int childHeight = child.getMeasuredHeight() + lp.topMargin + + lp.bottomMargin; + + // 换行 判断 当前的宽度大于 开辟新行 + if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) { + // 对比得到最大的宽度 + width = Math.max(width, lineWidth); + // 重置lineWidth + lineWidth = childWidth; + // 记录行高 + height += lineHeight; + lineHeight = childHeight; + } else + // 未换行 + { + // 叠加行宽 + lineWidth += childWidth; + // 得到当前行最大的高度 + lineHeight = Math.max(lineHeight, childHeight); + } + // 特殊情况,最后一个控件 + if (i == cCount - 1) { + width = Math.max(lineWidth, width); + height += lineHeight; + } + } + setMeasuredDimension( + modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(), + modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()// + ); + + } + + /** + * 存储所有的View + */ + private List> mAllViews = new ArrayList>(); + /** + * 每一行的高度 + */ + private List mLineHeight = new ArrayList(); + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mAllViews.clear(); + mLineHeight.clear(); + + // 当前ViewGroup的宽度 + int width = getWidth(); + + int lineWidth = 0; + int lineHeight = 0; + + // 存放每一行的子view + List lineViews = new ArrayList(); + + int cCount = getChildCount(); + + for (int i = 0; i < cCount; i++) { + View child = getChildAt(i); + MarginLayoutParams lp = (MarginLayoutParams) child + .getLayoutParams(); + + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + // 如果需要换行 + if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) { + // 记录LineHeight + mLineHeight.add(lineHeight); + // 记录当前行的Views + mAllViews.add(lineViews); + + // 重置我们的行宽和行高 + lineWidth = 0; + lineHeight = childHeight + lp.topMargin + lp.bottomMargin; + // 重置我们的View集合 + lineViews = new ArrayList(); + } + lineWidth += childWidth + lp.leftMargin + lp.rightMargin; + lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + + lp.bottomMargin); + lineViews.add(child); + + }// for end + // 处理最后一行 + mLineHeight.add(lineHeight); + mAllViews.add(lineViews); + + // 设置子View的位置 + + int left = getPaddingLeft(); + int top = getPaddingTop(); + + // 行数 + int lineNum = mAllViews.size(); + + for (int i = 0; i < lineNum; i++) { + // 当前行的所有的View + lineViews = mAllViews.get(i); + lineHeight = mLineHeight.get(i); + + for (int j = 0; j < lineViews.size(); j++) { + View child = lineViews.get(j); + // 判断child的状态 + if (child.getVisibility() == View.GONE) { + continue; + } + + MarginLayoutParams lp = (MarginLayoutParams) child + .getLayoutParams(); + + int lc = left + lp.leftMargin; + int tc = top + lp.topMargin; + int rc = lc + child.getMeasuredWidth(); + int bc = tc + child.getMeasuredHeight(); + + // 为子View进行布局 + child.layout(lc, tc, rc, bc); + + left += child.getMeasuredWidth() + lp.leftMargin + + lp.rightMargin; + } + left = getPaddingLeft(); + top += lineHeight; + } + + } + + /** + * 与当前ViewGroup对应的LayoutParams + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java b/widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java new file mode 100644 index 0000000..b562e8b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/RecycleViewDivider.java @@ -0,0 +1,148 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * RecyclerView 分割线 + * Created by tutu on 2017/4/19. + */ + +public class RecycleViewDivider extends RecyclerView.ItemDecoration { + private Paint mPaint; + private Drawable mDivider; + private int mDividerHeight = 2;//分割线高度,默认为1px + private int mOrientation;//列表的方向:LinearLayoutManager.VERTICAL或LinearLayoutManager.HORIZONTAL + private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; + private int marginLeft = 0; + private int marginRight = 0; + + /** + * 默认分割线:高度为2px,颜色为灰色 + * + * @param context + * @param orientation 列表方向 + */ + public RecycleViewDivider(Context context, int orientation) { + if (orientation != LinearLayoutManager.VERTICAL && orientation != LinearLayoutManager.HORIZONTAL) { + throw new IllegalArgumentException("请输入正确的参数!"); + } + mOrientation = orientation; + + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + } + + /** + * 自定义分割线 + * + * @param context + * @param orientation 列表方向 + * @param drawableId 分割线图片 + */ + public RecycleViewDivider(Context context, int orientation, int drawableId) { + this(context, orientation); + mDivider = ContextCompat.getDrawable(context, drawableId); + mDividerHeight = mDivider.getIntrinsicHeight(); + } + + /** + * 自定义分割线 + * + * @param context + * @param orientation 列表方向 + * @param dividerHeight 分割线高度 + * @param dividerColor 分割线颜色 + */ + public RecycleViewDivider(Context context, int orientation, int dividerHeight, int dividerColor) { + this(context, orientation); + mDividerHeight = dividerHeight; + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(dividerColor); + mPaint.setStyle(Paint.Style.FILL); + } + + + public int getMarginLeft() { + return marginLeft; + } + + public void setMarginLeft(int marginLeft) { + this.marginLeft = marginLeft; + } + + public int getMarginRight() { + return marginRight; + } + + public void setMarginRight(int marginRight) { + this.marginRight = marginRight; + } + + //获取分割线尺寸 + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + outRect.set(0, 0, 0, mDividerHeight); + } + + //绘制分割线 + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + super.onDraw(c, parent, state); + if (mOrientation == LinearLayoutManager.VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + //绘制横向 item 分割线 + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getMeasuredWidth() - parent.getPaddingRight(); + final int childSize = parent.getChildCount(); + for (int i = 0; i < childSize-1; i++) { + final View child = parent.getChildAt(i); + RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); + final int top = child.getBottom() + layoutParams.bottomMargin; + final int bottom = top + mDividerHeight; + if (mDivider != null) { + mDivider.setBounds(left+marginLeft, top, right-marginRight, bottom); + mDivider.draw(canvas); + } + if (mPaint != null) { + canvas.drawRect(left+marginLeft, top, right-marginRight, bottom, mPaint); + } + } + } + + //绘制纵向 item 分割线 + private void drawVertical(Canvas canvas, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom(); + final int childSize = parent.getChildCount(); + for (int i = 0; i < childSize; i++) { + final View child = parent.getChildAt(i); + RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams(); + final int left = child.getRight() + layoutParams.rightMargin; + final int right = left + mDividerHeight; + if (mDivider != null) { + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + if (mPaint != null) { + canvas.drawRect(left, top, right, bottom, mPaint); + } + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/WaveSideBar.java b/widget/src/main/java/com/hzecool/widget/WaveSideBar.java new file mode 100644 index 0000000..21fe252 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/WaveSideBar.java @@ -0,0 +1,398 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; + +import java.util.Arrays; + +/** + * 列表添加索引 + * add by tutu on 2017/4/12 + * refer to https://github.com/gjiazhe/WaveSideBar + */ +public class WaveSideBar extends View { + private final static int DEFAULT_TEXT_SIZE = 14; // sp + private final static int DEFAULT_MAX_OFFSET = 80; //dp + + private final static String[] DEFAULT_INDEX_ITEMS = {"A", "B", "C", "D", "E", "F", "G", "H", "I", + "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}; + + private String[] mIndexItems; + + /** + * the index in {@link #mIndexItems} of the current selected index item, + * it's reset to -1 when the finger up + */ + private int mCurrentIndex = -1; + + /** + * Y coordinate of the point where finger is touching, + * the baseline is top of {@link #mStartTouchingArea} + * it's reset to -1 when the finger up + */ + private float mCurrentY = -1; + + private Paint mPaint; + private int mTextColor; + private float mTextSize; + + /** + * the height of each index item + */ + private float mIndexItemHeight; + + /** + * offset of the current selected index item + */ + private float mMaxOffset; + + /** + * {@link #mStartTouching} will be set to true when {@link MotionEvent#ACTION_DOWN} + * happens in this area, and the side bar should start working. + */ + private RectF mStartTouchingArea = new RectF(); + + /** + * height and width of {@link #mStartTouchingArea} + */ + private float mBarHeight; + private float mBarWidth; + + /** + * Flag that the finger is starting touching. + * If true, it means the {@link MotionEvent#ACTION_DOWN} happened but + * {@link MotionEvent#ACTION_UP} not yet. + */ + private boolean mStartTouching = false; + + /** + * if true, the {@link OnSelectIndexItemListener#onSelectIndexItem(String)} + * will not be called until the finger up. + * if false, it will be called when the finger down, up and move. + */ + private boolean mLazyRespond = false; + + /** + * the position of the side bar, default is {@link #POSITION_RIGHT}. + * You can set it to {@link #POSITION_LEFT} for people who use phone with left hand. + */ + private int mSideBarPosition; + public static final int POSITION_RIGHT = 0; + public static final int POSITION_LEFT = 1; + + /** + * the alignment of items, default is {@link #TEXT_ALIGN_CENTER}. + */ + private int mTextAlignment; + public static final int TEXT_ALIGN_CENTER = 0; + public static final int TEXT_ALIGN_LEFT = 1; + public static final int TEXT_ALIGN_RIGHT = 2; + + + /** + * observe the current selected index item + */ + private OnSelectIndexItemListener onSelectIndexItemListener; + + /** + * the baseline of the first index item text to draw + */ + private float mFirstItemBaseLineY; + + /** + * for {@link #dp2px(int)} and {@link #sp2px(int)} + */ + private DisplayMetrics mDisplayMetrics; + + + public WaveSideBar(Context context) { + this(context, null); + } + + public WaveSideBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public WaveSideBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mDisplayMetrics = context.getResources().getDisplayMetrics(); + + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveSideBar); + mLazyRespond = typedArray.getBoolean(R.styleable.WaveSideBar_sidebar_lazy_respond, false); + mTextColor = typedArray.getColor(R.styleable.WaveSideBar_sidebar_text_color, Color.GRAY); + mTextSize = typedArray.getDimension(R.styleable.WaveSideBar_sidebar_text_size, sp2px(DEFAULT_TEXT_SIZE)); + mMaxOffset = typedArray.getDimension(R.styleable.WaveSideBar_sidebar_max_offset, dp2px(DEFAULT_MAX_OFFSET)); + mSideBarPosition = typedArray.getInt(R.styleable.WaveSideBar_sidebar_position, POSITION_RIGHT); + mTextAlignment = typedArray.getInt(R.styleable.WaveSideBar_sidebar_text_alignment, TEXT_ALIGN_CENTER); + typedArray.recycle(); + + mIndexItems = DEFAULT_INDEX_ITEMS; + + initPaint(); + } + + private void initPaint() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(mTextColor); + mPaint.setTextSize(mTextSize); + switch (mTextAlignment) { + case TEXT_ALIGN_CENTER: + mPaint.setTextAlign(Paint.Align.CENTER); + break; + case TEXT_ALIGN_LEFT: + mPaint.setTextAlign(Paint.Align.LEFT); + break; + case TEXT_ALIGN_RIGHT: + mPaint.setTextAlign(Paint.Align.RIGHT); + break; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + Paint.FontMetrics fontMetrics = mPaint.getFontMetrics(); + mIndexItemHeight = fontMetrics.bottom - fontMetrics.top; + mBarHeight = mIndexItems.length * mIndexItemHeight; + + // calculate the width of the longest text as the width of side bar + for (String indexItem : mIndexItems) { + mBarWidth = Math.max(mBarWidth, mPaint.measureText(indexItem)); + } + + float areaLeft = (mSideBarPosition == POSITION_LEFT) ? 0 : (width - mBarWidth - getPaddingRight()); + float areaRight = (mSideBarPosition == POSITION_LEFT) ? (getPaddingLeft() + areaLeft + mBarWidth) : width; + float areaTop = height / 2 - mBarHeight / 2; + float areaBottom = areaTop + mBarHeight; + mStartTouchingArea.set( + areaLeft, + areaTop, + areaRight, + areaBottom); + + // the baseline Y of the first item' text to draw + mFirstItemBaseLineY = (height / 2 - mIndexItems.length * mIndexItemHeight / 2) + + (mIndexItemHeight / 2 - (fontMetrics.descent - fontMetrics.ascent) / 2) + - fontMetrics.ascent; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // draw each item + for (int i = 0, mIndexItemsLength = mIndexItems.length; i < mIndexItemsLength; i++) { + float baseLineY = mFirstItemBaseLineY + mIndexItemHeight * i; + + // calculate the scale factor of the item to draw + float scale = getItemScale(i); + + int alphaScale = (i == mCurrentIndex) ? (255) : (int) (255 * (1 - scale)); + mPaint.setAlpha(alphaScale); + + mPaint.setTextSize(mTextSize + mTextSize * scale); + + float baseLineX = 0f; + if (mSideBarPosition == POSITION_LEFT) { + switch (mTextAlignment) { + case TEXT_ALIGN_CENTER: + baseLineX = getPaddingLeft() + mBarWidth / 2 + mMaxOffset * scale; + break; + case TEXT_ALIGN_LEFT: + baseLineX = getPaddingLeft() + mMaxOffset * scale; + break; + case TEXT_ALIGN_RIGHT: + baseLineX = getPaddingLeft() + mBarWidth + mMaxOffset * scale; + break; + } + } else { + switch (mTextAlignment) { + case TEXT_ALIGN_CENTER: + baseLineX = getWidth() - getPaddingRight() - mBarWidth / 2 - mMaxOffset * scale; + break; + case TEXT_ALIGN_RIGHT: + baseLineX = getWidth() - getPaddingRight() - mMaxOffset * scale; + break; + case TEXT_ALIGN_LEFT: + baseLineX = getWidth() - getPaddingRight() - mBarWidth - mMaxOffset * scale; + break; + } + } + + // draw + canvas.drawText( + mIndexItems[i], //item text to draw + baseLineX, //baseLine X + baseLineY, // baseLine Y + mPaint); + } + + // reset paint + mPaint.setAlpha(255); + mPaint.setTextSize(mTextSize); + } + + /** + * calculate the scale factor of the item to draw + * + * @param index the index of the item in array {@link #mIndexItems} + * @return the scale factor of the item to draw + */ + private float getItemScale(int index) { + float scale = 0; + if (mCurrentIndex != -1) { + float distance = Math.abs(mCurrentY - (mIndexItemHeight * index + mIndexItemHeight / 2)) / mIndexItemHeight; + scale = 1 - distance * distance / 16; + scale = Math.max(scale, 0); + } + return scale; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mIndexItems.length == 0) { + return super.onTouchEvent(event); + } + + float eventY = event.getY(); + float eventX = event.getX(); + mCurrentIndex = getSelectedIndex(eventY); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (mStartTouchingArea.contains(eventX, eventY)) { + mStartTouching = true; + if (!mLazyRespond && onSelectIndexItemListener != null) { + onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]); + } + invalidate(); + return true; + } else { + mCurrentIndex = -1; + return false; + } + + case MotionEvent.ACTION_MOVE: + if (mStartTouching && !mLazyRespond && onSelectIndexItemListener != null) { + onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]); + } + invalidate(); + return true; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (mLazyRespond && onSelectIndexItemListener != null) { + onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]); + } + mCurrentIndex = -1; + mStartTouching = false; + invalidate(); + return true; + } + + return super.onTouchEvent(event); + } + + private int getSelectedIndex(float eventY) { + mCurrentY = eventY - (getHeight() / 2 - mBarHeight / 2); + if (mCurrentY <= 0) { + return 0; + } + + int index = (int) (mCurrentY / this.mIndexItemHeight); + if (index >= this.mIndexItems.length) { + index = this.mIndexItems.length - 1; + } + return index; + } + + private float dp2px(int dp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.mDisplayMetrics); + } + + private float sp2px(int sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this.mDisplayMetrics); + } + + public void setIndexItems(String... indexItems) { + mIndexItems = Arrays.copyOf(indexItems, indexItems.length); + requestLayout(); + } + + public void setTextColor(int color) { + mTextColor = color; + mPaint.setColor(color); + invalidate(); + } + + public void setPosition(int position) { + if (position != POSITION_RIGHT && position != POSITION_LEFT) { + throw new IllegalArgumentException("the position must be POSITION_RIGHT or POSITION_LEFT"); + } + + mSideBarPosition = position; + requestLayout(); + } + + public void setMaxOffset(int offset) { + mMaxOffset = offset; + invalidate(); + } + + public void setLazyRespond(boolean lazyRespond) { + mLazyRespond = lazyRespond; + } + + public void setTextAlign(int align) { + if (mTextAlignment == align) { + return; + } + switch (align) { + case TEXT_ALIGN_CENTER: + mPaint.setTextAlign(Paint.Align.CENTER); + break; + case TEXT_ALIGN_LEFT: + mPaint.setTextAlign(Paint.Align.LEFT); + break; + case TEXT_ALIGN_RIGHT: + mPaint.setTextAlign(Paint.Align.RIGHT); + break; + default: + throw new IllegalArgumentException( + "the alignment must be TEXT_ALIGN_CENTER, TEXT_ALIGN_LEFT or TEXT_ALIGN_RIGHT"); + } + mTextAlignment = align; + invalidate(); + } + + public void setTextSize(float size) { + if (mTextSize == size) { + return; + } + mTextSize = size; + mPaint.setTextSize(size); + invalidate(); + } + + public void setOnSelectIndexItemListener(OnSelectIndexItemListener onSelectIndexItemListener) { + this.onSelectIndexItemListener = onSelectIndexItemListener; + } + + public interface OnSelectIndexItemListener { + void onSelectIndexItem(String index); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/WaveView.java b/widget/src/main/java/com/hzecool/widget/WaveView.java new file mode 100644 index 0000000..60366cf --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/WaveView.java @@ -0,0 +1,166 @@ +package com.hzecool.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * 水波纹特效 + * Created by fbchen2 on 2016/5/25. + */ +public class WaveView extends View { + private float mInitialRadius; // 初始波纹半径 + private float mMaxRadius; // 最大波纹半径 + private long mDuration = 2000; // 一个波纹从创建到消失的持续时间 + private int mSpeed = 500; // 波纹的创建速度,每500ms创建一个 + private float mMaxRadiusRate = 0.85f; + private boolean mMaxRadiusSet; + private boolean mIsRunning; + private long mLastCreateTime; + private List mCircleList = new ArrayList(); + + private Runnable mCreateCircle = new Runnable() { + @Override + public void run() { + if (mIsRunning) { + newCircle(); + postDelayed(mCreateCircle, mSpeed); + } + } + }; + + private Interpolator mInterpolator = new LinearInterpolator(); + + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + public WaveView(Context context) { + super(context); + } + + public WaveView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setStyle(Paint.Style style) { + mPaint.setStyle(style); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (!mMaxRadiusSet) { + mMaxRadius = Math.min(w, h) * mMaxRadiusRate / 2.0f; + } + } + + public void setMaxRadiusRate(float maxRadiusRate) { + mMaxRadiusRate = maxRadiusRate; + } + + public void setColor(int color) { + mPaint.setColor(color); + } + + /** + * 开始 + */ + public void start() { + if (!mIsRunning) { + mIsRunning = true; + mCreateCircle.run(); + } + } + + /** + * 缓慢停止 + */ + public void stop() { + mIsRunning = false; + } + + /** + * 立即停止 + */ + public void stopImmediately() { + mIsRunning = false; + mCircleList.clear(); + invalidate(); + } + + protected void onDraw(Canvas canvas) { + Iterator iterator = mCircleList.iterator(); + while (iterator.hasNext()) { + Circle circle = iterator.next(); + float radius = circle.getCurrentRadius(); + if (System.currentTimeMillis() - circle.mCreateTime < mDuration) { + mPaint.setAlpha(circle.getAlpha()); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint); + } else { + iterator.remove(); + } + } + if (mCircleList.size() > 0) { + postInvalidateDelayed(10); + } + } + + public void setInitialRadius(float radius) { + mInitialRadius = radius; + } + + public void setDuration(long duration) { + mDuration = duration; + } + + public void setMaxRadius(float maxRadius) { + mMaxRadius = maxRadius; + mMaxRadiusSet = true; + } + + public void setSpeed(int speed) { + mSpeed = speed; + } + + private void newCircle() { + long currentTime = System.currentTimeMillis(); + if (currentTime - mLastCreateTime < mSpeed) { + return; + } + Circle circle = new Circle(); + mCircleList.add(circle); + invalidate(); + mLastCreateTime = currentTime; + } + + private class Circle { + private long mCreateTime; + + Circle() { + mCreateTime = System.currentTimeMillis(); + } + + int getAlpha() { + float percent = (getCurrentRadius() - mInitialRadius) / (mMaxRadius - mInitialRadius); + return (int) (255 - mInterpolator.getInterpolation(percent) * 255); + } + + float getCurrentRadius() { + float percent = (System.currentTimeMillis() - mCreateTime) * 1.0f / mDuration; + return mInitialRadius + mInterpolator.getInterpolation(percent) * (mMaxRadius - mInitialRadius); + } + } + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + if (mInterpolator == null) { + mInterpolator = new LinearInterpolator(); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java b/widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java new file mode 100644 index 0000000..3fe637c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/badgeview/BadgeView.java @@ -0,0 +1,221 @@ +package com.hzecool.widget.badgeview; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.TabWidget; + +/** + * 自定义角标控件 + * Created by wangzg on 2017/4/12. + */ + +public class BadgeView extends AppCompatTextView { + + private boolean mHideOnNull = true; + + public BadgeView(Context context) { + this(context, null); + } + + public BadgeView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.textViewStyle); + } + + public BadgeView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(); + } + + private void init() { + if (!(getLayoutParams() instanceof LayoutParams)) { + LayoutParams layoutParams = + new LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.RIGHT | Gravity.TOP); + setLayoutParams(layoutParams); + } + + // set default font + setTextColor(Color.WHITE); + setTypeface(Typeface.DEFAULT_BOLD); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 11); + setPadding(dip2Px(5), dip2Px(1), dip2Px(5), dip2Px(1)); + + // set default background + setBackground(9, Color.parseColor("#d3321b")); + + setGravity(Gravity.CENTER); + + // default values + setHideOnNull(true); + setBadgeCount("0"); + } + + public void setBackground(int dipRadius, int badgeColor) { + int radius = dip2Px(dipRadius); + float[] radiusArray = new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; + + RoundRectShape roundRect = new RoundRectShape(radiusArray, null, null); + ShapeDrawable bgDrawable = new ShapeDrawable(roundRect); + bgDrawable.getPaint().setColor(badgeColor); + setBackground(bgDrawable); + } + + /** + * @return Returns true if view is hidden on badge value 0 or null; + */ + public boolean isHideOnNull() { + return mHideOnNull; + } + + /** + * @param hideOnNull the hideOnNull to set + */ + public void setHideOnNull(boolean hideOnNull) { + mHideOnNull = hideOnNull; + setText(getText()); + } + + /* + * (non-Javadoc) + * + * @see android.widget.TextView#setText(java.lang.CharSequence, android.widget.TextView.BufferType) + */ + @Override + public void setText(CharSequence text, BufferType type) { + if (isHideOnNull() && (text == null || text.toString().equalsIgnoreCase("0")) || text.toString().equalsIgnoreCase("0.0")) { + setVisibility(View.GONE); + } else { + setVisibility(View.VISIBLE); + } + super.setText(text, type); + } + + public void setBadgeCount(String count) { + setText(String.valueOf(count)); + } + + public Integer getBadgeCount() { + if (getText() == null) { + return null; + } + + String text = getText().toString(); + try { + return Integer.parseInt(text); + } catch (NumberFormatException e) { + return null; + } + } + + public void setBadgeGravity(int gravity) { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + params.gravity = gravity; + setLayoutParams(params); + } + + public int getBadgeGravity() { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + return params.gravity; + } + + public void setBadgeMargin(int dipMargin) { + setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin); + } + + public void setBadgeMargin(int leftDipMargin, int topDipMargin, int rightDipMargin, int bottomDipMargin) { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + params.leftMargin = dip2Px(leftDipMargin); + params.topMargin = dip2Px(topDipMargin); + params.rightMargin = dip2Px(rightDipMargin); + params.bottomMargin = dip2Px(bottomDipMargin); + setLayoutParams(params); + } + + public int[] getBadgeMargin() { + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + return new int[]{params.leftMargin, params.topMargin, params.rightMargin, params.bottomMargin}; + } + + public void incrementBadgeCount(String increment) { + Integer count = getBadgeCount(); + if (count == null) { + setBadgeCount(increment); + } else { + setBadgeCount(increment + count); + } + } + + /* + * Attach the BadgeView to the TabWidget + * + * @param target the TabWidget to attach the BadgeView + * + * @param tabIndex index of the tab + */ + public void setTargetView(TabWidget target, int tabIndex) { + View tabView = target.getChildTabViewAt(tabIndex); + setTargetView(tabView); + } + + /* + * Attach the BadgeView to the target view + * + * @param target the view to attach the BadgeView + */ + public void setTargetView(View target) { + if (getParent() != null) { + ((ViewGroup) getParent()).removeView(this); + } + + if (target == null) { + return; + } + + if (target.getParent() instanceof FrameLayout) { + ((FrameLayout) target.getParent()).addView(this); + + } else if (target.getParent() instanceof ViewGroup) { + // use a new Framelayout container for adding badge + ViewGroup parentContainer = (ViewGroup) target.getParent(); + int groupIndex = parentContainer.indexOfChild(target); + parentContainer.removeView(target); + + FrameLayout badgeContainer = new FrameLayout(getContext()); + ViewGroup.LayoutParams parentLayoutParams = target.getLayoutParams(); + + badgeContainer.setLayoutParams(parentLayoutParams); + target.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams); + badgeContainer.addView(target); + + badgeContainer.addView(this); + } else if (target.getParent() == null) { + Log.e(getClass().getSimpleName(), "ParentView is needed"); + } + + } + + /* + * converts dip to px + */ + private int dip2Px(float dip) { + return (int) (dip * getContext().getResources().getDisplayMetrics().density + 0.5f); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java b/widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java new file mode 100644 index 0000000..f697ef3 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/circleimageview/CircleImageView.java @@ -0,0 +1,440 @@ +/* + * Copyright 2014 - 2017 Henning Dodenhof + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hzecool.widget.circleimageview; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.hzecool.widget.R; + +public class CircleImageView extends android.support.v7.widget.AppCompatImageView { + + private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; + + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + private static final int COLORDRAWABLE_DIMENSION = 2; + + private static final int DEFAULT_BORDER_WIDTH = 0; + private static final int DEFAULT_BORDER_COLOR = Color.BLACK; + private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT; + private static final boolean DEFAULT_BORDER_OVERLAY = false; + + private final RectF mDrawableRect = new RectF(); + private final RectF mBorderRect = new RectF(); + + private final Matrix mShaderMatrix = new Matrix(); + private final Paint mBitmapPaint = new Paint(); + private final Paint mBorderPaint = new Paint(); + private final Paint mFillPaint = new Paint(); + + private int mBorderColor = DEFAULT_BORDER_COLOR; + private int mBorderWidth = DEFAULT_BORDER_WIDTH; + private int mFillColor = DEFAULT_FILL_COLOR; + + private Bitmap mBitmap; + private BitmapShader mBitmapShader; + private int mBitmapWidth; + private int mBitmapHeight; + + private float mDrawableRadius; + private float mBorderRadius; + + private ColorFilter mColorFilter; + + private boolean mReady; + private boolean mSetupPending; + private boolean mBorderOverlay; + private boolean mDisableCircularTransformation; + + public CircleImageView(Context context) { + super(context); + + init(); + } + + public CircleImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CircleImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0); + + mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH); + mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR); + mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY); + mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR); + + a.recycle(); + + init(); + } + + private void init() { + super.setScaleType(SCALE_TYPE); + mReady = true; + + if (mSetupPending) { + setup(); + mSetupPending = false; + } + } + + @Override + public ScaleType getScaleType() { + return SCALE_TYPE; + } + + @Override + public void setScaleType(ScaleType scaleType) { + if (scaleType != SCALE_TYPE) { + throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)); + } + } + + @Override + public void setAdjustViewBounds(boolean adjustViewBounds) { + if (adjustViewBounds) { + throw new IllegalArgumentException("adjustViewBounds not supported."); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDisableCircularTransformation) { + super.onDraw(canvas); + return; + } + + if (mBitmap == null) { + return; + } + + if (mFillColor != Color.TRANSPARENT) { + canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint); + } + canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint); + if (mBorderWidth > 0) { + canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + setup(); + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom); + setup(); + } + + @Override + public void setPaddingRelative(int start, int top, int end, int bottom) { + super.setPaddingRelative(start, top, end, bottom); + setup(); + } + + public int getBorderColor() { + return mBorderColor; + } + + public void setBorderColor(@ColorInt int borderColor) { + if (borderColor == mBorderColor) { + return; + } + + mBorderColor = borderColor; + mBorderPaint.setColor(mBorderColor); + invalidate(); + } + + /** + * @deprecated Use {@link #setBorderColor(int)} instead + */ + @Deprecated + public void setBorderColorResource(@ColorRes int borderColorRes) { + setBorderColor(getContext().getResources().getColor(borderColorRes)); + } + + /** + * Return the color drawn behind the circle-shaped drawable. + * + * @return The color drawn behind the drawable + * + * @deprecated Fill color support is going to be removed in the future + */ + @Deprecated + public int getFillColor() { + return mFillColor; + } + + /** + * Set a color to be drawn behind the circle-shaped drawable. Note that + * this has no effect if the drawable is opaque or no drawable is set. + * + * @param fillColor The color to be drawn behind the drawable + * + * @deprecated Fill color support is going to be removed in the future + */ + @Deprecated + public void setFillColor(@ColorInt int fillColor) { + if (fillColor == mFillColor) { + return; + } + + mFillColor = fillColor; + mFillPaint.setColor(fillColor); + invalidate(); + } + + /** + * Set a color to be drawn behind the circle-shaped drawable. Note that + * this has no effect if the drawable is opaque or no drawable is set. + * + * @param fillColorRes The color resource to be resolved to a color and + * drawn behind the drawable + * + * @deprecated Fill color support is going to be removed in the future + */ + @Deprecated + public void setFillColorResource(@ColorRes int fillColorRes) { + setFillColor(getContext().getResources().getColor(fillColorRes)); + } + + public int getBorderWidth() { + return mBorderWidth; + } + + public void setBorderWidth(int borderWidth) { + if (borderWidth == mBorderWidth) { + return; + } + + mBorderWidth = borderWidth; + setup(); + } + + public boolean isBorderOverlay() { + return mBorderOverlay; + } + + public void setBorderOverlay(boolean borderOverlay) { + if (borderOverlay == mBorderOverlay) { + return; + } + + mBorderOverlay = borderOverlay; + setup(); + } + + public boolean isDisableCircularTransformation() { + return mDisableCircularTransformation; + } + + public void setDisableCircularTransformation(boolean disableCircularTransformation) { + if (mDisableCircularTransformation == disableCircularTransformation) { + return; + } + + mDisableCircularTransformation = disableCircularTransformation; + initializeBitmap(); + } + + @Override + public void setImageBitmap(Bitmap bm) { + super.setImageBitmap(bm); + initializeBitmap(); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + initializeBitmap(); + } + + @Override + public void setImageResource(@DrawableRes int resId) { + super.setImageResource(resId); + initializeBitmap(); + } + + @Override + public void setImageURI(Uri uri) { + super.setImageURI(uri); + initializeBitmap(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + if (cf == mColorFilter) { + return; + } + + mColorFilter = cf; + applyColorFilter(); + invalidate(); + } + + @Override + public ColorFilter getColorFilter() { + return mColorFilter; + } + + private void applyColorFilter() { + if (mBitmapPaint != null) { + mBitmapPaint.setColorFilter(mColorFilter); + } + } + + private Bitmap getBitmapFromDrawable(Drawable drawable) { + if (drawable == null) { + return null; + } + + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + try { + Bitmap bitmap; + + if (drawable instanceof ColorDrawable) { + bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private void initializeBitmap() { + if (mDisableCircularTransformation) { + mBitmap = null; + } else { + mBitmap = getBitmapFromDrawable(getDrawable()); + } + setup(); + } + + private void setup() { + if (!mReady) { + mSetupPending = true; + return; + } + + if (getWidth() == 0 && getHeight() == 0) { + return; + } + + if (mBitmap == null) { + invalidate(); + return; + } + + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + + mBitmapPaint.setAntiAlias(true); + mBitmapPaint.setShader(mBitmapShader); + + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setAntiAlias(true); + mBorderPaint.setColor(mBorderColor); + mBorderPaint.setStrokeWidth(mBorderWidth); + + mFillPaint.setStyle(Paint.Style.FILL); + mFillPaint.setAntiAlias(true); + mFillPaint.setColor(mFillColor); + + mBitmapHeight = mBitmap.getHeight(); + mBitmapWidth = mBitmap.getWidth(); + + mBorderRect.set(calculateBounds()); + mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f); + + mDrawableRect.set(mBorderRect); + if (!mBorderOverlay && mBorderWidth > 0) { + mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f); + } + mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f); + + applyColorFilter(); + updateShaderMatrix(); + invalidate(); + } + + private RectF calculateBounds() { + int availableWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + + int sideLength = Math.min(availableWidth, availableHeight); + + float left = getPaddingLeft() + (availableWidth - sideLength) / 2f; + float top = getPaddingTop() + (availableHeight - sideLength) / 2f; + + return new RectF(left, top, left + sideLength, top + sideLength); + } + + private void updateShaderMatrix() { + float scale; + float dx = 0; + float dy = 0; + + mShaderMatrix.set(null); + + if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { + scale = mDrawableRect.height() / (float) mBitmapHeight; + dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; + } else { + scale = mDrawableRect.width() / (float) mBitmapWidth; + dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; + } + + mShaderMatrix.setScale(scale, scale); + mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top); + + mBitmapShader.setLocalMatrix(mShaderMatrix); + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java b/widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java new file mode 100644 index 0000000..90ebca9 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/colorpicker/ColorPickerView.java @@ -0,0 +1,187 @@ +package com.hzecool.widget.colorpicker; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.ImageView; + +import com.hzecool.widget.R; + +public class ColorPickerView extends ImageView { + Context context; + private Bitmap iconBitMap; + float iconRadius;// 吸管圆的半径 + float iconCenterX; + float iconCenterY; + PointF iconPoint;// 点击位置坐标 + + public ColorPickerView(Context context) { + this(context, null); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + this.context = context; + init(); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + init(); + } + + Paint mBitmapPaint; + Bitmap imageBitmap; + float viewRadius;// 整个view半径 + float radius;// 图片半径 + + /** + * 初始化画笔 + */ + private void init() { + iconBitMap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.pickup);// 吸管的图片 + iconRadius = iconBitMap.getWidth() / 2;// 吸管的图片一半 + + mBitmapPaint = new Paint(); + iconPoint = new PointF(); + + imageBitmap = ((BitmapDrawable) getDrawable()).getBitmap(); + radius = imageBitmap.getHeight() / 2;// 图片半径,180 + + // // 初始化 + iconPoint.x = radius; + iconPoint.y = radius; + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // TODO Auto-generated method stub + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + Canvas canvas; + + @Override + protected void onDraw(Canvas canvas) { + // TODO Auto-generated method stub + super.onDraw(canvas); + this.canvas = canvas; + + viewRadius = this.getWidth() / 2;// 整个view半径 + + canvas.drawBitmap(iconBitMap, iconPoint.x - iconRadius, iconPoint.y + - iconRadius, mBitmapPaint); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + int pixel; + int r; + int g; + int b; + switch (event.getAction()) { + case MotionEvent.ACTION_MOVE: + proofLeft(x, y); + pixel = getImagePixel(iconPoint.x, iconPoint.y); + r = Color.red(pixel); + g = Color.green(pixel); + b = Color.blue(pixel); + if (mChangedListener != null) { + mChangedListener.onMoveColor(r, g, b); + } + if (isMove) { + isMove = !isMove; + invalidate(); + } + break; + case MotionEvent.ACTION_UP: + pixel = getImagePixel(iconPoint.x, iconPoint.y); + r = Color.red(pixel); + g = Color.green(pixel); + b = Color.blue(pixel); + if (mChangedListener != null) { + mChangedListener.onColorChanged(r, g, b); + } + break; + + default: + break; + } + return true; + } + + public int getImagePixel(float x, float y) { + + Bitmap bitmap = imageBitmap; + // 为了防止越界 + int intX = (int) x; + int intY = (int) y; + if (intX < 0) + intX = 0; + if (intY < 0) + intY = 0; + if (intX >= bitmap.getWidth()) { + intX = bitmap.getWidth() - 1; + } + if (intY >= bitmap.getHeight()) { + intY = bitmap.getHeight() - 1; + } + int pixel = bitmap.getPixel(intX, intY); + return pixel; + + } + + /** + * R = sqrt(x * x + y * y)
+ * point.x = x * r / R + r
+ * point.y = y * r / R + r + */ + private void proofLeft(float x, float y) { + + float h = x - viewRadius; // 取xy点和圆点 的三角形宽 + float w = y - viewRadius;// 取xy点和圆点 的三角形长 + float h2 = h * h; + float w2 = w * w; + float distance = (float) Math.sqrt((h2 + w2)); // 勾股定理求 斜边距离 + if (distance > radius) { // 如果斜边距离大于半径,则取点和圆最近的一个点为x,y + float maxX = x - viewRadius; + float maxY = y - viewRadius; + x = ((radius * maxX) / distance) + viewRadius; // 通过三角形一边平行原理求出x,y + y = ((radius * maxY) / distance) + viewRadius; + } + iconPoint.x = x; + iconPoint.y = y; + + isMove = true; + } + + boolean isMove; + + public void setOnColorChangedListenner(OnColorChangedListener l) { + this.mChangedListener = l; + } + + private OnColorChangedListener mChangedListener; + + // 内部接口 回调颜色 rgb值 + public interface OnColorChangedListener { + // 手指抬起,确定颜色回调 + void onColorChanged(int r, int g, int b); + + // 移动时颜色回调 + void onMoveColor(int r, int g, int b); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java new file mode 100644 index 0000000..ab0a1a9 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelector.java @@ -0,0 +1,117 @@ +package com.hzecool.widget.imgselector; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; +import android.widget.Toast; + +import com.hzecool.widget.R; + +import java.util.ArrayList; + +/** + * 图片选择器 + * Created by nereo on 16/3/17. + */ +public class MultiImageSelector { + + public static final String EXTRA_RESULT = MultiImageSelectorActivity.EXTRA_RESULT; + + private boolean mShowCamera = true; + private int mMaxCount = 9; + private int mMode = MultiImageSelectorActivity.MODE_MULTI; + private ArrayList mOriginData; + private static MultiImageSelector sSelector; + + @Deprecated + private MultiImageSelector(Context context){ + + } + + private MultiImageSelector(){} + + @Deprecated + public static MultiImageSelector create(Context context){ + if(sSelector == null){ + sSelector = new MultiImageSelector(context); + } + return sSelector; + } + + public static MultiImageSelector create(){ + if(sSelector == null){ + sSelector = new MultiImageSelector(); + } + return sSelector; + } + + public MultiImageSelector showCamera(boolean show){ + mShowCamera = show; + return sSelector; + } + + public MultiImageSelector count(int count){ + mMaxCount = count; + return sSelector; + } + + public MultiImageSelector single(){ + mMode = MultiImageSelectorActivity.MODE_SINGLE; + return sSelector; + } + + public MultiImageSelector multi(){ + mMode = MultiImageSelectorActivity.MODE_MULTI; + return sSelector; + } + + public MultiImageSelector origin(ArrayList images){ + mOriginData = images; + return sSelector; + } + + public void start(Activity activity, int requestCode){ + final Context context = activity; + if(hasPermission(context)) { + activity.startActivityForResult(createIntent(context), requestCode); + }else{ + Toast.makeText(context, R.string.mis_error_no_permission, Toast.LENGTH_SHORT).show(); + } + } + + public void start(Fragment fragment, int requestCode){ + final Context context = fragment.getContext(); + if(hasPermission(context)) { + fragment.startActivityForResult(createIntent(context), requestCode); + }else{ + Toast.makeText(context, R.string.mis_error_no_permission, Toast.LENGTH_SHORT).show(); + } + } + + private boolean hasPermission(Context context){ + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){ + // Permission was added in API Level 16 + return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + private Intent createIntent(Context context){ + + Intent intent = new Intent(context, MultiImageSelectorActivity.class); + intent.putExtra(MultiImageSelectorActivity.EXTRA_SHOW_CAMERA, mShowCamera); + intent.putExtra(MultiImageSelectorActivity.EXTRA_SELECT_COUNT, mMaxCount); + if(mOriginData != null){ + intent.putStringArrayListExtra(MultiImageSelectorActivity.EXTRA_DEFAULT_SELECTED_LIST, mOriginData); + } + intent.putExtra(MultiImageSelectorActivity.EXTRA_SELECT_MODE, mMode); + + return intent; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java new file mode 100644 index 0000000..e52f714 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorActivity.java @@ -0,0 +1,182 @@ +package com.hzecool.widget.imgselector; + +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; + +import com.hzecool.widget.R; + +import java.io.File; +import java.util.ArrayList; + +/** + * Multi image selector + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/1/19. + * Updated by nereo on 2016/5/18. + */ +public class MultiImageSelectorActivity extends AppCompatActivity + implements MultiImageSelectorFragment.Callback{ + + // Single choice + public static final int MODE_SINGLE = 0; + // Multi choice + public static final int MODE_MULTI = 1; + + /** Max image size,int,{@link #DEFAULT_IMAGE_SIZE} by default */ + public static final String EXTRA_SELECT_COUNT = "max_select_count"; + /** Select mode,{@link #MODE_MULTI} by default */ + public static final String EXTRA_SELECT_MODE = "select_count_mode"; + /** Whether show camera,true by default */ + public static final String EXTRA_SHOW_CAMERA = "show_camera"; + /** Result data set,ArrayList<String>*/ + public static final String EXTRA_RESULT = "select_result"; + /** Original data set */ + public static final String EXTRA_DEFAULT_SELECTED_LIST = "default_list"; + // Default image size + private static final int DEFAULT_IMAGE_SIZE = 9; + + private ArrayList resultList = new ArrayList<>(); + private Button mSubmitButton; + private int mDefaultCount = DEFAULT_IMAGE_SIZE; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(R.style.MIS_NO_ACTIONBAR); + setContentView(R.layout.mis_activity_default); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().setStatusBarColor(Color.BLACK); + } + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + if(toolbar != null){ + setSupportActionBar(toolbar); + } + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + final Intent intent = getIntent(); + mDefaultCount = intent.getIntExtra(EXTRA_SELECT_COUNT, DEFAULT_IMAGE_SIZE); + final int mode = intent.getIntExtra(EXTRA_SELECT_MODE, MODE_MULTI); + final boolean isShow = intent.getBooleanExtra(EXTRA_SHOW_CAMERA, true); + if(mode == MODE_MULTI && intent.hasExtra(EXTRA_DEFAULT_SELECTED_LIST)) { + resultList = intent.getStringArrayListExtra(EXTRA_DEFAULT_SELECTED_LIST); + } + + mSubmitButton = (Button) findViewById(R.id.commit); + if(mode == MODE_MULTI){ + updateDoneText(resultList); + mSubmitButton.setVisibility(View.VISIBLE); + mSubmitButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if(resultList != null && resultList.size() >0){ + // Notify success + Intent data = new Intent(); + data.putStringArrayListExtra(EXTRA_RESULT, resultList); + setResult(RESULT_OK, data); + }else{ + setResult(RESULT_CANCELED); + } + finish(); + } + }); + }else{ + mSubmitButton.setVisibility(View.GONE); + } + + if(savedInstanceState == null){ + Bundle bundle = new Bundle(); + bundle.putInt(MultiImageSelectorFragment.EXTRA_SELECT_COUNT, mDefaultCount); + bundle.putInt(MultiImageSelectorFragment.EXTRA_SELECT_MODE, mode); + bundle.putBoolean(MultiImageSelectorFragment.EXTRA_SHOW_CAMERA, isShow); + bundle.putStringArrayList(MultiImageSelectorFragment.EXTRA_DEFAULT_SELECTED_LIST, resultList); + + getSupportFragmentManager().beginTransaction() + .add(R.id.image_grid, Fragment.instantiate(this, MultiImageSelectorFragment.class.getName(), bundle)) + .commit(); + } + + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + setResult(RESULT_CANCELED); + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * Update done button by select image data + * @param resultList selected image data + */ + private void updateDoneText(ArrayList resultList){ + int size = 0; + if(resultList == null || resultList.size()<=0){ + mSubmitButton.setText(R.string.mis_action_done); + mSubmitButton.setEnabled(false); + }else{ + size = resultList.size(); + mSubmitButton.setEnabled(true); + } + mSubmitButton.setText(getString(R.string.mis_action_button_string, + getString(R.string.mis_action_done), size, mDefaultCount)); + } + + @Override + public void onSingleImageSelected(String path) { + Intent data = new Intent(); + resultList.add(path); + data.putStringArrayListExtra(EXTRA_RESULT, resultList); + setResult(RESULT_OK, data); + finish(); + } + + @Override + public void onImageSelected(String path) { + if(!resultList.contains(path)) { + resultList.add(path); + } + updateDoneText(resultList); + } + + @Override + public void onImageUnselected(String path) { + if(resultList.contains(path)){ + resultList.remove(path); + } + updateDoneText(resultList); + } + + @Override + public void onCameraShot(File imageFile) { + if(imageFile != null) { + // notify system the image has change + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(imageFile))); + + Intent data = new Intent(); + resultList.add(imageFile.getAbsolutePath()); + data.putStringArrayListExtra(EXTRA_RESULT, resultList); + setResult(RESULT_OK, data); + finish(); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java new file mode 100644 index 0000000..d1b2381 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/MultiImageSelectorFragment.java @@ -0,0 +1,567 @@ +package com.hzecool.widget.imgselector; + +import android.Manifest; +import android.app.Activity; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.ListPopupWindow; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.TextView; +import android.widget.Toast; + +import com.hzecool.widget.R; +import com.hzecool.widget.imgselector.adapter.FolderAdapter; +import com.hzecool.widget.imgselector.adapter.ImageGridAdapter; +import com.hzecool.widget.imgselector.bean.Folder; +import com.hzecool.widget.imgselector.bean.Image; +import com.hzecool.widget.imgselector.utils.FileUtils; +import com.hzecool.widget.imgselector.utils.ScreenUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +/** + * Multi image selector Fragment + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/5/18. + */ +public class MultiImageSelectorFragment extends Fragment { + + public static final String TAG = "MultiImageSelectorFragment"; + + private static final int REQUEST_STORAGE_WRITE_ACCESS_PERMISSION = 110; + private static final int REQUEST_CAMERA = 100; + + private static final String KEY_TEMP_FILE = "key_temp_file"; + + // Single choice + public static final int MODE_SINGLE = 0; + // Multi choice + public static final int MODE_MULTI = 1; + + /** + * Max image size,int, + */ + public static final String EXTRA_SELECT_COUNT = "max_select_count"; + /** + * Select mode,{@link #MODE_MULTI} by default + */ + public static final String EXTRA_SELECT_MODE = "select_count_mode"; + /** + * Whether show camera,true by default + */ + public static final String EXTRA_SHOW_CAMERA = "show_camera"; + /** + * Original data set + */ + public static final String EXTRA_DEFAULT_SELECTED_LIST = "default_list"; + + // loaders + private static final int LOADER_ALL = 0; + private static final int LOADER_CATEGORY = 1; + + // image result data set + private ArrayList resultList = new ArrayList<>(); + // folder result data set + private ArrayList mResultFolder = new ArrayList<>(); + + private GridView mGridView; + private Callback mCallback; + + private ImageGridAdapter mImageAdapter; + private FolderAdapter mFolderAdapter; + + private ListPopupWindow mFolderPopupWindow; + + private TextView mCategoryText; + private View mPopupAnchorView; + + private boolean hasFolderGened = false; + + private File mTmpFile; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + mCallback = (Callback) getActivity(); + } catch (ClassCastException e) { + throw new ClassCastException("The Activity must implement MultiImageSelectorFragment.Callback interface..."); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.mis_fragment_multi_image, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final int mode = selectMode(); + if (mode == MODE_MULTI) { + ArrayList tmp = getArguments().getStringArrayList(EXTRA_DEFAULT_SELECTED_LIST); + if (tmp != null && tmp.size() > 0) { + resultList = tmp; + } + } + mImageAdapter = new ImageGridAdapter(getActivity(), showCamera(), 3); + mImageAdapter.showSelectIndicator(mode == MODE_MULTI); + + mPopupAnchorView = view.findViewById(R.id.footer); + + mCategoryText = (TextView) view.findViewById(R.id.category_btn); + mCategoryText.setText(R.string.mis_folder_all); + mCategoryText.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + if (mFolderPopupWindow == null) { + createPopupFolderList(); + } + + if (mFolderPopupWindow.isShowing()) { + mFolderPopupWindow.dismiss(); + } else { + mFolderPopupWindow.show(); + int index = mFolderAdapter.getSelectIndex(); + index = index == 0 ? index : index - 1; + mFolderPopupWindow.getListView().setSelection(index); + } + } + }); + + mGridView = (GridView) view.findViewById(R.id.grid); + mGridView.setAdapter(mImageAdapter); + mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + if (mImageAdapter.isShowCamera()) { + if (i == 0) { + showCameraAction(); + } else { + Image image = (Image) adapterView.getAdapter().getItem(i); + selectImageFromGrid(image, mode); + } + } else { + Image image = (Image) adapterView.getAdapter().getItem(i); + selectImageFromGrid(image, mode); + } + } + }); + mGridView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { +// if (scrollState == SCROLL_STATE_FLING) { +// Glide.with(view.getContext()); +// } else { +// Glide.with(view.getContext()); +// } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + + } + }); + + mFolderAdapter = new FolderAdapter(getActivity()); + } + + /** + * Create popup ListView + */ + private void createPopupFolderList() { + Point point = ScreenUtils.getScreenSize(getActivity()); + int width = point.x; + int height = (int) (point.y * (4.5f / 8.0f)); + mFolderPopupWindow = new ListPopupWindow(getActivity()); + mFolderPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.WHITE)); + mFolderPopupWindow.setAdapter(mFolderAdapter); + mFolderPopupWindow.setContentWidth(width); + mFolderPopupWindow.setWidth(width); + mFolderPopupWindow.setHeight(height); + mFolderPopupWindow.setAnchorView(mPopupAnchorView); + mFolderPopupWindow.setModal(true); + mFolderPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + + mFolderAdapter.setSelectIndex(i); + + final int index = i; + final AdapterView v = adapterView; + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + mFolderPopupWindow.dismiss(); + + if (index == 0) { + getActivity().getSupportLoaderManager().restartLoader(LOADER_ALL, null, mLoaderCallback); + mCategoryText.setText(R.string.mis_folder_all); + if (showCamera()) { + mImageAdapter.setShowCamera(true); + } else { + mImageAdapter.setShowCamera(false); + } + } else { + Folder folder = (Folder) v.getAdapter().getItem(index); + if (null != folder) { + mImageAdapter.setData(folder.images); + mCategoryText.setText(folder.name); + if (resultList != null && resultList.size() > 0) { + mImageAdapter.setDefaultSelected(resultList); + } + } + mImageAdapter.setShowCamera(false); + } + + mGridView.smoothScrollToPosition(0); + } + }, 100); + + } + }); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(KEY_TEMP_FILE, mTmpFile); + } + + @Override + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + if (savedInstanceState != null) { + mTmpFile = (File) savedInstanceState.getSerializable(KEY_TEMP_FILE); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // load image data + getActivity().getSupportLoaderManager().initLoader(LOADER_ALL, null, mLoaderCallback); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CAMERA) { + if (resultCode == Activity.RESULT_OK) { + if (mTmpFile != null) { + if (mCallback != null) { + mCallback.onCameraShot(mTmpFile); + } + } + } else { + // delete tmp file + while (mTmpFile != null && mTmpFile.exists()) { + boolean success = mTmpFile.delete(); + if (success) { + mTmpFile = null; + } + } + } + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + if (mFolderPopupWindow != null) { + if (mFolderPopupWindow.isShowing()) { + mFolderPopupWindow.dismiss(); + } + } + super.onConfigurationChanged(newConfig); + } + + /** + * Open camera + */ + private void showCameraAction() { + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, + getString(R.string.mis_permission_rationale_write_storage), + REQUEST_STORAGE_WRITE_ACCESS_PERMISSION); + } else { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + try { + mTmpFile = FileUtils.createTmpFile(getActivity()); + } catch (IOException e) { + e.printStackTrace(); + } + if (mTmpFile != null && mTmpFile.exists()) { +// intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTmpFile)); +// startActivityForResult(intent, REQUEST_CAMERA); + + int currentapiVersion = android.os.Build.VERSION.SDK_INT; + if (currentapiVersion<24){ + intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mTmpFile)); + startActivityForResult(intent, REQUEST_CAMERA); + }else { + ContentValues contentValues = new ContentValues(1); + contentValues.put(MediaStore.Images.Media.DATA, mTmpFile.getAbsolutePath()); + Uri uri = getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + startActivityForResult(intent, REQUEST_CAMERA); + } + + } else { + Toast.makeText(getActivity(), R.string.mis_error_image_not_exist, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity(), R.string.mis_msg_no_camera, Toast.LENGTH_SHORT).show(); + } + } + } + + + private void doTakePhoto() { + try { + ContentValues values = new ContentValues(1); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpg"); + Uri mCameraTempUri = getActivity().getContentResolver() + .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + + takePhoto(this, REQUEST_CAMERA, mCameraTempUri); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void takePhoto(Fragment fragment, int token, Uri uri) { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + if (uri != null) { + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1); + } + fragment.startActivityForResult(intent, token); + } + + private void requestPermission(final String permission, String rationale, final int requestCode) { + if (shouldShowRequestPermissionRationale(permission)) { + new AlertDialog.Builder(getContext()) + .setTitle(R.string.mis_permission_dialog_title) + .setMessage(rationale) + .setPositiveButton(R.string.mis_permission_dialog_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + requestPermissions(new String[]{permission}, requestCode); + } + }) + .setNegativeButton(R.string.mis_permission_dialog_cancel, null) + .create().show(); + } else { + requestPermissions(new String[]{permission}, requestCode); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == REQUEST_STORAGE_WRITE_ACCESS_PERMISSION) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + showCameraAction(); + } + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + /** + * notify callback + * + * @param image image data + */ + private void selectImageFromGrid(Image image, int mode) { + if (image != null) { + if (mode == MODE_MULTI) { + if (resultList.contains(image.path)) { + resultList.remove(image.path); + if (mCallback != null) { + mCallback.onImageUnselected(image.path); + } + } else { + if (selectImageCount() == resultList.size()) { + Toast.makeText(getActivity(), R.string.mis_msg_amount_limit, Toast.LENGTH_SHORT).show(); + return; + } + resultList.add(image.path); + if (mCallback != null) { + mCallback.onImageSelected(image.path); + } + } + mImageAdapter.select(image); + } else if (mode == MODE_SINGLE) { + if (mCallback != null) { + mCallback.onSingleImageSelected(image.path); + } + } + } + } + + private LoaderManager.LoaderCallbacks mLoaderCallback = new LoaderManager.LoaderCallbacks() { + + private final String[] IMAGE_PROJECTION = { + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATE_ADDED, + MediaStore.Images.Media.MIME_TYPE, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media._ID}; + + @Override + public Loader onCreateLoader(int id, Bundle args) { + CursorLoader cursorLoader = null; + if (id == LOADER_ALL) { + cursorLoader = new CursorLoader(getActivity(), + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, + IMAGE_PROJECTION[4] + ">0 AND " + IMAGE_PROJECTION[3] + "=? OR " + IMAGE_PROJECTION[3] + "=? ", + new String[]{"image/jpeg", "image/png"}, IMAGE_PROJECTION[2] + " DESC"); + } else if (id == LOADER_CATEGORY) { + cursorLoader = new CursorLoader(getActivity(), + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, IMAGE_PROJECTION, + IMAGE_PROJECTION[4] + ">0 AND " + IMAGE_PROJECTION[0] + " like '%" + args.getString("path") + "%'", + null, IMAGE_PROJECTION[2] + " DESC"); + } + return cursorLoader; + } + + private boolean fileExist(String path) { + if (!TextUtils.isEmpty(path)) { + return new File(path).exists(); + } + return false; + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (data != null) { + if (data.getCount() > 0) { + List images = new ArrayList<>(); + data.moveToFirst(); + do { + String path = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[0])); + String name = data.getString(data.getColumnIndexOrThrow(IMAGE_PROJECTION[1])); + long dateTime = data.getLong(data.getColumnIndexOrThrow(IMAGE_PROJECTION[2])); + if (!fileExist(path)) { + continue; + } + Image image = null; + if (!TextUtils.isEmpty(name)) { + image = new Image(path, name, dateTime); + images.add(image); + } + if (!hasFolderGened) { + // get all folder data + File folderFile = new File(path).getParentFile(); + if (folderFile != null && folderFile.exists()) { + String fp = folderFile.getAbsolutePath(); + Folder f = getFolderByPath(fp); + if (f == null) { + Folder folder = new Folder(); + folder.name = folderFile.getName(); + folder.path = fp; + folder.cover = image; + List imageList = new ArrayList<>(); + imageList.add(image); + folder.images = imageList; + mResultFolder.add(folder); + } else { + f.images.add(image); + } + } + } + + } while (data.moveToNext()); + + mImageAdapter.setData(images); + if (resultList != null && resultList.size() > 0) { + mImageAdapter.setDefaultSelected(resultList); + } + if (!hasFolderGened) { + mFolderAdapter.setData(mResultFolder); + hasFolderGened = true; + } + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + + } + }; + + private Folder getFolderByPath(String path) { + if (mResultFolder != null) { + for (Folder folder : mResultFolder) { + if (TextUtils.equals(folder.path, path)) { + return folder; + } + } + } + return null; + } + + private boolean showCamera() { + return getArguments() == null || getArguments().getBoolean(EXTRA_SHOW_CAMERA, true); + } + + private int selectMode() { + return getArguments() == null ? MODE_MULTI : getArguments().getInt(EXTRA_SELECT_MODE); + } + + private int selectImageCount() { + return getArguments() == null ? 9 : getArguments().getInt(EXTRA_SELECT_COUNT); + } + + /** + * Callback for host activity + */ + public interface Callback { + void onSingleImageSelected(String path); + + void onImageSelected(String path); + + void onImageUnselected(String path); + + void onCameraShot(File imageFile); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java new file mode 100644 index 0000000..142e027 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/FolderAdapter.java @@ -0,0 +1,178 @@ +package com.hzecool.widget.imgselector.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.hzecool.widget.R; +import com.hzecool.widget.imgselector.bean.Folder; +import com.hzecool.widget.utils.GlideSetting; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + + +/** + * 文件夹Adapter + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/1/19. + */ +public class FolderAdapter extends BaseAdapter { + + private Context mContext; + private LayoutInflater mInflater; + + private List mFolders = new ArrayList<>(); + + int mImageSize; + + int lastSelected = 0; + + public FolderAdapter(Context context) { + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mImageSize = mContext.getResources().getDimensionPixelOffset(R.dimen.mis_folder_cover_size); + } + + /** + * 设置数据集 + * + * @param folders + */ + public void setData(List folders) { + if (folders != null && folders.size() > 0) { + mFolders = folders; + } else { + mFolders.clear(); + } + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mFolders.size() + 1; + } + + @Override + public Folder getItem(int i) { + if (i == 0) return null; + return mFolders.get(i - 1); + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + ViewHolder holder; + if (view == null) { + view = mInflater.inflate(R.layout.mis_list_item_folder, viewGroup, false); + holder = new ViewHolder(view); + } else { + holder = (ViewHolder) view.getTag(); + } + if (holder != null) { + if (i == 0) { + holder.name.setText(R.string.mis_folder_all); + holder.path.setText("/sdcard"); + holder.size.setText(String.format("%d%s", + getTotalImageSize(), mContext.getResources().getString(R.string.mis_photo_unit))); + if (mFolders.size() > 0) { + Folder f = mFolders.get(0); + if (f != null) { + + Glide.with(mContext) + .load(new File(f.cover.path)) + .apply(GlideSetting.getGlideSetting() + .error(R.mipmap.mis_default_error) + .override(R.dimen.mis_folder_cover_size, R.dimen.mis_folder_cover_size) + .centerCrop() + ) + .into(holder.cover); + } else { + holder.cover.setImageResource(R.mipmap.mis_default_error); + } + } + } else { + holder.bindData(getItem(i)); + } + if (lastSelected == i) { + holder.indicator.setVisibility(View.VISIBLE); + } else { + holder.indicator.setVisibility(View.INVISIBLE); + } + } + return view; + } + + private int getTotalImageSize() { + int result = 0; + if (mFolders != null && mFolders.size() > 0) { + for (Folder f : mFolders) { + result += f.images.size(); + } + } + return result; + } + + public void setSelectIndex(int i) { + if (lastSelected == i) return; + + lastSelected = i; + notifyDataSetChanged(); + } + + public int getSelectIndex() { + return lastSelected; + } + + class ViewHolder { + ImageView cover; + TextView name; + TextView path; + TextView size; + ImageView indicator; + + ViewHolder(View view) { + cover = (ImageView) view.findViewById(R.id.cover); + name = (TextView) view.findViewById(R.id.name); + path = (TextView) view.findViewById(R.id.path); + size = (TextView) view.findViewById(R.id.size); + indicator = (ImageView) view.findViewById(R.id.indicator); + view.setTag(this); + } + + void bindData(Folder data) { + if (data == null) { + return; + } + name.setText(data.name); + path.setText(data.path); + if (data.images != null) { + size.setText(String.format("%d%s", data.images.size(), mContext.getResources().getString(R.string.mis_photo_unit))); + } else { + size.setText("*" + mContext.getResources().getString(R.string.mis_photo_unit)); + } + if (data.cover != null) { + Glide.with(mContext) + .load(new File(data.cover.path)) + .apply(GlideSetting.getGlideSetting() + .placeholder(R.mipmap.mis_default_error) + .override(R.dimen.mis_folder_cover_size, R.dimen.mis_folder_cover_size) + .centerCrop()) + .into(cover); + } else { + cover.setImageResource(R.mipmap.mis_default_error); + } + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java new file mode 100644 index 0000000..9303e0c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/adapter/ImageGridAdapter.java @@ -0,0 +1,243 @@ +package com.hzecool.widget.imgselector.adapter; + +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.BaseAdapter; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.hzecool.widget.R; +import com.hzecool.widget.imgselector.bean.Image; +import com.hzecool.widget.utils.GlideSetting; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + + +/** + * 图片Adapter + * Created by Nereo on 2015/4/7. + * Updated by nereo on 2016/1/19. + */ +public class ImageGridAdapter extends BaseAdapter { + + private static final int TYPE_CAMERA = 0; + private static final int TYPE_NORMAL = 1; + + private Context mContext; + + private LayoutInflater mInflater; + private boolean showCamera = true; + private boolean showSelectIndicator = true; + + private List mImages = new ArrayList<>(); + private List mSelectedImages = new ArrayList<>(); + + final int mGridWidth; + + public ImageGridAdapter(Context context, boolean showCamera, int column) { + mContext = context; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this.showCamera = showCamera; + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + int width = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + Point size = new Point(); + wm.getDefaultDisplay().getSize(size); + width = size.x; + } else { + width = wm.getDefaultDisplay().getWidth(); + } + mGridWidth = width / column; + } + + /** + * 显示选择指示器 + * + * @param b + */ + public void showSelectIndicator(boolean b) { + showSelectIndicator = b; + } + + public void setShowCamera(boolean b) { + if (showCamera == b) return; + + showCamera = b; + notifyDataSetChanged(); + } + + public boolean isShowCamera() { + return showCamera; + } + + /** + * 选择某个图片,改变选择状态 + * + * @param image + */ + public void select(Image image) { + if (mSelectedImages.contains(image)) { + mSelectedImages.remove(image); + } else { + mSelectedImages.add(image); + } + notifyDataSetChanged(); + } + + /** + * 通过图片路径设置默认选择 + * + * @param resultList + */ + public void setDefaultSelected(ArrayList resultList) { + for (String path : resultList) { + Image image = getImageByPath(path); + if (image != null) { + mSelectedImages.add(image); + } + } + if (mSelectedImages.size() > 0) { + notifyDataSetChanged(); + } + } + + private Image getImageByPath(String path) { + if (mImages != null && mImages.size() > 0) { + for (Image image : mImages) { + if (image.path.equalsIgnoreCase(path)) { + return image; + } + } + } + return null; + } + + /** + * 设置数据集 + * + * @param images + */ + public void setData(List images) { + mSelectedImages.clear(); + + if (images != null && images.size() > 0) { + mImages = images; + } else { + mImages.clear(); + } + notifyDataSetChanged(); + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @Override + public int getItemViewType(int position) { + if (showCamera) { + return position == 0 ? TYPE_CAMERA : TYPE_NORMAL; + } + return TYPE_NORMAL; + } + + @Override + public int getCount() { + return showCamera ? mImages.size() + 1 : mImages.size(); + } + + @Override + public Image getItem(int i) { + if (showCamera) { + if (i == 0) { + return null; + } + return mImages.get(i - 1); + } else { + return mImages.get(i); + } + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + + if (isShowCamera()) { + if (i == 0) { + view = mInflater.inflate(R.layout.mis_list_item_camera, viewGroup, false); + return view; + } + } + + ViewHolder holder; + if (view == null) { + view = mInflater.inflate(R.layout.mis_list_item_image, viewGroup, false); + holder = new ViewHolder(view); + } else { + holder = (ViewHolder) view.getTag(); + } + + if (holder != null) { + holder.bindData(getItem(i)); + } + + return view; + } + + class ViewHolder { + ImageView image; + ImageView indicator; + View mask; + + ViewHolder(View view) { + image = (ImageView) view.findViewById(R.id.image); + indicator = (ImageView) view.findViewById(R.id.checkmark); + mask = view.findViewById(R.id.mask); + view.setTag(this); + } + + void bindData(final Image data) { + if (data == null) return; + // 处理单选和多选状态 + if (showSelectIndicator) { + indicator.setVisibility(View.VISIBLE); + if (mSelectedImages.contains(data)) { + // 设置选中状态 + indicator.setImageResource(R.mipmap.mis_btn_selected); + mask.setVisibility(View.VISIBLE); + } else { + // 未选择 + indicator.setImageResource(R.mipmap.mis_btn_unselected); + mask.setVisibility(View.GONE); + } + } else { + indicator.setVisibility(View.GONE); + } + File imageFile = new File(data.path); + if (imageFile.exists()) { + + Glide.with(mContext) + .load(imageFile) + .apply(GlideSetting.getGlideSetting() + .placeholder(R.mipmap.mis_default_error) + .override(mGridWidth, mGridWidth) + .centerCrop()) + .into(image); + } else { + image.setImageResource(R.mipmap.mis_default_error); + } + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java new file mode 100644 index 0000000..3981711 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Folder.java @@ -0,0 +1,27 @@ +package com.hzecool.widget.imgselector.bean; + +import android.text.TextUtils; + +import java.util.List; + +/** + * 文件夹 + * Created by Nereo on 2015/4/7. + */ +public class Folder { + public String name; + public String path; + public Image cover; + public List images; + + @Override + public boolean equals(Object o) { + try { + Folder other = (Folder) o; + return TextUtils.equals(other.path, path); + }catch (ClassCastException e){ + e.printStackTrace(); + } + return super.equals(o); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java new file mode 100644 index 0000000..0fd1bbc --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/bean/Image.java @@ -0,0 +1,30 @@ +package com.hzecool.widget.imgselector.bean; + +import android.text.TextUtils; + +/** + * 图片实体 + * Created by Nereo on 2015/4/7. + */ +public class Image { + public String path; + public String name; + public long time; + + public Image(String path, String name, long time){ + this.path = path; + this.name = name; + this.time = time; + } + + @Override + public boolean equals(Object o) { + try { + Image other = (Image) o; + return TextUtils.equals(this.path, other.path); + }catch (ClassCastException e){ + e.printStackTrace(); + } + return super.equals(o); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java b/widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java new file mode 100644 index 0000000..57c0885 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/utils/FileUtils.java @@ -0,0 +1,129 @@ +package com.hzecool.widget.imgselector.utils; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.text.TextUtils; + +import java.io.File; +import java.io.IOException; + +import static android.os.Environment.MEDIA_MOUNTED; + +/** + * 文件操作类 + * Created by Nereo on 2015/4/8. + */ +public class FileUtils { + + private static final String JPEG_FILE_PREFIX = "IMG_"; + private static final String JPEG_FILE_SUFFIX = ".jpg"; + + public static File createTmpFile(Context context) throws IOException{ + File dir = null; + if(TextUtils.equals(Environment.getExternalStorageState(), Environment.MEDIA_MOUNTED)) { + dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + if (!dir.exists()) { + dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM + "/Camera"); + if (!dir.exists()) { + dir = getCacheDirectory(context, true); + } + } + }else{ + dir = getCacheDirectory(context, true); + } + return File.createTempFile(JPEG_FILE_PREFIX, JPEG_FILE_SUFFIX, dir); + } + + + private static final String EXTERNAL_STORAGE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE"; + + /** + * Returns application cache directory. Cache directory will be created on SD card + * ("/Android/data/[app_package_name]/cache") if card is mounted and app has appropriate permission. Else - + * Android defines cache directory on device's file system. + * + * @param context Application context + * @return Cache {@link File directory}.
+ * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and + * {@link Context#getCacheDir() Context.getCacheDir()} returns null). + */ + public static File getCacheDirectory(Context context) { + return getCacheDirectory(context, true); + } + + /** + * Returns application cache directory. Cache directory will be created on SD card + * ("/Android/data/[app_package_name]/cache") (if card is mounted and app has appropriate permission) or + * on device's file system depending incoming parameters. + * + * @param context Application context + * @param preferExternal Whether prefer external location for cache + * @return Cache {@link File directory}.
+ * NOTE: Can be null in some unpredictable cases (if SD card is unmounted and + * {@link Context#getCacheDir() Context.getCacheDir()} returns null). + */ + public static File getCacheDirectory(Context context, boolean preferExternal) { + File appCacheDir = null; + String externalStorageState; + try { + externalStorageState = Environment.getExternalStorageState(); + } catch (NullPointerException e) { // (sh)it happens (Issue #660) + externalStorageState = ""; + } catch (IncompatibleClassChangeError e) { // (sh)it happens too (Issue #989) + externalStorageState = ""; + } + if (preferExternal && MEDIA_MOUNTED.equals(externalStorageState) && hasExternalStoragePermission(context)) { + appCacheDir = getExternalCacheDir(context); + } + if (appCacheDir == null) { + appCacheDir = context.getCacheDir(); + } + if (appCacheDir == null) { + String cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/"; + appCacheDir = new File(cacheDirPath); + } + return appCacheDir; + } + + /** + * Returns individual application cache directory (for only image caching from ImageLoader). Cache directory will be + * created on SD card ("/Android/data/[app_package_name]/cache/uil-images") if card is mounted and app has + * appropriate permission. Else - Android defines cache directory on device's file system. + * + * @param context Application context + * @param cacheDir Cache directory path (e.g.: "AppCacheDir", "AppDir/cache/images") + * @return Cache {@link File directory} + */ + public static File getIndividualCacheDirectory(Context context, String cacheDir) { + File appCacheDir = getCacheDirectory(context); + File individualCacheDir = new File(appCacheDir, cacheDir); + if (!individualCacheDir.exists()) { + if (!individualCacheDir.mkdir()) { + individualCacheDir = appCacheDir; + } + } + return individualCacheDir; + } + + private static File getExternalCacheDir(Context context) { + File dataDir = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"); + File appCacheDir = new File(new File(dataDir, context.getPackageName()), "cache"); + if (!appCacheDir.exists()) { + if (!appCacheDir.mkdirs()) { + return null; + } + try { + new File(appCacheDir, ".nomedia").createNewFile(); + } catch (IOException e) { + } + } + return appCacheDir; + } + + private static boolean hasExternalStoragePermission(Context context) { + int perm = context.checkCallingOrSelfPermission(EXTERNAL_STORAGE_PERMISSION); + return perm == PackageManager.PERMISSION_GRANTED; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java b/widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java new file mode 100644 index 0000000..d8484ee --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/utils/ScreenUtils.java @@ -0,0 +1,29 @@ +package com.hzecool.widget.imgselector.utils; + +import android.content.Context; +import android.graphics.Point; +import android.os.Build; +import android.view.Display; +import android.view.WindowManager; + +/** + * 屏幕工具 + * Created by nereo on 15/11/19. + * Updated by nereo on 2016/1/19. + */ +public class ScreenUtils { + + public static Point getScreenSize(Context context){ + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point out = new Point(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + display.getSize(out); + }else{ + int width = display.getWidth(); + int height = display.getHeight(); + out.set(width, height); + } + return out; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java b/widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java new file mode 100644 index 0000000..99c9e2c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/utils/TimeUtils.java @@ -0,0 +1,31 @@ +package com.hzecool.widget.imgselector.utils; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * 时间处理工具 + * Created by Nereo on 2015/4/8. + */ +public class TimeUtils { + + public static String timeFormat(long timeMillis, String pattern){ + SimpleDateFormat format = new SimpleDateFormat(pattern, Locale.CHINA); + return format.format(new Date(timeMillis)); + } + + public static String formatPhotoDate(long time){ + return timeFormat(time, "yyyy-MM-dd"); + } + + public static String formatPhotoDate(String path){ + File file = new File(path); + if(file.exists()){ + long time = file.lastModified(); + return formatPhotoDate(time); + } + return "1970-01-01"; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java new file mode 100644 index 0000000..7d0e43d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquareFrameLayout.java @@ -0,0 +1,24 @@ +package com.hzecool.widget.imgselector.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * Created by nereo on 15/11/10. + */ +public class SquareFrameLayout extends FrameLayout{ + public SquareFrameLayout(Context context) { + super(context); + } + + public SquareFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java new file mode 100644 index 0000000..96a8840 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/imgselector/view/SquaredImageView.java @@ -0,0 +1,24 @@ +package com.hzecool.widget.imgselector.view; + +import android.content.Context; +import android.support.v7.widget.AppCompatImageView; +import android.util.AttributeSet; + +/** + * An image view which always remains square with respect to its width. + */ +public class SquaredImageView extends AppCompatImageView { + public SquaredImageView(Context context) { + super(context); + } + + public SquaredImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java new file mode 100644 index 0000000..695ee79 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayout.java @@ -0,0 +1,685 @@ +package com.hzecool.widget.loadingLayout; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.hzecool.widget.R; + + +/** + * create by tutu 2017/1/11 + * 全局加载底层布局 + * 提供加载成功,加载失败,网络连接失败,加载中四种状态 + */ +public class LoadingLayout extends FrameLayout { + + public final static int Success = 0; + public final static int Empty = 1; + public final static int Error = 2; + public final static int No_Network = 3; + public final static int Loading = 4; + private int state; + + private Context mContext; + private View loadingPage; + private View errorPage; + private View emptyPage; + private View networkPage; + private View defineLoadingPage; + + private ImageView errorImg; + private ImageView emptyImg; + private ImageView networkImg; + + private TextView errorText; + private TextView emptyText; + private TextView networkText; + + private TextView errorReloadBtn; + private TextView networkReloadBtn; + + private View contentView; + private OnReloadListener listener; + private boolean isFirstVisible; //是否一开始显示contentview,默认不显示 + + //配置 + private static Config mConfig = new Config(); + private static String emptyStr = "暂无数据"; + private static String errorStr = "加载失败,请稍后重试···"; + private static String netwrokStr = "无网络连接,请检查网络···"; + private static String reloadBtnStr = "点击重试"; + private static int emptyImgId = R.mipmap.empty; + private static int errorImgId = R.mipmap.error; + private static int networkImgId = R.mipmap.no_network; + private static int reloadBtnId = R.drawable.selector_btn_back_gray; + private static int tipTextSize = 14; + private static int buttonTextSize = 14; + private static int tipTextColor = R.color.black; + private static int buttonTextColor = R.color.black; + private static int buttonWidth = -1; + private static int buttonHeight = -1; + private static int loadingLayoutId = R.layout.widget_loading_page; + private static int backgroundColor = R.color.loading_background; + + public LoadingLayout(Context context, AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LoadingLayout); + isFirstVisible = a.getBoolean(R.styleable.LoadingLayout_isFirstVisible, false); + a.recycle(); + } + + public LoadingLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.mContext = context; + } + + public LoadingLayout(Context context) { + super(context); + this.mContext = context; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + if (getChildCount() > 1) { + throw new IllegalStateException("LoadingLayout can host only one direct child"); + } + contentView = this.getChildAt(0); + if (!isFirstVisible) { + contentView.setVisibility(View.GONE); + } + build(); + } + + private void build() { + + loadingPage = LayoutInflater.from(mContext).inflate(loadingLayoutId, null); + errorPage = LayoutInflater.from(mContext).inflate(R.layout.widget_error_page, null); + emptyPage = LayoutInflater.from(mContext).inflate(R.layout.widget_empty_page, null); + networkPage = LayoutInflater.from(mContext).inflate(R.layout.widget_nonetwork_page, null); + defineLoadingPage = null; + loadingPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + errorPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + emptyPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + networkPage.setBackgroundColor(LoadingLayoutUtils.getColor(mContext, backgroundColor)); + + errorText = LoadingLayoutUtils.findViewById(errorPage, R.id.error_text); + emptyText = LoadingLayoutUtils.findViewById(emptyPage, R.id.empty_text); + networkText = LoadingLayoutUtils.findViewById(networkPage, R.id.no_network_text); + + errorImg = LoadingLayoutUtils.findViewById(errorPage, R.id.error_img); + emptyImg = LoadingLayoutUtils.findViewById(emptyPage, R.id.empty_img); + networkImg = LoadingLayoutUtils.findViewById(networkPage, R.id.no_network_img); + + errorReloadBtn = LoadingLayoutUtils.findViewById(errorPage, R.id.error_reload_btn); + networkReloadBtn = LoadingLayoutUtils.findViewById(networkPage, R.id.no_network_reload_btn); + + errorReloadBtn.setOnClickListener(v -> { + + if (listener != null) { + listener.onReload(v); + } + }); + networkReloadBtn.setOnClickListener(v -> { + + if (listener != null) { + listener.onReload(v); + } + }); + + errorText.setText(errorStr); + emptyText.setText(emptyStr); + networkText.setText(netwrokStr); + + errorText.setTextSize(tipTextSize); + emptyText.setTextSize(tipTextSize); + networkText.setTextSize(tipTextSize); + + errorText.setTextColor(LoadingLayoutUtils.getColor(mContext, tipTextColor)); + emptyText.setTextColor(LoadingLayoutUtils.getColor(mContext, tipTextColor)); + networkText.setTextColor(LoadingLayoutUtils.getColor(mContext, tipTextColor)); + + errorImg.setImageResource(errorImgId); + emptyImg.setImageResource(emptyImgId); + networkImg.setImageResource(networkImgId); + + + errorReloadBtn.setBackgroundResource(reloadBtnId); + networkReloadBtn.setBackgroundResource(reloadBtnId); + + errorReloadBtn.setText(reloadBtnStr); + networkReloadBtn.setText(reloadBtnStr); + + errorReloadBtn.setTextSize(buttonTextSize); + networkReloadBtn.setTextSize(buttonTextSize); + + errorReloadBtn.setTextColor(LoadingLayoutUtils.getColor(mContext, buttonTextColor)); + networkReloadBtn.setTextColor(LoadingLayoutUtils.getColor(mContext, buttonTextColor)); + + if (buttonHeight != -1) { + + errorReloadBtn.setHeight(LoadingLayoutUtils.dp2px(mContext, buttonHeight)); + networkReloadBtn.setHeight(LoadingLayoutUtils.dp2px(mContext, buttonHeight)); + } + if (buttonWidth != -1) { + + errorReloadBtn.setWidth(LoadingLayoutUtils.dp2px(mContext, buttonWidth)); + networkReloadBtn.setWidth(LoadingLayoutUtils.dp2px(mContext, buttonWidth)); + } + + this.addView(networkPage); + this.addView(emptyPage); + this.addView(errorPage); + this.addView(loadingPage); + } + + public void setStatus(@Flavour int status) { + + this.state = status; + + switch (status) { + case Success: + + contentView.setVisibility(View.VISIBLE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + case Loading: + + contentView.setVisibility(View.GONE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + defineLoadingPage.setVisibility(View.VISIBLE); + } else { + loadingPage.setVisibility(View.VISIBLE); + } + break; + + case Empty: + + contentView.setVisibility(View.GONE); + emptyPage.setVisibility(View.VISIBLE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + case Error: + + contentView.setVisibility(View.GONE); + loadingPage.setVisibility(View.GONE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.VISIBLE); + networkPage.setVisibility(View.GONE); + if (defineLoadingPage != null) { + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + case No_Network: + + contentView.setVisibility(View.GONE); + loadingPage.setVisibility(View.GONE); + emptyPage.setVisibility(View.GONE); + errorPage.setVisibility(View.GONE); + networkPage.setVisibility(View.VISIBLE); + if (defineLoadingPage != null) { + + defineLoadingPage.setVisibility(View.GONE); + } else { + loadingPage.setVisibility(View.GONE); + } + break; + + default: + break; + } + + } + + + /** + * 返回当前状态{Success, Empty, Error, No_Network, Loading} + * + * @return + */ + public int getStatus() { + + return state; + } + + /** + * 设置Empty状态提示文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setEmptyText(String text) { + + emptyText.setText(text); + return this; + } + + /** + * 设置Error状态提示文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setErrorText(String text) { + + errorText.setText(text); + return this; + } + + /** + * 设置No_Network状态提示文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setNoNetworkText(String text) { + + networkText.setText(text); + return this; + } + + /** + * 设置Empty状态显示图片,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setEmptyImage(@DrawableRes int id) { + + + emptyImg.setImageResource(id); + return this; + } + + /** + * 设置Error状态显示图片,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setErrorImage(@DrawableRes int id) { + + errorImg.setImageResource(id); + return this; + } + + /** + * 设置No_Network状态显示图片,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setNoNetworkImage(@DrawableRes int id) { + + networkImg.setImageResource(id); + return this; + } + + /** + * 设置Empty状态提示文本的字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setEmptyTextSize(int sp) { + + emptyText.setTextSize(sp); + return this; + } + + /** + * 设置Error状态提示文本的字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setErrorTextSize(int sp) { + + errorText.setTextSize(sp); + return this; + } + + /** + * 设置No_Network状态提示文本的字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setNoNetworkTextSize(int sp) { + + networkText.setTextSize(sp); + return this; + } + + /** + * 设置Empty状态图片的显示与否,仅对当前所在的地方有效 + * + * @param bool + * @return + */ + public LoadingLayout setEmptyImageVisible(boolean bool) { + + if (bool) { + emptyImg.setVisibility(View.VISIBLE); + } else { + emptyImg.setVisibility(View.GONE); + } + return this; + } + + /** + * 设置Error状态图片的显示与否,仅对当前所在的地方有效 + * + * @param bool + * @return + */ + public LoadingLayout setErrorImageVisible(boolean bool) { + + if (bool) { + errorImg.setVisibility(View.VISIBLE); + } else { + errorImg.setVisibility(View.GONE); + } + return this; + } + + /** + * 设置No_Network状态图片的显示与否,仅对当前所在的地方有效 + * + * @param bool + * @return + */ + public LoadingLayout setNoNetworkImageVisible(boolean bool) { + + if (bool) { + networkImg.setVisibility(View.VISIBLE); + } else { + networkImg.setVisibility(View.GONE); + } + return this; + } + + /** + * 设置ReloadButton的文本,仅对当前所在的地方有效 + * + * @param text + * @return + */ + public LoadingLayout setReloadButtonText(@NonNull String text) { + + errorReloadBtn.setText(text); + networkReloadBtn.setText(text); + return this; + } + + /** + * 设置ReloadButton的文本字体大小,仅对当前所在的地方有效 + * + * @param sp + * @return + */ + public LoadingLayout setReloadButtonTextSize(int sp) { + + errorReloadBtn.setTextSize(sp); + networkReloadBtn.setTextSize(sp); + return this; + } + + /** + * 设置ReloadButton的文本颜色,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setReloadButtonTextColor(@ColorRes int id) { + + errorReloadBtn.setTextColor(LoadingLayoutUtils.getColor(mContext, id)); + networkReloadBtn.setTextSize(LoadingLayoutUtils.getColor(mContext, id)); + return this; + } + + /** + * 设置ReloadButton的背景,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setReloadButtonBackgroundResource(@DrawableRes int id) { + + errorReloadBtn.setBackgroundResource(id); + networkReloadBtn.setBackgroundResource(id); + return this; + } + + /** + * 设置ReloadButton的监听器 + * + * @param listener + * @return + */ + public LoadingLayout setOnReloadListener(OnReloadListener listener) { + + this.listener = listener; + return this; + } + + /** + * 自定义加载页面,仅对当前所在的Activity有效 + * + * @param view + * @return + */ + public LoadingLayout setLoadingPage(View view) { + + defineLoadingPage = view; + this.removeView(loadingPage); + defineLoadingPage.setVisibility(View.GONE); + this.addView(view); + return this; + } + + public LoadingLayout setEmptyImageViewClickListener(OnClickListener viewClickListener){ + emptyImg.setOnClickListener(viewClickListener); + return this; + } + + /** + * 自定义加载页面,仅对当前所在的地方有效 + * + * @param id + * @return + */ + public LoadingLayout setLoadingPage(@LayoutRes int id) { + + this.removeView(loadingPage); + View view = LayoutInflater.from(mContext).inflate(id, null); + defineLoadingPage = view; + defineLoadingPage.setVisibility(View.GONE); + this.addView(view); + return this; + } + + /** + * 获取当前自定义的loadingpage + * + * @return + */ + public View getLoadingPage() { + + return defineLoadingPage; + } + + + /** + * 获取全局使用的loadingpage + * + * @return + */ + public View getGlobalLoadingPage() { + + return loadingPage; + } + + @IntDef({Success, Empty, Error, No_Network, Loading}) + public @interface Flavour { + + } + + public interface OnReloadListener { + + void onReload(View v); + } + + /** + * 获取全局配置的class + * + * @return + */ + public static Config getConfig() { + + return mConfig; + } + + /** + * 全局配置的Class,对所有使用到的地方有效 + */ + public static class Config { + + public Config setErrorText(@NonNull String text) { + + errorStr = text; + return mConfig; + } + + public Config setEmptyText(@NonNull String text) { + + emptyStr = text; + return mConfig; + } + + public Config setNoNetworkText(@NonNull String text) { + + netwrokStr = text; + return mConfig; + } + + public Config setReloadButtonText(@NonNull String text) { + + reloadBtnStr = text; + return mConfig; + } + + /** + * 设置所有提示文本的字体大小 + * + * @param sp + * @return + */ + public Config setAllTipTextSize(int sp) { + + tipTextSize = sp; + return mConfig; + } + + /** + * 设置所有提示文本的字体颜色 + * + * @param color + * @return + */ + public Config setAllTipTextColor(@ColorRes int color) { + + tipTextColor = color; + return mConfig; + } + + public Config setReloadButtonTextSize(int sp) { + + buttonTextSize = sp; + return mConfig; + } + + public Config setReloadButtonTextColor(@ColorRes int color) { + + buttonTextColor = color; + return mConfig; + } + + public Config setReloadButtonBackgroundResource(@DrawableRes int id) { + + reloadBtnId = id; + return mConfig; + } + + public Config setReloadButtonWidthAndHeight(int width_dp, int height_dp) { + + buttonWidth = width_dp; + buttonHeight = height_dp; + return mConfig; + } + + public Config setErrorImage(@DrawableRes int id) { + + errorImgId = id; + return mConfig; + } + + public Config setEmptyImage(@DrawableRes int id) { + + emptyImgId = id; + return this; + } + + public Config setNoNetworkImage(@DrawableRes int id) { + + networkImgId = id; + return mConfig; + } + + public Config setLoadingPageLayout(@LayoutRes int id) { + + loadingLayoutId = id; + return mConfig; + } + + public Config setAllPageBackgroundColor(@ColorRes int color) { + + backgroundColor = color; + return mConfig; + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java new file mode 100644 index 0000000..81d26f6 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/loadingLayout/LoadingLayoutUtils.java @@ -0,0 +1,48 @@ +package com.hzecool.widget.loadingLayout; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.view.View; + +/** + * create by tutu + * on date 2017-01-11 + */ + +public class LoadingLayoutUtils { + + + public static Drawable getDrawble(Context conetxt, @DrawableRes int id) { + return ContextCompat.getDrawable(conetxt, id); + } + + public static int getColor(Context conetxt, @ColorRes int id) { + return ContextCompat.getColor(conetxt, id); + } + + public static String getString(Context conetxt, @StringRes int id) { + return conetxt.getResources().getString(id); + } + + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics() + .scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + public static int dp2px(Context context, int dip) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dip * scale + 0.5f); + } + + public static T findViewById(View v, int id) { + + + return (T) v.findViewById(id); + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java new file mode 100644 index 0000000..22970d4 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DefaultRvAdapter.java @@ -0,0 +1,210 @@ +package com.hzecool.widget.materialdialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.res.Configuration; +import android.os.Build; +import android.support.annotation.LayoutRes; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.internal.MDTintHelper; +import com.hzecool.widget.materialdialog.util.DialogUtils; + + +/** + * @author Aidan Follestad (afollestad) + */ +class DefaultRvAdapter extends RecyclerView.Adapter { + + private final MaterialDialog dialog; + @LayoutRes + private final int layout; + private final GravityEnum itemGravity; + private InternalListCallback callback; + + DefaultRvAdapter(MaterialDialog dialog, @LayoutRes int layout) { + this.dialog = dialog; + this.layout = layout; + this.itemGravity = dialog.builder.itemsGravity; + } + + void setCallback(InternalListCallback callback) { + this.callback = callback; + } + + @Override + public DefaultVH onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(parent.getContext()) + .inflate(layout, parent, false); + DialogUtils.setBackgroundCompat(view, dialog.getListSelector()); + return new DefaultVH(view, this); + } + + @Override + public void onBindViewHolder(DefaultVH holder, int index) { + final View view = holder.itemView; + boolean disabled = DialogUtils.isIn(index, dialog.builder.disabledIndices); + switch (dialog.listType) { + case SINGLE: { + @SuppressLint("CutPasteId") RadioButton radio = (RadioButton) holder.control; + boolean selected = dialog.builder.selectedIndex == index; + if (dialog.builder.choiceWidgetColor != null) { + MDTintHelper.setTint(radio, dialog.builder.choiceWidgetColor); + } else { + MDTintHelper.setTint(radio, dialog.builder.widgetColor); + } + radio.setChecked(selected); + radio.setEnabled(!disabled); + break; + } + case MULTI: { + @SuppressLint("CutPasteId") CheckBox checkbox = (CheckBox) holder.control; + boolean selected = dialog.selectedIndicesList.contains(index); + if (dialog.builder.choiceWidgetColor != null) { + MDTintHelper.setTint(checkbox, dialog.builder.choiceWidgetColor); + } else { + MDTintHelper.setTint(checkbox, dialog.builder.widgetColor); + } + checkbox.setChecked(selected); + checkbox.setEnabled(!disabled); + break; + } + } + + holder.title.setText(dialog.builder.items.get(index)); + holder.title.setTextColor(dialog.builder.itemColor); + dialog.setTypeface(holder.title, dialog.builder.regularFont); + + setupGravity((ViewGroup) view); + + if (dialog.builder.itemIds != null) { + if (index < dialog.builder.itemIds.length) { + view.setId(dialog.builder.itemIds[index]); + } else { + view.setId(-1); + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ViewGroup group = (ViewGroup) view; + if (group.getChildCount() == 2) { + // Remove circular selector from check boxes and radio buttons on Lollipop + if (group.getChildAt(0) instanceof CompoundButton) { + group.getChildAt(0).setBackground(null); + } else if (group.getChildAt(1) instanceof CompoundButton) { + group.getChildAt(1).setBackground(null); + } + } + } + } + + @Override + public int getItemCount() { + return dialog.builder.items != null ? dialog.builder.items.size() : 0; + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void setupGravity(ViewGroup view) { + final LinearLayout itemRoot = (LinearLayout) view; + final int gravityInt = itemGravity.getGravityInt(); + itemRoot.setGravity(gravityInt | Gravity.CENTER_VERTICAL); + + if (view.getChildCount() == 2) { + if (itemGravity == GravityEnum.END && !isRTL() && view + .getChildAt(0) instanceof CompoundButton) { + CompoundButton first = (CompoundButton) view.getChildAt(0); + view.removeView(first); + + TextView second = (TextView) view.getChildAt(0); + view.removeView(second); + second.setPadding(second.getPaddingRight(), second.getPaddingTop(), + second.getPaddingLeft(), second.getPaddingBottom()); + + view.addView(second); + view.addView(first); + } else if (itemGravity == GravityEnum.START && isRTL() && view + .getChildAt(1) instanceof CompoundButton) { + CompoundButton first = (CompoundButton) view.getChildAt(1); + view.removeView(first); + + TextView second = (TextView) view.getChildAt(0); + view.removeView(second); + second.setPadding(second.getPaddingRight(), second.getPaddingTop(), + second.getPaddingRight(), second.getPaddingBottom()); + + view.addView(first); + view.addView(second); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private boolean isRTL() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return false; + } + Configuration config = dialog.getBuilder().getContext().getResources().getConfiguration(); + return config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + interface InternalListCallback { + + boolean onItemSelected(MaterialDialog dialog, View itemView, int position, CharSequence text, + boolean longPress); + } + + static class DefaultVH extends RecyclerView.ViewHolder implements View.OnClickListener, + View.OnLongClickListener { + + final CompoundButton control; + final TextView title; + final DefaultRvAdapter adapter; + + DefaultVH(View itemView, DefaultRvAdapter adapter) { + super(itemView); + control = (CompoundButton) itemView.findViewById(R.id.md_control); + title = (TextView) itemView.findViewById(R.id.md_title); + this.adapter = adapter; + itemView.setOnClickListener(this); + if (adapter.dialog.builder.listLongCallback != null) { + itemView.setOnLongClickListener(this); + } + } + + @Override + public void onClick(View view) { + if (adapter.callback != null && getAdapterPosition() != RecyclerView.NO_POSITION) { + CharSequence text = null; + if (adapter.dialog.builder.items != null && + getAdapterPosition() < adapter.dialog.builder.items.size()) { + text = adapter.dialog.builder.items.get(getAdapterPosition()); + } + adapter.callback.onItemSelected(adapter.dialog, view, getAdapterPosition(), text, false); + } + } + + @Override + public boolean onLongClick(View view) { + if (adapter.callback != null && getAdapterPosition() != RecyclerView.NO_POSITION) { + CharSequence text = null; + if (adapter.dialog.builder.items != null && + getAdapterPosition() < adapter.dialog.builder.items.size()) { + text = adapter.dialog.builder.items.get(getAdapterPosition()); + } + return adapter.callback + .onItemSelected(adapter.dialog, view, getAdapterPosition(), text, true); + } + return false; + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java new file mode 100644 index 0000000..d6ae2dd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogAction.java @@ -0,0 +1,10 @@ +package com.hzecool.widget.materialdialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public enum DialogAction { + POSITIVE, + NEUTRAL, + NEGATIVE +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java new file mode 100644 index 0000000..e4cfef8 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogBase.java @@ -0,0 +1,71 @@ +package com.hzecool.widget.materialdialog; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; + +import com.hzecool.widget.materialdialog.internal.MDRootLayout; + + +/** + * @author Aidan Follestad (afollestad) + */ +class DialogBase extends Dialog implements DialogInterface.OnShowListener { + + protected MDRootLayout view; + private OnShowListener showListener; + + DialogBase(Context context, int theme) { + super(context, theme); + } + + @Override + public View findViewById(int id) { + return view.findViewById(id); + } + + @Override + public final void setOnShowListener(OnShowListener listener) { + showListener = listener; + } + + final void setOnShowListenerInternal() { + super.setOnShowListener(this); + } + + final void setViewInternal(View view) { + super.setContentView(view); + } + + @Override + public void onShow(DialogInterface dialog) { + if (showListener != null) { + showListener.onShow(dialog); + } + } + + @Override + @Deprecated + public void setContentView(int layoutResID) throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } + + @Override + @Deprecated + public void setContentView(@NonNull View view) throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } + + @Override + @Deprecated + public void setContentView(@NonNull View view, ViewGroup.LayoutParams params) + throws IllegalAccessError { + throw new IllegalAccessError( + "setContentView() is not supported in MaterialDialog. Specify a custom view in the Builder instead."); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java new file mode 100644 index 0000000..8bb6d25 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/DialogInit.java @@ -0,0 +1,554 @@ +package com.hzecool.widget.materialdialog; + +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.StyleRes; +import android.support.annotation.UiThread; +import android.support.v7.widget.RecyclerView; +import android.text.InputType; +import android.text.method.LinkMovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.ScrollView; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.internal.MDAdapter; +import com.hzecool.widget.materialdialog.internal.MDButton; +import com.hzecool.widget.materialdialog.internal.MDRootLayout; +import com.hzecool.widget.materialdialog.internal.MDTintHelper; +import com.hzecool.widget.materialdialog.util.DialogUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +import me.zhanghai.android.materialprogressbar.HorizontalProgressDrawable; +import me.zhanghai.android.materialprogressbar.IndeterminateHorizontalProgressDrawable; +import me.zhanghai.android.materialprogressbar.IndeterminateProgressDrawable; + + +/** + * Used by MaterialDialog while initializing the dialog. Offloads some of the code to make the main + * class cleaner and easier to read/maintain. + * + * @author Aidan Follestad (afollestad) + */ +class DialogInit { + + @StyleRes + static int getTheme(@NonNull MaterialDialog.Builder builder) { + boolean darkTheme = DialogUtils + .resolveBoolean(builder.context, R.attr.md_dark_theme, builder.theme == Theme.DARK); + builder.theme = darkTheme ? Theme.DARK : Theme.LIGHT; + return darkTheme ? R.style.MD_Dark : R.style.MD_Light; + } + + @LayoutRes + static int getInflateLayout(MaterialDialog.Builder builder) { + if (builder.customView != null) { + return R.layout.md_dialog_custom; + } else if (builder.items != null || builder.adapter != null) { + if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_list_check; + } + return R.layout.md_dialog_list; + } else if (builder.progress > -2) { + return R.layout.md_dialog_progress; + } else if (builder.indeterminateProgress) { + if (builder.indeterminateIsHorizontalProgress) { + return R.layout.md_dialog_progress_indeterminate_horizontal; + } + return R.layout.md_dialog_progress_indeterminate; + } else if (builder.inputCallback != null) { + if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_input_check; + } + return R.layout.md_dialog_input; + } else if (builder.checkBoxPrompt != null) { + return R.layout.md_dialog_basic_check; + } else { + return R.layout.md_dialog_basic; + } + } + + @SuppressWarnings("ConstantConditions") + @UiThread + public static void init(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + + // Set cancelable flag and dialog background color + dialog.setCancelable(builder.cancelable); + dialog.setCanceledOnTouchOutside(builder.canceledOnTouchOutside); + if (builder.backgroundColor == 0) { + builder.backgroundColor = DialogUtils + .resolveColor(builder.context, R.attr.md_background_color, + DialogUtils.resolveColor(dialog.getContext(), R.attr.colorBackgroundFloating)); + } + if (builder.backgroundColor != 0) { + GradientDrawable drawable = new GradientDrawable(); + drawable.setCornerRadius( + builder.context.getResources().getDimension(R.dimen.md_bg_corner_radius)); + drawable.setColor(builder.backgroundColor); + dialog.getWindow().setBackgroundDrawable(drawable); + } + + // Retrieve color theme attributes + if (!builder.positiveColorSet) { + builder.positiveColor = DialogUtils + .resolveActionTextColorStateList(builder.context, R.attr.md_positive_color, + builder.positiveColor); + } + if (!builder.neutralColorSet) { + builder.neutralColor = DialogUtils + .resolveActionTextColorStateList(builder.context, R.attr.md_neutral_color, + builder.neutralColor); + } + if (!builder.negativeColorSet) { + builder.negativeColor = DialogUtils + .resolveActionTextColorStateList(builder.context, R.attr.md_negative_color, + builder.negativeColor); + } + if (!builder.widgetColorSet) { + builder.widgetColor = DialogUtils + .resolveColor(builder.context, R.attr.md_widget_color, builder.widgetColor); + } + + // Retrieve default title/content colors + if (!builder.titleColorSet) { + final int titleColorFallback = DialogUtils + .resolveColor(dialog.getContext(), android.R.attr.textColorPrimary); + builder.titleColor = DialogUtils + .resolveColor(builder.context, R.attr.md_title_color, titleColorFallback); + } + if (!builder.contentColorSet) { + final int contentColorFallback = DialogUtils + .resolveColor(dialog.getContext(), android.R.attr.textColorSecondary); + builder.contentColor = DialogUtils + .resolveColor(builder.context, R.attr.md_content_color, contentColorFallback); + } + if (!builder.itemColorSet) { + builder.itemColor = DialogUtils + .resolveColor(builder.context, R.attr.md_item_color, builder.contentColor); + } + + // Retrieve references to views + dialog.title = (TextView) dialog.view.findViewById(R.id.md_title); + dialog.icon = (ImageView) dialog.view.findViewById(R.id.md_icon); + dialog.titleFrame = dialog.view.findViewById(R.id.md_titleFrame); + dialog.content = (TextView) dialog.view.findViewById(R.id.md_content); + dialog.recyclerView = (RecyclerView) dialog.view.findViewById(R.id.md_contentRecyclerView); + dialog.checkBoxPrompt = (CheckBox) dialog.view.findViewById(R.id.md_promptCheckbox); + + // Button views initially used by checkIfStackingNeeded() + dialog.positiveButton = (MDButton) dialog.view.findViewById(R.id.md_buttonDefaultPositive); + dialog.neutralButton = (MDButton) dialog.view.findViewById(R.id.md_buttonDefaultNeutral); + dialog.negativeButton = (MDButton) dialog.view.findViewById(R.id.md_buttonDefaultNegative); + + // Don't allow the submit button to not be shown for input dialogs + if (builder.inputCallback != null && builder.positiveText == null) { + builder.positiveText = builder.context.getText(android.R.string.ok); + } + + // Set up the initial visibility of action buttons based on whether or not text was set + dialog.positiveButton.setVisibility(builder.positiveText != null ? View.VISIBLE : View.GONE); + dialog.neutralButton.setVisibility(builder.neutralText != null ? View.VISIBLE : View.GONE); + dialog.negativeButton.setVisibility(builder.negativeText != null ? View.VISIBLE : View.GONE); + + // Set up the focus of action buttons + dialog.positiveButton.setFocusable(true); + dialog.neutralButton.setFocusable(true); + dialog.negativeButton.setFocusable(true); + if (builder.positiveFocus) { + dialog.positiveButton.requestFocus(); + } + if (builder.neutralFocus) { + dialog.neutralButton.requestFocus(); + } + if (builder.negativeFocus) { + dialog.negativeButton.requestFocus(); + } + + // Setup icon + if (builder.icon != null) { + dialog.icon.setVisibility(View.VISIBLE); + dialog.icon.setImageDrawable(builder.icon); + } else { + Drawable d = DialogUtils.resolveDrawable(builder.context, R.attr.md_icon); + if (d != null) { + dialog.icon.setVisibility(View.VISIBLE); + dialog.icon.setImageDrawable(d); + } else { + dialog.icon.setVisibility(View.GONE); + } + } + + // Setup icon size limiting + int maxIconSize = builder.maxIconSize; + if (maxIconSize == -1) { + maxIconSize = DialogUtils.resolveDimension(builder.context, R.attr.md_icon_max_size); + } + if (builder.limitIconToDefaultSize || DialogUtils + .resolveBoolean(builder.context, R.attr.md_icon_limit_icon_to_default_size)) { + maxIconSize = builder.context.getResources().getDimensionPixelSize(R.dimen.md_icon_max_size); + } + if (maxIconSize > -1) { + dialog.icon.setAdjustViewBounds(true); + dialog.icon.setMaxHeight(maxIconSize); + dialog.icon.setMaxWidth(maxIconSize); + dialog.icon.requestLayout(); + } + + // Setup divider color in case content scrolls + if (!builder.dividerColorSet) { + final int dividerFallback = DialogUtils.resolveColor(dialog.getContext(), R.attr.md_divider); + builder.dividerColor = DialogUtils + .resolveColor(builder.context, R.attr.md_divider_color, dividerFallback); + } + dialog.view.setDividerColor(builder.dividerColor); + + // Setup title and title frame + if (dialog.title != null) { + dialog.setTypeface(dialog.title, builder.mediumFont); + dialog.title.setTextColor(builder.titleColor); + dialog.title.setGravity(builder.titleGravity.getGravityInt()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + //noinspection ResourceType + dialog.title.setTextAlignment(builder.titleGravity.getTextAlignment()); + } + + if (builder.title == null) { + dialog.titleFrame.setVisibility(View.GONE); + } else { + dialog.title.setText(builder.title); + dialog.titleFrame.setVisibility(View.VISIBLE); + } + } + + // Setup content + if (dialog.content != null) { + dialog.content.setMovementMethod(new LinkMovementMethod()); + dialog.setTypeface(dialog.content, builder.regularFont); + dialog.content.setLineSpacing(0f, builder.contentLineSpacingMultiplier); + if (builder.linkColor == null) { + dialog.content.setLinkTextColor( + DialogUtils.resolveColor(dialog.getContext(), android.R.attr.textColorPrimary)); + } else { + dialog.content.setLinkTextColor(builder.linkColor); + } + dialog.content.setTextColor(builder.contentColor); + dialog.content.setGravity(builder.contentGravity.getGravityInt()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + //noinspection ResourceType + dialog.content.setTextAlignment(builder.contentGravity.getTextAlignment()); + } + + if (builder.content != null) { + dialog.content.setText(builder.content); + dialog.content.setVisibility(View.VISIBLE); + } else { + dialog.content.setVisibility(View.GONE); + } + } + + // Setup prompt checkbox + if (dialog.checkBoxPrompt != null) { + dialog.checkBoxPrompt.setText(builder.checkBoxPrompt); + dialog.checkBoxPrompt.setChecked(builder.checkBoxPromptInitiallyChecked); + dialog.checkBoxPrompt.setOnCheckedChangeListener(builder.checkBoxPromptListener); + dialog.setTypeface(dialog.checkBoxPrompt, builder.regularFont); + dialog.checkBoxPrompt.setTextColor(builder.contentColor); + MDTintHelper.setTint(dialog.checkBoxPrompt, builder.widgetColor); + } + + // Setup action buttons + dialog.view.setButtonGravity(builder.buttonsGravity); + dialog.view.setButtonStackedGravity(builder.btnStackedGravity); + dialog.view.setStackingBehavior(builder.stackingBehavior); + boolean textAllCaps; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + textAllCaps = DialogUtils.resolveBoolean(builder.context, android.R.attr.textAllCaps, true); + if (textAllCaps) { + textAllCaps = DialogUtils.resolveBoolean(builder.context, R.attr.textAllCaps, true); + } + } else { + textAllCaps = DialogUtils.resolveBoolean(builder.context, R.attr.textAllCaps, true); + } + + MDButton positiveTextView = dialog.positiveButton; + dialog.setTypeface(positiveTextView, builder.mediumFont); + positiveTextView.setAllCapsCompat(textAllCaps); + positiveTextView.setText(builder.positiveText); + positiveTextView.setTextColor(builder.positiveColor); + dialog.positiveButton.setStackedSelector(dialog.getButtonSelector(DialogAction.POSITIVE, true)); + dialog.positiveButton + .setDefaultSelector(dialog.getButtonSelector(DialogAction.POSITIVE, false)); + dialog.positiveButton.setTag(DialogAction.POSITIVE); + dialog.positiveButton.setOnClickListener(dialog); + dialog.positiveButton.setVisibility(View.VISIBLE); + + MDButton negativeTextView = dialog.negativeButton; + dialog.setTypeface(negativeTextView, builder.mediumFont); + negativeTextView.setAllCapsCompat(textAllCaps); + negativeTextView.setText(builder.negativeText); + negativeTextView.setTextColor(builder.negativeColor); + dialog.negativeButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, true)); + dialog.negativeButton + .setDefaultSelector(dialog.getButtonSelector(DialogAction.NEGATIVE, false)); + dialog.negativeButton.setTag(DialogAction.NEGATIVE); + dialog.negativeButton.setOnClickListener(dialog); + dialog.negativeButton.setVisibility(View.VISIBLE); + + MDButton neutralTextView = dialog.neutralButton; + dialog.setTypeface(neutralTextView, builder.mediumFont); + neutralTextView.setAllCapsCompat(textAllCaps); + neutralTextView.setText(builder.neutralText); + neutralTextView.setTextColor(builder.neutralColor); + dialog.neutralButton.setStackedSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, true)); + dialog.neutralButton.setDefaultSelector(dialog.getButtonSelector(DialogAction.NEUTRAL, false)); + dialog.neutralButton.setTag(DialogAction.NEUTRAL); + dialog.neutralButton.setOnClickListener(dialog); + dialog.neutralButton.setVisibility(View.VISIBLE); + + // Setup list dialog stuff + if (builder.listCallbackMultiChoice != null) { + dialog.selectedIndicesList = new ArrayList<>(); + } + if (dialog.recyclerView != null) { + if (builder.adapter == null) { + // Determine list type + if (builder.listCallbackSingleChoice != null) { + dialog.listType = MaterialDialog.ListType.SINGLE; + } else if (builder.listCallbackMultiChoice != null) { + dialog.listType = MaterialDialog.ListType.MULTI; + if (builder.selectedIndices != null) { + dialog.selectedIndicesList = new ArrayList<>(Arrays.asList(builder.selectedIndices)); + builder.selectedIndices = null; + } + } else { + dialog.listType = MaterialDialog.ListType.REGULAR; + } + builder.adapter = new DefaultRvAdapter(dialog, + MaterialDialog.ListType.getLayoutForType(dialog.listType)); + } else if (builder.adapter instanceof MDAdapter) { + // Notify simple list adapter of the dialog it belongs to + ((MDAdapter) builder.adapter).setDialog(dialog); + } + } + + // Setup progress dialog stuff if needed + setupProgressDialog(dialog); + + // Setup input dialog stuff if needed + setupInputDialog(dialog); + + // Setup custom views + if (builder.customView != null) { + ((MDRootLayout) dialog.view.findViewById(R.id.md_root)).noTitleNoPadding(); + FrameLayout frame = (FrameLayout) dialog.view.findViewById(R.id.md_customViewFrame); + dialog.customViewFrame = frame; + View innerView = builder.customView; + if (innerView.getParent() != null) { + ((ViewGroup) innerView.getParent()).removeView(innerView); + } + if (builder.wrapCustomViewInScroll) { + /* Apply the frame padding to the content, this allows the ScrollView to draw it's + over scroll glow without clipping */ + final Resources r = dialog.getContext().getResources(); + final int framePadding = r.getDimensionPixelSize(R.dimen.md_dialog_frame_margin); + final ScrollView sv = new ScrollView(dialog.getContext()); + int paddingTop = r.getDimensionPixelSize(R.dimen.md_content_padding_top); + int paddingBottom = r.getDimensionPixelSize(R.dimen.md_content_padding_bottom); + sv.setClipToPadding(false); + if (innerView instanceof EditText) { + // Setting padding to an EditText causes visual errors, set it to the parent instead + sv.setPadding(framePadding, paddingTop, framePadding, paddingBottom); + } else { + // Setting padding to scroll view pushes the scroll bars out, don't do it if not necessary (like above) + sv.setPadding(0, paddingTop, 0, paddingBottom); + innerView.setPadding(framePadding, 0, framePadding, 0); + } + sv.addView(innerView, new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + innerView = sv; + } + frame.addView(innerView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + // Setup user listeners + if (builder.showListener != null) { + dialog.setOnShowListener(builder.showListener); + } + if (builder.cancelListener != null) { + dialog.setOnCancelListener(builder.cancelListener); + } + if (builder.dismissListener != null) { + dialog.setOnDismissListener(builder.dismissListener); + } + if (builder.keyListener != null) { + dialog.setOnKeyListener(builder.keyListener); + } + + // Setup internal show listener + dialog.setOnShowListenerInternal(); + + // Other internal initialization + dialog.invalidateList(); + dialog.setViewInternal(dialog.view); + dialog.checkIfListInitScroll(); + + // Min height and max width calculations + WindowManager wm = dialog.getWindow().getWindowManager(); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + final int windowWidth = size.x; + final int windowHeight = size.y; + + final int windowVerticalPadding = builder.context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_vertical_margin); + final int windowHorizontalPadding = builder.context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_horizontal_margin); + final int maxWidth = builder.context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_max_width); + final int calculatedWidth = windowWidth - (windowHorizontalPadding * 2); + + dialog.view.setMaxHeight(windowHeight - windowVerticalPadding * 2); + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = Math.min(maxWidth, calculatedWidth); + dialog.getWindow().setAttributes(lp); + } + + private static void fixCanvasScalingWhenHardwareAccelerated(ProgressBar pb) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + // Canvas scaling when hardware accelerated results in artifacts on older API levels, so + // we need to use software rendering + if (pb.isHardwareAccelerated() && pb.getLayerType() != View.LAYER_TYPE_SOFTWARE) { + pb.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + } + } + + private static void setupProgressDialog(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + if (builder.indeterminateProgress || builder.progress > -2) { + dialog.progressBar = (ProgressBar) dialog.view.findViewById(android.R.id.progress); + if (dialog.progressBar == null) { + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (builder.indeterminateProgress) { + if (builder.indeterminateIsHorizontalProgress) { + IndeterminateHorizontalProgressDrawable d = new IndeterminateHorizontalProgressDrawable( + builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } else { + IndeterminateProgressDrawable d = new IndeterminateProgressDrawable( + builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } + } else { + HorizontalProgressDrawable d = new HorizontalProgressDrawable(builder.getContext()); + d.setTint(builder.widgetColor); + dialog.progressBar.setProgressDrawable(d); + dialog.progressBar.setIndeterminateDrawable(d); + } + } else { + MDTintHelper.setTint(dialog.progressBar, builder.widgetColor); + } + + if (!builder.indeterminateProgress || builder.indeterminateIsHorizontalProgress) { + dialog.progressBar.setIndeterminate( + builder.indeterminateProgress && builder.indeterminateIsHorizontalProgress); + dialog.progressBar.setProgress(0); + dialog.progressBar.setMax(builder.progressMax); + dialog.progressLabel = (TextView) dialog.view.findViewById(R.id.md_label); + if (dialog.progressLabel != null) { + dialog.progressLabel.setTextColor(builder.contentColor); + dialog.setTypeface(dialog.progressLabel, builder.mediumFont); + dialog.progressLabel.setText(builder.progressPercentFormat.format(0)); + } + dialog.progressMinMax = (TextView) dialog.view.findViewById(R.id.md_minMax); + if (dialog.progressMinMax != null) { + dialog.progressMinMax.setTextColor(builder.contentColor); + dialog.setTypeface(dialog.progressMinMax, builder.regularFont); + + if (builder.showMinMax) { + dialog.progressMinMax.setVisibility(View.VISIBLE); + dialog.progressMinMax.setText(String.format(builder.progressNumberFormat, + 0, builder.progressMax)); + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) dialog.progressBar + .getLayoutParams(); + lp.leftMargin = 0; + lp.rightMargin = 0; + } else { + dialog.progressMinMax.setVisibility(View.GONE); + } + } else { + builder.showMinMax = false; + } + } + } + + if (dialog.progressBar != null) { + fixCanvasScalingWhenHardwareAccelerated(dialog.progressBar); + } + } + + private static void setupInputDialog(final MaterialDialog dialog) { + final MaterialDialog.Builder builder = dialog.builder; + dialog.input = (EditText) dialog.view.findViewById(android.R.id.input); + if (dialog.input == null) { + return; + } + dialog.setTypeface(dialog.input, builder.regularFont); + if (builder.inputPrefill != null) { + dialog.input.setText(builder.inputPrefill); + } + dialog.setInternalInputCallback(); + dialog.input.setHint(builder.inputHint); + dialog.input.setSingleLine(); + dialog.input.setTextColor(builder.contentColor); + dialog.input.setHintTextColor(DialogUtils.adjustAlpha(builder.contentColor, 0.3f)); + MDTintHelper.setTint(dialog.input, dialog.builder.widgetColor); + + if (builder.inputType != -1) { + dialog.input.setInputType(builder.inputType); + if (builder.inputType != InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD && + (builder.inputType & InputType.TYPE_TEXT_VARIATION_PASSWORD) + == InputType.TYPE_TEXT_VARIATION_PASSWORD) { + // If the flags contain TYPE_TEXT_VARIATION_PASSWORD, apply the password transformation method automatically + dialog.input.setTransformationMethod(PasswordTransformationMethod.getInstance()); + } + } + + dialog.inputMinMax = (TextView) dialog.view.findViewById(R.id.md_minMax); + if (builder.inputMinLength > 0 || builder.inputMaxLength > -1) { + dialog.invalidateInputMinMaxIndicator(dialog.input.getText().toString().length(), + !builder.inputAllowEmpty); + } else { + dialog.inputMinMax.setVisibility(View.GONE); + dialog.inputMinMax = null; + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java b/widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java new file mode 100644 index 0000000..f4c1846 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/GravityEnum.java @@ -0,0 +1,40 @@ +package com.hzecool.widget.materialdialog; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.view.Gravity; +import android.view.View; + +public enum GravityEnum { + START, CENTER, END; + + private static final boolean HAS_RTL = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; + + @SuppressLint("RtlHardcoded") + public int getGravityInt() { + switch (this) { + case START: + return HAS_RTL ? Gravity.START : Gravity.LEFT; + case CENTER: + return Gravity.CENTER_HORIZONTAL; + case END: + return HAS_RTL ? Gravity.END : Gravity.RIGHT; + default: + throw new IllegalStateException("Invalid gravity constant"); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public int getTextAlignment() { + switch (this) { + case CENTER: + return View.TEXT_ALIGNMENT_CENTER; + case END: + return View.TEXT_ALIGNMENT_VIEW_END; + default: + return View.TEXT_ALIGNMENT_VIEW_START; + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java b/widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java new file mode 100644 index 0000000..f4cb88d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/MaterialDialog.java @@ -0,0 +1,2270 @@ +package com.hzecool.widget.materialdialog; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Handler; +import android.support.annotation.ArrayRes; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.DimenRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntRange; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.UiThread; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.Editable; +import android.text.Html; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.internal.MDButton; +import com.hzecool.widget.materialdialog.internal.MDRootLayout; +import com.hzecool.widget.materialdialog.internal.MDTintHelper; +import com.hzecool.widget.materialdialog.internal.ThemeSingleton; +import com.hzecool.widget.materialdialog.util.DialogUtils; +import com.hzecool.widget.materialdialog.util.RippleHelper; +import com.hzecool.widget.materialdialog.util.TypefaceHelper; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * @author Aidan Follestad (afollestad) + */ +public class MaterialDialog extends DialogBase implements + View.OnClickListener, DefaultRvAdapter.InternalListCallback { + + protected final Builder builder; + private final Handler handler; + protected ImageView icon; + protected TextView title; + protected TextView content; + + EditText input; + RecyclerView recyclerView; + View titleFrame; + FrameLayout customViewFrame; + ProgressBar progressBar; + TextView progressLabel; + TextView progressMinMax; + TextView inputMinMax; + CheckBox checkBoxPrompt; + MDButton positiveButton; + MDButton neutralButton; + MDButton negativeButton; + ListType listType; + List selectedIndicesList; + + @SuppressLint("InflateParams") + protected MaterialDialog(Builder builder) { + super(builder.context, DialogInit.getTheme(builder)); + handler = new Handler(); + this.builder = builder; + final LayoutInflater inflater = LayoutInflater.from(builder.context); + view = (MDRootLayout) inflater.inflate(DialogInit.getInflateLayout(builder), null); + DialogInit.init(this); + } + + public final Builder getBuilder() { + return builder; + } + + public final void setTypeface(TextView target, Typeface t) { + if (t == null) { + return; + } + int flags = target.getPaintFlags() | Paint.SUBPIXEL_TEXT_FLAG; + target.setPaintFlags(flags); + target.setTypeface(t); + } + + @SuppressWarnings("unused") + @Nullable + public Object getTag() { + return builder.tag; + } + + final void checkIfListInitScroll() { + if (recyclerView == null) { + return; + } + recyclerView.getViewTreeObserver() + .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @SuppressWarnings("ConstantConditions") + @Override + public void onGlobalLayout() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + //noinspection deprecation + recyclerView.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + if (listType == ListType.SINGLE || listType == ListType.MULTI) { + int selectedIndex; + if (listType == ListType.SINGLE) { + if (builder.selectedIndex < 0) { + return; + } + selectedIndex = builder.selectedIndex; + } else { + if (selectedIndicesList == null || selectedIndicesList.size() == 0) { + return; + } + Collections.sort(selectedIndicesList); + selectedIndex = selectedIndicesList.get(0); + } + + final int fSelectedIndex = selectedIndex; + recyclerView.post(new Runnable() { + @Override + public void run() { + recyclerView.requestFocus(); + builder.layoutManager.scrollToPosition(fSelectedIndex); + } + }); + } + } + }); + } + + /** + * Sets the dialog RecyclerView's adapter/layout manager, and it's item click listener. + */ + final void invalidateList() { + if (recyclerView == null) { + return; + } else if ((builder.items == null || + builder.items.size() == 0) && builder.adapter == null) { + return; + } + if (builder.layoutManager == null) { + builder.layoutManager = new LinearLayoutManager(getContext()); + } + recyclerView.setLayoutManager(builder.layoutManager); + recyclerView.setAdapter(builder.adapter); + if (listType != null) { + ((DefaultRvAdapter) builder.adapter).setCallback(this); + } + } + + @Override + public boolean onItemSelected(MaterialDialog dialog, View view, + int position, CharSequence text, boolean longPress) { + if (!view.isEnabled()) { + return false; + } + if (listType == null || listType == ListType.REGULAR) { + // Default adapter, non choice mode + if (builder.autoDismiss) { + // If auto dismiss is enabled, dismiss the dialog when a list item is selected + dismiss(); + } + if (!longPress && builder.listCallback != null) { + builder.listCallback.onSelection(this, view, position, + builder.items.get(position)); + } + if (longPress && builder.listLongCallback != null) { + return builder.listLongCallback.onLongSelection(this, view, + position, builder.items.get(position)); + } + } else { + // Default adapter, choice mode + if (listType == ListType.MULTI) { + final CheckBox cb = (CheckBox) view.findViewById(R.id.md_control); + if (!cb.isEnabled()) { + return false; + } + final boolean shouldBeChecked = !selectedIndicesList.contains(position); + if (shouldBeChecked) { + // Add the selection to the states first so the callback includes it (when alwaysCallMultiChoiceCallback) + selectedIndicesList.add(position); + if (builder.alwaysCallMultiChoiceCallback) { + // If the checkbox wasn't previously selected, and the callback returns true, add it to the states and check it + if (sendMultiChoiceCallback()) { + cb.setChecked(true); + } else { + // The callback cancelled selection, remove it from the states + selectedIndicesList.remove(Integer.valueOf(position)); + } + } else { + // The callback was not used to check if selection is allowed, just select it + cb.setChecked(true); + } + } else { + // Remove the selection from the states first so the callback does not include it (when alwaysCallMultiChoiceCallback) + selectedIndicesList.remove(Integer.valueOf(position)); + if (builder.alwaysCallMultiChoiceCallback) { + // If the checkbox was previously selected, and the callback returns true, remove it from the states and uncheck it + if (sendMultiChoiceCallback()) { + cb.setChecked(false); + } else { + // The callback cancelled unselection, re-add it to the states + selectedIndicesList.add(position); + } + } else { + // The callback was not used to check if the unselection is allowed, just uncheck it + cb.setChecked(false); + } + } + } else if (listType == ListType.SINGLE) { + final RadioButton radio = (RadioButton) view.findViewById(R.id.md_control); + if (!radio.isEnabled()) { + return false; + } + boolean allowSelection = true; + final int oldSelected = builder.selectedIndex; + + if (builder.autoDismiss && builder.positiveText == null) { + // If auto dismiss is enabled, and no action button is visible to approve the selection, dismiss the dialog + dismiss(); + // Don't allow the selection to be updated since the dialog is being dismissed anyways + allowSelection = false; + // Update selected index and send callback + builder.selectedIndex = position; + sendSingleChoiceCallback(view); + } else if (builder.alwaysCallSingleChoiceCallback) { + // Temporarily set the new index so the callback uses the right one + builder.selectedIndex = position; + // Only allow the radio button to be checked if the callback returns true + allowSelection = sendSingleChoiceCallback(view); + // Restore the old selected index, so the state is updated below + builder.selectedIndex = oldSelected; + } + // Update the checked states + if (allowSelection) { + builder.selectedIndex = position; + radio.setChecked(true); + builder.adapter.notifyItemChanged(oldSelected); + builder.adapter.notifyItemChanged(position); + } + } + } + return true; + } + + final Drawable getListSelector() { + if (builder.listSelector != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.listSelector, null); + } + final Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_list_selector); + if (d != null) { + return d; + } + return DialogUtils.resolveDrawable(getContext(), R.attr.md_list_selector); + } + + public RecyclerView getRecyclerView() { + return recyclerView; + } + + public boolean isPromptCheckBoxChecked() { + return checkBoxPrompt != null && checkBoxPrompt.isChecked(); + } + + @SuppressWarnings("unused") + public void setPromptCheckBoxChecked(boolean checked) { + if (checkBoxPrompt != null) { + checkBoxPrompt.setChecked(checked); + } + } + + /* package */ Drawable getButtonSelector(DialogAction which, boolean isStacked) { + if (isStacked) { + if (builder.btnSelectorStacked != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorStacked, null); + } + final Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_stacked_selector); + if (d != null) { + return d; + } + return DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_stacked_selector); + } else { + switch (which) { + default: { + if (builder.btnSelectorPositive != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorPositive, null); + } + Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_positive_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), + R.attr.md_btn_positive_selector); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleHelper.applyColor(d, builder.buttonRippleColor); + } + return d; + } + case NEUTRAL: { + if (builder.btnSelectorNeutral != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorNeutral, null); + } + Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_neutral_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), R.attr.md_btn_neutral_selector); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleHelper.applyColor(d, builder.buttonRippleColor); + } + return d; + } + case NEGATIVE: { + if (builder.btnSelectorNegative != 0) { + return ResourcesCompat.getDrawable(builder.context.getResources(), + builder.btnSelectorNegative, null); + } + Drawable d = DialogUtils.resolveDrawable(builder.context, + R.attr.md_btn_negative_selector); + if (d != null) { + return d; + } + d = DialogUtils.resolveDrawable(getContext(), + R.attr.md_btn_negative_selector); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + RippleHelper.applyColor(d, builder.buttonRippleColor); + } + return d; + } + } + } + } + + private boolean sendSingleChoiceCallback(View v) { + if (builder.listCallbackSingleChoice == null) { + return false; + } + CharSequence text = null; + if (builder.selectedIndex >= 0 && builder.selectedIndex < builder.items.size()) { + text = builder.items.get(builder.selectedIndex); + } + return builder.listCallbackSingleChoice.onSelection(this, v, builder.selectedIndex, text); + } + + private boolean sendMultiChoiceCallback() { + if (builder.listCallbackMultiChoice == null) { + return false; + } + Collections.sort(selectedIndicesList); // make sure the indices are in order + List selectedTitles = new ArrayList<>(); + for (Integer i : selectedIndicesList) { + if (i < 0 || i > builder.items.size() - 1) { + continue; + } + selectedTitles.add(builder.items.get(i)); + } + return builder.listCallbackMultiChoice.onSelection(this, + selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]), + selectedTitles.toArray(new CharSequence[selectedTitles.size()])); + } + + @Override + public final void onClick(View v) { + DialogAction tag = (DialogAction) v.getTag(); + switch (tag) { + case POSITIVE: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onPositive(this); + } + if (builder.onPositiveCallback != null) { + builder.onPositiveCallback.onClick(this, tag); + } + if (!builder.alwaysCallSingleChoiceCallback) { + sendSingleChoiceCallback(v); + } + if (!builder.alwaysCallMultiChoiceCallback) { + sendMultiChoiceCallback(); + } + if (builder.inputCallback != null && input != null && + !builder.alwaysCallInputCallback) { + builder.inputCallback.onInput(this, input.getText()); + } + if (builder.autoDismiss) { + dismiss(); + } + break; + } + case NEGATIVE: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onNegative(this); + } + if (builder.onNegativeCallback != null) { + builder.onNegativeCallback.onClick(this, tag); + } + if (builder.autoDismiss) { + cancel(); + } + break; + } + case NEUTRAL: { + if (builder.callback != null) { + builder.callback.onAny(this); + builder.callback.onNeutral(this); + } + if (builder.onNeutralCallback != null) { + builder.onNeutralCallback.onClick(this, tag); + } + if (builder.autoDismiss) { + dismiss(); + } + break; + } + } + if (builder.onAnyCallback != null) { + builder.onAnyCallback.onClick(this, tag); + } + } + + @Override + @UiThread + public void show() { + try { + super.show(); + } catch (WindowManager.BadTokenException e) { + throw new DialogException("Bad window token, you cannot show a dialog " + + "before an Activity is created or after it's hidden."); + } + } + + /** + * Retrieves the view of an action button, allowing you to modify properties such as whether or + * not it's enabled. Use {@link #setActionButton(DialogAction, int)} to change text, since the + * view returned here is not the view that displays text. + * + * @param which The action button of which to get the view for. + * @return The view from the dialog's layout representing this action button. + */ + public final MDButton getActionButton(@NonNull DialogAction which) { + switch (which) { + default: + return positiveButton; + case NEUTRAL: + return neutralButton; + case NEGATIVE: + return negativeButton; + } + } + + /** + * Retrieves the view representing the dialog as a whole. Be careful with this. + */ + public final View getView() { + return view; + } + + @Nullable + public final EditText getInputEditText() { + return input; + } + + /** + * Retrieves the TextView that contains the dialog title. If you want to update the title, use + * #{@link #setTitle(CharSequence)} instead. + */ + @SuppressWarnings("unused") + public final TextView getTitleView() { + return title; + } + + /** + * Retrieves the ImageView that contains the dialog icon. + */ + @SuppressWarnings("unused") + public ImageView getIconView() { + return icon; + } + + /** + * Retrieves the TextView that contains the dialog content. If you want to update the content + * (message), use #{@link #setContent(CharSequence)} instead. + */ + @Nullable + @SuppressWarnings("unused") + public final TextView getContentView() { + return content; + } + + /** + * Retrieves the custom view that was inflated or set to the MaterialDialog during building. + * + * @return The custom view that was passed into the Builder. + */ + @Nullable + public final View getCustomView() { + return builder.customView; + } + + /** + * Updates an action button's title, causing invalidation to check if the action buttons should + * be stacked. Setting an action button's text to null is a shortcut for hiding it, too. + * + * @param which The action button to update. + * @param title The new title of the action button. + */ + @SuppressWarnings("WeakerAccess") + @UiThread + public final void setActionButton(@NonNull final DialogAction which, final CharSequence title) { + switch (which) { + default: + builder.positiveText = title; + positiveButton.setText(title); + positiveButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + case NEUTRAL: + builder.neutralText = title; + neutralButton.setText(title); + neutralButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + case NEGATIVE: + builder.negativeText = title; + negativeButton.setText(title); + negativeButton.setVisibility(title == null ? View.GONE : View.VISIBLE); + break; + } + } + + /** + * Updates an action button's title, causing invalidation to check if the action buttons should + * be stacked. + * + * @param which The action button to update. + * @param titleRes The string resource of the new title of the action button. + */ + public final void setActionButton(DialogAction which, @StringRes int titleRes) { + setActionButton(which, getContext().getText(titleRes)); + } + + /** + * Gets whether or not the positive, neutral, or negative action button is visible. + * + * @return Whether or not 1 or more action buttons is visible. + */ + public final boolean hasActionButtons() { + return numberOfActionButtons() > 0; + } + + /** + * Gets the number of visible action buttons. + * + * @return 0 through 3, depending on how many should be or are visible. + */ + @SuppressWarnings("WeakerAccess") + public final int numberOfActionButtons() { + int number = 0; + if (builder.positiveText != null && positiveButton.getVisibility() == View.VISIBLE) { + number++; + } + if (builder.neutralText != null && neutralButton.getVisibility() == View.VISIBLE) { + number++; + } + if (builder.negativeText != null && negativeButton.getVisibility() == View.VISIBLE) { + number++; + } + return number; + } + + @UiThread + @Override + public final void setTitle(CharSequence newTitle) { + title.setText(newTitle); + } + + @UiThread + @Override + public final void setTitle(@StringRes int newTitleRes) { + setTitle(builder.context.getString(newTitleRes)); + } + + @SuppressWarnings("unused") + @UiThread + public final void setTitle(@StringRes int newTitleRes, @Nullable Object... formatArgs) { + setTitle(builder.context.getString(newTitleRes, formatArgs)); + } + + @UiThread + public void setIcon(@DrawableRes final int resId) { + icon.setImageResource(resId); + icon.setVisibility(resId != 0 ? View.VISIBLE : View.GONE); + } + + @UiThread + public void setIcon(final Drawable d) { + icon.setImageDrawable(d); + icon.setVisibility(d != null ? View.VISIBLE : View.GONE); + } + + @SuppressWarnings("unused") + @UiThread + public void setIconAttribute(@AttrRes int attrId) { + Drawable d = DialogUtils.resolveDrawable(builder.context, attrId); + setIcon(d); + } + + @UiThread + public void setDigitsListener(DigitsKeyListener digitsListener){ + input.setKeyListener(digitsListener); + } + + @UiThread + public final void setContent(CharSequence newContent) { + content.setText(newContent); + content.setVisibility(TextUtils.isEmpty(newContent) ? View.GONE : View.VISIBLE); + } + + @UiThread + public final void setContent(@StringRes int newContentRes) { + setContent(builder.context.getString(newContentRes)); + } + + @SuppressWarnings("unused") + @UiThread + public final void setContent(@StringRes int newContentRes, @Nullable Object... formatArgs) { + setContent(builder.context.getString(newContentRes, formatArgs)); + } + + @Nullable + public final ArrayList getItems() { + return builder.items; + } + + @UiThread + public final void setItems(CharSequence... items) { + if (builder.adapter == null) { + throw new IllegalStateException("This MaterialDialog instance does not " + + "yet have an adapter set to it. You cannot use setItems()."); + } + if (items != null) { + builder.items = new ArrayList<>(items.length); + Collections.addAll(builder.items, items); + } else { + builder.items = null; + } + if (!(builder.adapter instanceof DefaultRvAdapter)) { + throw new IllegalStateException("When using a custom adapter, setItems() " + + "cannot be used. Set items through the adapter instead."); + } + notifyItemsChanged(); + } + + @UiThread + public final void notifyItemInserted( + @IntRange(from = 0, to = Integer.MAX_VALUE) int index) { + builder.adapter.notifyItemInserted(index); + } + + @SuppressWarnings("unused") + @UiThread + public final void notifyItemChanged( + @IntRange(from = 0, to = Integer.MAX_VALUE) int index) { + builder.adapter.notifyItemChanged(index); + } + + @UiThread + public final void notifyItemsChanged() { + builder.adapter.notifyDataSetChanged(); + } + + public final int getCurrentProgress() { + if (progressBar == null) { + return -1; + } + return progressBar.getProgress(); + } + + @SuppressWarnings("unused") + public ProgressBar getProgressBar() { + return progressBar; + } + + public final void incrementProgress(final int by) { + setProgress(getCurrentProgress() + by); + } + + public final void setProgress(final int progress) { + if (builder.progress <= -2) { + Log.w("MaterialDialog", "Calling setProgress(int) on an " + + "indeterminate progress dialog has no effect!"); + return; + } + progressBar.setProgress(progress); + handler.post(new Runnable() { + @Override + public void run() { + if (progressLabel != null) { + progressLabel.setText(builder.progressPercentFormat.format( + (float) getCurrentProgress() / (float) getMaxProgress())); + } + if (progressMinMax != null) { + progressMinMax.setText(String.format(builder.progressNumberFormat, + getCurrentProgress(), getMaxProgress())); + } + } + }); + } + + @SuppressWarnings("unused") + public final boolean isIndeterminateProgress() { + return builder.indeterminateProgress; + } + + public final int getMaxProgress() { + if (progressBar == null) { + return -1; + } + return progressBar.getMax(); + } + + @SuppressWarnings("unused") + public final void setMaxProgress(final int max) { + if (builder.progress <= -2) { + throw new IllegalStateException("Cannot use setMaxProgress() on this dialog."); + } + progressBar.setMax(max); + } + + /** + * Change the format of the small text showing the percentage of progress. The default is + * NumberFormat.getPercentageInstance(). + */ + @SuppressWarnings("unused") + public final void setProgressPercentFormat(NumberFormat format) { + builder.progressPercentFormat = format; + setProgress(getCurrentProgress()); // invalidates display + } + + /** + * Change the format of the small text showing current and maximum units of progress. The + * default is "%1d/%2d". + */ + @SuppressWarnings("unused") + public final void setProgressNumberFormat(String format) { + builder.progressNumberFormat = format; + setProgress(getCurrentProgress()); // invalidates display + } + + public final boolean isCancelled() { + return !isShowing(); + } + + /** + * Convenience method for getting the currently selected index of a single choice list. + * + * @return Currently selected index of a single choice list, or -1 if not showing a single choice + * list + */ + @SuppressWarnings("unused") + public int getSelectedIndex() { + if (builder.listCallbackSingleChoice != null) { + return builder.selectedIndex; + } else { + return -1; + } + } + + /** + * Convenience method for setting the currently selected index of a single choice list. This + * only works if you are not using a custom adapter; if you're using a custom adapter, an + * IllegalStateException is thrown. Note that this does not call the respective single choice + * callback. + * + * @param index The index of the list item to check. + */ + @UiThread + @SuppressWarnings("unused") + public void setSelectedIndex(int index) { + builder.selectedIndex = index; + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + builder.adapter.notifyDataSetChanged(); + } else { + throw new IllegalStateException("You can only use setSelectedIndex() " + + "with the default adapter implementation."); + } + } + + /** + * Convenience method for getting the currently selected indices of a multi choice list + * + * @return Currently selected index of a multi choice list, or null if not showing a multi choice + * list + */ + @Nullable + @SuppressWarnings("unused") + public Integer[] getSelectedIndices() { + if (builder.listCallbackMultiChoice != null) { + return selectedIndicesList.toArray(new Integer[selectedIndicesList.size()]); + } else { + return null; + } + } + + /** + * Convenience method for setting the currently selected indices of a multi choice list. This + * only works if you are not using a custom adapter; if you're using a custom adapter, an + * IllegalStateException is thrown. Note that this does not call the respective multi choice + * callback. + * + * @param indices The indices of the list items to check. + */ + @UiThread + @SuppressWarnings("unused") + public void setSelectedIndices(@NonNull Integer[] indices) { + selectedIndicesList = new ArrayList<>(Arrays.asList(indices)); + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + builder.adapter.notifyDataSetChanged(); + } else { + throw new IllegalStateException("You can only use setSelectedIndices() " + + "with the default adapter implementation."); + } + } + + /** + * Clears all selected checkboxes from multi choice list dialogs. + */ + public void clearSelectedIndices() { + clearSelectedIndices(true); + } + + /** + * Clears all selected checkboxes from multi choice list dialogs. + * + * @param sendCallback Defaults to true. True will notify the multi-choice callback, if any. + */ + @SuppressWarnings("WeakerAccess") + public void clearSelectedIndices(boolean sendCallback) { + if (listType == null || listType != ListType.MULTI) { + throw new IllegalStateException("You can only use clearSelectedIndices() " + + "with multi choice list dialogs."); + } + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + if (selectedIndicesList != null) { + selectedIndicesList.clear(); + } + builder.adapter.notifyDataSetChanged(); + if (sendCallback && builder.listCallbackMultiChoice != null) { + sendMultiChoiceCallback(); + } + } else { + throw new IllegalStateException("You can only use clearSelectedIndices() " + + "with the default adapter implementation."); + } + } + + /** + * Selects all checkboxes in multi choice list dialogs. + */ + @SuppressWarnings("unused") + public void selectAllIndices() { + selectAllIndices(true); + } + + /** + * Selects all checkboxes in multi choice list dialogs. + * + * @param sendCallback Defaults to true. True will notify the multi-choice callback, if any. + */ + @SuppressWarnings("WeakerAccess") + public void selectAllIndices(boolean sendCallback) { + if (listType == null || listType != ListType.MULTI) { + throw new IllegalStateException("You can only use selectAllIndices() with " + + "multi choice list dialogs."); + } + if (builder.adapter != null && builder.adapter instanceof DefaultRvAdapter) { + if (selectedIndicesList == null) { + selectedIndicesList = new ArrayList<>(); + } + for (int i = 0; i < builder.adapter.getItemCount(); i++) { + if (!selectedIndicesList.contains(i)) { + selectedIndicesList.add(i); + } + } + builder.adapter.notifyDataSetChanged(); + if (sendCallback && builder.listCallbackMultiChoice != null) { + sendMultiChoiceCallback(); + } + } else { + throw new IllegalStateException("You can only use selectAllIndices() with the " + + "default adapter implementation."); + } + } + + @Override + public final void onShow(DialogInterface dialog) { + if (input != null) { + DialogUtils.showKeyboard(this, builder); + if (input.getText().length() > 0) { + input.setSelection(input.getText().length()); + } + } + super.onShow(dialog); + } + + void setInternalInputCallback() { + if (input == null) { + return; + } + input.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int length = s.toString().length(); + boolean emptyDisabled = false; + if (!builder.inputAllowEmpty) { + emptyDisabled = length == 0; + final View positiveAb = getActionButton(DialogAction.POSITIVE); + positiveAb.setEnabled(!emptyDisabled); + } + invalidateInputMinMaxIndicator(length, emptyDisabled); + if (builder.alwaysCallInputCallback) { + builder.inputCallback.onInput(MaterialDialog.this, s); + } + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + void invalidateInputMinMaxIndicator(int currentLength, boolean emptyDisabled) { + if (inputMinMax != null) { + if (builder.inputMaxLength > 0) { + inputMinMax.setText( + String.format(Locale.getDefault(), "%d/%d", currentLength, builder.inputMaxLength)); + inputMinMax.setVisibility(View.VISIBLE); + } else { + inputMinMax.setVisibility(View.GONE); + } + final boolean isDisabled = (emptyDisabled && currentLength == 0) || + (builder.inputMaxLength > 0 && currentLength > builder.inputMaxLength) || + currentLength < builder.inputMinLength; + final int colorText = isDisabled ? builder.inputRangeErrorColor : builder.contentColor; + final int colorWidget = isDisabled ? builder.inputRangeErrorColor : builder.widgetColor; + if (builder.inputMaxLength > 0) { + inputMinMax.setTextColor(colorText); + } + MDTintHelper.setTint(input, colorWidget); + final View positiveAb = getActionButton(DialogAction.POSITIVE); + positiveAb.setEnabled(!isDisabled); + } + } + + @Override + public void dismiss() { + if (input != null) { + DialogUtils.hideKeyboard(this, builder); + } + super.dismiss(); + } + + enum ListType { + REGULAR, + SINGLE, + MULTI; + + public static int getLayoutForType(ListType type) { + switch (type) { + case REGULAR: + return R.layout.md_listitem; + case SINGLE: + return R.layout.md_listitem_singlechoice; + case MULTI: + return R.layout.md_listitem_multichoice; + default: + throw new IllegalArgumentException("Not a valid list type"); + } + } + } + + /** + * A callback used for regular list dialogs. + */ + public interface ListCallback { + + void onSelection(MaterialDialog dialog, View itemView, int position, CharSequence text); + } + + /** + * A callback used for regular list dialogs. + */ + public interface ListLongCallback { + + boolean onLongSelection(MaterialDialog dialog, View itemView, int position, CharSequence text); + } + + /** + * A callback used for multi choice (check box) list dialogs. + */ + public interface ListCallbackSingleChoice { + + /** + * Return true to allow the radio button to be checked, if the alwaysCallSingleChoice() + * option is used. + * + * @param dialog The dialog of which a list item was selected. + * @param which The index of the item that was selected. + * @param text The text of the item that was selected. + * @return True to allow the radio button to be selected. + */ + boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text); + } + + /** + * A callback used for multi choice (check box) list dialogs. + */ + public interface ListCallbackMultiChoice { + + /** + * Return true to allow the check box to be checked, if the alwaysCallSingleChoice() option + * is used. + * + * @param dialog The dialog of which a list item was selected. + * @param which The indices of the items that were selected. + * @param text The text of the items that were selected. + * @return True to allow the checkbox to be selected. + */ + boolean onSelection(MaterialDialog dialog, Integer[] which, CharSequence[] text); + } + + /** + * An alternate way to define a single callback. + */ + public interface SingleButtonCallback { + + void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which); + } + + public interface InputCallback { + + void onInput(@NonNull MaterialDialog dialog, CharSequence input); + } + + private static class DialogException extends WindowManager.BadTokenException { + + DialogException(@SuppressWarnings("SameParameterValue") String message) { + super(message); + } + } + + /** + * The class used to construct a MaterialDialog. + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public static class Builder { + + protected final Context context; + protected CharSequence title; + protected GravityEnum titleGravity = GravityEnum.START; + protected GravityEnum contentGravity = GravityEnum.START; + protected GravityEnum btnStackedGravity = GravityEnum.END; + protected GravityEnum itemsGravity = GravityEnum.START; + protected GravityEnum buttonsGravity = GravityEnum.START; + protected int buttonRippleColor = 0; + protected int titleColor = -1; + protected int contentColor = -1; + protected CharSequence content; + protected ArrayList items; + protected CharSequence positiveText; + protected CharSequence neutralText; + protected CharSequence negativeText; + protected boolean positiveFocus; + protected boolean neutralFocus; + protected boolean negativeFocus; + protected View customView; + protected int widgetColor; + protected ColorStateList choiceWidgetColor; + protected ColorStateList positiveColor; + protected ColorStateList negativeColor; + protected ColorStateList neutralColor; + protected ColorStateList linkColor; + protected ButtonCallback callback; + protected SingleButtonCallback onPositiveCallback; + protected SingleButtonCallback onNegativeCallback; + protected SingleButtonCallback onNeutralCallback; + protected SingleButtonCallback onAnyCallback; + protected ListCallback listCallback; + protected ListLongCallback listLongCallback; + protected ListCallbackSingleChoice listCallbackSingleChoice; + protected ListCallbackMultiChoice listCallbackMultiChoice; + protected boolean alwaysCallMultiChoiceCallback = false; + protected boolean alwaysCallSingleChoiceCallback = false; + protected Theme theme = Theme.LIGHT; + protected boolean cancelable = true; + protected boolean canceledOnTouchOutside = true; + protected float contentLineSpacingMultiplier = 1.2f; + protected int selectedIndex = -1; + protected Integer[] selectedIndices = null; + protected Integer[] disabledIndices = null; + protected boolean autoDismiss = true; + protected Typeface regularFont; + protected Typeface mediumFont; + protected Drawable icon; + protected boolean limitIconToDefaultSize; + protected int maxIconSize = -1; + protected RecyclerView.Adapter adapter; + protected RecyclerView.LayoutManager layoutManager; + protected OnDismissListener dismissListener; + protected OnCancelListener cancelListener; + protected OnKeyListener keyListener; + protected DigitsKeyListener digitsKeyListener; + protected OnShowListener showListener; + protected StackingBehavior stackingBehavior; + protected boolean wrapCustomViewInScroll; + protected int dividerColor; + protected int backgroundColor; + protected int itemColor; + protected boolean indeterminateProgress; + protected boolean showMinMax; + protected int progress = -2; + protected int progressMax = 0; + protected CharSequence inputPrefill; + protected CharSequence inputHint; + protected InputCallback inputCallback; + protected boolean inputAllowEmpty; + protected int inputType = -1; + protected boolean alwaysCallInputCallback; + protected int inputMinLength = -1; + protected int inputMaxLength = -1; + protected int inputRangeErrorColor = 0; + protected int[] itemIds; + protected CharSequence checkBoxPrompt; + protected boolean checkBoxPromptInitiallyChecked; + protected CheckBox.OnCheckedChangeListener checkBoxPromptListener; + + protected String progressNumberFormat; + protected NumberFormat progressPercentFormat; + protected boolean indeterminateIsHorizontalProgress; + + protected boolean titleColorSet = false; + protected boolean contentColorSet = false; + protected boolean itemColorSet = false; + protected boolean positiveColorSet = false; + protected boolean neutralColorSet = false; + protected boolean negativeColorSet = false; + protected boolean widgetColorSet = false; + protected boolean dividerColorSet = false; + + @DrawableRes + protected int listSelector; + @DrawableRes + protected int btnSelectorStacked; + @DrawableRes + protected int btnSelectorPositive; + @DrawableRes + protected int btnSelectorNeutral; + @DrawableRes + protected int btnSelectorNegative; + + protected Object tag; + + public Builder(@NonNull Context context) { + this.context = context; + final int materialBlue = DialogUtils.getColor(context, R.color.md_material_blue_600); + + // Retrieve default accent colors, which are used on the action buttons and progress bars + this.widgetColor = DialogUtils.resolveColor(context, + R.attr.colorAccent, materialBlue); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.widgetColor = DialogUtils.resolveColor(context, + android.R.attr.colorAccent, this.widgetColor); + } + + this.positiveColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.negativeColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.neutralColor = DialogUtils.getActionTextStateList(context, this.widgetColor); + this.linkColor = DialogUtils.getActionTextStateList(context, + DialogUtils.resolveColor(context, R.attr.md_link_color, this.widgetColor)); + + int fallback = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + fallback = DialogUtils.resolveColor(context, android.R.attr.colorControlHighlight); + } + this.buttonRippleColor = DialogUtils.resolveColor(context, + R.attr.md_btn_ripple_color, + DialogUtils.resolveColor(context, + R.attr.colorControlHighlight, fallback)); + + this.progressPercentFormat = NumberFormat.getPercentInstance(); + this.progressNumberFormat = "%1d/%2d"; + + // Set the default theme based on the Activity theme's primary color darkness (more white or more black) + final int primaryTextColor = DialogUtils.resolveColor(context, + android.R.attr.textColorPrimary); + this.theme = DialogUtils.isColorDark(primaryTextColor) ? Theme.LIGHT : Theme.DARK; + + // Load theme values from the ThemeSingleton if needed + checkSingleton(); + + // Retrieve gravity settings from global theme attributes if needed + this.titleGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_title_gravity, this.titleGravity); + this.contentGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_content_gravity, this.contentGravity); + this.btnStackedGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_btnstacked_gravity, this.btnStackedGravity); + this.itemsGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_items_gravity, this.itemsGravity); + this.buttonsGravity = DialogUtils.resolveGravityEnum(context, + R.attr.md_buttons_gravity, this.buttonsGravity); + + final String mediumFont = DialogUtils.resolveString(context, + R.attr.md_medium_font); + final String regularFont = DialogUtils.resolveString(context, + R.attr.md_regular_font); + typeface(mediumFont, regularFont); + + if (this.mediumFont == null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.mediumFont = Typeface.create("sans-serif-medium", Typeface.NORMAL); + } else { + this.mediumFont = Typeface.create("sans-serif", Typeface.BOLD); + } + } catch (Exception ignored) { + this.mediumFont = Typeface.DEFAULT_BOLD; + } + } + if (this.regularFont == null) { + try { + this.regularFont = Typeface.create("sans-serif", Typeface.NORMAL); + } catch (Exception ignored) { + this.regularFont = Typeface.SANS_SERIF; + if (this.regularFont == null) { + this.regularFont = Typeface.DEFAULT; + } + } + } + } + + public final Context getContext() { + return context; + } + + public final int getItemColor() { + return itemColor; + } + + public final Typeface getRegularFont() { + return regularFont; + } + + @SuppressWarnings("ConstantConditions") + private void checkSingleton() { + if (ThemeSingleton.get(false) == null) { + return; + } + ThemeSingleton s = ThemeSingleton.get(); + if (s.darkTheme) { + this.theme = Theme.DARK; + } + if (s.titleColor != 0) { + this.titleColor = s.titleColor; + } + if (s.contentColor != 0) { + this.contentColor = s.contentColor; + } + if (s.positiveColor != null) { + this.positiveColor = s.positiveColor; + } + if (s.neutralColor != null) { + this.neutralColor = s.neutralColor; + } + if (s.negativeColor != null) { + this.negativeColor = s.negativeColor; + } + if (s.itemColor != 0) { + this.itemColor = s.itemColor; + } + if (s.icon != null) { + this.icon = s.icon; + } + if (s.backgroundColor != 0) { + this.backgroundColor = s.backgroundColor; + } + if (s.dividerColor != 0) { + this.dividerColor = s.dividerColor; + } + if (s.btnSelectorStacked != 0) { + this.btnSelectorStacked = s.btnSelectorStacked; + } + if (s.listSelector != 0) { + this.listSelector = s.listSelector; + } + if (s.btnSelectorPositive != 0) { + this.btnSelectorPositive = s.btnSelectorPositive; + } + if (s.btnSelectorNeutral != 0) { + this.btnSelectorNeutral = s.btnSelectorNeutral; + } + if (s.btnSelectorNegative != 0) { + this.btnSelectorNegative = s.btnSelectorNegative; + } + if (s.widgetColor != 0) { + this.widgetColor = s.widgetColor; + } + if (s.linkColor != null) { + this.linkColor = s.linkColor; + } + this.titleGravity = s.titleGravity; + this.contentGravity = s.contentGravity; + this.btnStackedGravity = s.btnStackedGravity; + this.itemsGravity = s.itemsGravity; + this.buttonsGravity = s.buttonsGravity; + } + + public Builder title(@StringRes int titleRes) { + title(this.context.getText(titleRes)); + return this; + } + + public Builder title(@NonNull CharSequence title) { + this.title = title; + return this; + } + + public Builder titleGravity(@NonNull GravityEnum gravity) { + this.titleGravity = gravity; + return this; + } + + public Builder buttonRippleColor(@ColorInt int color) { + this.buttonRippleColor = color; + return this; + } + + public Builder buttonRippleColorRes(@ColorRes int colorRes) { + return buttonRippleColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder buttonRippleColorAttr(@AttrRes int colorAttr) { + return buttonRippleColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder titleColor(@ColorInt int color) { + this.titleColor = color; + this.titleColorSet = true; + return this; + } + + public Builder titleColorRes(@ColorRes int colorRes) { + return titleColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder titleColorAttr(@AttrRes int colorAttr) { + return titleColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + /** + * Sets the fonts used in the dialog. It's recommended that you use {@link #typeface(String, + * String)} instead, to avoid duplicate Typeface allocations and high memory usage. + * + * @param medium The font used on titles and action buttons. Null uses device default. + * @param regular The font used everywhere else, like on the content and list items. Null uses + * device default. + * @return The Builder instance so you can chain calls to it. + */ + public Builder typeface(@Nullable Typeface medium, @Nullable Typeface regular) { + this.mediumFont = medium; + this.regularFont = regular; + return this; + } + + /** + * Sets the fonts used in the dialog, by file names. This also uses TypefaceHelper in order + * to avoid any un-needed allocations (it recycles typefaces for you). + * + * @param medium The name of font in assets/fonts used on titles and action buttons (null uses + * device default). E.g. [your-project]/app/main/assets/fonts/[medium] + * @param regular The name of font in assets/fonts used everywhere else, like content and list + * items (null uses device default). E.g. [your-project]/app/main/assets/fonts/[regular] + * @return The Builder instance so you can chain calls to it. + */ + public Builder typeface(@Nullable String medium, @Nullable String regular) { + if (medium != null) { + this.mediumFont = TypefaceHelper.get(this.context, medium); + if (this.mediumFont == null) { + throw new IllegalArgumentException("No font asset found for " + medium); + } + } + if (regular != null) { + this.regularFont = TypefaceHelper.get(this.context, regular); + if (this.regularFont == null) { + throw new IllegalArgumentException("No font asset found for " + regular); + } + } + return this; + } + + public Builder icon(@NonNull Drawable icon) { + this.icon = icon; + return this; + } + + public Builder iconRes(@DrawableRes int icon) { + this.icon = ResourcesCompat.getDrawable(context.getResources(), icon, null); + return this; + } + + public Builder iconAttr(@AttrRes int iconAttr) { + this.icon = DialogUtils.resolveDrawable(context, iconAttr); + return this; + } + + public Builder content(@StringRes int contentRes) { + return content(contentRes, false); + } + + public Builder content(@StringRes int contentRes, boolean html) { + CharSequence text = this.context.getText(contentRes); + if (html) { + text = Html.fromHtml(text.toString().replace("\n", "
")); + } + return content(text); + } + + public Builder content(@NonNull CharSequence content) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set content() " + + "when you're using a custom view."); + } + this.content = content; + return this; + } + + public Builder content(@StringRes int contentRes, Object... formatArgs) { + String str = String.format(this.context.getString(contentRes), formatArgs) + .replace("\n", "
"); + //noinspection deprecation + return content(Html.fromHtml(str)); + } + + public Builder contentColor(@ColorInt int color) { + this.contentColor = color; + this.contentColorSet = true; + return this; + } + + public Builder contentColorRes(@ColorRes int colorRes) { + contentColor(DialogUtils.getColor(this.context, colorRes)); + return this; + } + + public Builder contentColorAttr(@AttrRes int colorAttr) { + contentColor(DialogUtils.resolveColor(this.context, colorAttr)); + return this; + } + + public Builder contentGravity(@NonNull GravityEnum gravity) { + this.contentGravity = gravity; + return this; + } + + public Builder contentLineSpacing(float multiplier) { + this.contentLineSpacingMultiplier = multiplier; + return this; + } + + public Builder items(@NonNull Collection collection) { + if (collection.size() > 0) { + final CharSequence[] array = new CharSequence[collection.size()]; + int i = 0; + for (Object obj : collection) { + array[i] = obj.toString(); + i++; + } + items(array); + } else if (collection.size() == 0) { + items = new ArrayList<>(); + } + return this; + } + + public Builder items(@ArrayRes int itemsRes) { + items(this.context.getResources().getTextArray(itemsRes)); + return this; + } + + public Builder items(@NonNull CharSequence... items) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set items()" + + " when you're using a custom view."); + } + this.items = new ArrayList<>(); + Collections.addAll(this.items, items); + return this; + } + + public Builder itemsCallback(@NonNull ListCallback callback) { + this.listCallback = callback; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = null; + return this; + } + + public Builder itemsLongCallback(@NonNull ListLongCallback callback) { + this.listLongCallback = callback; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = null; + return this; + } + + public Builder itemsColor(@ColorInt int color) { + this.itemColor = color; + this.itemColorSet = true; + return this; + } + + public Builder itemsColorRes(@ColorRes int colorRes) { + return itemsColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder itemsColorAttr(@AttrRes int colorAttr) { + return itemsColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder itemsGravity(@NonNull GravityEnum gravity) { + this.itemsGravity = gravity; + return this; + } + + public Builder itemsIds(@NonNull int[] idsArray) { + this.itemIds = idsArray; + return this; + } + + public Builder itemsIds(@ArrayRes int idsArrayRes) { + return itemsIds(context.getResources().getIntArray(idsArrayRes)); + } + + public Builder buttonsGravity(@NonNull GravityEnum gravity) { + this.buttonsGravity = gravity; + return this; + } + + /** + * Pass anything below 0 (such as -1) for the selected index to leave all options unselected + * initially. Otherwise pass the index of an item that will be selected initially. + * + * @param selectedIndex The checkbox index that will be selected initially. + * @param callback The callback that will be called when the presses the positive button. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsCallbackSingleChoice(int selectedIndex, + @NonNull ListCallbackSingleChoice callback) { + this.selectedIndex = selectedIndex; + this.listCallback = null; + this.listCallbackSingleChoice = callback; + this.listCallbackMultiChoice = null; + return this; + } + + /** + * By default, the single choice callback is only called when the user clicks the positive + * button or if there are no buttons. Call this to force it to always call on item clicks + * even if the positive button exists. + * + * @return The Builder instance so you can chain calls to it. + */ + public Builder alwaysCallSingleChoiceCallback() { + this.alwaysCallSingleChoiceCallback = true; + return this; + } + + /** + * Pass null for the selected indices to leave all options unselected initially. Otherwise + * pass an array of indices that will be selected initially. + * + * @param selectedIndices The radio button indices that will be selected initially. + * @param callback The callback that will be called when the presses the positive button. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsCallbackMultiChoice(@Nullable Integer[] selectedIndices, + @NonNull ListCallbackMultiChoice callback) { + this.selectedIndices = selectedIndices; + this.listCallback = null; + this.listCallbackSingleChoice = null; + this.listCallbackMultiChoice = callback; + return this; + } + + /** + * Sets indices of items that are not clickable. If they are checkboxes or radio buttons, + * they will not be toggleable. + * + * @param disabledIndices The item indices that will be disabled from selection. + * @return The Builder instance so you can chain calls to it. + */ + public Builder itemsDisabledIndices(@Nullable Integer... disabledIndices) { + this.disabledIndices = disabledIndices; + return this; + } + + /** + * By default, the multi choice callback is only called when the user clicks the positive + * button or if there are no buttons. Call this to force it to always call on item clicks + * even if the positive button exists. + * + * @return The Builder instance so you can chain calls to it. + */ + public Builder alwaysCallMultiChoiceCallback() { + this.alwaysCallMultiChoiceCallback = true; + return this; + } + + public Builder positiveText(@StringRes int positiveRes) { + if (positiveRes == 0) { + return this; + } + positiveText(this.context.getText(positiveRes)); + return this; + } + + public Builder positiveText(@NonNull CharSequence message) { + this.positiveText = message; + return this; + } + + public Builder positiveColor(@ColorInt int color) { + return positiveColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder positiveColorRes(@ColorRes int colorRes) { + return positiveColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder positiveColorAttr(@AttrRes int colorAttr) { + return positiveColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder positiveColor(@NonNull ColorStateList colorStateList) { + this.positiveColor = colorStateList; + this.positiveColorSet = true; + return this; + } + + public Builder positiveFocus(boolean isFocusedDefault) { + this.positiveFocus = isFocusedDefault; + return this; + } + + public Builder neutralText(@StringRes int neutralRes) { + if (neutralRes == 0) { + return this; + } + return neutralText(this.context.getText(neutralRes)); + } + + public Builder neutralText(@NonNull CharSequence message) { + this.neutralText = message; + return this; + } + + public Builder negativeColor(@ColorInt int color) { + return negativeColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder negativeColorRes(@ColorRes int colorRes) { + return negativeColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder negativeColorAttr(@AttrRes int colorAttr) { + return negativeColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder negativeColor(@NonNull ColorStateList colorStateList) { + this.negativeColor = colorStateList; + this.negativeColorSet = true; + return this; + } + + public Builder negativeText(@StringRes int negativeRes) { + if (negativeRes == 0) { + return this; + } + return negativeText(this.context.getText(negativeRes)); + } + + public Builder negativeText(@NonNull CharSequence message) { + this.negativeText = message; + return this; + } + + public Builder negativeFocus(boolean isFocusedDefault) { + this.negativeFocus = isFocusedDefault; + return this; + } + + public Builder neutralColor(@ColorInt int color) { + return neutralColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder neutralColorRes(@ColorRes int colorRes) { + return neutralColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder neutralColorAttr(@AttrRes int colorAttr) { + return neutralColor( + DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder neutralColor(@NonNull ColorStateList colorStateList) { + this.neutralColor = colorStateList; + this.neutralColorSet = true; + return this; + } + + public Builder neutralFocus(boolean isFocusedDefault) { + this.neutralFocus = isFocusedDefault; + return this; + } + + public Builder linkColor(@ColorInt int color) { + return linkColor(DialogUtils.getActionTextStateList(context, color)); + } + + public Builder linkColorRes(@ColorRes int colorRes) { + return linkColor(DialogUtils.getActionTextColorStateList(this.context, colorRes)); + } + + public Builder linkColorAttr(@AttrRes int colorAttr) { + return linkColor(DialogUtils.resolveActionTextColorStateList(this.context, colorAttr, null)); + } + + public Builder linkColor(@NonNull ColorStateList colorStateList) { + this.linkColor = colorStateList; + return this; + } + + public Builder listSelector(@DrawableRes int selectorRes) { + this.listSelector = selectorRes; + return this; + } + + public Builder btnSelectorStacked(@DrawableRes int selectorRes) { + this.btnSelectorStacked = selectorRes; + return this; + } + + public Builder btnSelector(@DrawableRes int selectorRes) { + this.btnSelectorPositive = selectorRes; + this.btnSelectorNeutral = selectorRes; + this.btnSelectorNegative = selectorRes; + return this; + } + + public Builder btnSelector(@DrawableRes int selectorRes, @NonNull DialogAction which) { + switch (which) { + default: + this.btnSelectorPositive = selectorRes; + break; + case NEUTRAL: + this.btnSelectorNeutral = selectorRes; + break; + case NEGATIVE: + this.btnSelectorNegative = selectorRes; + break; + } + return this; + } + + /** + * Sets the gravity used for the text in stacked action buttons. By default, it's #{@link + * GravityEnum#END}. + * + * @param gravity The gravity to use. + * @return The Builder instance so calls can be chained. + */ + public Builder btnStackedGravity(@NonNull GravityEnum gravity) { + this.btnStackedGravity = gravity; + return this; + } + + public Builder checkBoxPrompt(@NonNull CharSequence prompt, boolean initiallyChecked, + @Nullable CheckBox.OnCheckedChangeListener checkListener) { + this.checkBoxPrompt = prompt; + this.checkBoxPromptInitiallyChecked = initiallyChecked; + this.checkBoxPromptListener = checkListener; + return this; + } + + public Builder checkBoxPromptRes(@StringRes int prompt, boolean initiallyChecked, + @Nullable CheckBox.OnCheckedChangeListener checkListener) { + return checkBoxPrompt(context.getResources().getText(prompt), + initiallyChecked, checkListener); + } + + public Builder customView(@LayoutRes int layoutRes, boolean wrapInScrollView) { + LayoutInflater li = LayoutInflater.from(this.context); + return customView(li.inflate(layoutRes, null), wrapInScrollView); + } + + public Builder customView(@NonNull View view, boolean wrapInScrollView) { + if (this.content != null) { + throw new IllegalStateException("You cannot use customView() when you have content set."); + } else if (this.items != null) { + throw new IllegalStateException("You cannot use customView() when you have items set."); + } else if (this.inputCallback != null) { + throw new IllegalStateException("You cannot use customView() with an input dialog"); + } else if (this.progress > -2 || this.indeterminateProgress) { + throw new IllegalStateException("You cannot use customView() with a progress dialog"); + } + if (view.getParent() != null && view.getParent() instanceof ViewGroup) { + ((ViewGroup) view.getParent()).removeView(view); + } + this.customView = view; + this.wrapCustomViewInScroll = wrapInScrollView; + return this; + } + + /** + * Makes this dialog a progress dialog. + * + * @param indeterminate If true, an infinite circular spinner is shown. If false, a horizontal + * progress bar is shown that is incremented or set via the built MaterialDialog instance. + * @param max When indeterminate is false, the max value the horizontal progress bar can get + * to. + * @return An instance of the Builder so calls can be chained. + */ + public Builder progress(boolean indeterminate, int max) { + if (this.customView != null) { + throw new IllegalStateException( + "You cannot set progress() when you're using a custom view."); + } + if (indeterminate) { + this.indeterminateProgress = true; + this.progress = -2; + } else { + this.indeterminateIsHorizontalProgress = false; + this.indeterminateProgress = false; + this.progress = -1; + this.progressMax = max; + } + return this; + } + + /** + * Makes this dialog a progress dialog. + * + * @param indeterminate If true, an infinite circular spinner is shown. If false, a horizontal + * progress bar is shown that is incremented or set via the built MaterialDialog instance. + * @param max When indeterminate is false, the max value the horizontal progress bar can get + * to. + * @param showMinMax For determinate dialogs, the min and max will be displayed to the left + * (start) of the progress bar, e.g. 50/100. + * @return An instance of the Builder so calls can be chained. + */ + public Builder progress(boolean indeterminate, int max, boolean showMinMax) { + this.showMinMax = showMinMax; + return progress(indeterminate, max); + } + + /** + * hange the format of the small text showing current and maximum units of progress. The + * default is "%1d/%2d". + */ + public Builder progressNumberFormat(@NonNull String format) { + this.progressNumberFormat = format; + return this; + } + + /** + * Change the format of the small text showing the percentage of progress. The default is + * NumberFormat.getPercentageInstance(). + */ + public Builder progressPercentFormat(@NonNull NumberFormat format) { + this.progressPercentFormat = format; + return this; + } + + /** + * By default, indeterminate progress dialogs will use a circular indicator. You can change + * it to use a horizontal progress indicator. + */ + public Builder progressIndeterminateStyle(boolean horizontal) { + this.indeterminateIsHorizontalProgress = horizontal; + return this; + } + + public Builder widgetColor(@ColorInt int color) { + this.widgetColor = color; + this.widgetColorSet = true; + return this; + } + + public Builder widgetColorRes(@ColorRes int colorRes) { + return widgetColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder widgetColorAttr(@AttrRes int colorAttr) { + return widgetColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder choiceWidgetColor(@Nullable ColorStateList colorStateList) { + this.choiceWidgetColor = colorStateList; + return this; + } + + public Builder dividerColor(@ColorInt int color) { + this.dividerColor = color; + this.dividerColorSet = true; + return this; + } + + public Builder dividerColorRes(@ColorRes int colorRes) { + return dividerColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder dividerColorAttr(@AttrRes int colorAttr) { + return dividerColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder backgroundColor(@ColorInt int color) { + this.backgroundColor = color; + return this; + } + + public Builder backgroundColorRes(@ColorRes int colorRes) { + return backgroundColor(DialogUtils.getColor(this.context, colorRes)); + } + + public Builder backgroundColorAttr(@AttrRes int colorAttr) { + return backgroundColor(DialogUtils.resolveColor(this.context, colorAttr)); + } + + public Builder callback(@NonNull ButtonCallback callback) { + this.callback = callback; + return this; + } + + public Builder onPositive(@NonNull SingleButtonCallback callback) { + this.onPositiveCallback = callback; + return this; + } + + public Builder onNegative(@NonNull SingleButtonCallback callback) { + this.onNegativeCallback = callback; + return this; + } + + public Builder onNeutral(@NonNull SingleButtonCallback callback) { + this.onNeutralCallback = callback; + return this; + } + + public Builder onAny(@NonNull SingleButtonCallback callback) { + this.onAnyCallback = callback; + return this; + } + + public Builder theme(@NonNull Theme theme) { + this.theme = theme; + return this; + } + + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + this.canceledOnTouchOutside = cancelable; + return this; + } + + public Builder canceledOnTouchOutside(boolean canceledOnTouchOutside) { + this.canceledOnTouchOutside = canceledOnTouchOutside; + return this; + } + + /** + * This defaults to true. If set to false, the dialog will not automatically be dismissed + * when an action button is pressed, and not automatically dismissed when the user selects a + * list item. + * + * @param dismiss Whether or not to dismiss the dialog automatically. + * @return The Builder instance so you can chain calls to it. + */ + public Builder autoDismiss(boolean dismiss) { + this.autoDismiss = dismiss; + return this; + } + + /** + * Sets a custom {@link RecyclerView.Adapter} for the dialog's + * list + * + * @param adapter The adapter to set to the list. + * @param layoutManager The layout manager to use in the RecyclerView. Pass null to use the + * default linear manager. + * @return This Builder object to allow for chaining of calls to set methods + */ + @SuppressWarnings("ConstantConditions") + public Builder adapter( + @NonNull RecyclerView.Adapter adapter, + @Nullable RecyclerView.LayoutManager layoutManager) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set adapter() when " + + "you're using a custom view."); + } + if (layoutManager != null && !(layoutManager instanceof LinearLayoutManager) && + !(layoutManager instanceof GridLayoutManager)) { + throw new IllegalStateException("You can currently only use LinearLayoutManager" + + " and GridLayoutManager with this library."); + } + this.adapter = adapter; + this.layoutManager = layoutManager; + return this; + } + + /** + * Limits the display size of a set icon to 48dp. + */ + public Builder limitIconToDefaultSize() { + this.limitIconToDefaultSize = true; + return this; + } + + public Builder maxIconSize(int maxIconSize) { + this.maxIconSize = maxIconSize; + return this; + } + + public Builder maxIconSizeRes(@DimenRes int maxIconSizeRes) { + return maxIconSize((int) this.context.getResources().getDimension(maxIconSizeRes)); + } + + public Builder showListener(@NonNull OnShowListener listener) { + this.showListener = listener; + return this; + } + + public Builder dismissListener(@NonNull OnDismissListener listener) { + this.dismissListener = listener; + return this; + } + + public Builder cancelListener(@NonNull OnCancelListener listener) { + this.cancelListener = listener; + return this; + } + + public Builder keyListener(@NonNull OnKeyListener listener) { + this.keyListener = listener; + return this; + } + + public Builder DigitsKeyListener(@NonNull DigitsKeyListener digitsKeyListener) { + this.digitsKeyListener = digitsKeyListener; + return this; + } + + /** + * Sets action button stacking behavior. + * + * @param behavior The behavior of the action button stacking logic. + * @return The Builder instance so you can chain calls to it. + */ + public Builder stackingBehavior(@NonNull StackingBehavior behavior) { + this.stackingBehavior = behavior; + return this; + } + + public Builder input(@Nullable CharSequence hint, @Nullable CharSequence prefill, + boolean allowEmptyInput, @NonNull InputCallback callback) { + if (this.customView != null) { + throw new IllegalStateException("You cannot set content() when " + + "you're using a custom view."); + } + this.inputCallback = callback; + this.inputHint = hint; + this.inputPrefill = prefill; + this.inputAllowEmpty = allowEmptyInput; + return this; + } + + public Builder input(@Nullable CharSequence hint, @Nullable CharSequence prefill, + @NonNull InputCallback callback) { + return input(hint, prefill, true, callback); + } + + public Builder input(@StringRes int hint, @StringRes int prefill, + boolean allowEmptyInput, @NonNull InputCallback callback) { + return input(hint == 0 ? null : context.getText(hint), prefill == 0 ? null : + context.getText(prefill), allowEmptyInput, callback); + } + + public Builder input(@StringRes int hint, @StringRes int prefill, + @NonNull InputCallback callback) { + return input(hint, prefill, true, callback); + } + + public Builder inputType(int type) { + this.inputType = type; + return this; + } + + public Builder inputRange(@IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength) { + return inputRange(minLength, maxLength, 0); + } + + /** + * @param errorColor Pass in 0 for the default red error color (as specified in guidelines). + */ + public Builder inputRange(@IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength, + @ColorInt int errorColor) { + if (minLength < 0) { + throw new IllegalArgumentException("Min length for input dialogs " + + "cannot be less than 0."); + } + this.inputMinLength = minLength; + this.inputMaxLength = maxLength; + if (errorColor == 0) { + this.inputRangeErrorColor = DialogUtils.getColor(context, + R.color.md_edittext_error); + } else { + this.inputRangeErrorColor = errorColor; + } + if (this.inputMinLength > 0) { + this.inputAllowEmpty = false; + } + return this; + } + + /** + * Same as #{@link #inputRange(int, int, int)}, but it takes a color resource ID for the + * error color. + */ + public Builder inputRangeRes(@IntRange(from = 0, to = Integer.MAX_VALUE) int minLength, + @IntRange(from = -1, to = Integer.MAX_VALUE) int maxLength, + @ColorRes int errorColor) { + return inputRange(minLength, maxLength, + DialogUtils.getColor(context, errorColor)); + } + + public Builder alwaysCallInputCallback() { + this.alwaysCallInputCallback = true; + return this; + } + + public Builder tag(@Nullable Object tag) { + this.tag = tag; + return this; + } + + @UiThread + public MaterialDialog build() { + return new MaterialDialog(this); + } + + @UiThread + public MaterialDialog show() { + MaterialDialog dialog = build(); + dialog.show(); + return dialog; + } + } + + /** + * Override these as needed, so no needing to sub empty methods from an interface + * + * @deprecated Use the individual onPositive, onNegative, onNeutral, or onAny Builder methods + * instead. + */ + @SuppressWarnings({"WeakerAccess", "UnusedParameters"}) + @Deprecated + public static abstract class ButtonCallback { + + public ButtonCallback() { + super(); + } + + @Deprecated + public void onAny(MaterialDialog dialog) { + } + + @Deprecated + public void onPositive(MaterialDialog dialog) { + } + + @Deprecated + public void onNegative(MaterialDialog dialog) { + } + + // The overidden methods below prevent Android Studio from suggesting that they are overidden by developers + + @Deprecated + public void onNeutral(MaterialDialog dialog) { + } + + @Override + protected final Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + @Override + public final boolean equals(Object o) { + return super.equals(o); + } + + @Override + protected final void finalize() throws Throwable { + super.finalize(); + } + + @Override + public final int hashCode() { + return super.hashCode(); + } + + @Override + public final String toString() { + return super.toString(); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java b/widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java new file mode 100644 index 0000000..112a86a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/StackingBehavior.java @@ -0,0 +1,19 @@ +package com.hzecool.widget.materialdialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public enum StackingBehavior { + /** + * The action buttons are always stacked vertically. + */ + ALWAYS, + /** + * The action buttons are stacked vertically IF it is necessary for them to fit in the dialog. + */ + ADAPTIVE, + /** + * The action buttons are never stacked, even if they should be. + */ + NEVER +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java b/widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java new file mode 100644 index 0000000..5f2cf77 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/Theme.java @@ -0,0 +1,8 @@ +package com.hzecool.widget.materialdialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public enum Theme { + LIGHT, DARK +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java new file mode 100644 index 0000000..7ca27d1 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDAdapter.java @@ -0,0 +1,12 @@ +package com.hzecool.widget.materialdialog.internal; + + +import com.hzecool.widget.materialdialog.MaterialDialog; + +/** + * @author Aidan Follestad (afollestad) + */ +public interface MDAdapter { + + void setDialog(MaterialDialog dialog); +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java new file mode 100644 index 0000000..5ba7d82 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDButton.java @@ -0,0 +1,97 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.v7.text.AllCapsTransformationMethod; +import android.support.v7.widget.AppCompatTextView; +import android.util.AttributeSet; +import android.view.Gravity; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.GravityEnum; +import com.hzecool.widget.materialdialog.util.DialogUtils; + + +/** + * @author Kevin Barry (teslacoil) 4/02/2015 + */ +public class MDButton extends AppCompatTextView { + + private boolean stacked = false; + private GravityEnum stackedGravity; + + private int stackedEndPadding; + private Drawable stackedBackground; + private Drawable defaultBackground; + + public MDButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public MDButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + stackedEndPadding = context.getResources() + .getDimensionPixelSize(R.dimen.md_dialog_frame_margin); + stackedGravity = GravityEnum.END; + } + + /** + * Set if the button should be displayed in stacked mode. + * This should only be called from MDRootLayout's onMeasure, and we must be measured + * after calling this. + */ + /* package */ void setStacked(boolean stacked, boolean force) { + if (this.stacked != stacked || force) { + + setGravity( + stacked ? (Gravity.CENTER_VERTICAL | stackedGravity.getGravityInt()) : Gravity.CENTER); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + //noinspection ResourceType + setTextAlignment(stacked ? stackedGravity.getTextAlignment() : TEXT_ALIGNMENT_CENTER); + } + + DialogUtils.setBackgroundCompat(this, stacked ? stackedBackground : defaultBackground); + if (stacked) { + setPadding(stackedEndPadding, getPaddingTop(), stackedEndPadding, getPaddingBottom()); + } /* Else the padding was properly reset by the drawable */ + + this.stacked = stacked; + } + } + + public void setStackedGravity(GravityEnum gravity) { + stackedGravity = gravity; + } + + public void setStackedSelector(Drawable d) { + stackedBackground = d; + if (stacked) { + setStacked(true, true); + } + } + + public void setDefaultSelector(Drawable d) { + defaultBackground = d; + if (!stacked) { + setStacked(false, true); + } + } + + public void setAllCapsCompat(boolean allCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + setAllCaps(allCaps); + } else { + if (allCaps) { + setTransformationMethod(new AllCapsTransformationMethod(getContext())); + } else { + setTransformationMethod(null); + } + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java new file mode 100644 index 0000000..2f3a128 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDRootLayout.java @@ -0,0 +1,631 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.webkit.WebView; +import android.widget.AdapterView; +import android.widget.ScrollView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.GravityEnum; +import com.hzecool.widget.materialdialog.StackingBehavior; +import com.hzecool.widget.materialdialog.util.DialogUtils; + +/** + * @author Kevin Barry (teslacoil) 4/02/2015 This is the top level view for all MaterialDialogs It + * handles the layout of: titleFrame (md_stub_titleframe) content (text, custom view, + * listview, etc) buttonDefault... (either stacked or horizontal) + */ +public class MDRootLayout extends ViewGroup { + + private static final int INDEX_NEUTRAL = 0; + private static final int INDEX_NEGATIVE = 1; + private static final int INDEX_POSITIVE = 2; + private final MDButton[] buttons = new MDButton[3]; + private int maxHeight; + private View titleBar; + private View content; + private boolean drawTopDivider = false; + private boolean drawBottomDivider = false; + private StackingBehavior stackBehavior = StackingBehavior.ADAPTIVE; + private boolean isStacked = false; + private boolean useFullPadding = true; + private boolean reducePaddingNoTitleNoButtons; + private boolean noTitleNoPadding; + + private int noTitlePaddingFull; + private int buttonPaddingFull; + private int buttonBarHeight; + + private GravityEnum buttonGravity = GravityEnum.START; + + /* Margin from dialog frame to first button */ + private int buttonHorizontalEdgeMargin; + + private Paint dividerPaint; + + private ViewTreeObserver.OnScrollChangedListener topOnScrollChangedListener; + private ViewTreeObserver.OnScrollChangedListener bottomOnScrollChangedListener; + private int dividerWidth; + + public MDRootLayout(Context context) { + super(context); + init(context, null, 0); + } + + public MDRootLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public MDRootLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr); + } + + private static boolean isVisible(View v) { + boolean visible = v != null && v.getVisibility() != View.GONE; + if (visible && v instanceof MDButton) { + visible = ((MDButton) v).getText().toString().trim().length() > 0; + } + return visible; + } + + public static boolean canRecyclerViewScroll(RecyclerView view) { + return view != null && view.getLayoutManager() != null && view.getLayoutManager() + .canScrollVertically(); + } + + private static boolean canScrollViewScroll(ScrollView sv) { + if (sv.getChildCount() == 0) { + return false; + } + final int childHeight = sv.getChildAt(0).getMeasuredHeight(); + return sv.getMeasuredHeight() - sv.getPaddingTop() - sv.getPaddingBottom() < childHeight; + } + + private static boolean canWebViewScroll(WebView view) { + //noinspection deprecation + return view.getMeasuredHeight() < view.getContentHeight() * view.getScale(); + } + + private static boolean canAdapterViewScroll(AdapterView lv) { + /* Force it to layout it's children */ + if (lv.getLastVisiblePosition() == -1) { + return false; + } + + /* We can scroll if the first or last item is not visible */ + boolean firstItemVisible = lv.getFirstVisiblePosition() == 0; + boolean lastItemVisible = lv.getLastVisiblePosition() == lv.getCount() - 1; + + if (firstItemVisible && lastItemVisible && lv.getChildCount() > 0) { + /* Or the first item's top is above or own top */ + if (lv.getChildAt(0).getTop() < lv.getPaddingTop()) { + return true; + } + /* or the last item's bottom is beyond our own bottom */ + return lv.getChildAt(lv.getChildCount() - 1).getBottom() > + lv.getHeight() - lv.getPaddingBottom(); + } + + return true; + } + + /** + * Find the view touching the bottom of this ViewGroup. Non visible children are ignored, + * however getChildDrawingOrder is not taking into account for simplicity and because it behaves + * inconsistently across platform versions. + * + * @return View touching the bottom of this ViewGroup or null + */ + @Nullable + private static View getBottomView(ViewGroup viewGroup) { + if (viewGroup == null || viewGroup.getChildCount() == 0) { + return null; + } + View bottomView = null; + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && child.getBottom() == viewGroup + .getMeasuredHeight()) { + bottomView = child; + break; + } + } + return bottomView; + } + + @Nullable + private static View getTopView(ViewGroup viewGroup) { + if (viewGroup == null || viewGroup.getChildCount() == 0) { + return null; + } + View topView = null; + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + View child = viewGroup.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && child.getTop() == 0) { + topView = child; + break; + } + } + return topView; + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + Resources r = context.getResources(); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MDRootLayout, defStyleAttr, 0); + reducePaddingNoTitleNoButtons = a + .getBoolean(R.styleable.MDRootLayout_md_reduce_padding_no_title_no_buttons, true); + a.recycle(); + + noTitlePaddingFull = r.getDimensionPixelSize(R.dimen.md_notitle_vertical_padding); + buttonPaddingFull = r.getDimensionPixelSize(R.dimen.md_button_frame_vertical_padding); + + buttonHorizontalEdgeMargin = r.getDimensionPixelSize(R.dimen.md_button_padding_frame_side); + buttonBarHeight = r.getDimensionPixelSize(R.dimen.md_button_height); + + dividerPaint = new Paint(); + dividerWidth = r.getDimensionPixelSize(R.dimen.md_divider_height); + dividerPaint.setColor(DialogUtils.resolveColor(context, R.attr.md_divider_color)); + setWillNotDraw(false); + } + + public void setMaxHeight(int maxHeight) { + this.maxHeight = maxHeight; + } + + public void noTitleNoPadding() { + noTitleNoPadding = true; + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + if (v.getId() == R.id.md_titleFrame) { + titleBar = v; + } else if (v.getId() == R.id.md_buttonDefaultNeutral) { + buttons[INDEX_NEUTRAL] = (MDButton) v; + } else if (v.getId() == R.id.md_buttonDefaultNegative) { + buttons[INDEX_NEGATIVE] = (MDButton) v; + } else if (v.getId() == R.id.md_buttonDefaultPositive) { + buttons[INDEX_POSITIVE] = (MDButton) v; + } else { + content = v; + } + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + if (height > maxHeight) { + height = maxHeight; + } + + useFullPadding = true; + boolean hasButtons = false; + + final boolean stacked; + if (stackBehavior == StackingBehavior.ALWAYS) { + stacked = true; + } else if (stackBehavior == StackingBehavior.NEVER) { + stacked = false; + } else { + int buttonsWidth = 0; + for (MDButton button : buttons) { + if (button != null && isVisible(button)) { + button.setStacked(false, false); + measureChild(button, widthMeasureSpec, heightMeasureSpec); + buttonsWidth += button.getMeasuredWidth(); + hasButtons = true; + } + } + + int buttonBarPadding = getContext().getResources() + .getDimensionPixelSize(R.dimen.md_neutral_button_margin); + final int buttonFrameWidth = width - 2 * buttonBarPadding; + stacked = buttonsWidth > buttonFrameWidth; + } + + int stackedHeight = 0; + isStacked = stacked; + if (stacked) { + for (MDButton button : buttons) { + if (button != null && isVisible(button)) { + button.setStacked(true, false); + measureChild(button, widthMeasureSpec, heightMeasureSpec); + stackedHeight += button.getMeasuredHeight(); + hasButtons = true; + } + } + } + + int availableHeight = height; + int fullPadding = 0; + int minPadding = 0; + if (hasButtons) { + if (isStacked) { + availableHeight -= stackedHeight; + fullPadding += 2 * buttonPaddingFull; + minPadding += 2 * buttonPaddingFull; + } else { + availableHeight -= buttonBarHeight; + fullPadding += 2 * buttonPaddingFull; + /* No minPadding */ + } + } else { + /* Content has 8dp, we add 16dp and get 24dp, the frame margin */ + fullPadding += 2 * buttonPaddingFull; + } + + if (isVisible(titleBar)) { + titleBar.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED); + availableHeight -= titleBar.getMeasuredHeight(); + } else if (!noTitleNoPadding) { + fullPadding += noTitlePaddingFull; + } + + if (isVisible(content)) { + content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(availableHeight - minPadding, MeasureSpec.AT_MOST)); + + if (content.getMeasuredHeight() <= availableHeight - fullPadding) { + if (!reducePaddingNoTitleNoButtons || isVisible(titleBar) || hasButtons) { + useFullPadding = true; + availableHeight -= content.getMeasuredHeight() + fullPadding; + } else { + useFullPadding = false; + availableHeight -= content.getMeasuredHeight() + minPadding; + } + } else { + useFullPadding = false; + availableHeight = 0; + } + + } + + setMeasuredDimension(width, height - availableHeight); + } + + @Override + public void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (content != null) { + if (drawTopDivider) { + int y = content.getTop(); + canvas.drawRect(0, y - dividerWidth, getMeasuredWidth(), y, dividerPaint); + } + + if (drawBottomDivider) { + int y = content.getBottom(); + canvas.drawRect(0, y, getMeasuredWidth(), y + dividerWidth, dividerPaint); + } + } + } + + @Override + protected void onLayout(boolean changed, final int l, int t, final int r, int b) { + if (isVisible(titleBar)) { + int height = titleBar.getMeasuredHeight(); + titleBar.layout(l, t, r, t + height); + t += height; + } else if (!noTitleNoPadding && useFullPadding) { + t += noTitlePaddingFull; + } + + if (isVisible(content)) { + content.layout(l, t, r, t + content.getMeasuredHeight()); + } + + if (isStacked) { + b -= buttonPaddingFull; + for (MDButton mButton : buttons) { + if (isVisible(mButton)) { + mButton.layout(l, b - mButton.getMeasuredHeight(), r, b); + b -= mButton.getMeasuredHeight(); + } + } + } else { + int barTop; + int barBottom = b; + if (useFullPadding) { + barBottom -= buttonPaddingFull; + } + barTop = barBottom - buttonBarHeight; + /* START: + Neutral Negative Positive + + CENTER: + Negative Neutral Positive + + END: + Positive Negative Neutral + + (With no Positive, Negative takes it's place except for CENTER) + */ + int offset = buttonHorizontalEdgeMargin; + + /* Used with CENTER gravity */ + int neutralLeft = -1; + int neutralRight = -1; + + if (isVisible(buttons[INDEX_POSITIVE])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + bl = l + offset; + br = bl + buttons[INDEX_POSITIVE].getMeasuredWidth(); + } else { /* START || CENTER */ + br = r - offset; + bl = br - buttons[INDEX_POSITIVE].getMeasuredWidth(); + neutralRight = bl; + } + buttons[INDEX_POSITIVE].layout(bl, barTop, br, barBottom); + offset += buttons[INDEX_POSITIVE].getMeasuredWidth(); + } + + if (isVisible(buttons[INDEX_NEGATIVE])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + bl = l + offset; + br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth(); + } else if (buttonGravity == GravityEnum.START) { + br = r - offset; + bl = br - buttons[INDEX_NEGATIVE].getMeasuredWidth(); + } else { /* CENTER */ + bl = l + buttonHorizontalEdgeMargin; + br = bl + buttons[INDEX_NEGATIVE].getMeasuredWidth(); + neutralLeft = br; + } + buttons[INDEX_NEGATIVE].layout(bl, barTop, br, barBottom); + } + + if (isVisible(buttons[INDEX_NEUTRAL])) { + int bl, br; + if (buttonGravity == GravityEnum.END) { + br = r - buttonHorizontalEdgeMargin; + bl = br - buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (buttonGravity == GravityEnum.START) { + bl = l + buttonHorizontalEdgeMargin; + br = bl + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else { /* CENTER */ + if (neutralLeft == -1 && neutralRight != -1) { + neutralLeft = neutralRight - buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (neutralRight == -1 && neutralLeft != -1) { + neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } else if (neutralRight == -1) { + neutralLeft = (r - l) / 2 - buttons[INDEX_NEUTRAL].getMeasuredWidth() / 2; + neutralRight = neutralLeft + buttons[INDEX_NEUTRAL].getMeasuredWidth(); + } + bl = neutralLeft; + br = neutralRight; + } + + buttons[INDEX_NEUTRAL].layout(bl, barTop, br, barBottom); + } + } + + setUpDividersVisibility(content, true, true); + } + + public void setStackingBehavior(StackingBehavior behavior) { + stackBehavior = behavior; + invalidate(); + } + + public void setDividerColor(int color) { + dividerPaint.setColor(color); + invalidate(); + } + + public void setButtonGravity(GravityEnum gravity) { + buttonGravity = gravity; + invertGravityIfNecessary(); + } + + private void invertGravityIfNecessary() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return; + } + Configuration config = getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + switch (buttonGravity) { + case START: + buttonGravity = GravityEnum.END; + break; + case END: + buttonGravity = GravityEnum.START; + break; + } + } + } + + public void setButtonStackedGravity(GravityEnum gravity) { + for (MDButton mButton : buttons) { + if (mButton != null) { + mButton.setStackedGravity(gravity); + } + } + } + + private void setUpDividersVisibility(final View view, final boolean setForTop, + final boolean setForBottom) { + if (view == null) { + return; + } + if (view instanceof ScrollView) { + final ScrollView sv = (ScrollView) view; + if (canScrollViewScroll(sv)) { + addScrollListener(sv, setForTop, setForBottom); + } else { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } + } else if (view instanceof AdapterView) { + final AdapterView sv = (AdapterView) view; + if (canAdapterViewScroll(sv)) { + addScrollListener(sv, setForTop, setForBottom); + } else { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } + } else if (view instanceof WebView) { + view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (view.getMeasuredHeight() != 0) { + if (!canWebViewScroll((WebView) view)) { + if (setForTop) { + drawTopDivider = false; + } + if (setForBottom) { + drawBottomDivider = false; + } + } else { + addScrollListener((ViewGroup) view, setForTop, setForBottom); + } + view.getViewTreeObserver().removeOnPreDrawListener(this); + } + return true; + } + }); + } else if (view instanceof RecyclerView) { + boolean canScroll = canRecyclerViewScroll((RecyclerView) view); + if (setForTop) { + drawTopDivider = canScroll; + } + if (setForBottom) { + drawBottomDivider = canScroll; + } + if (canScroll) { + addScrollListener((ViewGroup) view, setForTop, setForBottom); + } + } else if (view instanceof ViewGroup) { + View topView = getTopView((ViewGroup) view); + setUpDividersVisibility(topView, setForTop, setForBottom); + View bottomView = getBottomView((ViewGroup) view); + if (bottomView != topView) { + setUpDividersVisibility(bottomView, false, true); + } + } + } + + private void addScrollListener(final ViewGroup vg, final boolean setForTop, + final boolean setForBottom) { + if ((!setForBottom && topOnScrollChangedListener == null + || (setForBottom && bottomOnScrollChangedListener == null))) { + if (vg instanceof RecyclerView) { + RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + boolean hasButtons = false; + for (MDButton button : buttons) { + if (button != null && button.getVisibility() != View.GONE) { + hasButtons = true; + break; + } + } + invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons); + invalidate(); + } + }; + ((RecyclerView) vg).addOnScrollListener(scrollListener); + scrollListener.onScrolled((RecyclerView) vg, 0, 0); + } else { + ViewTreeObserver.OnScrollChangedListener onScrollChangedListener = new ViewTreeObserver.OnScrollChangedListener() { + @Override + public void onScrollChanged() { + boolean hasButtons = false; + for (MDButton button : buttons) { + if (button != null && button.getVisibility() != View.GONE) { + hasButtons = true; + break; + } + } + if (vg instanceof WebView) { + invalidateDividersForWebView((WebView) vg, setForTop, setForBottom, hasButtons); + } else { + invalidateDividersForScrollingView(vg, setForTop, setForBottom, hasButtons); + } + invalidate(); + } + }; + if (!setForBottom) { + topOnScrollChangedListener = onScrollChangedListener; + vg.getViewTreeObserver().addOnScrollChangedListener(topOnScrollChangedListener); + } else { + bottomOnScrollChangedListener = onScrollChangedListener; + vg.getViewTreeObserver().addOnScrollChangedListener(bottomOnScrollChangedListener); + } + onScrollChangedListener.onScrollChanged(); + } + } + } + + private void invalidateDividersForScrollingView(ViewGroup view, final boolean setForTop, + boolean setForBottom, boolean hasButtons) { + if (setForTop && view.getChildCount() > 0) { + drawTopDivider = titleBar != null && + titleBar.getVisibility() != View.GONE && + //Not scrolled to the top. + view.getScrollY() + view.getPaddingTop() > view.getChildAt(0).getTop(); + + } + if (setForBottom && view.getChildCount() > 0) { + drawBottomDivider = hasButtons && + view.getScrollY() + view.getHeight() - view.getPaddingBottom() < view + .getChildAt(view.getChildCount() - 1).getBottom(); + } + } + + private void invalidateDividersForWebView(WebView view, final boolean setForTop, + boolean setForBottom, boolean hasButtons) { + if (setForTop) { + drawTopDivider = titleBar != null && + titleBar.getVisibility() != View.GONE && + //Not scrolled to the top. + view.getScrollY() + view.getPaddingTop() > 0; + } + if (setForBottom) { + //noinspection deprecation + drawBottomDivider = hasButtons && + view.getScrollY() + view.getMeasuredHeight() - view.getPaddingBottom() + < view.getContentHeight() * view.getScale(); + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java new file mode 100644 index 0000000..3410332 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/MDTintHelper.java @@ -0,0 +1,194 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v7.widget.AppCompatEditText; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.RadioButton; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.materialdialog.util.DialogUtils; + +import java.lang.reflect.Field; + +/** + * Tints widgets + */ +public class MDTintHelper { + + public static void setTint(@NonNull RadioButton radioButton, + @NonNull ColorStateList colors) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + radioButton.setButtonTintList(colors); + } else { + Drawable radioDrawable = ContextCompat.getDrawable(radioButton.getContext(), + R.drawable.abc_btn_radio_material); + Drawable d = DrawableCompat.wrap(radioDrawable); + DrawableCompat.setTintList(d, colors); + radioButton.setButtonDrawable(d); + } + } + + public static void setTint(@NonNull RadioButton radioButton, + @ColorInt int color) { + final int disabledColor = DialogUtils.getDisabledColor(radioButton.getContext()); + ColorStateList sl = new ColorStateList(new int[][]{ + new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked} + }, new int[]{ + DialogUtils.resolveColor(radioButton.getContext(), R.attr.colorControlNormal), + color, + disabledColor, + disabledColor + }); + setTint(radioButton, sl); + } + + public static void setTint(@NonNull CheckBox box, + @NonNull ColorStateList colors) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + box.setButtonTintList(colors); + } else { + Drawable checkDrawable = ContextCompat.getDrawable(box.getContext(), + R.drawable.abc_btn_check_material); + Drawable drawable = DrawableCompat.wrap(checkDrawable); + DrawableCompat.setTintList(drawable, colors); + box.setButtonDrawable(drawable); + } + } + + public static void setTint(@NonNull CheckBox box, @ColorInt int color) { + final int disabledColor = DialogUtils.getDisabledColor(box.getContext()); + ColorStateList sl = new ColorStateList(new int[][]{ + new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked}, + new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked} + }, new int[]{ + DialogUtils.resolveColor(box.getContext(), R.attr.colorControlNormal), + color, + disabledColor, + disabledColor + }); + setTint(box, sl); + } + + public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color) { + ColorStateList s1 = ColorStateList.valueOf(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + seekBar.setThumbTintList(s1); + seekBar.setProgressTintList(s1); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) { + Drawable progressDrawable = DrawableCompat.wrap(seekBar.getProgressDrawable()); + seekBar.setProgressDrawable(progressDrawable); + DrawableCompat.setTintList(progressDrawable, s1); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + Drawable thumbDrawable = DrawableCompat.wrap(seekBar.getThumb()); + DrawableCompat.setTintList(thumbDrawable, s1); + seekBar.setThumb(thumbDrawable); + } + } else { + PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { + mode = PorterDuff.Mode.MULTIPLY; + } + if (seekBar.getIndeterminateDrawable() != null) { + seekBar.getIndeterminateDrawable().setColorFilter(color, mode); + } + if (seekBar.getProgressDrawable() != null) { + seekBar.getProgressDrawable().setColorFilter(color, mode); + } + } + } + + public static void setTint(@NonNull ProgressBar progressBar, + @ColorInt int color) { + setTint(progressBar, color, false); + } + + private static void setTint(@NonNull ProgressBar progressBar, + @ColorInt int color, + boolean skipIndeterminate) { + ColorStateList sl = ColorStateList.valueOf(color); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + progressBar.setProgressTintList(sl); + progressBar.setSecondaryProgressTintList(sl); + if (!skipIndeterminate) { + progressBar.setIndeterminateTintList(sl); + } + } else { + PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { + mode = PorterDuff.Mode.MULTIPLY; + } + if (!skipIndeterminate && progressBar.getIndeterminateDrawable() != null) { + progressBar.getIndeterminateDrawable().setColorFilter(color, mode); + } + if (progressBar.getProgressDrawable() != null) { + progressBar.getProgressDrawable().setColorFilter(color, mode); + } + } + } + + private static ColorStateList createEditTextColorStateList( + @NonNull Context context, @ColorInt int color) { + int[][] states = new int[3][]; + int[] colors = new int[3]; + int i = 0; + states[i] = new int[]{-android.R.attr.state_enabled}; + colors[i] = DialogUtils.resolveColor(context, R.attr.colorControlNormal); + i++; + states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused}; + colors[i] = DialogUtils.resolveColor(context, R.attr.colorControlNormal); + i++; + states[i] = new int[]{}; + colors[i] = color; + return new ColorStateList(states, colors); + } + + public static void setTint(@NonNull EditText editText, @ColorInt int color) { + ColorStateList editTextColorStateList = createEditTextColorStateList(editText.getContext(), + color); + if (editText instanceof AppCompatEditText) { + ((AppCompatEditText) editText).setSupportBackgroundTintList(editTextColorStateList); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + editText.setBackgroundTintList(editTextColorStateList); + } + setCursorTint(editText, color); + } + + private static void setCursorTint(@NonNull EditText editText, @ColorInt int color) { + try { + Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes"); + fCursorDrawableRes.setAccessible(true); + int mCursorDrawableRes = fCursorDrawableRes.getInt(editText); + Field fEditor = TextView.class.getDeclaredField("mEditor"); + fEditor.setAccessible(true); + Object editor = fEditor.get(editText); + Class clazz = editor.getClass(); + Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable"); + fCursorDrawable.setAccessible(true); + Drawable[] drawables = new Drawable[2]; + drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes); + drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN); + drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN); + fCursorDrawable.set(editor, drawables); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java new file mode 100644 index 0000000..631d695 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/internal/ThemeSingleton.java @@ -0,0 +1,62 @@ +package com.hzecool.widget.materialdialog.internal; + +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.DrawableRes; + +import com.hzecool.widget.materialdialog.GravityEnum; + + +/** + * Use of this is discouraged for now; for internal use only. See the Global Theming section of the + * README. + */ +public class ThemeSingleton { + + private static ThemeSingleton singleton; + public boolean darkTheme = false; + @ColorInt + public int titleColor = 0; + @ColorInt + public int contentColor = 0; + public ColorStateList positiveColor = null; + public ColorStateList neutralColor = null; + public ColorStateList negativeColor = null; + @ColorInt + public int widgetColor = 0; + @ColorInt + public int itemColor = 0; + public Drawable icon = null; + @ColorInt + public int backgroundColor = 0; + @ColorInt + public int dividerColor = 0; + public ColorStateList linkColor = null; + @DrawableRes + public int listSelector = 0; + @DrawableRes + public int btnSelectorStacked = 0; + @DrawableRes + public int btnSelectorPositive = 0; + @DrawableRes + public int btnSelectorNeutral = 0; + @DrawableRes + public int btnSelectorNegative = 0; + public GravityEnum titleGravity = GravityEnum.START; + public GravityEnum contentGravity = GravityEnum.START; + public GravityEnum btnStackedGravity = GravityEnum.END; + public GravityEnum itemsGravity = GravityEnum.START; + public GravityEnum buttonsGravity = GravityEnum.START; + + public static ThemeSingleton get(boolean createIfNull) { + if (singleton == null && createIfNull) { + singleton = new ThemeSingleton(); + } + return singleton; + } + + public static ThemeSingleton get() { + return get(true); + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java b/widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java new file mode 100644 index 0000000..3627b55 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/util/DialogUtils.java @@ -0,0 +1,305 @@ +package com.hzecool.widget.materialdialog.util; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.ArrayRes; +import android.support.annotation.AttrRes; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.util.TypedValue; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import com.hzecool.widget.materialdialog.GravityEnum; +import com.hzecool.widget.materialdialog.MaterialDialog; + + +/** + * @author Aidan Follestad (afollestad) + */ +public class DialogUtils { + +// @SuppressWarnings("ConstantConditions") +// public static float resolveFloat(Context context, int attr) { +// TypedArray a = context.obtainStyledAttributes(null, new int[]{attr}); +// try { +// return a.getFloat(0, 0); +// } finally { +// a.recycle(); +// } +// } + + @ColorInt + public static int getDisabledColor(Context context) { + final int primaryColor = resolveColor(context, android.R.attr.textColorPrimary); + final int disabledColor = isColorDark(primaryColor) ? Color.BLACK : Color.WHITE; + return adjustAlpha(disabledColor, 0.3f); + } + + @ColorInt + public static int adjustAlpha(@ColorInt int color, + @SuppressWarnings("SameParameterValue") float factor) { + int alpha = Math.round(Color.alpha(color) * factor); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + return Color.argb(alpha, red, green, blue); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr) { + return resolveColor(context, attr, 0); + } + + @ColorInt + public static int resolveColor(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getColor(0, fallback); + } finally { + a.recycle(); + } + } + + // Try to resolve the colorAttr attribute. + public static ColorStateList resolveActionTextColorStateList( + Context context, @AttrRes int colorAttr, ColorStateList fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{colorAttr}); + try { + final TypedValue value = a.peekValue(0); + if (value == null) { + return fallback; + } + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + final ColorStateList stateList = a.getColorStateList(0); + if (stateList != null) { + return stateList; + } else { + return fallback; + } + } + } finally { + a.recycle(); + } + } + + // Get the specified color resource, creating a ColorStateList if the resource + // points to a color value. + public static ColorStateList getActionTextColorStateList(Context context, @ColorRes int colorId) { + final TypedValue value = new TypedValue(); + context.getResources().getValue(colorId, value, true); + if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT + && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { + return getActionTextStateList(context, value.data); + } else { + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + //noinspection deprecation + return context.getResources().getColorStateList(colorId); + } else { + return context.getColorStateList(colorId); + } + } + } + + /** + * Returns a color associated with a particular resource ID + * Starting in {@link Build.VERSION_CODES#M}, the returned + * color will be styled for the specified Context's theme. + * + * @param colorId The desired resource identifier, as generated by the aapt tool. This integer + * encodes the package, type, and resource entry. The value 0 is an invalid identifier. + * @return A single color value in the form 0xAARRGGBB. + */ + @ColorInt + public static int getColor(Context context, @ColorRes int colorId) { + return ContextCompat.getColor(context, colorId); + } + + public static String resolveString(Context context, @AttrRes int attr) { + TypedValue v = new TypedValue(); + context.getTheme().resolveAttribute(attr, v, true); + return (String) v.string; + } + + private static int gravityEnumToAttrInt(GravityEnum value) { + switch (value) { + case CENTER: + return 1; + case END: + return 2; + default: + return 0; + } + } + + public static GravityEnum resolveGravityEnum(Context context, + @AttrRes int attr, + GravityEnum defaultGravity) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + switch (a.getInt(0, gravityEnumToAttrInt(defaultGravity))) { + case 1: + return GravityEnum.CENTER; + case 2: + return GravityEnum.END; + default: + return GravityEnum.START; + } + } finally { + a.recycle(); + } + } + + public static Drawable resolveDrawable(Context context, @AttrRes int attr) { + return resolveDrawable(context, attr, null); + } + + private static Drawable resolveDrawable(Context context, + @AttrRes int attr, + @SuppressWarnings( + "SameParameterValue") Drawable fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + Drawable d = a.getDrawable(0); + if (d == null && fallback != null) { + d = fallback; + } + return d; + } finally { + a.recycle(); + } + } + + public static int resolveDimension(Context context, @AttrRes int attr) { + return resolveDimension(context, attr, -1); + } + + private static int resolveDimension(Context context, @AttrRes int attr, int fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getDimensionPixelSize(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr, boolean fallback) { + TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + try { + return a.getBoolean(0, fallback); + } finally { + a.recycle(); + } + } + + public static boolean resolveBoolean(Context context, @AttrRes int attr) { + return resolveBoolean(context, attr, false); + } + + public static boolean isColorDark(@ColorInt int color) { + double darkness = 1 + - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255; + return darkness >= 0.5; + } + + public static void setBackgroundCompat(View view, Drawable d) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + //noinspection deprecation + view.setBackgroundDrawable(d); + } else { + view.setBackground(d); + } + } + + public static void showKeyboard(@NonNull final DialogInterface di, + @NonNull final MaterialDialog.Builder builder) { + final MaterialDialog dialog = (MaterialDialog) di; + if (dialog.getInputEditText() == null) { + return; + } + dialog.getInputEditText().post(new Runnable() { + @Override + public void run() { + dialog.getInputEditText().requestFocus(); + InputMethodManager imm = (InputMethodManager) builder.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(dialog.getInputEditText(), InputMethodManager.SHOW_IMPLICIT); + } + } + }); + } + + public static void hideKeyboard(@NonNull final DialogInterface di, + @NonNull final MaterialDialog.Builder builder) { + final MaterialDialog dialog = (MaterialDialog) di; + if (dialog.getInputEditText() == null) { + return; + } + InputMethodManager imm = (InputMethodManager) builder.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + final View currentFocus = dialog.getCurrentFocus(); + final IBinder windowToken = currentFocus != null ? + currentFocus.getWindowToken() : dialog.getView().getWindowToken(); + if (windowToken != null) { + imm.hideSoftInputFromWindow(windowToken, 0); + } + } + } + + public static ColorStateList getActionTextStateList(Context context, int newPrimaryColor) { + final int fallBackButtonColor = DialogUtils + .resolveColor(context, android.R.attr.textColorPrimary); + if (newPrimaryColor == 0) { + newPrimaryColor = fallBackButtonColor; + } + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_enabled}, // disabled + new int[]{} // enabled + }; + int[] colors = new int[]{ + DialogUtils.adjustAlpha(newPrimaryColor, 0.4f), + newPrimaryColor + }; + return new ColorStateList(states, colors); + } + + public static int[] getColorArray(@NonNull Context context, @ArrayRes int array) { + if (array == 0) { + return null; + } + TypedArray ta = context.getResources().obtainTypedArray(array); + int[] colors = new int[ta.length()]; + for (int i = 0; i < ta.length(); i++) { + colors[i] = ta.getColor(i, 0); + } + ta.recycle(); + return colors; + } + + public static boolean isIn(@NonNull T find, @Nullable T[] ary) { + if (ary == null || ary.length == 0) { + return false; + } + for (T item : ary) { + if (item.equals(find)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java b/widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java new file mode 100644 index 0000000..6fa42db --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/util/RippleHelper.java @@ -0,0 +1,21 @@ +package com.hzecool.widget.materialdialog.util; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.os.Build; +import android.support.annotation.ColorInt; + +/** + * @author Aidan Follestad (afollestad) + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class RippleHelper { + + public static void applyColor(Drawable d, @ColorInt int color) { + if (d instanceof RippleDrawable) { + ((RippleDrawable) d).setColor(ColorStateList.valueOf(color)); + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java b/widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java new file mode 100644 index 0000000..c521f48 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/materialdialog/util/TypefaceHelper.java @@ -0,0 +1,47 @@ +package com.hzecool.widget.materialdialog.util; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.util.SimpleArrayMap; + +/* + Each call to Typeface.createFromAsset will load a new instance of the typeface into memory, + and this memory is not consistently get garbage collected + http://code.google.com/p/android/issues/detail?id=9904 + (It states released but even on Lollipop you can see the typefaces accumulate even after + multiple GC passes) + + You can detect this by running: + adb shell dumpsys meminfo com.your.packagenage + + You will see output like: + + Asset Allocations + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Regular.ttf: 123K + zip:/data/app/com.your.packagenage-1.apk:/assets/Roboto-Medium.ttf: 125K + +*/ +public class TypefaceHelper { + + private static final SimpleArrayMap cache = new SimpleArrayMap<>(); + + public static Typeface get(Context c, String name) { + synchronized (cache) { + if (!cache.containsKey(name)) { + try { + Typeface t = Typeface.createFromAsset( + c.getAssets(), String.format("fonts/%s", name)); + cache.put(name, t); + return t; + } catch (RuntimeException e) { + return null; + } + } + return cache.get(name); + } + } +} + diff --git a/widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java b/widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java new file mode 100644 index 0000000..6909c68 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ninegridview/ColorFilterImageView.java @@ -0,0 +1,51 @@ +package com.hzecool.widget.ninegridview; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff.Mode; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ImageView; + +/** + * @author hnclca + * @ClassName: ColorFilterImageView + * 实现图像根据按下抬起动作变化颜色 + * @date 2016-02-26 + */ +public class ColorFilterImageView extends android.support.v7.widget.AppCompatImageView implements OnTouchListener { + public ColorFilterImageView(Context context) { + this(context, null, 0); + } + + public ColorFilterImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorFilterImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: // 按下时图像变灰 + setColorFilter(Color.GRAY, Mode.MULTIPLY); + break; + case MotionEvent.ACTION_UP: // 手指离开或取消操作时恢复原色 + case MotionEvent.ACTION_CANCEL: + setColorFilter(Color.TRANSPARENT); + break; + default: + break; + } + return false; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java b/widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java new file mode 100644 index 0000000..0f3a2d0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ninegridview/DisplayUtils.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2016 venshine.cn@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hzecool.widget.ninegridview; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.DisplayMetrics; +import android.view.Window; + +/** + * 屏幕显示相关信息 + * + * @author venshine + */ +public class DisplayUtils { + + /** + * 是否横屏 + * + * @param context + * @return + */ + public static boolean isLandscape(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + /** + * 是否竖屏 + * + * @param context + * @return + */ + public static boolean isPortrait(Context context) { + return context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT; + } + + /** + * Get screen width, in pixels + * + * @param context + * @return + */ + public static int getScreenWidth(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + /** + * Get screen height, in pixels + * + * @param context + * @return + */ + public static int getScreenHeight(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } + + /** + * Get screen density, the logical density of the display + * + * @param context + * @return + */ + public static float getScreenDensity(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.density; + } + + /** + * Get screen density dpi, the screen density expressed as dots-per-inch + * + * @param context + * @return + */ + public static int getScreenDensityDPI(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.densityDpi; + } + + /** + * Get titlebar height, this method cannot be used in onCreate(),onStart(),onResume(), unless it is called in the + * post(Runnable). + * + * @param activity + * @return + */ + public static int getTitleBarHeight(Activity activity) { + int statusBarHeight = getStatusBarHeight(activity); + int contentViewTop = activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT).getTop(); + int titleBarHeight = contentViewTop - statusBarHeight; + return titleBarHeight < 0 ? 0 : titleBarHeight; + } + + /** + * Get statusbar height, this method cannot be used in onCreate(),onStart(),onResume(), unless it is called in the + * post(Runnable). + * + * @param activity + * @return + */ + public static int getStatusBarHeight(Activity activity) { + Rect rect = new Rect(); + activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect); + return rect.top; + } + + /** + * Get statusbar height + * + * @param activity + * @return + */ + public static int getStatusBarHeight2(Activity activity) { + int statusBarHeight = getStatusBarHeight(activity); + if (0 == statusBarHeight) { + Class localClass; + try { + localClass = Class.forName("com.android.internal.R$dimen"); + Object localObject = localClass.newInstance(); + int id = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString()); + statusBarHeight = activity.getResources().getDimensionPixelSize(id); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (NumberFormatException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + } + return statusBarHeight; + } + + /** + * Convert dp to px by the density of phone + * + * @param context + * @param dp + * @return + */ + public static int dip2px(Context context, float dp) { + if (context == null) { + return -1; + } + return (int) (dipToPx(context, dp) + 0.5f); + } + + /** + * Convert dp to px + * + * @param context + * @param dp + * @return + */ + private static float dipToPx(Context context, float dp) { + if (context == null) { + return -1; + } + float scale = context.getResources().getDisplayMetrics().density; + return dp * scale; + } + + /** + * Convert px to dp by the density of phone + * + * @param context + * @param px + * @return + */ + public static int px2dip(Context context, float px) { + if (context == null) { + return -1; + } + return (int) (pxToDip(context, px) + 0.5f); + } + + /** + * Convert px to dp + * + * @param context + * @param px + * @return + */ + private static float pxToDip(Context context, float px) { + if (context == null) { + return -1; + } + float scale = context.getResources().getDisplayMetrics().density; + return px / scale; + } + + /** + * Convert px to sp + * + * @param context + * @param px + * @return + */ + public static int px2sp(Context context, float px) { + return (int) (pxToSp(context, px) + 0.5f); + } + + /** + * Convert px to sp + * + * @param context + * @param px + * @return + */ + private static float pxToSp(Context context, float px) { + float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return px / fontScale; + } + + /** + * Convert sp to px + * + * @param context + * @param sp + * @return + */ + public static int sp2px(Context context, float sp) { + return (int) (spToPx(context, sp) + 0.5f); + } + + /** + * Convert sp to px + * + * @param context + * @param sp + * @return + */ + private static float spToPx(Context context, float sp) { + float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return sp * fontScale; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java b/widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java new file mode 100644 index 0000000..98e9619 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/ninegridview/MultiImageView.java @@ -0,0 +1,214 @@ +package com.hzecool.widget.ninegridview; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.hzecool.widget.utils.GlideSetting; + +import java.util.List; + +/** + * Created by song on 2017/8/7. + */ + +public class MultiImageView extends LinearLayout { + public static int MAX_WIDTH = 0; + + // 照片的Url列表 + private List imagesList; + + /** + * 长度 单位为Pixel + **/ + private int pxOneMaxWandH; // 单张图最大允许宽高 + private int pxMoreWandH = 0;// 多张图的宽高 + private int pxImagePadding = DisplayUtils.dip2px(getContext(), 3);// 图片间的间距 + + private int MAX_PER_ROW_COUNT = 3;// 每行显示最大数 + + private LayoutParams onePicPara; + private LayoutParams morePara, moreParaColumnFirst; + private LayoutParams rowPara; + + private OnItemClickListener mOnItemClickListener; + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + mOnItemClickListener = onItemClickListener; + } + + public MultiImageView(Context context) { + super(context); + } + + public MultiImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setList(List lists) throws IllegalArgumentException { + if (lists == null) { + throw new IllegalArgumentException("imageList is null..."); + } + imagesList = lists; + + if (MAX_WIDTH > 0) { + pxMoreWandH = (MAX_WIDTH - pxImagePadding * 2) / 3; //解决右侧图片和内容对不齐问题 + pxOneMaxWandH = MAX_WIDTH * 2 / 3; + initImageLayoutParams(); + } + + initView(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (MAX_WIDTH == 0) { + int width = measureWidth(widthMeasureSpec); + if (width > 0) { + MAX_WIDTH = width; + if (imagesList != null && imagesList.size() > 0) { + setList(imagesList); + } + } + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + /** + * Determines the width of this view + * + * @param measureSpec A measureSpec packed into an int + * @return The width of the view, honoring constraints from measureSpec + */ + private int measureWidth(int measureSpec) { + int result = 0; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + if (specMode == MeasureSpec.EXACTLY) { + // We were told how big to be + result = specSize; + } else { + // Measure the text + // result = (int) mTextPaint.measureText(mText) + getPaddingLeft() + // + getPaddingRight(); + if (specMode == MeasureSpec.AT_MOST) { + // Respect AT_MOST value if that was what is called for by + // measureSpec + result = Math.min(result, specSize); + } + } + return result; + } + + private void initImageLayoutParams() { + int wrap = LayoutParams.WRAP_CONTENT; + int match = LayoutParams.MATCH_PARENT; + + onePicPara = new LayoutParams(wrap, wrap); + + moreParaColumnFirst = new LayoutParams(pxMoreWandH, pxMoreWandH); + morePara = new LayoutParams(pxMoreWandH, pxMoreWandH); + morePara.setMargins(pxImagePadding, 0, 0, 0); + + rowPara = new LayoutParams(match, wrap); + } + + // 根据imageView的数量初始化不同的View布局,还要为每一个View作点击效果 + private void initView() { + this.setOrientation(VERTICAL); + this.removeAllViews(); + if (MAX_WIDTH == 0) { + //为了触发onMeasure()来测量MultiImageView的最大宽度,MultiImageView的宽设置为match_parent + addView(new View(getContext())); + return; + } + + if (imagesList == null || imagesList.size() == 0) { + return; + } + + if (imagesList.size() == 1) { + addView(createImageView(0, false)); + } else { + int allCount = imagesList.size(); + if (allCount == 4) { + MAX_PER_ROW_COUNT = 2; + } else { + MAX_PER_ROW_COUNT = 3; + } + int rowCount = allCount / MAX_PER_ROW_COUNT + (allCount % MAX_PER_ROW_COUNT > 0 ? 1 : 0);// 行数 + for (int rowCursor = 0; rowCursor < rowCount; rowCursor++) { + LinearLayout rowLayout = new LinearLayout(getContext()); + rowLayout.setOrientation(LinearLayout.HORIZONTAL); + + rowLayout.setLayoutParams(rowPara); + if (rowCursor != 0) { + rowLayout.setPadding(0, pxImagePadding, 0, 0); + } + + int columnCount = allCount % MAX_PER_ROW_COUNT == 0 ? MAX_PER_ROW_COUNT : allCount % + MAX_PER_ROW_COUNT;//每行的列数 + if (rowCursor != rowCount - 1) { + columnCount = MAX_PER_ROW_COUNT; + } + addView(rowLayout); + + int rowOffset = rowCursor * MAX_PER_ROW_COUNT;// 行偏移 + for (int columnCursor = 0; columnCursor < columnCount; columnCursor++) { + int position = columnCursor + rowOffset; + rowLayout.addView(createImageView(position, true)); + } + } + } + } + + private ImageView createImageView(int position, final boolean isMultiImage) { + String url = imagesList.get(position); + ImageView imageView = new ColorFilterImageView(getContext()); + if (isMultiImage) { + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + imageView.setLayoutParams(position % MAX_PER_ROW_COUNT == 0 ? moreParaColumnFirst : morePara); + } else { + imageView.setAdjustViewBounds(true); + imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + imageView.setMaxHeight(pxOneMaxWandH); + imageView.setLayoutParams(onePicPara); + } + + imageView.setId(url.hashCode()); + imageView.setOnClickListener(new ImageOnClickListener(position)); + Glide.with(getContext()) + .load(url) + .apply(GlideSetting.getGlideSetting() + .diskCacheStrategy(DiskCacheStrategy.ALL) + ) + + .into(imageView); + return imageView; + } + + private class ImageOnClickListener implements View.OnClickListener { + + private int position; + + public ImageOnClickListener(int position) { + this.position = position; + } + + @Override + public void onClick(View view) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(view, position); + } + } + } + + public interface OnItemClickListener { + public void onItemClick(View view, int position); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java b/widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java new file mode 100644 index 0000000..d09bd92 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/popwindowlist/PopAdapter.java @@ -0,0 +1,27 @@ +package com.hzecool.widget.popwindowlist; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.chad.library.adapter.base.BaseViewHolder; +import com.hzecool.widget.R; + +import java.util.List; + +/** + * Created by tutu on 2017/4/17. + */ + +public class PopAdapter extends BaseQuickAdapter { + + + public PopAdapter(int resId, List data) { + + super(resId, data); + + + } + + @Override + protected void convert(BaseViewHolder helper, String item) { + helper.setText(R.id.tv, item); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java b/widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java new file mode 100644 index 0000000..5259f3c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/popwindowlist/RecyclerViewPop.java @@ -0,0 +1,81 @@ +package com.hzecool.widget.popwindowlist; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +import com.chad.library.adapter.base.BaseQuickAdapter; +import com.hzecool.widget.R; +import com.hzecool.widget.RecycleViewDivider; +import com.hzecool.widget.utils.SizeUtils; + +import java.util.List; + +/** + * Created by tutu on 2017/4/17. + */ + +public class RecyclerViewPop { + + private PopupWindow popupWindow; + private List datas; + private View target; + private View content; + private RecyclerView recyclerView; + private PopAdapter popAdapter; + private Context context; + private int itemViewResId = -1; + + public void setItemView(int resId) { + itemViewResId = resId; + } + + + public RecyclerViewPop(List datas, View target, Context context, BaseQuickAdapter.OnItemClickListener onItemClickListener) { + this.datas = datas; + this.target = target; + this.context = context; + + content = LayoutInflater.from(context).inflate(R.layout.recyclerview_pop, null); + + recyclerView = (RecyclerView) content.findViewById(R.id.rv); + + if (itemViewResId == -1) { + popAdapter = new PopAdapter(R.layout.rv_pop_item, datas); + } else { + popAdapter = new PopAdapter(itemViewResId, datas); + } + + RecycleViewDivider recycleViewDivider = new RecycleViewDivider(context, LinearLayout.HORIZONTAL, SizeUtils.dp2px(context,0.5),context.getResources().getColor(R.color.white)); + recycleViewDivider.setMarginLeft(SizeUtils.dp2px(context,7)); + recycleViewDivider.setMarginRight(SizeUtils.dp2px(context,7)); + recyclerView.addItemDecoration(recycleViewDivider); + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + recyclerView.setAdapter(popAdapter); + + popupWindow = new PopupWindow(content, + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true); + + popupWindow.setBackgroundDrawable(new BitmapDrawable()); + + popAdapter.setOnItemClickListener(onItemClickListener); + } + + + public void show() { + popupWindow.showAsDropDown(target); + } + + public void dissmiss() { + if (popupWindow != null && popupWindow.isShowing()) { + popupWindow.dismiss(); + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java b/widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java new file mode 100644 index 0000000..3460512 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/AppUtil.java @@ -0,0 +1,23 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; +import android.util.DisplayMetrics; + +/** + * Created by dd on 16/1/13. + */ +public class AppUtil { + + public static int getScreenWidth(Context context) { + + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + public static int getScreenHeight(Context context) { + + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java b/widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java new file mode 100644 index 0000000..68fa118 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/Coordinates.java @@ -0,0 +1,32 @@ +package com.hzecool.widget.puzzle; + +/** + * Created by dd on 16/1/13. + * 坐标实体 + */ +public class Coordinates { + + private float x; + private float y; + + public Coordinates(float x, float y){ + this.x = x; + this.y = y; + } + + public float getX() { + return x; + } + + public void setX(float x) { + this.x = x; + } + + public float getY() { + return y; + } + + public void setY(float y) { + this.y = y; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java b/widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java new file mode 100644 index 0000000..f7128c0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/DensityUtil.java @@ -0,0 +1,41 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; + +/** + * Created by dd on 16/1/13. + */ +public class DensityUtil { + + /** + * 根据手机的分辨率从 px(像素) 的单位 转成为 dp + */ + public static int px2dip(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * 根据手机的分辨率从 dp 的单位 转成为 px(像素) + */ + public static int dip2px(Context context, float dipValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + 0.5f); + } + + /** + * 将px值转换为sp值,保证文字大小不变 + */ + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 将sp值转换为px值,保证文字大小不变 + */ + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java b/widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java new file mode 100644 index 0000000..f2ab1b4 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/ImageBean.java @@ -0,0 +1,186 @@ +package com.hzecool.widget.puzzle; + +import java.io.Serializable; +import java.util.Date; + +/** + * Created by dd on 16/1/11. + */ +public class ImageBean implements Serializable { + + private static final long serialVersionUID = 5700379542385619938L; + + private long pictureId = -1; + + private int id; + public String parentName; + public long size; + public String displayName; + + public String path; + public boolean isChecked = false; + private String uploadTaskId; + private String pictureUrl; + private int photoYear; + private int photoMonth; + private int monthlyNum; + + private Long photoId; + private Date photoTakenDate; + private String memo; + private Boolean monthlyCover; + + private long photoDate; + + public ImageBean() { + super(); + } + + public ImageBean(String parentName, long size, String displayName, String path, boolean isChecked) { + super(); + this.parentName = parentName; + this.size = size; + this.displayName = displayName; + this.path = path; + this.isChecked = isChecked; + } + + public long getPictureId() { + return pictureId; + } + + public void setPictureId(long pictureId) { + this.pictureId = pictureId; + } + + public Date getPhotoTakenDate() { + return photoTakenDate; + } + + public void setPhotoTakenDate(Date photoTakenDate) { + this.photoTakenDate = photoTakenDate; + } + + + public long getPhotoDate() { + return photoDate; + } + + public void setPhotoDate(long photoDate) { + this.photoDate = photoDate; + } + + public String getPictureUrl() { + return pictureUrl; + } + + public void setPictureUrl(String pictureUrl) { + this.pictureUrl = pictureUrl; + } + + public int getPhotoYear() { + return photoYear; + } + + public void setPhotoYear(int photoYear) { + this.photoYear = photoYear; + } + + public int getPhotoMonth() { + return photoMonth; + } + + public void setPhotoMonth(int photoMonth) { + this.photoMonth = photoMonth; + } + + public int getMonthlyNum() { + return monthlyNum; + } + + public void setMonthlyNum(int monthlyNum) { + this.monthlyNum = monthlyNum; + } + + public Long getPhotoId() { + return photoId; + } + + public void setPhotoId(Long photoId) { + this.photoId = photoId; + } + + + public String getMemo() { + return memo; + } + + public void setMemo(String memo) { + this.memo = memo; + } + + public Boolean getMonthlyCover() { + return monthlyCover; + } + + public void setMonthlyCover(Boolean monthlyCover) { + this.monthlyCover = monthlyCover; + } + + + public String getUploadTaskId() { + return uploadTaskId; + } + + public void setUploadTaskId(String uploadTaskId) { + this.uploadTaskId = uploadTaskId; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getParentName() { + return parentName; + } + + public void setParentName(String parentName) { + this.parentName = parentName; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public boolean isChecked() { + return isChecked; + } + + public void setChecked(boolean isChecked) { + this.isChecked = isChecked; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java b/widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java new file mode 100644 index 0000000..f1c2da5 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/ImageItem.java @@ -0,0 +1,20 @@ +package com.hzecool.widget.puzzle; + +import java.util.List; + +/** + * Created by dd on 16/1/13. + * 单张图片实体 + */ +public class ImageItem { + + private List coordinates; + + public List getCoordinates() { + return coordinates; + } + + public void setCoordinates(List coordinates) { + this.coordinates = coordinates; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java b/widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java new file mode 100644 index 0000000..d9304c0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/PuzzleView.java @@ -0,0 +1,347 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Created by dd on 16/1/13. + * 拼图重要部分 + */ +public class PuzzleView extends View { + + private Context context; + private Path[] path; + private Bitmap[] bitmaps; + private boolean[] bitmapsFlag; + private float[][] pathLT; + private float[][] pathOffset; + private int pathNum; + private int viewWdh, viewHgt; + private int leftMargin; + private List pics; + private final static int MARGIN_HEIGHT = 100; + private List coordinateSetList; + + + public PuzzleView(Context context) { + super(context); + this.context = context; + } + + public PuzzleView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + this.context = context; + initPath(); + } + + public PuzzleView(Context context, AttributeSet attributeSet, int defStyle) { + super(context, attributeSet, defStyle); + this.context = context; + initPath(); + } + + public void setPathCoordinate(List pathCoordinate) { + this.coordinateSetList = pathCoordinate; + initPath(); + } + + public void setPics(List imageBeans) { + + leftMargin = (AppUtil.getScreenWidth(context) - dp2px(320)) / 2; + viewWdh = dp2px(320); + viewHgt = dp2px(450); + pics = new ArrayList<>(); + if (imageBeans != null) { + pics.addAll(imageBeans); + } + pathNum = pics.size(); + } + + private void initPath() { + path = new Path[pathNum]; + for (int i = 0; i < pathNum; i++) { + path[i] = new Path(); + } + bitmapsFlag = new boolean[pathNum]; + + pathLT = new float[pathNum][2]; + pathOffset = new float[pathNum][2]; + for (int i = 0; i < pathNum; i++) { + bitmapsFlag[i] = false; + pathLT[i][0] = 0f; + pathLT[i][1] = 0f; + pathOffset[i][0] = 0f; + pathOffset[i][1] = 0f; + } + + for (int i = 0; i < pathNum; i++) { + for (int j = 0; j < coordinateSetList.get(i).getCoordinates().size(); j++) { + float x = coordinateSetList.get(i).getCoordinates().get(j).getX(); + float y = coordinateSetList.get(i).getCoordinates().get(j).getY(); + if (j == 0) { + path[i].moveTo(dp2px(x), dp2px(y)); + } else { + path[i].lineTo(dp2px(x), dp2px(y)); + } + } + path[i].close(); + } + + // get bitmap + bitmaps = new Bitmap[pathNum]; + for (int i = 0; i < pathNum; i++) { + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + BitmapFactory.decodeFile(pics.get(i).getAbsolutePath(), opt); + + int bmpWdh = opt.outWidth; + int bmpHgt = opt.outHeight; + + Coordinates coordinate = caculateViewSize(coordinateSetList.get(i).getCoordinates()); + int size = caculateSampleSize(bmpWdh, bmpHgt, dp2px(coordinate.getX()), dp2px(coordinate.getY())); + opt.inJustDecodeBounds = false; + opt.inSampleSize = size; + + bitmaps[i] = scaleImage(BitmapFactory.decodeFile(pics.get(i).getAbsolutePath(), opt), dp2px(coordinate.getX()), dp2px(coordinate.getY())); + } + } + + private Coordinates caculateViewSize(List list) { + + float viewWidth; + float viewHeight; + + viewWidth = caculateMaxCoordinateX(list) - caculateMinCoordinateX(list); + viewHeight = caculateMaxCoordinateY(list) - caculateMinCoordinateY(list); + + return new Coordinates(viewWidth, viewHeight); + } + + + private int caculateSampleSize(int picWdh, int picHgt, int showWdh, + int showHgt) { + // 如果此时显示区域比图片大,直接返回 + if ((showWdh < picWdh) && (showHgt < picHgt)) { + int wdhSample = picWdh / showWdh; + int hgtSample = picHgt / showHgt; + // 利用小的来处理 + int sample = wdhSample > hgtSample ? hgtSample : wdhSample; + int minSample = 2; + while (sample > minSample) { + minSample *= 2; + } + return minSample >> 1; + } else { + return 0; + } + } + + private float caculateMinCoordinateX(List list) { + + float minX; + minX = list.get(0).getX(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getX() < minX) { + minX = list.get(i).getX(); + } + } + return minX; + } + + private float caculateMaxCoordinateX(List list) { + + float maxX; + maxX = list.get(0).getX(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getX() > maxX) { + maxX = list.get(i).getX(); + } + } + return maxX; + } + + private float caculateMinCoordinateY(List list) { + + float minY; + minY = list.get(0).getY(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getY() < minY) { + minY = list.get(i).getY(); + } + } + return minY; + } + + private float caculateMaxCoordinateY(List list) { + + float maxY; + maxY = list.get(0).getY(); + for (int i = 1; i < list.size(); i++) { + if (list.get(i).getY() > maxY) { + maxY = list.get(i).getY(); + } + } + return maxY; + } + + //图片缩放 + private static Bitmap scaleImage(Bitmap bm, int newWidth, int newHeight) { + if (bm == null) { + return null; + } + int width = bm.getWidth(); + int height = bm.getHeight(); + float scaleWidth = ((float) newWidth) / width; + float scaleHeight = ((float) newHeight) / height; + float scale = 1; + if (scaleWidth >= scaleHeight) { + scale = scaleWidth; + } else { + scale = scaleHeight; + } + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, + true); + return newbm; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // TODO Auto-generated method stub + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(viewWdh, viewHgt); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + canvas.drawColor(Color.TRANSPARENT);// 显示背景颜色 + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(Color.WHITE); +// canvas.drawPaint(paint); + // draw1(canvas); + startDraw(canvas, paint); + } + + + private void startDraw(Canvas canvas, Paint paint) { + for (int i = 0; i < pathNum; i++) { + canvas.save(); + drawScene(canvas, paint, i); + canvas.restore(); + } + } + + private void drawScene(Canvas canvas, Paint paint, int idx) { + canvas.clipPath(path[idx]); + canvas.drawColor(Color.GRAY); + if (bitmapsFlag[idx]) { + canvas.drawBitmap(bitmaps[idx], dp2px(caculateMinCoordinateX(coordinateSetList.get(idx).getCoordinates())) + pathOffsetX + pathOffset[idx][0], + dp2px(caculateMinCoordinateY(coordinateSetList.get(idx).getCoordinates())) + pathOffsetY + pathOffset[idx][1], paint); + } else { + canvas.drawBitmap(bitmaps[idx], dp2px(caculateMinCoordinateX(coordinateSetList.get(idx).getCoordinates())) + pathOffset[idx][0], + dp2px(caculateMinCoordinateY(coordinateSetList.get(idx).getCoordinates())) + pathOffset[idx][1], paint); + } + } + + private int dp2px(float point) { + return DensityUtil.dip2px(getContext(), point); + } + + float ptx, pty; + float pathOffsetX, pathOffsetY; + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + for (int i = 0; i < pathNum; i++) { + bitmapsFlag[i] = false; + } + ptx = event.getRawX() - dp2px(leftMargin); + pty = event.getRawY() - dp2px(MARGIN_HEIGHT); + pathOffsetX = 0; + pathOffsetY = 0; + int cflag = 0; + for (cflag = 0; cflag < pathNum; cflag++) { + if (contains(path[cflag], ptx, pty)) { + bitmapsFlag[cflag] = true; + break; + } + } + break; + case MotionEvent.ACTION_MOVE: + pathOffsetX = event.getRawX() - dp2px(leftMargin) - ptx; + pathOffsetY = event.getRawY() - dp2px(MARGIN_HEIGHT) - pty; + invalidate(); + break; + case MotionEvent.ACTION_POINTER_DOWN: + break; + case MotionEvent.ACTION_UP: + for (int i = 0; i < pathNum; i++) { + if (bitmapsFlag[i]) { + pathOffset[i][0] += event.getRawX() - dp2px(leftMargin) - ptx; + pathOffset[i][1] += event.getRawY() - dp2px(MARGIN_HEIGHT) - pty; + + if (pathOffset[i][0] > 0) { + pathOffset[i][0] = 0; + } + if (pathOffset[i][0] < -(bitmaps[i].getWidth() - getViewWidth(coordinateSetList.get(i).getCoordinates()))) { + pathOffset[i][0] = -(bitmaps[i].getWidth() - getViewWidth(coordinateSetList.get(i).getCoordinates())); + } + if (pathOffset[i][1] > 0) { + pathOffset[i][1] = 0; + } + if (pathOffset[i][1] < -(bitmaps[i].getHeight() - getViewHeight(coordinateSetList.get(i).getCoordinates()))) { + pathOffset[i][1] = -(bitmaps[i].getHeight() - getViewHeight(coordinateSetList.get(i).getCoordinates())); + } + bitmapsFlag[i] = false; + break; + } + } + invalidate(); + break; + default: + break; + } + + return true; + } + + private boolean contains(Path parapath, float pointx, float pointy) { + RectF localRectF = new RectF(); + parapath.computeBounds(localRectF, true); + Region localRegion = new Region(); + localRegion.setPath(parapath, new Region((int) localRectF.left, + (int) localRectF.top, (int) localRectF.right, + (int) localRectF.bottom)); + return localRegion.contains((int) pointx, (int) pointy); + } + + private float getViewWidth(List list) { + + return dp2px(caculateMaxCoordinateX(list) - caculateMinCoordinateX(list)); + } + + private float getViewHeight(List list) { + + return dp2px(caculateMaxCoordinateY(list) - caculateMinCoordinateY(list)); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java b/widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java new file mode 100644 index 0000000..16477cf --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/puzzle/SelectItem.java @@ -0,0 +1,29 @@ +package com.hzecool.widget.puzzle; + +import android.content.Context; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +/** + * Created by dd on 16/1/13. + */ +public class SelectItem extends RelativeLayout { + + + private ImageView mImageView; + + public SelectItem(Context context) { + super(context); + mImageView = new ImageView(context); + mImageView.setAdjustViewBounds(true); + mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + LayoutParams mImageViewRLP = new LayoutParams(DensityUtil.dip2px(context, 60), DensityUtil.dip2px(context, 60)); + mImageViewRLP.addRule(RelativeLayout.CENTER_IN_PARENT); + mImageViewRLP.setMargins(0, DensityUtil.dip2px(context, 30), DensityUtil.dip2px(context, 20), 0); + addView(mImageView, mImageViewRLP); + } + + public ImageView getmImageView() { + return mImageView; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java new file mode 100644 index 0000000..19d907a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditText.java @@ -0,0 +1,161 @@ +package com.hzecool.widget.riseedittext; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.hzecool.widget.R; +import com.hzecool.widget.utils.SizeUtils; + +/** + * 可以随着传入的字符串的长度增加或减少显示的方框形状的edittext,讲字符串分开显示 + * Created by song on 2017/6/19. + */ + +public class RiseEditText extends LinearLayout { + + private LinearLayout mLl_editext; + + public RiseEditText(Context context) { + this(context, null); + } + + public RiseEditText(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public RiseEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + View.inflate(getContext(), R.layout.edittext_layout, this); + mLl_editext = (LinearLayout) findViewById(R.id.ll_edittext); + } + + private String content; + + public void setContent(String content) { + this.content = content; + //初始化edittext的数量 + initEditextNum(); + } + + private void initEditextNum() { + mLl_editext.removeAllViews(); + char[] chars = content.toCharArray(); + for (int i = 0; i < content.length(); i++) { + EditText editText = new EditText(getContext()); + // MaterialEditText materialEditText = new MaterialEditText(getContext()); + editText.setBackgroundResource(R.drawable.bg_square_voice); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + editText.setPadding(SizeUtils.dp2px(getContext(), 10), 0, SizeUtils.dp2px(getContext(), 10), 0); + params.topMargin = SizeUtils.dp2px(getContext(), 4); + params.bottomMargin = SizeUtils.dp2px(getContext(), 4); + params.leftMargin = SizeUtils.dp2px(getContext(), 10); + editText.setTextSize(SizeUtils.sp2px(getContext(), 8)); + //限制输入内容的长度 + editText.setGravity(Gravity.CENTER); + editText.setText(String.valueOf(chars[i])); + //去除滑动内容弹出复制粘贴 + editText.setCustomSelectionActionModeCallback(mCallback); + //去除长按复制功能 + editText.setLongClickable(false); + //默认不全部选中内容 + editText.setSelectAllOnFocus(false); + //清除焦点 + editText.clearFocus(); + //默认不选中内容 + editText.setSelected(false); + //不显示光标 + editText.setCursorVisible(false); + //添加view + mLl_editext.addView(editText, params); + editText.setOnClickListener(mPointClickListener); + /*editText.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + ((EditText) v).setText(null); + } + });*/ + } + } + + + /** + * 限制输入类型,1仅输入数字 0任意字符 默认是任意字符. + * + * @param type + */ + public void setInputType(int type) { + int childCount = mLl_editext.getChildCount(); + for (int i = 0; i < childCount; i++) { + EditText editText = (EditText) mLl_editext.getChildAt(i); + if (type == 0) { + //输入任意字符 + editText.setInputType(InputType.TYPE_CLASS_TEXT); + } else if (type == 1) { + //仅仅输入数字 + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + } + } + } + + /** + * 去掉复制粘贴的功能 + */ + private ActionMode.Callback mCallback = new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + + } + }; + + private OnClickListener mPointClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + ((EditText) v).setText(null); + } + }; + + /** + * 实现editText的getText()功能 + * + * @return + */ + public String getText() { + String str = ""; + int count = mLl_editext.getChildCount(); + for (int i = 0; i < count; i++) { + EditText child = (EditText) mLl_editext.getChildAt(i); + str = str + child.getText(); + } + return str; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java new file mode 100644 index 0000000..f62c839 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/riseedittext/RiseEditTextNum.java @@ -0,0 +1,144 @@ +package com.hzecool.widget.riseedittext; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.text.InputType; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.LinearLayout; + +import com.hzecool.widget.R; +import com.hzecool.widget.utils.SizeUtils; + +/** + * 仅支持输入数字的自增的edittext + * Created by song on 2017/7/3. + */ + +public class RiseEditTextNum extends LinearLayout { + private LinearLayout mLl_editext; + + public RiseEditTextNum(Context context) { + this(context,null); + } + + public RiseEditTextNum(Context context, @Nullable AttributeSet attrs) { + this(context, attrs,0); + } + + public RiseEditTextNum(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + View.inflate(getContext(), R.layout.edittext_layout, this); + mLl_editext = (LinearLayout) findViewById(R.id.ll_edittext); + } + + private String content; + + public void setContent(String content) { + this.content = content; + //初始化edittext的数量 + initEditextNum(); + } + + private void initEditextNum() { + mLl_editext.removeAllViews(); + char[] chars = content.toCharArray(); + for (int i = 0; i < content.length(); i++) { + EditText editText = new EditText(getContext()); + // MaterialEditText materialEditText = new MaterialEditText(getContext()); + editText.setBackgroundResource(R.drawable.bg_square_voice); + LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + editText.setPadding(SizeUtils.dp2px(getContext(), 10), 0, SizeUtils.dp2px(getContext(), 10), 0); + params.topMargin = SizeUtils.dp2px(getContext(), 4); + params.bottomMargin = SizeUtils.dp2px(getContext(), 4); + params.leftMargin = SizeUtils.dp2px(getContext(), 10); + editText.setTextSize(SizeUtils.sp2px(getContext(), 8)); + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + //限制输入内容的长度 + // editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1)}); + editText.setGravity(Gravity.CENTER); + editText.setText(String.valueOf(chars[i])); + //去除滑动内容弹出复制粘贴 + editText.setCustomSelectionActionModeCallback(mCallback); + //去除长按复制功能 + editText.setLongClickable(false); + //默认不全部选中内容 + editText.setSelectAllOnFocus(false); + //清除焦点 + editText.clearFocus(); + //默认不选中内容 + editText.setSelected(false); + //不显示光标 + editText.setCursorVisible(false); + //添加view + mLl_editext.addView(editText, params); + editText.setOnClickListener(mPointClickListener); + /*editText.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + ((EditText) v).setText(null); + } + });*/ + } + } + + + + /** + * 去掉复制粘贴的功能 + */ + private ActionMode.Callback mCallback = new ActionMode.Callback() { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + + } + }; + + private OnClickListener mPointClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + ((EditText) v).setText(null); + } + }; + + /** + * 实现editText的getText()功能 + * + * @return + */ + public String getText() { + String str = ""; + int count = mLl_editext.getChildCount(); + for (int i = 0; i < count; i++) { + EditText child = (EditText) mLl_editext.getChildAt(i); + str = str + child.getText(); + } + return str; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java b/widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java new file mode 100644 index 0000000..d9bc4fd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/risenumber/IRiseNumber.java @@ -0,0 +1,43 @@ +package com.hzecool.widget.risenumber; + +/** + * Created by song on 2017/5/16. + */ + +public interface IRiseNumber { + /** + * 开始播放动画的方法 + */ + public void start(); + + /** + * 设置小数 + * + * @param number + * @return + */ + public void withNumber(float number); + + /** + * 设置整数 + * + * @param number + * @return + */ + public void withNumber(int number); + + /** + * 设置动画播放时长 + * + * @param duration + * @return + */ + public void setDuration(long duration); + + /** + * 设置动画结束监听器 + * + * @param callback + */ + public void setOnEndListener(RiseNumberTextView.EndListener callback); +} diff --git a/widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java b/widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java new file mode 100644 index 0000000..3d88587 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/risenumber/RiseNumberTextView.java @@ -0,0 +1,219 @@ +package com.hzecool.widget.risenumber; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Color; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.text.DecimalFormat; + +/** + * Created by song on 2017/5/16. + */ + +public class RiseNumberTextView extends TextView implements IRiseNumber{ + private static final int STOPPED = 0; + + private static final int RUNNING = 1; + + private int mPlayingState = STOPPED; + + private float number; + + private float fromNumber; + + /** + * 动画播放时长 + */ + private long duration = 1500; + /** + * 1.int 2.float + */ + private int numberType = 2; + + private DecimalFormat fnum; + + private EndListener mEndListener = null; + + final static int[] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, + 99999999, 999999999, Integer.MAX_VALUE }; + + /** + * 构造方法 + * + * @param context + */ + public RiseNumberTextView(Context context) { + super(context); + } + + /** + * 使用xml布局文件默认的被调用的构造方法 + * + * @param context + * @param attr + */ + public RiseNumberTextView(Context context, AttributeSet attr) { + super(context, attr); + setTextColor(Color.RED); + setTextSize(30); + } + + public RiseNumberTextView(Context context, AttributeSet attr, int defStyle) { + super(context, attr, defStyle); + } + + /** + * 判断动画是否正在播放 + * + * @return + */ + public boolean isRunning() { + return (mPlayingState == RUNNING); + } + + /** + * 跑小数动画 + */ + private void runFloat() { + ValueAnimator valueAnimator = ValueAnimator.ofFloat(fromNumber, number); + valueAnimator.setDuration(duration); + + valueAnimator + .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + + setText(fnum.format(Float.parseFloat(valueAnimator + .getAnimatedValue().toString()))); + if (valueAnimator.getAnimatedFraction() >= 1) { + mPlayingState = STOPPED; + if (mEndListener != null) + mEndListener.onEndFinish(); + } + } + + + }); + + valueAnimator.start(); + } + + /** + * 跑整数动画 + */ + private void runInt() { + + ValueAnimator valueAnimator = ValueAnimator.ofInt((int) fromNumber, + (int) number); + valueAnimator.setDuration(duration); + + valueAnimator + .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + //设置瞬时的数据值到界面上 + setText(valueAnimator.getAnimatedValue().toString()); + if (valueAnimator.getAnimatedFraction() >= 1) { + //设置状态为停止 + mPlayingState = STOPPED; + if (mEndListener != null) + //通知监听器,动画结束事件 + mEndListener.onEndFinish(); + } + } + }); + valueAnimator.start(); + } + + static int sizeOfInt(int x) { + for (int i = 0;; i++){ + if (x <= sizeTable[i]) + return i + 1; + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + fnum = new DecimalFormat("##0.00"); + } + + /** + * 开始播放动画 + */ + @Override + public void start() { + + if (!isRunning()) { + mPlayingState = RUNNING; + if (numberType == 1) + runInt(); + else + runFloat(); + } + } + + /** + * 设置一个小数进来 + */ + @Override + public void withNumber(float number) { + + this.number = number; + numberType = 2; + /*if (number > 1000) { + fromNumber = number + - (float) Math.pow(10, sizeOfInt((int) number) - 1); + } else { + fromNumber = number / 2; + }*/ + fromNumber = number / 2; + + } + + /** + * 设置一个整数进来 + */ + @Override + public void withNumber(int number) { + this.number = number; + numberType = 1; + /* if (number > 1000) { + fromNumber = number + - (float) Math.pow(10, sizeOfInt((int) number) - 2); + } else { + fromNumber = number / 2; + }*/ + fromNumber = number / 2; + } + + /** + * 设置动画播放时间 + */ + @Override + public void setDuration(long duration) { + this.duration = duration; + } + + /** + * 设置动画结束监听器 + */ + @Override + public void setOnEndListener(EndListener callback) { + mEndListener = callback; + } + + /** + * 定义动画结束接口 + * + * + */ + public interface EndListener { + /** + * 当动画播放结束时的回调方法 + */ + public void onEndFinish(); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java new file mode 100644 index 0000000..c3a0b0b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUD.java @@ -0,0 +1,348 @@ +package com.hzecool.widget.svprogresshud; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.os.Message; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; + +import com.hzecool.widget.R; +import com.hzecool.widget.svprogresshud.listener.OnDismissListener; +import com.hzecool.widget.svprogresshud.view.SVCircleProgressBar; +import com.hzecool.widget.svprogresshud.view.SVProgressDefaultView; + +import java.lang.ref.WeakReference; + +/** + * Created by Sai on 15/8/15. + */ +public class SVProgressHUD { + private WeakReference contextWeak; + private static final long DISMISSDELAYED = 1000; + private SVProgressHUDMaskType mSVProgressHUDMaskType; + private boolean isShowing; + private boolean isDismissing; + + public enum SVProgressHUDMaskType { + None, // 允许遮罩下面控件点击 + Clear, // 不允许遮罩下面控件点击 + Black, // 不允许遮罩下面控件点击,背景黑色半透明 + Gradient, // 不允许遮罩下面控件点击,背景渐变半透明 + ClearCancel, // 不允许遮罩下面控件点击,点击遮罩消失 + BlackCancel, // 不允许遮罩下面控件点击,背景黑色半透明,点击遮罩消失 + GradientCancel // 不允许遮罩下面控件点击,背景渐变半透明,点击遮罩消失 + ; + } + + private final FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM + ); + private ViewGroup decorView;//activity的根View + private ViewGroup rootView;// mSharedView 的 根View + private SVProgressDefaultView mSharedView; + + private Animation outAnim; + private Animation inAnim; + private int gravity = Gravity.CENTER; + private OnDismissListener onDismissListener; + + + public SVProgressHUD(Context context){ + this.contextWeak = new WeakReference<>(context); + gravity = Gravity.CENTER; + initViews(); + initDefaultView(); + initAnimation(); + } + + protected void initViews() { + Context context = contextWeak.get(); + if(context == null) return; + + LayoutInflater layoutInflater = LayoutInflater.from(context); + decorView = (ViewGroup) ((Activity) context).getWindow().getDecorView().findViewById(android.R.id.content); + rootView = (ViewGroup) layoutInflater.inflate(R.layout.layout_svprogresshud, null, false); + rootView.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + )); + } + protected void initDefaultView(){ + Context context = contextWeak.get(); + if(context == null) return; + + mSharedView = new SVProgressDefaultView(context); + params.gravity = gravity; + mSharedView.setLayoutParams(params); + } + + protected void initAnimation() { + if(inAnim == null) + inAnim = getInAnimation(); + if(outAnim == null) + outAnim = getOutAnimation(); + } + + /** + * show的时候调用 + */ + private void onAttached() { + isShowing = true; + decorView.addView(rootView); + if(mSharedView.getParent()!=null)((ViewGroup)mSharedView.getParent()).removeView(mSharedView); + rootView.addView(mSharedView); + } + + /** + * 添加这个View到Activity的根视图 + */ + private void svShow() { + + mHandler.removeCallbacksAndMessages(null); + onAttached(); + + mSharedView.startAnimation(inAnim); + + } + + public void show() { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.show(); + svShow(); + } + + public void showWithMaskType(SVProgressHUDMaskType maskType) { + if(isShowing())return; + //判断maskType + setMaskType(maskType); + mSharedView.show(); + svShow(); + } + + public void showWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showWithStatus(string); + svShow(); + } + + public void showWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showWithStatus(string); + svShow(); + } + + public void showInfoWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showInfoWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showInfoWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showInfoWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showSuccessWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showSuccessWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showSuccessWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showSuccessWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showErrorWithStatus(String string) { + if(isShowing())return; + setMaskType(SVProgressHUDMaskType.Black); + mSharedView.showErrorWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showErrorWithStatus(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showErrorWithStatus(string); + svShow(); + scheduleDismiss(); + } + + public void showWithProgress(String string, SVProgressHUDMaskType maskType) { + if(isShowing())return; + setMaskType(maskType); + mSharedView.showWithProgress(string); + svShow(); + } + + public SVCircleProgressBar getProgressBar(){ + return mSharedView.getCircleProgressBar(); + } + public void setText(String string){ + mSharedView.setText(string); + } + + private void setMaskType(SVProgressHUDMaskType maskType) { + mSVProgressHUDMaskType = maskType; + switch (mSVProgressHUDMaskType) { + case None: + configMaskType(android.R.color.transparent, false, false); + break; + case Clear: + configMaskType(android.R.color.transparent, true, false); + break; + case ClearCancel: + configMaskType(android.R.color.transparent, true, true); + break; + case Black: + configMaskType(R.color.bgColor_overlay, true, false); + break; + case BlackCancel: + configMaskType(R.color.bgColor_overlay, true, true); + break; + case Gradient: + configMaskType(R.drawable.bg_overlay_gradient, true, false); + break; + case GradientCancel: + configMaskType(R.drawable.bg_overlay_gradient, true, true); + break; + default: + break; + } + } + + private void configMaskType(int bg, boolean clickable, boolean cancelable) { + rootView.setBackgroundResource(bg); + rootView.setClickable(clickable); + setCancelable(cancelable); + } + + /** + * 检测该View是不是已经添加到根视图 + * + * @return 如果视图已经存在该View返回true + */ + public boolean isShowing() { + return rootView.getParent() != null || isShowing; + } + + public void dismiss() { + if(isDismissing)return; + isDismissing = true; + //消失动画 + outAnim.setAnimationListener(outAnimListener); + mSharedView.dismiss(); + mSharedView.startAnimation(outAnim); + } + + public void dismissImmediately() { + mSharedView.dismiss(); + rootView.removeView(mSharedView); + decorView.removeView(rootView); + isShowing = false; + isDismissing = false; + if(onDismissListener != null){ + onDismissListener.onDismiss(this); + } + + } + + public Animation getInAnimation() { + Context context = contextWeak.get(); + if(context == null) return null; + + int res = SVProgressHUDAnimateUtil.getAnimationResource(this.gravity, true); + return AnimationUtils.loadAnimation(context, res); + } + + public Animation getOutAnimation() { + Context context = contextWeak.get(); + if(context == null) return null; + + int res = SVProgressHUDAnimateUtil.getAnimationResource(this.gravity, false); + return AnimationUtils.loadAnimation(context, res); + } + + private void setCancelable(boolean isCancelable) { + View view = rootView.findViewById(R.id.sv_outmost_container); + + if (isCancelable) { + view.setOnTouchListener(onCancelableTouchListener); + } else { + view.setOnTouchListener(null); + } + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + dismiss(); + } + }; + + private void scheduleDismiss() { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessageDelayed(0, DISMISSDELAYED); + } + + /** + * Called when the user touch on black overlay in order to dismiss the dialog + */ + private final View.OnTouchListener onCancelableTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + dismiss(); + setCancelable(false); + } + return false; + } + }; + + private Animation.AnimationListener outAnimListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + dismissImmediately(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }; + + public void setOnDismissListener(OnDismissListener listener){ + this.onDismissListener = listener; + } + + public OnDismissListener getOnDismissListener(){ + return onDismissListener; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java new file mode 100644 index 0000000..734acf3 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/SVProgressHUDAnimateUtil.java @@ -0,0 +1,25 @@ +package com.hzecool.widget.svprogresshud; + +import android.view.Gravity; + +import com.hzecool.widget.R; + +/** + * Created by Sai on 15/8/16. + */ +public class SVProgressHUDAnimateUtil { + private static final int INVALID = -1; + static int getAnimationResource(int gravity, boolean isInAnimation) { + switch (gravity) { + case Gravity.TOP: + return isInAnimation ? R.anim.svslide_in_top : R.anim.svslide_out_top; + case Gravity.BOTTOM: + return isInAnimation ? R.anim.svslide_in_bottom : R.anim.svslide_out_bottom; + case Gravity.CENTER: + return isInAnimation ? R.anim.svfade_in_center : R.anim.svfade_out_center; + default: + // This case is not implemented because we don't expect any other gravity at the moment + } + return INVALID; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java new file mode 100644 index 0000000..50f2530 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/listener/OnDismissListener.java @@ -0,0 +1,10 @@ +package com.hzecool.widget.svprogresshud.listener; + +import com.hzecool.widget.svprogresshud.SVProgressHUD; + +/** + * Created by Sai on 16/7/31. + */ +public interface OnDismissListener { + void onDismiss(SVProgressHUD hud); +} diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java new file mode 100644 index 0000000..c651c69 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVCircleProgressBar.java @@ -0,0 +1,195 @@ +package com.hzecool.widget.svprogresshud.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import com.hzecool.widget.R; + + +/** + * Created by Sai on 15/9/1. + */ +public class SVCircleProgressBar extends View { + /** + * 画笔对象的引用 + */ + private Paint paint; + + /** + * 圆环的颜色 + */ + private int roundColor; + + /** + * 圆环进度的颜色 + */ + private int roundProgressColor; + + /** + * 圆环的宽度 + */ + private float roundWidth; + + /** + * 最大进度 + */ + private int max; + + /** + * 当前进度 + */ + private int progress; + + /** + * 进度的风格,实心或者空心 + */ + private int style; + + public static final int STROKE = 0; + public static final int FILL = 1; + + public SVCircleProgressBar(Context context) { + this(context, null); + } + + public SVCircleProgressBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SVCircleProgressBar(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + paint = new Paint(); + + TypedArray mTypedArray = context.obtainStyledAttributes(attrs, + R.styleable.SVCircleProgressBar); + + // 获取自定义属性和默认值 + roundColor = mTypedArray.getColor(R.styleable.SVCircleProgressBar_svprogress_roundColor, Color.BLUE); + roundProgressColor = mTypedArray.getColor(R.styleable.SVCircleProgressBar_svprogress_roundProgressColor, + Color.GRAY); + roundWidth = mTypedArray.getDimension(R.styleable.SVCircleProgressBar_svprogress_roundWidth, 5); + max = mTypedArray.getInteger(R.styleable.SVCircleProgressBar_svprogress_max, 100); + style = mTypedArray.getInt(R.styleable.SVCircleProgressBar_svprogress_style, 0); + + mTypedArray.recycle(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + /** + * 画最外层的大圆环 + */ + int centre = getWidth() / 2; // 获取圆心的x坐标 + int radius = (int) (centre - roundWidth / 2); // 圆环的半径 + paint.setAntiAlias(true); // 消除锯齿 + paint.setColor(roundColor); // 设置圆环的颜色 + paint.setStyle(Paint.Style.STROKE); // 设置空心 + paint.setStrokeWidth(roundWidth); // 设置圆环的宽度 + canvas.drawCircle(centre, centre, radius, paint); // 画出圆环 + + + /** + * 画圆弧 ,画圆环的进度 + */ + + // 设置进度是实心还是空心 + paint.setStrokeWidth(roundWidth); // 设置圆环的宽度 + paint.setColor(roundProgressColor); // 设置进度的颜色 + RectF oval = new RectF(centre - radius, centre - radius, centre + + radius, centre + radius); // 用于定义的圆弧的形状和大小的界限 + + switch (style) { + case STROKE: { + paint.setStyle(Paint.Style.STROKE); + canvas.drawArc(oval, 270, 360 * progress / max, false, paint); // 根据进度画圆弧 + break; + } + case FILL: { + paint.setStyle(Paint.Style.FILL_AND_STROKE); + if (progress != 0) + canvas.drawArc(oval, 270, 360 * progress / max, true, paint); // 根据进度画圆弧 + break; + } + } + + } + + public synchronized int getMax() { + return max; + } + + /** + * 设置进度的最大值 + * + * @param max + */ + public synchronized void setMax(int max) { + if (max < 0) { + throw new IllegalArgumentException("max not less than 0"); + } + this.max = max; + } + + /** + * 获取进度.需要同步 + * + * @return + */ + public synchronized int getProgress() { + return progress; + } + + /** + * 设置进度,此为线程安全控件,由于考虑多线的问题,需要同步 + * 刷新界面调用postInvalidate()能在非UI线程刷新 + * + * @param progress + */ + public synchronized void setProgress(int progress) { + if (progress < 0) { + throw new IllegalArgumentException("progress not less than 0"); + } + if (progress > max) { + progress = max; + } + if (progress <= max) { + this.progress = progress; + postInvalidate(); + } + + } + + public int getCircleColor() { + return roundColor; + } + + public void setCircleColor(int circleColor) { + this.roundColor = circleColor; + } + + public int getCircleProgressColor() { + return roundProgressColor; + } + + public void setCircleProgressColor(int circleProgressColor) { + this.roundProgressColor = circleProgressColor; + } + + public float getRoundWidth() { + return roundWidth; + } + + public void setRoundWidth(float roundWidth) { + this.roundWidth = roundWidth; + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java new file mode 100644 index 0000000..de6ced6 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/svprogresshud/view/SVProgressDefaultView.java @@ -0,0 +1,126 @@ +package com.hzecool.widget.svprogresshud.view; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.hzecool.widget.R; + + +/** + * Created by Sai on 15/8/15. + * 默认的SVProgress效果 + */ +public class SVProgressDefaultView extends LinearLayout { + private int resBigLoading = R.mipmap.ic_svstatus_loading; + private int resInfo = R.mipmap.ic_svstatus_info; + private int resSuccess = R.mipmap.ic_svstatus_success; + private int resError = R.mipmap.ic_svstatus_error; + private ImageView ivBigLoading, ivSmallLoading; + private SVCircleProgressBar circleProgressBar; + private TextView tvMsg; + + private RotateAnimation mRotateAnimation; + + public SVProgressDefaultView(Context context) { + super(context); + initViews(); + init(); + } + + private void initViews() { + LayoutInflater.from(getContext()).inflate(R.layout.view_svprogressdefault, this, true); + ivBigLoading = (ImageView) findViewById(R.id.ivBigLoading); + ivSmallLoading = (ImageView) findViewById(R.id.ivSmallLoading); + circleProgressBar = (SVCircleProgressBar) findViewById(R.id.circleProgressBar); + tvMsg = (TextView) findViewById(R.id.tvMsg); + } + + private void init() { + mRotateAnimation = new RotateAnimation(0f, 359f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mRotateAnimation.setDuration(1000L); + mRotateAnimation.setInterpolator(new LinearInterpolator()); + mRotateAnimation.setRepeatCount(-1); + mRotateAnimation.setRepeatMode(Animation.RESTART); + } + + public void show() { + clearAnimations(); + ivBigLoading.setImageResource(resBigLoading); + ivBigLoading.setVisibility(View.VISIBLE); + ivSmallLoading.setVisibility(View.GONE); + circleProgressBar.setVisibility(View.GONE); + tvMsg.setVisibility(View.GONE); + //开启旋转动画 + ivBigLoading.startAnimation(mRotateAnimation); + } + + public void showWithStatus(String string) { + if (string == null) { + show(); + return; + } + showBaseStatus(resBigLoading, string); + //开启旋转动画 + ivSmallLoading.startAnimation(mRotateAnimation); + } + + public void showInfoWithStatus(String string) { + showBaseStatus(resInfo, string); + } + + public void showSuccessWithStatus(String string) { + showBaseStatus(resSuccess, string); + } + + public void showErrorWithStatus(String string) { + showBaseStatus(resError, string); + } + public void showWithProgress(String string) { + showProgress(string); + } + + public SVCircleProgressBar getCircleProgressBar() { + return circleProgressBar; + } + + public void setText(String string){ + tvMsg.setText(string); + } + + public void showProgress(String string) { + clearAnimations(); + tvMsg.setText(string); + ivBigLoading.setVisibility(View.GONE); + ivSmallLoading.setVisibility(View.GONE); + circleProgressBar.setVisibility(View.VISIBLE); + tvMsg.setVisibility(View.VISIBLE); + } + + public void showBaseStatus(int res, String string) { + clearAnimations(); + ivSmallLoading.setImageResource(res); + tvMsg.setText(string); + ivBigLoading.setVisibility(View.GONE); + circleProgressBar.setVisibility(View.GONE); + ivSmallLoading.setVisibility(View.VISIBLE); + tvMsg.setVisibility(View.VISIBLE); + } + + public void dismiss() { + clearAnimations(); + } + + private void clearAnimations() { + ivBigLoading.clearAnimation(); + ivSmallLoading.clearAnimation(); + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java b/widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java new file mode 100644 index 0000000..0b4f5ce --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/swipdelete/SwipeMenuLayout.java @@ -0,0 +1,621 @@ +package com.hzecool.widget.swipdelete; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PointF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.OvershootInterpolator; + +import com.hzecool.widget.R; + +/** + * Created by tutu on 2017/5/17. + */ + +public class SwipeMenuLayout extends ViewGroup { + private static final String TAG = "zxt/SwipeMenuLayout"; + + private int mScaleTouchSlop;//为了处理单击事件的冲突 + private int mMaxVelocity;//计算滑动速度用 + private int mPointerId;//多点触摸只算第一根手指的速度 + private int mHeight;//自己的高度 + //右侧菜单宽度总和(最大滑动距离) + private int mRightMenuWidths; + + //滑动判定临界值(右侧菜单宽度的40%) 手指抬起时,超过了展开,没超过收起menu + private int mLimit; + + private View mContentView;//2016 11 13 add ,存储contentView(第一个View) + + //private Scroller mScroller;//以前item的滑动动画靠它做,现在用属性动画做 + //上一次的xy + private PointF mLastP = new PointF(); + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击除侧滑菜单之外的区域,关闭侧滑菜单。 + //增加一个布尔值变量,dispatch函数里,每次down时,为true,move时判断,如果是滑动动作,设为false。 + //在Intercept函数的up时,判断这个变量,如果仍为true 说明是点击事件,则关闭菜单。 + private boolean isUnMoved = true; + + //2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + //up-down的坐标,判断是否是滑动,如果是,则屏蔽一切点击事件 + private PointF mFirstP = new PointF(); + private boolean isUserSwiped; + + //存储的是当前正在展开的View + private static SwipeMenuLayout mViewCache; + + //防止多只手指一起滑我的flag 在每次down里判断, touch事件结束清空 + private static boolean isTouching; + + private VelocityTracker mVelocityTracker;//滑动速度变量 + private android.util.Log LogUtils; + + /** + * 右滑删除功能的开关,默认开 + */ + private boolean isSwipeEnable; + + /** + * IOS、QQ式交互,默认开 + */ + private boolean isIos; + + private boolean iosInterceptFlag;//IOS类型下,是否拦截事件的flag + + /** + * 20160929add 左滑右滑的开关,默认左滑打开菜单 + */ + private boolean isLeftSwipe; + + public SwipeMenuLayout(Context context) { + this(context, null); + } + + public SwipeMenuLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + public boolean isSwipeEnable() { + return isSwipeEnable; + } + + /** + * 设置侧滑功能开关 + * + * @param swipeEnable + */ + public void setSwipeEnable(boolean swipeEnable) { + isSwipeEnable = swipeEnable; + } + + + public boolean isIos() { + return isIos; + } + + /** + * 设置是否开启IOS阻塞式交互 + * + * @param ios + */ + public SwipeMenuLayout setIos(boolean ios) { + isIos = ios; + return this; + } + + public boolean isLeftSwipe() { + return isLeftSwipe; + } + + /** + * 设置是否开启左滑出菜单,设置false 为右滑出菜单 + * + * @param leftSwipe + * @return + */ + public SwipeMenuLayout setLeftSwipe(boolean leftSwipe) { + isLeftSwipe = leftSwipe; + return this; + } + + /** + * 返回ViewCache + * + * @return + */ + public static SwipeMenuLayout getViewCache() { + return mViewCache; + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + mScaleTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMaxVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); + //初始化滑动帮助类对象 + //mScroller = new Scroller(context); + + //右滑删除功能的开关,默认开 + isSwipeEnable = true; + //IOS、QQ式交互,默认开 + isIos = true; + //左滑右滑的开关,默认左滑打开菜单 + isLeftSwipe = true; + TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout, defStyleAttr, 0); + int count = ta.getIndexCount(); + for (int i = 0; i < count; i++) { + int attr = ta.getIndex(i); + //如果引用成AndroidLib 资源都不是常量,无法使用switch case + if (attr == R.styleable.SwipeMenuLayout_swipeEnable) { + isSwipeEnable = ta.getBoolean(attr, true); + } else if (attr == R.styleable.SwipeMenuLayout_ios) { + isIos = ta.getBoolean(attr, true); + } else if (attr == R.styleable.SwipeMenuLayout_leftSwipe) { + isLeftSwipe = ta.getBoolean(attr, true); + } + } + ta.recycle(); + + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //Log.d(TAG, "onMeasure() called with: " + "widthMeasureSpec = [" + widthMeasureSpec + "], heightMeasureSpec = [" + heightMeasureSpec + "]"); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + setClickable(true);//令自己可点击,从而获取触摸事件 + + mRightMenuWidths = 0;//由于ViewHolder的复用机制,每次这里要手动恢复初始值 + mHeight = 0; + int contentWidth = 0;//2016 11 09 add,适配GridLayoutManager,将以第一个子Item(即ContentItem)的宽度为控件宽度 + int childCount = getChildCount(); + + //add by 2016 08 11 为了子View的高,可以matchParent(参考的FrameLayout 和LinearLayout的Horizontal) + final boolean measureMatchParentChildren = MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; + boolean isNeedMeasureChildHeight = false; + + for (int i = 0; i < childCount; i++) { + View childView = getChildAt(i); + //令每一个子View可点击,从而获取触摸事件 + childView.setClickable(true); + if (childView.getVisibility() != GONE) { + //后续计划加入上滑、下滑,则将不再支持Item的margin + measureChild(childView, widthMeasureSpec, heightMeasureSpec); + //measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0); + final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams(); + mHeight = Math.max(mHeight, childView.getMeasuredHeight()/* + lp.topMargin + lp.bottomMargin*/); + if (measureMatchParentChildren && lp.height == LayoutParams.MATCH_PARENT) { + isNeedMeasureChildHeight = true; + } + if (i > 0) {//第一个布局是Left item,从第二个开始才是RightMenu + mRightMenuWidths += childView.getMeasuredWidth(); + } else { + mContentView = childView; + contentWidth = childView.getMeasuredWidth(); + } + } + } + setMeasuredDimension(getPaddingLeft() + getPaddingRight() + contentWidth, + mHeight + getPaddingTop() + getPaddingBottom());//宽度取第一个Item(Content)的宽度 + mLimit = mRightMenuWidths * 4 / 10;//滑动判断的临界值 + //Log.d(TAG, "onMeasure() called with: " + "mRightMenuWidths = [" + mRightMenuWidths); + if (isNeedMeasureChildHeight) {//如果子View的height有MatchParent属性的,设置子View高度 + forceUniformHeight(childCount, widthMeasureSpec); + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + /** + * 给MatchParent的子View设置高度 + * + * @param count + * @param widthMeasureSpec + * @see android.widget.LinearLayout# 同名方法 + */ + private void forceUniformHeight(int count, int widthMeasureSpec) { + // Pretend that the linear layout has an exact size. This is the measured height of + // ourselves. The measured height should be the max height of the children, changed + // to accommodate the heightMeasureSpec from the parent + int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), + MeasureSpec.EXACTLY);//以父布局高度构建一个Exactly的测量参数 + for (int i = 0; i < count; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + if (lp.height == LayoutParams.MATCH_PARENT) { + // Temporarily force children to reuse their old measured width + // FIXME: this may not be right for something like wrapping text? + int oldWidth = lp.width;//measureChildWithMargins 这个函数会用到宽,所以要保存一下 + lp.width = child.getMeasuredWidth(); + // Remeasure with new dimensions + measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0); + lp.width = oldWidth; + } + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + //LogUtils.e(TAG, "onLayout() called with: " + "changed = [" + changed + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]"); + int childCount = getChildCount(); + int left = 0 + getPaddingLeft(); + int right = 0 + getPaddingLeft(); + for (int i = 0; i < childCount; i++) { + View childView = getChildAt(i); + if (childView.getVisibility() != GONE) { + if (i == 0) {//第一个子View是内容 宽度设置为全屏 + childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight()); + left = left + childView.getMeasuredWidth(); + } else { + if (isLeftSwipe) { + childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight()); + left = left + childView.getMeasuredWidth(); + } else { + childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight()); + right = right - childView.getMeasuredWidth(); + } + + } + } + } + //Log.d(TAG, "onLayout() called with: " + "maxScrollGap = [" + maxScrollGap + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]"); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + //LogUtils.d(TAG, "dispatchTouchEvent() called with: " + "ev = [" + ev + "]"); + if (isSwipeEnable) { + acquireVelocityTracker(ev); + final VelocityTracker verTracker = mVelocityTracker; + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + isUserSwiped = false;//2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + isUnMoved = true;//2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。 + iosInterceptFlag = false;//add by 2016 09 11 ,每次DOWN时,默认是不拦截的 + if (isTouching) {//如果有别的指头摸过了,那么就return false。这样后续的move..等事件也不会再来找这个View了。 + return false; + } else { + isTouching = true;//第一个摸的指头,赶紧改变标志,宣誓主权。 + } + mLastP.set(ev.getRawX(), ev.getRawY()); + mFirstP.set(ev.getRawX(), ev.getRawY());//2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + + //如果down,view和cacheview不一样,则立马让它还原。且把它置为null + if (mViewCache != null) { + if (mViewCache != this) { + mViewCache.smoothClose(); + + iosInterceptFlag = isIos;//add by 2016 09 11 ,IOS模式开启的话,且当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。 + } + //只要有一个侧滑菜单处于打开状态, 就不给外层布局上下滑动了 + getParent().requestDisallowInterceptTouchEvent(true); + } + //求第一个触点的id, 此时可能有多个触点,但至少一个,计算滑动速率用 + mPointerId = ev.getPointerId(0); + break; + case MotionEvent.ACTION_MOVE: + //add by 2016 09 11 ,IOS模式开启的话,且当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。滑动也不该出现 + if (iosInterceptFlag) { + break; + } + float gap = mLastP.x - ev.getRawX(); + //为了在水平滑动中禁止父类ListView等再竖直滑动 + if (Math.abs(gap) > 10 || Math.abs(getScrollX()) > 10) {//2016 09 29 修改此处,使屏蔽父布局滑动更加灵敏, + getParent().requestDisallowInterceptTouchEvent(true); + } + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。begin + if (Math.abs(gap) > mScaleTouchSlop) { + isUnMoved = false; + } + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。end + //如果scroller还没有滑动结束 停止滑动动画 +/* if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + }*/ + scrollBy((int) (gap), 0);//滑动使用scrollBy + //越界修正 + if (isLeftSwipe) {//左滑 + if (getScrollX() < 0) { + scrollTo(0, 0); + } + if (getScrollX() > mRightMenuWidths) { + scrollTo(mRightMenuWidths, 0); + } + } else {//右滑 + if (getScrollX() < -mRightMenuWidths) { + scrollTo(-mRightMenuWidths, 0); + } + if (getScrollX() > 0) { + scrollTo(0, 0); + } + } + + mLastP.set(ev.getRawX(), ev.getRawY()); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + //2016 11 03 add,判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) { + isUserSwiped = true; + } + + //add by 2016 09 11 ,IOS模式开启的话,且当前有侧滑菜单的View,且不是自己的,就该拦截事件咯。滑动也不该出现 + if (!iosInterceptFlag) {//且滑动了 才判断是否要收起、展开menu + //求伪瞬时速度 + verTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float velocityX = verTracker.getXVelocity(mPointerId); + if (Math.abs(velocityX) > 1000) {//滑动速度超过阈值 + if (velocityX < -1000) { + if (isLeftSwipe) {//左滑 + //平滑展开Menu + smoothExpand(); + + } else { + //平滑关闭Menu + smoothClose(); + } + } else { + if (isLeftSwipe) {//左滑 + // 平滑关闭Menu + smoothClose(); + } else { + //平滑展开Menu + smoothExpand(); + + } + } + } else { + if (Math.abs(getScrollX()) > mLimit) {//否则就判断滑动距离 + //平滑展开Menu + smoothExpand(); + } else { + // 平滑关闭Menu + smoothClose(); + } + } + } + //释放 + releaseVelocityTracker(); + //LogUtils.i(TAG, "onTouch A ACTION_UP ACTION_CANCEL:velocityY:" + velocityX); + isTouching = false;//没有手指在摸我了 + break; + default: + break; + } + } + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + //add by zhangxutong 2016 12 07 begin: + //禁止侧滑时,点击事件不受干扰。 + if (isSwipeEnable) { + switch (ev.getAction()) { + //add by zhangxutong 2016 11 04 begin : + // fix 长按事件和侧滑的冲突。 + case MotionEvent.ACTION_MOVE: + //屏蔽滑动时的事件 + if (Math.abs(ev.getRawX() - mFirstP.x) > mScaleTouchSlop) { + return true; + } + break; + //add by zhangxutong 2016 11 04 end + case MotionEvent.ACTION_UP: + //为了在侧滑时,屏蔽子View的点击事件 + if (isLeftSwipe) { + if (getScrollX() > mScaleTouchSlop) { + //add by 2016 09 10 解决一个智障问题~ 居然不给点击侧滑菜单 我跪着谢罪 + //这里判断落点在内容区域屏蔽点击,内容区域外,允许传递事件继续向下的的。。。 + if (ev.getX() < getWidth() - getScrollX()) { + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。 + if (isUnMoved) { + smoothClose(); + } + return true;//true表示拦截 + } + } + } else { + if (-getScrollX() > mScaleTouchSlop) { + if (ev.getX() > -getScrollX()) {//点击范围在菜单外 屏蔽 + //2016 10 22 add , 仿QQ,侧滑菜单展开时,点击内容区域,关闭侧滑菜单。 + if (isUnMoved) { + smoothClose(); + } + return true; + } + } + } + //add by zhangxutong 2016 11 03 begin: + // 判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件。 + if (isUserSwiped) { + return true; + } + //add by zhangxutong 2016 11 03 end + + break; + } + //模仿IOS 点击其他区域关闭: + if (iosInterceptFlag) { + //IOS模式开启,且当前有菜单的View,且不是自己的 拦截点击事件给子View + return true; + } + } + return super.onInterceptTouchEvent(ev); + } + + /** + * 平滑展开 + */ + private ValueAnimator mExpandAnim, mCloseAnim; + + private boolean isExpand;//代表当前是否是展开状态 2016 11 03 add + + public void smoothExpand() { + //Log.d(TAG, "smoothExpand() called" + this); + /*mScroller.startScroll(getScrollX(), 0, mRightMenuWidths - getScrollX(), 0); + invalidate();*/ + //展开就加入ViewCache: + mViewCache = SwipeMenuLayout.this; + + //2016 11 13 add 侧滑菜单展开,屏蔽content长按 + if (null != mContentView) { + mContentView.setLongClickable(false); + } + + cancelAnim(); + mExpandAnim = ValueAnimator.ofInt(getScrollX(), isLeftSwipe ? mRightMenuWidths : -mRightMenuWidths); + mExpandAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scrollTo((Integer) animation.getAnimatedValue(), 0); + } + }); + mExpandAnim.setInterpolator(new OvershootInterpolator()); + mExpandAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + isExpand = true; + } + }); + mExpandAnim.setDuration(300).start(); + } + + /** + * 每次执行动画之前都应该先取消之前的动画 + */ + private void cancelAnim() { + if (mCloseAnim != null && mCloseAnim.isRunning()) { + mCloseAnim.cancel(); + } + if (mExpandAnim != null && mExpandAnim.isRunning()) { + mExpandAnim.cancel(); + } + } + + /** + * 平滑关闭 + */ + public void smoothClose() { + //Log.d(TAG, "smoothClose() called" + this); +/* mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0); + invalidate();*/ + mViewCache = null; + + //2016 11 13 add 侧滑菜单展开,屏蔽content长按 + if (null != mContentView) { + mContentView.setLongClickable(true); + } + + cancelAnim(); + mCloseAnim = ValueAnimator.ofInt(getScrollX(), 0); + mCloseAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scrollTo((Integer) animation.getAnimatedValue(), 0); + } + }); + mCloseAnim.setInterpolator(new AccelerateInterpolator()); + mCloseAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + isExpand = false; + + } + }); + mCloseAnim.setDuration(300).start(); + //LogUtils.d(TAG, "smoothClose() called with:getScrollX() " + getScrollX()); + } + + + /** + * @param event 向VelocityTracker添加MotionEvent + * @see VelocityTracker#obtain() + * @see VelocityTracker#addMovement(MotionEvent) + */ + private void acquireVelocityTracker(final MotionEvent event) { + if (null == mVelocityTracker) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + } + + /** + * * 释放VelocityTracker + * + * @see VelocityTracker#clear() + * @see VelocityTracker#recycle() + */ + private void releaseVelocityTracker() { + if (null != mVelocityTracker) { + mVelocityTracker.clear(); + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + //每次ViewDetach的时候,判断一下 ViewCache是不是自己,如果是自己,关闭侧滑菜单,且ViewCache设置为null, + // 理由:1 防止内存泄漏(ViewCache是一个静态变量) + // 2 侧滑删除后自己后,这个View被Recycler回收,复用,下一个进入屏幕的View的状态应该是普通状态,而不是展开状态。 + @Override + protected void onDetachedFromWindow() { + if (this == mViewCache) { + mViewCache.smoothClose(); + mViewCache = null; + } + super.onDetachedFromWindow(); + } + + //展开时,禁止长按 + @Override + public boolean performLongClick() { + if (Math.abs(getScrollX()) > mScaleTouchSlop) { + return false; + } + return super.performLongClick(); + } + + //平滑滚动 弃用 改属性动画实现 +/* @Override + public void computeScroll() { + //判断Scroller是否执行完毕: + if (mScroller.computeScrollOffset()) { + scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); + //通知View重绘-invalidate()->onDraw()->computeScroll() + invalidate(); + } + }*/ + + /** + * 快速关闭。 + * 用于 点击侧滑菜单上的选项,同时想让它快速关闭(删除 置顶)。 + * 这个方法在ListView里是必须调用的, + * 在RecyclerView里,视情况而定,如果是mAdapter.notifyItemRemoved(pos)方法不用调用。 + */ + public void quickClose() { + if (this == mViewCache) { + //先取消展开动画 + cancelAnim(); + mViewCache.scrollTo(0, 0);//关闭 + mViewCache = null; + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java b/widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java new file mode 100644 index 0000000..0e0f6fe --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/holder/SimpleViewHolder.java @@ -0,0 +1,29 @@ +package com.hzecool.widget.treeview.holder; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; + +import com.hzecool.widget.treeview.model.TreeNode; + +/** + * Created by Bogdan Melnychuk on 2/11/15. + */ +public class SimpleViewHolder extends TreeNode.BaseNodeViewHolder { + + public SimpleViewHolder(Context context) { + super(context); + } + + @Override + public View createNodeView(TreeNode node, Object value) { + final TextView tv = new TextView(context); + tv.setText(String.valueOf(value)); + return tv; + } + + @Override + public void toggle(boolean active) { + + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java b/widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java new file mode 100644 index 0000000..b157662 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/model/TreeNode.java @@ -0,0 +1,284 @@ +package com.hzecool.widget.treeview.model; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.hzecool.widget.R; +import com.hzecool.widget.treeview.view.AndroidTreeView; +import com.hzecool.widget.treeview.view.TreeNodeWrapperView; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Created by Bogdan Melnychuk on 2/10/15. + */ +public class TreeNode { + public static final String NODES_ID_SEPARATOR = ":"; + + private int mId; + private int mLastId; + private TreeNode mParent; + private boolean mSelected; + private boolean mSelectable = true; + private final List children; + private BaseNodeViewHolder mViewHolder; + private TreeNodeClickListener mClickListener; + private TreeNodeLongClickListener mLongClickListener; + private Object mValue; + private boolean mExpanded; + + public static TreeNode root() { + TreeNode root = new TreeNode(null); + root.setSelectable(false); + return root; + } + + private int generateId() { + return ++mLastId; + } + + public TreeNode(Object value) { + children = new ArrayList<>(); + mValue = value; + } + + public TreeNode addChild(TreeNode childNode) { + childNode.mParent = this; + childNode.mId = generateId(); + children.add(childNode); + return this; + } + + public TreeNode addChildren(TreeNode... nodes) { + for (TreeNode n : nodes) { + addChild(n); + } + return this; + } + + public TreeNode addChildren(Collection nodes) { + for (TreeNode n : nodes) { + addChild(n); + } + return this; + } + + public int deleteChild(TreeNode child) { + for (int i = 0; i < children.size(); i++) { + if (child.mId == children.get(i).mId) { + children.remove(i); + return i; + } + } + return -1; + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public int size() { + return children.size(); + } + + public TreeNode getParent() { + return mParent; + } + + public int getId() { + return mId; + } + + public boolean isLeaf() { + return size() == 0; + } + + public Object getValue() { + return mValue; + } + + public boolean isExpanded() { + return mExpanded; + } + + public TreeNode setExpanded(boolean expanded) { + mExpanded = expanded; + return this; + } + + public void setSelected(boolean selected) { + mSelected = selected; + } + + public boolean isSelected() { + return mSelectable && mSelected; + } + + public void setSelectable(boolean selectable) { + mSelectable = selectable; + } + + public boolean isSelectable() { + return mSelectable; + } + + public String getPath() { + final StringBuilder path = new StringBuilder(); + TreeNode node = this; + while (node.mParent != null) { + path.append(node.getId()); + node = node.mParent; + if (node.mParent != null) { + path.append(NODES_ID_SEPARATOR); + } + } + return path.toString(); + } + + + public int getLevel() { + int level = 0; + TreeNode root = this; + while (root.mParent != null) { + root = root.mParent; + level++; + } + return level; + } + + public boolean isLastChild() { + if (!isRoot()) { + int parentSize = mParent.children.size(); + if (parentSize > 0) { + final List parentChildren = mParent.children; + return parentChildren.get(parentSize - 1).mId == mId; + } + } + return false; + } + + public TreeNode setViewHolder(BaseNodeViewHolder viewHolder) { + mViewHolder = viewHolder; + if (viewHolder != null) { + viewHolder.mNode = this; + } + return this; + } + + public TreeNode setClickListener(TreeNodeClickListener listener) { + mClickListener = listener; + return this; + } + + public TreeNodeClickListener getClickListener() { + return this.mClickListener; + } + + public TreeNode setLongClickListener(TreeNodeLongClickListener listener) { + mLongClickListener = listener; + return this; + } + + public TreeNodeLongClickListener getLongClickListener() { + return mLongClickListener; + } + + public BaseNodeViewHolder getViewHolder() { + return mViewHolder; + } + + public boolean isFirstChild() { + if (!isRoot()) { + List parentChildren = mParent.children; + return parentChildren.get(0).mId == mId; + } + return false; + } + + public boolean isRoot() { + return mParent == null; + } + + public TreeNode getRoot() { + TreeNode root = this; + while (root.mParent != null) { + root = root.mParent; + } + return root; + } + + public interface TreeNodeClickListener { + void onClick(TreeNode node, Object value); + } + + public interface TreeNodeLongClickListener { + boolean onLongClick(TreeNode node, Object value); + } + + public static abstract class BaseNodeViewHolder { + protected AndroidTreeView tView; + protected TreeNode mNode; + private View mView; + protected int containerStyle; + protected Context context; + + public BaseNodeViewHolder(Context context) { + this.context = context; + } + + public View getView() { + if (mView != null) { + return mView; + } + final View nodeView = getNodeView(); + final TreeNodeWrapperView nodeWrapperView = new TreeNodeWrapperView(nodeView.getContext(), getContainerStyle()); + nodeWrapperView.insertNodeView(nodeView); + mView = nodeWrapperView; + + return mView; + } + + public void setTreeViev(AndroidTreeView treeViev) { + this.tView = treeViev; + } + + public AndroidTreeView getTreeView() { + return tView; + } + + public void setContainerStyle(int style) { + containerStyle = style; + } + + public View getNodeView() { + return createNodeView(mNode, (E) mNode.getValue()); + } + + public ViewGroup getNodeItemsView() { + return (ViewGroup) getView().findViewById(R.id.node_items); + } + + public boolean isInitialized() { + return mView != null; + } + + public int getContainerStyle() { + return containerStyle; + } + + + public abstract View createNodeView(TreeNode node, E value); + + public void toggle(boolean active) { + // empty + } + + public void toggleSelectionMode(boolean editModeEnabled) { + // empty + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java b/widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java new file mode 100644 index 0000000..2bb33b9 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/view/AndroidTreeView.java @@ -0,0 +1,486 @@ +package com.hzecool.widget.treeview.view; + +import android.content.Context; +import android.text.TextUtils; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Transformation; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import com.hzecool.widget.R; +import com.hzecool.widget.treeview.holder.SimpleViewHolder; +import com.hzecool.widget.treeview.model.TreeNode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Created by wzg + */ +public class AndroidTreeView { + private static final String NODES_PATH_SEPARATOR = ";"; + + protected TreeNode mRoot; + private Context mContext; + private boolean applyForRoot; + private int containerStyle = 0; + private Class defaultViewHolderClass = SimpleViewHolder.class; + private TreeNode.TreeNodeClickListener nodeClickListener; + private TreeNode.TreeNodeLongClickListener nodeLongClickListener; + private boolean mSelectionModeEnabled; + private boolean mUseDefaultAnimation = false; + private boolean use2dScroll = false; + private boolean enableAutoToggle = true; + + public AndroidTreeView(Context context) { + mContext = context; + } + + public void setRoot(TreeNode mRoot) { + this.mRoot = mRoot; + } + + public AndroidTreeView(Context context, TreeNode root) { + mRoot = root; + mContext = context; + } + + public void setDefaultAnimation(boolean defaultAnimation) { + this.mUseDefaultAnimation = defaultAnimation; + } + + public void setDefaultContainerStyle(int style) { + setDefaultContainerStyle(style, false); + } + + public void setDefaultContainerStyle(int style, boolean applyForRoot) { + containerStyle = style; + this.applyForRoot = applyForRoot; + } + + public void setUse2dScroll(boolean use2dScroll) { + this.use2dScroll = use2dScroll; + } + + public boolean is2dScrollEnabled() { + return use2dScroll; + } + + public void setUseAutoToggle(boolean enableAutoToggle) { + this.enableAutoToggle = enableAutoToggle; + } + + public boolean isAutoToggleEnabled() { + return enableAutoToggle; + } + + public void setDefaultViewHolder(Class viewHolder) { + defaultViewHolderClass = viewHolder; + } + + public void setDefaultNodeClickListener(TreeNode.TreeNodeClickListener listener) { + nodeClickListener = listener; + } + + public void setDefaultNodeLongClickListener(TreeNode.TreeNodeLongClickListener listener) { + nodeLongClickListener = listener; + } + + public void expandAll() { + expandNode(mRoot, true); + } + + public void collapseAll() { + for (TreeNode n : mRoot.getChildren()) { + collapseNode(n, true); + } + } + + + public View getView(int style) { + final ViewGroup view; + if (style > 0) { + ContextThemeWrapper newContext = new ContextThemeWrapper(mContext, style); + view = use2dScroll ? new TwoDScrollView(newContext) : new ScrollView(newContext); + } else { + view = use2dScroll ? new TwoDScrollView(mContext) : new ScrollView(mContext); + } + + Context containerContext = mContext; + if (containerStyle != 0 && applyForRoot) { + containerContext = new ContextThemeWrapper(mContext, containerStyle); + } + final LinearLayout viewTreeItems = new LinearLayout(containerContext, null, containerStyle); + + viewTreeItems.setId(R.id.tree_items); + viewTreeItems.setOrientation(LinearLayout.VERTICAL); + view.addView(viewTreeItems); + + mRoot.setViewHolder(new TreeNode.BaseNodeViewHolder(mContext) { + @Override + public View createNodeView(TreeNode node, Object value) { + return null; + } + + @Override + public ViewGroup getNodeItemsView() { + return viewTreeItems; + } + }); + + expandNode(mRoot, false); + return view; + } + + public View getView() { + return getView(-1); + } + + + public void expandLevel(int level) { + for (TreeNode n : mRoot.getChildren()) { + expandLevel(n, level); + } + } + + private void expandLevel(TreeNode node, int level) { + if (node.getLevel() <= level) { + expandNode(node, false); + } + for (TreeNode n : node.getChildren()) { + expandLevel(n, level); + } + } + + public void expandNode(TreeNode node) { + expandNode(node, false); + } + + public void collapseNode(TreeNode node) { + collapseNode(node, false); + } + + public String getSaveState() { + final StringBuilder builder = new StringBuilder(); + getSaveState(mRoot, builder); + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + return builder.toString(); + } + + public void restoreState(String saveState) { + if (!TextUtils.isEmpty(saveState)) { + collapseAll(); + final String[] openNodesArray = saveState.split(NODES_PATH_SEPARATOR); + final Set openNodes = new HashSet<>(Arrays.asList(openNodesArray)); + restoreNodeState(mRoot, openNodes); + } + } + + private void restoreNodeState(TreeNode node, Set openNodes) { + for (TreeNode n : node.getChildren()) { + if (openNodes.contains(n.getPath())) { + expandNode(n); + restoreNodeState(n, openNodes); + } + } + } + + private void getSaveState(TreeNode root, StringBuilder sBuilder) { + for (TreeNode node : root.getChildren()) { + if (node.isExpanded()) { + sBuilder.append(node.getPath()); + sBuilder.append(NODES_PATH_SEPARATOR); + getSaveState(node, sBuilder); + } + } + } + + public void toggleNode(TreeNode node) { + if (node.isExpanded()) { + collapseNode(node, false); + } else { + expandNode(node, false); + } + + } + + private void collapseNode(TreeNode node, final boolean includeSubnodes) { + node.setExpanded(false); + TreeNode.BaseNodeViewHolder nodeViewHolder = getViewHolderForNode(node); + + if (mUseDefaultAnimation) { + collapse(nodeViewHolder.getNodeItemsView()); + } else { + nodeViewHolder.getNodeItemsView().setVisibility(View.GONE); + } + nodeViewHolder.toggle(false); + if (includeSubnodes) { + for (TreeNode n : node.getChildren()) { + collapseNode(n, includeSubnodes); + } + } + } + + private void expandNode(final TreeNode node, boolean includeSubnodes) { + node.setExpanded(true); + final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(node); + parentViewHolder.getNodeItemsView().removeAllViews(); + + + parentViewHolder.toggle(true); + + for (final TreeNode n : node.getChildren()) { + addNode(parentViewHolder.getNodeItemsView(), n); + + if (n.isExpanded() || includeSubnodes) { + expandNode(n, includeSubnodes); + } + + } + if (mUseDefaultAnimation) { + expand(parentViewHolder.getNodeItemsView()); + } else { + parentViewHolder.getNodeItemsView().setVisibility(View.VISIBLE); + } + + } + + private void addNode(ViewGroup container, final TreeNode n) { + final TreeNode.BaseNodeViewHolder viewHolder = getViewHolderForNode(n); + final View nodeView = viewHolder.getView(); + container.addView(nodeView); + if (mSelectionModeEnabled) { + viewHolder.toggleSelectionMode(mSelectionModeEnabled); + } + + nodeView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (n.getClickListener() != null) { + n.getClickListener().onClick(n, n.getValue()); + } else if (nodeClickListener != null) { + nodeClickListener.onClick(n, n.getValue()); + } + if (enableAutoToggle) { + toggleNode(n); + } + } + }); + + nodeView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + if (n.getLongClickListener() != null) { + return n.getLongClickListener().onLongClick(n, n.getValue()); + } else if (nodeLongClickListener != null) { + return nodeLongClickListener.onLongClick(n, n.getValue()); + } + if (enableAutoToggle) { + toggleNode(n); + } + return false; + } + }); + } + + //------------------------------------------------------------ + // Selection methods + + public void setSelectionModeEnabled(boolean selectionModeEnabled) { + if (!selectionModeEnabled) { + // TODO fix double iteration over tree + deselectAll(); + } + mSelectionModeEnabled = selectionModeEnabled; + + for (TreeNode node : mRoot.getChildren()) { + toggleSelectionMode(node, selectionModeEnabled); + } + + } + + public List getSelectedValues(Class clazz) { + List result = new ArrayList<>(); + List selected = getSelected(); + for (TreeNode n : selected) { + Object value = n.getValue(); + if (value != null && value.getClass().equals(clazz)) { + result.add((E) value); + } + } + return result; + } + + public boolean isSelectionModeEnabled() { + return mSelectionModeEnabled; + } + + private void toggleSelectionMode(TreeNode parent, boolean mSelectionModeEnabled) { + toogleSelectionForNode(parent, mSelectionModeEnabled); + if (parent.isExpanded()) { + for (TreeNode node : parent.getChildren()) { + toggleSelectionMode(node, mSelectionModeEnabled); + } + } + } + + public List getSelected() { + if (mSelectionModeEnabled) { + return getSelected(mRoot); + } else { + return new ArrayList<>(); + } + } + + // TODO Do we need to go through whole tree? Save references or consider collapsed nodes as not selected + private List getSelected(TreeNode parent) { + List result = new ArrayList<>(); + for (TreeNode n : parent.getChildren()) { + if (n.isSelected()) { + result.add(n); + } + result.addAll(getSelected(n)); + } + return result; + } + + public void selectAll(boolean skipCollapsed) { + makeAllSelection(true, skipCollapsed); + } + + public void deselectAll() { + makeAllSelection(false, false); + } + + private void makeAllSelection(boolean selected, boolean skipCollapsed) { + if (mSelectionModeEnabled) { + for (TreeNode node : mRoot.getChildren()) { + selectNode(node, selected, skipCollapsed); + } + } + } + + public void selectNode(TreeNode node, boolean selected) { + if (mSelectionModeEnabled) { + node.setSelected(selected); + toogleSelectionForNode(node, true); + } + } + + private void selectNode(TreeNode parent, boolean selected, boolean skipCollapsed) { + parent.setSelected(selected); + toogleSelectionForNode(parent, true); + boolean toContinue = skipCollapsed ? parent.isExpanded() : true; + if (toContinue) { + for (TreeNode node : parent.getChildren()) { + selectNode(node, selected, skipCollapsed); + } + } + } + + private void toogleSelectionForNode(TreeNode node, boolean makeSelectable) { + TreeNode.BaseNodeViewHolder holder = getViewHolderForNode(node); + if (holder.isInitialized()) { + getViewHolderForNode(node).toggleSelectionMode(makeSelectable); + } + } + + private TreeNode.BaseNodeViewHolder getViewHolderForNode(TreeNode node) { + TreeNode.BaseNodeViewHolder viewHolder = node.getViewHolder(); + if (viewHolder == null) { + try { + final Object object = defaultViewHolderClass.getConstructor(Context.class).newInstance(mContext); + viewHolder = (TreeNode.BaseNodeViewHolder) object; + node.setViewHolder(viewHolder); + } catch (Exception e) { + throw new RuntimeException("Could not instantiate class " + defaultViewHolderClass); + } + } + if (viewHolder.getContainerStyle() <= 0) { + viewHolder.setContainerStyle(containerStyle); + } + if (viewHolder.getTreeView() == null) { + viewHolder.setTreeViev(this); + } + return viewHolder; + } + + private static void expand(final View v) { + v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + final int targetHeight = v.getMeasuredHeight(); + + v.getLayoutParams().height = 0; + v.setVisibility(View.VISIBLE); + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + v.getLayoutParams().height = interpolatedTime == 1 + ? LinearLayout.LayoutParams.WRAP_CONTENT + : (int) (targetHeight * interpolatedTime); + v.requestLayout(); + } + + @Override + public boolean willChangeBounds() { + return true; + } + }; + + // 1dp/ms + a.setDuration((int) (targetHeight / v.getContext().getResources().getDisplayMetrics().density)); + v.startAnimation(a); + } + + private static void collapse(final View v) { + final int initialHeight = v.getMeasuredHeight(); + + Animation a = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + if (interpolatedTime == 1) { + v.setVisibility(View.GONE); + } else { + v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime); + v.requestLayout(); + } + } + + @Override + public boolean willChangeBounds() { + return true; + } + }; + + // 1dp/ms + a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density)); + v.startAnimation(a); + } + + //----------------------------------------------------------------- + //Add / Remove + + public void addNode(TreeNode parent, final TreeNode nodeToAdd) { + parent.addChild(nodeToAdd); + if (parent.isExpanded()) { + final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent); + addNode(parentViewHolder.getNodeItemsView(), nodeToAdd); + } + } + + public void removeNode(TreeNode node) { + if (node.getParent() != null) { + TreeNode parent = node.getParent(); + int index = parent.deleteChild(node); + if (parent.isExpanded() && index >= 0) { + final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent); + parentViewHolder.getNodeItemsView().removeViewAt(index); + } + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java b/widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java new file mode 100644 index 0000000..a96a2af --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/view/TreeNodeWrapperView.java @@ -0,0 +1,53 @@ +package com.hzecool.widget.treeview.view; + +import android.content.Context; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import com.hzecool.widget.R; + + +/** + * Created by Bogdan Melnychuk on 2/10/15. + */ +public class TreeNodeWrapperView extends LinearLayout { + private LinearLayout nodeItemsContainer; + private ViewGroup nodeContainer; + private final int containerStyle; + + public TreeNodeWrapperView(Context context, int containerStyle) { + super(context); + this.containerStyle = containerStyle; + init(); + } + + private void init() { + setOrientation(LinearLayout.VERTICAL); + + nodeContainer = new RelativeLayout(getContext()); + nodeContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + nodeContainer.setId(R.id.node_header); + + ContextThemeWrapper newContext = new ContextThemeWrapper(getContext(), containerStyle); + nodeItemsContainer = new LinearLayout(newContext, null, containerStyle); + nodeItemsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + nodeItemsContainer.setId(R.id.node_items); + nodeItemsContainer.setOrientation(LinearLayout.VERTICAL); + nodeItemsContainer.setVisibility(View.GONE); + + addView(nodeContainer); + addView(nodeItemsContainer); + } + + + public void insertNodeView(View nodeView) { + nodeContainer.addView(nodeView); + } + + public ViewGroup getNodeContainer() { + return nodeContainer; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java b/widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java new file mode 100644 index 0000000..2982314 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/treeview/view/TwoDScrollView.java @@ -0,0 +1,1107 @@ +package com.hzecool.widget.treeview.view; + + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Scroller; +import android.widget.TextView; + +import java.util.List; + +/** + * Layout container for a view hierarchy that can be scrolled by the user, + * allowing it to be larger than the physical display. A TwoDScrollView + * is a {@link FrameLayout}, meaning you should place one child in it + * containing the entire contents to scroll; this child may itself be a layout + * manager with a complex hierarchy of objects. A child that is often used + * is a {@link LinearLayout} in a vertical orientation, presenting a vertical + * array of top-level items that the user can scroll through. + * he {@link TextView} class also + * takes care of its own scrolling, so does not require a TwoDScrollView, but + * using the two together is possible to achieve the effect of a text view + * within a larger container. + */ +public class TwoDScrollView extends FrameLayout { + static final int ANIMATED_SCROLL_GAP = 250; + static final float MAX_SCROLL_FACTOR = 0.5f; + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private Scroller mScroller; + + /** + * Flag to indicate that we are moving focus ourselves. This is so the + * code that watches for focus changes initiated outside this TwoDScrollView + * knows that it does not have to do anything. + */ + private boolean mTwoDScrollViewMovedFocus; + + /** + * Position of the last motion event. + */ + private float mLastMotionY; + private float mLastMotionX; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this TwoDScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * Whether arrow scrolling is animated. + */ + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + public TwoDScrollView(Context context) { + super(context); + initTwoDScrollView(); + } + + public TwoDScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + initTwoDScrollView(); + } + + public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initTwoDScrollView(); + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + if (getScrollY() < length) { + return getScrollY() / (float) length; + } + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + return 1.0f; + } + + @Override + protected float getLeftFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + if (getScrollX() < length) { + return getScrollX() / (float) length; + } + return 1.0f; + } + + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (span < length) { + return span / (float) length; + } + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmountVertical() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + public int getMaxScrollAmountHorizontal() { + return (int) (MAX_SCROLL_FACTOR * getWidth()); + } + + private void initTwoDScrollView() { + mScroller = new Scroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("TwoDScrollView can host only one direct child"); + } + super.addView(child, index, params); + } + + /** + * @return Returns true this TwoDScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + int childWidth = child.getWidth(); + return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) || + (getWidth() < childWidth + getPaddingLeft() + getPaddingRight()); + } + return false; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + boolean handled = super.dispatchKeyEvent(event); + if (handled) { + return true; + } + return executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + if (!canScroll()) { + if (isFocused()) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); + return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP, false); + } else { + handled = fullScroll(View.FOCUS_UP, false); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN, false); + } else { + handled = fullScroll(View.FOCUS_DOWN, false); + } + break; + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_LEFT, true); + } else { + handled = fullScroll(View.FOCUS_LEFT, true); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_RIGHT, true); + } else { + handled = fullScroll(View.FOCUS_RIGHT, true); + } + break; + } + } + return handled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + * + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + if (!canScroll()) { + mIsBeingDragged = false; + return false; + } + final float y = ev.getY(); + final float x = ev.getX(); + switch (action) { + case MotionEvent.ACTION_MOVE: + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int yDiff = (int) Math.abs(y - mLastMotionY); + final int xDiff = (int) Math.abs(x - mLastMotionX); + if (yDiff > mTouchSlop || xDiff > mTouchSlop) { + mIsBeingDragged = true; + } + break; + + case MotionEvent.ACTION_DOWN: + /* Remember location of down touch */ + mLastMotionY = y; + mLastMotionX = x; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (!canScroll()) { + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + final float y = ev.getY(); + final float x = ev.getX(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = y; + mLastMotionX = x; + break; + case MotionEvent.ACTION_MOVE: + // Scroll to follow the motion event + int deltaX = (int) (mLastMotionX - x); + int deltaY = (int) (mLastMotionY - y); + mLastMotionX = x; + mLastMotionY = y; + + if (deltaX < 0) { + if (getScrollX() < 0) { + deltaX = 0; + } + } else if (deltaX > 0) { + final int rightEdge = getWidth() - getPaddingRight(); + final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge; + if (availableToScroll > 0) { + deltaX = Math.min(availableToScroll, deltaX); + } else { + deltaX = 0; + } + } + if (deltaY < 0) { + if (getScrollY() < 0) { + deltaY = 0; + } + } else if (deltaY > 0) { + final int bottomEdge = getHeight() - getPaddingBottom(); + final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (availableToScroll > 0) { + deltaY = Math.min(availableToScroll, deltaY); + } else { + deltaY = 0; + } + } + if (deltaY != 0 || deltaX != 0) + scrollBy(deltaX, deltaY); + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int) velocityTracker.getXVelocity(); + int initialYVelocity = (int) velocityTracker.getYVelocity(); + if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && getChildCount() > 0) { + fling(-initialXVelocity, -initialYVelocity); + } + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + return true; + } + + /** + * Finds the next focusable component that fits in this View's bounds + * (excluding fading edges) pretending that this View's top is located at + * the parameter top. + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found (the fading edge is assumed to start at this position) + * @param preferredFocusable the View that has highest priority and will be + * returned if it is within my bounds (null is valid) + * @return the next focusable component in the bounds or null if none can be + * found + */ + private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) { + /* + * The fading edge's transparent side should be considered for focus + * since it's mostly visible, so we divide the actual fading edge length + * by 2. + */ + final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2; + final int topWithoutFadingEdge = top + verticalFadingEdgeLength; + final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength; + final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2; + final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength; + final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength; + + if ((preferredFocusable != null) + && (preferredFocusable.getTop() < bottomWithoutFadingEdge) + && (preferredFocusable.getBottom() > topWithoutFadingEdge) + && (preferredFocusable.getLeft() < rightWithoutFadingEdge) + && (preferredFocusable.getRight() > leftWithoutFadingEdge)) { + return preferredFocusable; + } + return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge); + } + + /** + * Finds the next focusable component that fits in the specified bounds. + *

+ * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) { + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + int viewLeft = view.getLeft(); + int viewRight = view.getRight(); + + if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && (left < viewLeft) && (viewRight < right); + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToVerticalBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate.getBottom()); + final boolean viewIsCloserToHorizontalBoundary = + (leftFocus && viewLeft < focusCandidate.getLeft()) || + (!leftFocus && viewRight > focusCandidate.getRight()); + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + return focusCandidate; + } + + /** + *

Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction, boolean horizontal) { + if (!horizontal) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + mTempRect.top = 0; + mTempRect.bottom = height; + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0); + } else { + boolean right = direction == View.FOCUS_DOWN; + int width = getWidth(); + mTempRect.left = 0; + mTempRect.right = width; + if (right) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.right = view.getBottom(); + mTempRect.left = mTempRect.right - width; + } + } + return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom); + } + } + + /** + *

Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this scrollview.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward + * {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) { + boolean handled = true; + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = directionY == View.FOCUS_UP; + int width = getWidth(); + int containerLeft = getScrollX(); + int containerRight = containerLeft + width; + boolean leftwards = directionX == View.FOCUS_UP; + View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right); + if (newFocused == null) { + newFocused = this; + } + if ((top >= containerTop && bottom <= containerBottom) || (left >= containerLeft && right <= containerRight)) { + handled = false; + } else { + int deltaY = up ? (top - containerTop) : (bottom - containerBottom); + int deltaX = leftwards ? (left - containerLeft) : (right - containerRight); + doScroll(deltaX, deltaY); + } + if (newFocused != findFocus() && newFocused.requestFocus(directionY)) { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction, boolean horizontal) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical(); + + if (!horizontal) { + if (nextFocused != null) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(0, scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + } else { + if (nextFocused != null) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta, 0); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0); + } + } + return true; + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScroll(int deltaX, int deltaY) { + if (deltaX != 0 || deltaY != 0) { + smoothScrollBy(deltaX, deltaY); + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + mScroller.startScroll(getScrollX(), getScrollY(), dx, dy); + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

The scroll range of a scroll view is the overall height of all of its + * children.

+ */ + @Override + protected int computeVerticalScrollRange() { + int count = getChildCount(); + return count == 0 ? getHeight() : (getChildAt(0)).getBottom(); + } + + @Override + protected int computeHorizontalScrollRange() { + int count = getChildCount(); + return count == 0 ? getWidth() : (getChildAt(0)).getRight(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + // This is called at drawing time by ViewGroup. We don't want to + // re-show the scrollbars at this point, which scrollTo will do, + // so we replicate most of scrollTo here. + // + // It's a little odd to call onScrollChanged from inside the drawing. + // + // It is, except when you remember that computeScroll() is used to + // animate scrolling. So unless we want to defer the onScrollChanged() + // until the end of the animated scrolling, we don't really have a + // choice here. + // + // I agree. The alternative, which I think would be worse, is to post + // something and tell the subclasses later. This is bad because there + // will be a window where mScrollX/Y is different from what the app + // thinks it is. + // + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()), + clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight())); + } else { + scrollTo(x, y); + } + if (oldX != getScrollX() || oldY != getScrollY()) { + onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); + } + + // Keep on drawing until the animation has finished. + postInvalidate(); + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + /* Offset from child's local coordinates to TwoDScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + int fadingEdge = getVerticalFadingEdgeLength(); + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + int scrollYDelta = 0; + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mTwoDScrollViewMovedFocus) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + } + super.requestChildFocus(child, focused); + } + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null ? + FocusFinder.getInstance().findNextFocus(this, null, direction) : + FocusFinder.getInstance().findNextFocusFromRect(this, + previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + // Calling this with the present values causes it to re-clam them + scrollTo(getScrollX(), getScrollY()); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDeltaX, scrollDeltaY); + } + + /** + * Return true if child is an descendant of parent, (or equal to the parent). + */ + private boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/curor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + int width = getWidth() - getPaddingRight() - getPaddingLeft(); + int right = getChildAt(0).getWidth(); + + mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height); + + final boolean movingDown = velocityY > 0; + final boolean movingRight = velocityX > 0; + + View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus()); + if (newFocused == null) { + newFocused = this; + } + + if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) { + mTwoDScrollViewMovedFocus = true; + mTwoDScrollViewMovedFocus = false; + } + + awakenScrollBars(mScroller.getDuration()); + invalidate(); + } + } + + /** + * {@inheritDoc} + * This version also clamps the scrolling to the bounds of our child. + */ + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java b/widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java new file mode 100644 index 0000000..cec9a5e --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/GlideSetting.java @@ -0,0 +1,22 @@ +package com.hzecool.widget.utils; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.request.RequestOptions; + +/** + * Created by 47066 on 2017/9/7. + */ + +public class GlideSetting { + public static RequestOptions getGlideSetting() { + RequestOptions options = new RequestOptions() + .centerCrop() + //.placeholder(R.drawable.placeholder) + //.error(R.drawable.error) + .priority(Priority.HIGH) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC); + return options; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java b/widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java new file mode 100644 index 0000000..b92d95b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/ImageUtils.java @@ -0,0 +1,146 @@ +package com.hzecool.widget.utils; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ThumbnailUtils; +import android.net.Uri; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.util.DisplayMetrics; +import android.util.Log; + +/** + * 图片简单处理工具类 + */ +public class ImageUtils { + + /** + * 屏幕宽 + * + * @param context + * @return + */ + public static int getWidth(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.widthPixels; + } + + /** + * 屏幕高 + * + * @param context + * @return + */ + public static int getHeight(Context context) { + DisplayMetrics dm = context.getResources().getDisplayMetrics(); + return dm.heightPixels; + } + + /** + * 解决小米、魅族等定制ROM + * @param context + * @param intent + * @return + */ + public static Uri getUri(Context context , Intent intent) { + Uri uri = intent.getData(); + String type = intent.getType(); + if (uri.getScheme().equals("file") && (type.contains("image/"))) { + String path = uri.getEncodedPath(); + if (path != null) { + path = Uri.decode(path); + ContentResolver cr = context.getContentResolver(); + StringBuffer buff = new StringBuffer(); + buff.append("(").append(Images.ImageColumns.DATA).append("=") + .append("'" + path + "'").append(")"); + Cursor cur = cr.query(Images.Media.EXTERNAL_CONTENT_URI, + new String[] { Images.ImageColumns._ID }, + buff.toString(), null, null); + int index = 0; + for (cur.moveToFirst(); !cur.isAfterLast(); cur.moveToNext()) { + index = cur.getColumnIndex(Images.ImageColumns._ID); + // set _id value + index = cur.getInt(index); + } + if (index == 0) { + // do nothing + } else { + Uri uri_temp = Uri + .parse("content://media/external/images/media/" + + index); + if (uri_temp != null) { + uri = uri_temp; + Log.i("urishi", uri.toString()); + } + } + } + } + return uri; + } + + /** + * 根据文件Uri获取路径 + * + * @param context + * @param uri + * @return + */ + public static String getFilePathByFileUri(Context context, Uri uri) { + String filePath = null; + Cursor cursor = context.getContentResolver().query(uri, null, null, + null, null); + if (cursor.moveToFirst()) { + filePath = cursor.getString(cursor + .getColumnIndex(MediaStore.Images.Media.DATA)); + } + cursor.close(); + return filePath; + } + + /** + * 根据图片原始路径获取图片缩略图 + * + * @param imagePath 图片原始路径 + * @param width 缩略图宽度 + * @param height 缩略图高度 + * @return + */ + public static Bitmap getImageThumbnail(String imagePath, int width, + int height) { + Bitmap bitmap = null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true;//不加载直接获取Bitmap宽高 + // 获取这个图片的宽和高,注意此处的bitmap为null + bitmap = BitmapFactory.decodeFile(imagePath, options); + if(bitmap == null){ + // 计算缩放比 + int h = options.outHeight; + int w = options.outWidth; + Log.i("test", "optionsH"+h+"optionsW"+w); + int beWidth = w / width; + int beHeight = h / height; + int rate = 1; + if (beWidth < beHeight) { + rate = beWidth; + } else { + rate = beHeight; + } + if (rate <= 0) {//图片实际大小小于缩略图,不缩放 + rate = 1; + } + options.inSampleSize = rate; + options.inJustDecodeBounds = false; + // 重新读入图片,读取缩放后的bitmap,注意这次要把options.inJustDecodeBounds 设为 false + bitmap = BitmapFactory.decodeFile(imagePath, options); + // 利用ThumbnailUtils来创建缩略图,这里要指定要缩放哪个Bitmap对象 + bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height, + ThumbnailUtils.OPTIONS_RECYCLE_INPUT); + } + return bitmap; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java b/widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java new file mode 100644 index 0000000..6d3135a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/SizeUtils.java @@ -0,0 +1,179 @@ +package com.hzecool.widget.utils; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2016/8/2
+ *     desc  : 尺寸相关工具类
+ * 
+ */ +public class SizeUtils { + + private SizeUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * dp转px + * + * @param dpValue dp值 + * @return px值 + */ + public static int dp2px(Context context, double dpValue) { + final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + /** + * px转dp + * + * @param pxValue px值 + * @return dp值 + */ + public static int px2dp(Context context, float pxValue) { + final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } + + /** + * sp转px + * + * @param spValue sp值 + * @return px值 + */ + public static int sp2px(Context context, float spValue) { + final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity; + return (int) (spValue * fontScale + 0.5f); + } + + /** + * px转sp + * + * @param pxValue px值 + * @return sp值 + */ + public static int px2sp(Context context, float pxValue) { + final float fontScale = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity; + return (int) (pxValue / fontScale + 0.5f); + } + + /** + * 各种单位转换 + *

该方法存在于TypedValue

+ * + * @param unit 单位 + * @param value 值 + * @param metrics DisplayMetrics + * @return 转换结果 + */ + public static float applyDimension(int unit, float value, DisplayMetrics metrics) { + switch (unit) { + case TypedValue.COMPLEX_UNIT_PX: + return value; + case TypedValue.COMPLEX_UNIT_DIP: + return value * metrics.density; + case TypedValue.COMPLEX_UNIT_SP: + return value * metrics.scaledDensity; + case TypedValue.COMPLEX_UNIT_PT: + return value * metrics.xdpi * (1.0f / 72); + case TypedValue.COMPLEX_UNIT_IN: + return value * metrics.xdpi; + case TypedValue.COMPLEX_UNIT_MM: + return value * metrics.xdpi * (1.0f / 25.4f); + } + return 0; + } + + /** + * 在onCreate中获取视图的尺寸 + *

需回调onGetSizeListener接口,在onGetSize中获取view宽高

+ *

用法示例如下所示

+ *
+     * SizeUtils.forceGetViewSize(view, new SizeUtils.onGetSizeListener() {
+     *     Override
+     *     public void onGetSize(View view) {
+     *         view.getWidth();
+     *     }
+     * });
+     * 
+ * + * @param view 视图 + * @param listener 监听器 + */ + public static void forceGetViewSize(final View view, final onGetSizeListener listener) { + view.post(new Runnable() { + @Override + public void run() { + if (listener != null) { + listener.onGetSize(view); + } + } + }); + } + + /** + * 获取到View尺寸的监听 + */ + public interface onGetSizeListener { + void onGetSize(View view); + } + + public static void setListener(onGetSizeListener listener) { + mListener = listener; + } + + private static onGetSizeListener mListener; + + /** + * 测量视图尺寸 + * + * @param view 视图 + * @return arr[0]: 视图宽度, arr[1]: 视图高度 + */ + public static int[] measureView(View view) { + ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp == null) { + lp = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + } + int widthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width); + int lpHeight = lp.height; + int heightSpec; + if (lpHeight > 0) { + heightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + return new int[]{view.getMeasuredWidth(), view.getMeasuredHeight()}; + } + + /** + * 获取测量视图宽度 + * + * @param view 视图 + * @return 视图宽度 + */ + public static int getMeasuredWidth(View view) { + return measureView(view)[0]; + } + + /** + * 获取测量视图高度 + * + * @param view 视图 + * @return 视图高度 + */ + public static int getMeasuredHeight(View view) { + return measureView(view)[1]; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java b/widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java new file mode 100644 index 0000000..3e733ac --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/utils/UpLoadUtils.java @@ -0,0 +1,101 @@ +package com.hzecool.widget.utils; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +/** + * 文件上传util + * Created by song on 2017/5/9. + */ + +public class UpLoadUtils { + private static final String TAG = "uploadFile"; + private static final int TIME_OUT = 10*1000; //超时时间 + private static final String CHARSET = "utf-8"; //设置编码 + /** + * android上传文件到服务器 + * @param file 需要上传的文件 + * @param RequestURL 请求的rul + * @return 返回响应的内容 + */ + public static String uploadFile(File file, String RequestURL){ + String result = null; + String BOUNDARY = UUID.randomUUID().toString(); //边界标识 随机生成 + String PREFIX = "--" , LINE_END = "\r\n"; + String CONTENT_TYPE = "multipart/form-data"; //内容类型 + + try { + URL url = new URL(RequestURL); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setReadTimeout(TIME_OUT); + conn.setConnectTimeout(TIME_OUT); + conn.setDoInput(true); //允许输入流 + conn.setDoOutput(true); //允许输出流 + conn.setUseCaches(false); //不允许使用缓存 + conn.setRequestMethod("POST"); //请求方式 + conn.setRequestProperty("Charset", CHARSET); //设置编码 + conn.setRequestProperty("connection", "keep-alive"); + conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY); + conn.connect(); + + if(file!=null){ + /** + * 当文件不为空,把文件包装并且上传 + */ + DataOutputStream dos = new DataOutputStream( conn.getOutputStream()); + StringBuffer sb = new StringBuffer(); + sb.append(PREFIX); + sb.append(BOUNDARY); + sb.append(LINE_END); + /** + * 这里重点注意: + * name里面的值为服务器端需要key 只有这个key 才可以得到对应的文件 + * filename是文件的名字,包含后缀名的 比如:abc.png + */ + + sb.append("Content-Disposition: form-data; name=\"img\"; filename=\""+file.getName()+"\""+LINE_END); + sb.append("Content-Type: application/octet-stream; charset="+CHARSET+LINE_END); + sb.append(LINE_END); + dos.write(sb.toString().getBytes()); + InputStream is = new FileInputStream(file); + byte[] bytes = new byte[1024]; + int len = 0; + while((len=is.read(bytes))!=-1){ + dos.write(bytes, 0, len); + } + is.close(); + dos.write(LINE_END.getBytes()); + byte[] end_data = (PREFIX+BOUNDARY+PREFIX+LINE_END).getBytes(); + dos.write(end_data); + dos.flush(); + /** + * 获取响应码 200=成功 + * 当响应成功,获取响应的流 + */ + int res = conn.getResponseCode(); + if(res==200){ + InputStream input = conn.getInputStream(); + StringBuffer sb1= new StringBuffer(); + int ss ; + while((ss=input.read())!=-1){ + sb1.append((char)ss); + } + result = sb1.toString(); + System.out.println(result); + } + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return result; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java new file mode 100644 index 0000000..f8e48cd --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/AppBarStateChangeListener.java @@ -0,0 +1,40 @@ +package com.hzecool.widget.xRecyclerView; + +import android.support.design.widget.AppBarLayout; + +/** + * Created by jianghejie on 16/6/19. + */ + +public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener { + + public enum State { + EXPANDED, + COLLAPSED, + IDLE + } + + private State mCurrentState = State.IDLE; + + @Override + public final void onOffsetChanged(AppBarLayout appBarLayout, int i) { + if (i == 0) { + if (mCurrentState != State.EXPANDED) { + onStateChanged(appBarLayout, State.EXPANDED); + } + mCurrentState = State.EXPANDED; + } else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) { + if (mCurrentState != State.COLLAPSED) { + onStateChanged(appBarLayout, State.COLLAPSED); + } + mCurrentState = State.COLLAPSED; + } else { + if (mCurrentState != State.IDLE) { + onStateChanged(appBarLayout, State.IDLE); + } + mCurrentState = State.IDLE; + } + } + public abstract void onStateChanged(AppBarLayout appBarLayout, State state); +} + diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java new file mode 100644 index 0000000..2941ef6 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ArrowRefreshHeader.java @@ -0,0 +1,268 @@ +package com.hzecool.widget.xRecyclerView; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.xRecyclerView.progressindicator.AVLoadingIndicatorView; + +import java.util.Date; + +public class ArrowRefreshHeader extends LinearLayout implements BaseRefreshHeader { + + private LinearLayout mContainer; + private ImageView mArrowImageView; + private SimpleViewSwitcher mProgressBar; + private TextView mStatusTextView; + private int mState = STATE_NORMAL; + + private TextView mHeaderTimeView; + + private Animation mRotateUpAnim; + private Animation mRotateDownAnim; + + private static final int ROTATE_ANIM_DURATION = 180; + + public int mMeasuredHeight; + + public ArrowRefreshHeader(Context context) { + super(context); + initView(); + } + + /** + * @param context + * @param attrs + */ + public ArrowRefreshHeader(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + private void initView() { + // 初始情况,设置下拉刷新view高度为0 + mContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate( + R.layout.listview_header, null); + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + lp.setMargins(0, 0, 0, 0); + this.setLayoutParams(lp); + this.setPadding(0, 0, 0, 0); + + addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0)); + setGravity(Gravity.BOTTOM); + + mArrowImageView = (ImageView)findViewById(R.id.listview_header_arrow); + mStatusTextView = (TextView)findViewById(R.id.refresh_status_textview); + + //init the progress view + mProgressBar = (SimpleViewSwitcher)findViewById(R.id.listview_header_progressbar); + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(ProgressStyle.BallSpinFadeLoader); + mProgressBar.setView(progressView); + + + mRotateUpAnim = new RotateAnimation(0.0f, -180.0f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION); + mRotateUpAnim.setFillAfter(true); + mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f, + Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); + mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION); + mRotateDownAnim.setFillAfter(true); + + mHeaderTimeView = (TextView)findViewById(R.id.last_refresh_time); + measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); + mMeasuredHeight = getMeasuredHeight(); + } + + public void setProgressStyle(int style) { + if(style == ProgressStyle.SysProgress){ + mProgressBar.setView(new ProgressBar(getContext(), null, android.R.attr.progressBarStyle)); + }else{ + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(this.getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(style); + mProgressBar.setView(progressView); + } + } + + public void setArrowImageView(int resid){ + mArrowImageView.setImageResource(resid); + } + + public void setState(int state) { + if (state == mState) return ; + + if (state == STATE_REFRESHING) { // 显示进度 + mArrowImageView.clearAnimation(); + mArrowImageView.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.VISIBLE); + smoothScrollTo(mMeasuredHeight); + } else if(state == STATE_DONE) { + mArrowImageView.setVisibility(View.INVISIBLE); + mProgressBar.setVisibility(View.INVISIBLE); + } else { // 显示箭头图片 + mArrowImageView.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.INVISIBLE); + } + + switch(state){ + case STATE_NORMAL: + if (mState == STATE_RELEASE_TO_REFRESH) { + mArrowImageView.startAnimation(mRotateDownAnim); + } + if (mState == STATE_REFRESHING) { + mArrowImageView.clearAnimation(); + } + mStatusTextView.setText(R.string.listview_header_hint_normal); + break; + case STATE_RELEASE_TO_REFRESH: + if (mState != STATE_RELEASE_TO_REFRESH) { + mArrowImageView.clearAnimation(); + mArrowImageView.startAnimation(mRotateUpAnim); + mStatusTextView.setText(R.string.listview_header_hint_release); + } + break; + case STATE_REFRESHING: + mStatusTextView.setText(R.string.refreshing); + break; + case STATE_DONE: + mStatusTextView.setText(R.string.refresh_done); + break; + default: + } + + mState = state; + } + + public int getState() { + return mState; + } + + @Override + public void refreshComplete(){ + mHeaderTimeView.setText(friendlyTime(new Date())); + setState(STATE_DONE); + new Handler().postDelayed(new Runnable(){ + public void run() { + reset(); + } + }, 200); + } + + public void setVisibleHeight(int height) { + if (height < 0) height = 0; + LayoutParams lp = (LayoutParams) mContainer .getLayoutParams(); + lp.height = height; + mContainer.setLayoutParams(lp); + } + + public int getVisibleHeight() { + LayoutParams lp = (LayoutParams) mContainer.getLayoutParams(); + return lp.height; + } + + @Override + public void onMove(float delta) { + if(getVisibleHeight() > 0 || delta > 0) { + setVisibleHeight((int) delta + getVisibleHeight()); + if (mState <= STATE_RELEASE_TO_REFRESH) { // 未处于刷新状态,更新箭头 + if (getVisibleHeight() > mMeasuredHeight) { + setState(STATE_RELEASE_TO_REFRESH); + }else { + setState(STATE_NORMAL); + } + } + } + } + + @Override + public boolean releaseAction() { + boolean isOnRefresh = false; + int height = getVisibleHeight(); + if (height == 0) // not visible. + isOnRefresh = false; + + if(getVisibleHeight() > mMeasuredHeight && mState < STATE_REFRESHING){ + setState(STATE_REFRESHING); + isOnRefresh = true; + } + // refreshing and header isn't shown fully. do nothing. + if (mState == STATE_REFRESHING && height <= mMeasuredHeight) { + //return; + } + if (mState != STATE_REFRESHING) { + smoothScrollTo(0); + } + + if (mState == STATE_REFRESHING) { + int destHeight = mMeasuredHeight; + smoothScrollTo(destHeight); + } + + return isOnRefresh; + } + + public void reset() { + smoothScrollTo(0); + new Handler().postDelayed(new Runnable() { + public void run() { + setState(STATE_NORMAL); + } + }, 500); + } + + private void smoothScrollTo(int destHeight) { + ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight); + animator.setDuration(300).start(); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) + { + setVisibleHeight((int) animation.getAnimatedValue()); + } + }); + animator.start(); + } + + public static String friendlyTime(Date time) { + //获取time距离当前的秒数 + int ct = (int)((System.currentTimeMillis() - time.getTime())/1000); + + if(ct == 0) { + return "刚刚"; + } + + if(ct > 0 && ct < 60) { + return ct + "秒前"; + } + + if(ct >= 60 && ct < 3600) { + return Math.max(ct / 60,1) + "分钟前"; + } + if(ct >= 3600 && ct < 86400) + return ct / 3600 + "小时前"; + if(ct >= 86400 && ct < 2592000){ //86400 * 30 + int day = ct / 86400 ; + return day + "天前"; + } + if(ct >= 2592000 && ct < 31104000) { //86400 * 30 + return ct / 2592000 + "月前"; + } + return ct / 31104000 + "年前"; + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java new file mode 100644 index 0000000..eafef3c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/BaseRefreshHeader.java @@ -0,0 +1,19 @@ +package com.hzecool.widget.xRecyclerView; + +/** + * Created by jianghejie on 15/11/22. + */ +interface BaseRefreshHeader { + + int STATE_NORMAL = 0; + int STATE_RELEASE_TO_REFRESH = 1; + int STATE_REFRESHING = 2; + int STATE_DONE = 3; + + void onMove(float delta); + + boolean releaseAction(); + + void refreshComplete(); + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java new file mode 100644 index 0000000..e79cf35 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ItemTouchHelperAdapter.java @@ -0,0 +1,39 @@ +package com.hzecool.widget.xRecyclerView; + +import android.support.v7.widget.RecyclerView; + +/** + * Created by jianghejie on 16/6/20. + */ + +public interface ItemTouchHelperAdapter { + + /** + * Called when an item has been dragged far enough to trigger a move. This is called every time + * an item is shifted, and not at the end of a "drop" event.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after + * adjusting the underlying data to reflect this move. + * + * @param fromPosition The start position of the moved item. + * @param toPosition Then resolved position of the moved item. + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemMove(int fromPosition, int toPosition); + + + /** + * Called when an item has been dismissed by a swipe.
+ *
+ * Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after + * adjusting the underlying data to reflect this removal. + * + * @param position The position of the item dismissed. + * + * @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder) + * @see RecyclerView.ViewHolder#getAdapterPosition() + */ + void onItemDismiss(int position); +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java new file mode 100644 index 0000000..799a507 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/JellyView.java @@ -0,0 +1,108 @@ +package com.hzecool.widget.xRecyclerView; + +/** + * Created by jianghejie on 15/11/22. + */ + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + + +public class JellyView extends View implements BaseRefreshHeader{ + Path path; + + Paint paint; + + private int minimumHeight = 0; + + private int jellyHeight =0; + + public JellyView(Context context) { + super(context); + init(); + } + + public JellyView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public JellyView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @SuppressWarnings("unused") + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public JellyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + if (isInEditMode()) { + return; + } + path = new Path(); + paint = new Paint(); + paint.setColor(getContext().getResources().getColor(android.R.color.holo_blue_bright)); + paint.setAntiAlias(true); + } + + public void setJellyColor(int jellyColor) { + paint.setColor(jellyColor); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + path.reset(); + path.lineTo(0, minimumHeight); + path.quadTo(getMeasuredWidth() / 2, minimumHeight + jellyHeight, getMeasuredWidth(), minimumHeight); + path.lineTo(getMeasuredWidth(), 0); + canvas.drawPath(path, paint); + } + + @Override + public void setMinimumHeight(int minimumHeight) { + this.minimumHeight = minimumHeight; + } + + public void setJellyHeight(int ribbonHeight) { + this.jellyHeight = ribbonHeight; + } + + @Override + public int getMinimumHeight() { + return minimumHeight; + } + + public int getJellyHeight() { + return jellyHeight; + } + + + @Override + public void refreshComplete(){ + + } + + @Override + public void onMove(float delta) { + jellyHeight = jellyHeight + (int)delta; + Log.i("jellyHeight", "delta = " + delta); + this.invalidate(); + } + + @Override + public boolean releaseAction() { + return false; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java new file mode 100644 index 0000000..e9df633 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/LoadingMoreFooter.java @@ -0,0 +1,110 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.xRecyclerView.progressindicator.AVLoadingIndicatorView; + + +public class LoadingMoreFooter extends LinearLayout { + + private SimpleViewSwitcher progressCon; + public final static int STATE_LOADING = 0; + public final static int STATE_COMPLETE = 1; + public final static int STATE_NOMORE = 2; + private TextView mText; + private String loadingHint; + private String noMoreHint; + private String loadingDoneHint; + + public LoadingMoreFooter(Context context) { + super(context); + initView(); + } + + /** + * @param context + * @param attrs + */ + public LoadingMoreFooter(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + public void setLoadingHint(String hint) { + loadingHint = hint; + } + + public void setNoMoreHint(String hint) { + noMoreHint = hint; + } + + public void setLoadingDoneHint(String hint) { + loadingDoneHint = hint; + } + + public void initView() { + setGravity(Gravity.CENTER); + setLayoutParams(new RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + progressCon = new SimpleViewSwitcher(getContext()); + progressCon.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(this.getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(ProgressStyle.BallSpinFadeLoader); + progressCon.setView(progressView); + + addView(progressCon); + mText = new TextView(getContext()); + mText.setText("正在加载..."); + loadingHint = (String) getContext().getText(R.string.listview_loading); + noMoreHint = (String) getContext().getText(R.string.nomore_loading); + loadingDoneHint = (String) getContext().getText(R.string.loading_done); + LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams + .WRAP_CONTENT); + layoutParams.setMargins((int) getResources().getDimension(R.dimen.textandiconmargin), 0, 0, 0); + + mText.setLayoutParams(layoutParams); + addView(mText); + } + + public void setProgressStyle(int style) { + if (style == ProgressStyle.SysProgress) { + progressCon.setView(new ProgressBar(getContext(), null, android.R.attr.progressBarStyle)); + } else { + AVLoadingIndicatorView progressView = new AVLoadingIndicatorView(this.getContext()); + progressView.setIndicatorColor(0xffB5B5B5); + progressView.setIndicatorId(style); + progressCon.setView(progressView); + } + } + + public void setState(int state) { + switch (state) { + case STATE_LOADING: + progressCon.setVisibility(View.VISIBLE); + mText.setText(loadingHint); + this.setVisibility(View.VISIBLE); + break; + case STATE_COMPLETE: + mText.setText(loadingDoneHint); + this.setVisibility(View.GONE); + break; + case STATE_NOMORE: + mText.setText(noMoreHint); + progressCon.setVisibility(View.GONE); + this.setVisibility(View.VISIBLE); + break; + } + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java new file mode 100644 index 0000000..3de062c --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/ProgressStyle.java @@ -0,0 +1,36 @@ +package com.hzecool.widget.xRecyclerView; + +/** + * Created by jianghejie on 15/11/23. + */ +public class ProgressStyle { + public static final int SysProgress=-1; + public static final int BallPulse=0; + public static final int BallGridPulse=1; + public static final int BallClipRotate=2; + public static final int BallClipRotatePulse=3; + public static final int SquareSpin=4; + public static final int BallClipRotateMultiple=5; + public static final int BallPulseRise=6; + public static final int BallRotate=7; + public static final int CubeTransition=8; + public static final int BallZigZag=9; + public static final int BallZigZagDeflect=10; + public static final int BallTrianglePath=11; + public static final int BallScale=12; + public static final int LineScale=13; + public static final int LineScaleParty=14; + public static final int BallScaleMultiple=15; + public static final int BallPulseSync=16; + public static final int BallBeat=17; + public static final int LineScalePulseOut=18; + public static final int LineScalePulseOutRapid=19; + public static final int BallScaleRipple=20; + public static final int BallScaleRippleMultiple=21; + public static final int BallSpinFadeLoader=22; + public static final int LineSpinFadeLoader=23; + public static final int TriangleSkewSpin=24; + public static final int Pacman=25; + public static final int BallGridBeat=26; + public static final int SemiCircleSpin=27; +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java new file mode 100644 index 0000000..642be62 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/RecyclerViewEmptyView.java @@ -0,0 +1,92 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.support.annotation.ColorInt; +import android.support.annotation.DimenRes; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.hzecool.widget.R; +import com.hzecool.widget.utils.SizeUtils; + + +/** + * Created by tutu on 2017/1/12. + */ + +public class RecyclerViewEmptyView extends RelativeLayout { + private ImageView iv; + private TextView tv; + /** + * textview 距离 imageview 的距离 + */ + private int tvTopMargin = 0; + + private LayoutParams params; + + + public RecyclerViewEmptyView(Context context) { + this(context, null); + initView(); + } + + public RecyclerViewEmptyView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecyclerViewEmptyView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + + private void initView() { + params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + iv = new ImageView(getContext()); + tv = new TextView(getContext()); + + params.addRule(CENTER_IN_PARENT, TRUE); + iv.setLayoutParams(params); + iv.setImageResource(R.mipmap.empty); + + params.addRule(CENTER_HORIZONTAL, TRUE); + params.addRule(BELOW, iv.getId()); + params.topMargin = SizeUtils.dp2px(getContext(),10); + tv.setLayoutParams(params); + tv.setText(R.string.empty_data); + + + this.addView(iv); + this.addView(tv); + } + + + public RecyclerViewEmptyView setText(String text) { + tv.setText(text); + return this; + } + + public RecyclerViewEmptyView setTextColor(@ColorInt int color) { + tv.setTextColor(color); + return this; + } + + public RecyclerViewEmptyView setTextSize(@DimenRes int size) { + tv.setTextSize(size); + return this; + } + + public RecyclerViewEmptyView setTvIvMargin(int dp) { + params.topMargin = SizeUtils.dp2px(getContext(),dp); + tv.setLayoutParams(params); + return this; + } + + public RecyclerViewEmptyView setImageSrc(int resId) { + iv.setImageResource(resId); + return this; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java new file mode 100644 index 0000000..5a2c2ef --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleItemTouchHelperCallback.java @@ -0,0 +1,87 @@ +package com.hzecool.widget.xRecyclerView; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.View; + +/** + * Created by jianghejie on 16/6/20. + */ + +public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback { + + public static final float ALPHA_FULL = 1.0f; + + private final ItemTouchHelperAdapter mAdapter; + private XRecyclerView mXrecyclerView; + + public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter, XRecyclerView recyclerView) { + mAdapter = adapter; + this.mXrecyclerView = recyclerView; + } + + @Override + public boolean isLongPressDragEnabled() { + return true; + } + + @Override + public boolean isItemViewSwipeEnabled() { + return true; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + // Enable drag and swipe in both directions + final int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; + final int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; + return makeMovementFlags(dragFlags, swipeFlags); + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) { + if (source.getItemViewType() != target.getItemViewType()) { + return false; + } + // Notify the adapter of the move + mAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { + // Notify the adapter of the dismissal + mAdapter.onItemDismiss(viewHolder.getAdapterPosition()); + } + + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + + // Fade out the view as it is swiped out of the parent's bounds + if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { + View itemView = viewHolder.itemView; + final float alpha = ALPHA_FULL - Math.abs(dX) / (float) itemView.getWidth(); + itemView.setAlpha(alpha); + } + } + + @Override + public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { + if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { + // Let the view holder know that this item is being moved or dragged + viewHolder.itemView.setBackgroundColor(Color.LTGRAY); + } + + super.onSelectedChanged(viewHolder, actionState); + } + + @Override + public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + viewHolder.itemView.setAlpha(ALPHA_FULL); + viewHolder.itemView.setBackgroundColor(0); + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java new file mode 100644 index 0000000..ec8e8ce --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/SimpleViewSwitcher.java @@ -0,0 +1,60 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * Created by jianghejie on 15/11/22. + */ +public class SimpleViewSwitcher extends ViewGroup { + + public SimpleViewSwitcher(Context context) { + super(context); + } + + public SimpleViewSwitcher(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SimpleViewSwitcher(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int childCount = this.getChildCount(); + int maxHeight = 0; + int maxWidth = 0; + for (int i = 0; i < childCount; i++) { + View child = this.getChildAt(i); + this.measureChild(child, widthMeasureSpec, heightMeasureSpec); + int cw = child.getMeasuredWidth(); + // int ch = child.getMeasuredHeight(); + maxWidth = child.getMeasuredWidth(); + maxHeight = child.getMeasuredHeight(); + } + setMeasuredDimension(maxWidth, maxHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + child.layout(0, 0, r - l, b - t); + + } + } + } + + public void setView(View view) { + if (this.getChildCount() != 0){ + this.removeViewAt(0); + } + this.addView(view,0); + } + +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java new file mode 100644 index 0000000..721e1c5 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/XRecyclerView.java @@ -0,0 +1,727 @@ +package com.hzecool.widget.xRecyclerView; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.design.widget.AppBarLayout; +import android.support.design.widget.CoordinatorLayout; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import java.util.ArrayList; +import java.util.List; + +public class XRecyclerView extends RecyclerView { + public static String REFRESH = "refresh"; + public static String ADDMORE = "addmore"; + + + private boolean isLoadingData = false; + private boolean isNoMore = false; + private int mRefreshProgressStyle = ProgressStyle.SysProgress; + private int mLoadingMoreProgressStyle = ProgressStyle.SysProgress; + private ArrayList mHeaderViews = new ArrayList<>(); + private WrapAdapter mWrapAdapter; + private float mLastY = -1; + private static final float DRAG_RATE = 3; + private LoadingListener mLoadingListener; + private ArrowRefreshHeader mRefreshHeader; + private boolean pullRefreshEnabled = true; + private boolean loadingMoreEnabled = true; + //下面的ItemViewType是保留值(ReservedItemViewType),如果用户的adapter与它们重复将会强制抛出异常。不过为了简化,我们检测到重复时对用户的提示是ItemViewType必须小于10000 + private static final int TYPE_REFRESH_HEADER = 10000;//设置一个很大的数字,尽可能避免和用户的adapter冲突 + private static final int TYPE_FOOTER = 10001; + private static final int HEADER_INIT_INDEX = 10002; + private static List sHeaderTypes = new ArrayList<>();//每个header必须有不同的type,不然滚动的时候顺序会变化 + private int mPageCount = 0; + //adapter没有数据的时候显示,类似于listView的emptyView + private View mEmptyView; + private View mFootView; + private final AdapterDataObserver mDataObserver = new DataObserver(); + private AppBarStateChangeListener.State appbarState = AppBarStateChangeListener.State.EXPANDED; + + public XRecyclerView(Context context) { + this(context, null); + } + + public XRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public XRecyclerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + if (pullRefreshEnabled) { + mRefreshHeader = new ArrowRefreshHeader(getContext()); + mRefreshHeader.setProgressStyle(mRefreshProgressStyle); + } + LoadingMoreFooter footView = new LoadingMoreFooter(getContext()); + footView.setProgressStyle(mLoadingMoreProgressStyle); + mFootView = footView; + mFootView.setVisibility(GONE); + } + + public void setFootViewText(String loading, String noMore) { + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setLoadingHint(loading); + ((LoadingMoreFooter) mFootView).setNoMoreHint(noMore); + } + } + + public void addHeaderView(View view) { + sHeaderTypes.add(HEADER_INIT_INDEX + mHeaderViews.size()); + mHeaderViews.add(view); + if (mWrapAdapter != null) { + mWrapAdapter.notifyDataSetChanged(); + } + } + + //根据header的ViewType判断是哪个header + private View getHeaderViewByType(int itemType) { + if (!isHeaderType(itemType)) { + return null; + } + return mHeaderViews.get(itemType - HEADER_INIT_INDEX); + } + + //判断一个type是否为HeaderType + private boolean isHeaderType(int itemViewType) { + return mHeaderViews.size() > 0 && sHeaderTypes.contains(itemViewType); + } + + //判断是否是XRecyclerView保留的itemViewType + private boolean isReservedItemViewType(int itemViewType) { + if (itemViewType == TYPE_REFRESH_HEADER || itemViewType == TYPE_FOOTER || sHeaderTypes.contains + (itemViewType)) { + return true; + } else { + return false; + } + } + + public void setFootView(final View view) { + mFootView = view; + } + + public void loadMoreComplete() { + isLoadingData = false; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_COMPLETE); + } else { + mFootView.setVisibility(GONE); + } + } + + public void setNoMore(boolean noMore) { + isLoadingData = false; + isNoMore = noMore; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(isNoMore ? LoadingMoreFooter.STATE_NOMORE : LoadingMoreFooter + .STATE_COMPLETE); + } else { + mFootView.setVisibility(GONE); + } + } + + public void refresh() { + if (pullRefreshEnabled && mLoadingListener != null) { + mRefreshHeader.setState(ArrowRefreshHeader.STATE_REFRESHING); + mLoadingListener.onRefresh(); + } + } + + public void reset() { + setNoMore(false); + loadMoreComplete(); + refreshComplete(); + } + + public void refreshComplete() { + mRefreshHeader.refreshComplete(); + setNoMore(false); + } + + public void setRefreshHeader(ArrowRefreshHeader refreshHeader) { + mRefreshHeader = refreshHeader; + } + + public void setPullRefreshEnabled(boolean enabled) { + pullRefreshEnabled = enabled; + } + + public void setLoadingMoreEnabled(boolean enabled) { + loadingMoreEnabled = enabled; + if (!enabled) { + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_COMPLETE); + } + } + } + + public void setRefreshProgressStyle(int style) { + mRefreshProgressStyle = style; + if (mRefreshHeader != null) { + mRefreshHeader.setProgressStyle(style); + } + } + + public void setLoadingMoreProgressStyle(int style) { + mLoadingMoreProgressStyle = style; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setProgressStyle(style); + } + } + + public void setArrowImageView(int resId) { + if (mRefreshHeader != null) { + mRefreshHeader.setArrowImageView(resId); + } + } + + public void setEmptyView(View emptyView) { + this.mEmptyView = emptyView; + mDataObserver.onChanged(); + } + + public View getEmptyView() { + return mEmptyView; + } + + @Override + public void setAdapter(Adapter adapter) { + mWrapAdapter = new WrapAdapter(adapter); + super.setAdapter(mWrapAdapter); + adapter.registerAdapterDataObserver(mDataObserver); + mDataObserver.onChanged(); + } + + //避免用户自己调用getAdapter() 引起的ClassCastException + @Override + public Adapter getAdapter() { + if (mWrapAdapter != null) + return mWrapAdapter.getOriginalAdapter(); + else + return null; + } + + @Override + public void setLayoutManager(LayoutManager layout) { + super.setLayoutManager(layout); + if (mWrapAdapter != null) { + if (layout instanceof GridLayoutManager) { + final GridLayoutManager gridManager = ((GridLayoutManager) layout); + gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return (mWrapAdapter.isHeader(position) || mWrapAdapter.isFooter(position) || mWrapAdapter + .isRefreshHeader(position)) + ? gridManager.getSpanCount() : 1; + } + }); + + } + } + } + + @Override + public void onScrollStateChanged(int state) { + super.onScrollStateChanged(state); + if (state == RecyclerView.SCROLL_STATE_IDLE && mLoadingListener != null && !isLoadingData && + loadingMoreEnabled) { + LayoutManager layoutManager = getLayoutManager(); + int lastVisibleItemPosition; + if (layoutManager instanceof GridLayoutManager) { + lastVisibleItemPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition(); + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + int[] into = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()]; + ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(into); + lastVisibleItemPosition = findMax(into); + } else { + lastVisibleItemPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition(); + } + if (layoutManager.getChildCount() > 0 + && lastVisibleItemPosition >= layoutManager.getItemCount() - 1 && layoutManager.getItemCount() > + layoutManager.getChildCount() && !isNoMore && mRefreshHeader.getState() < ArrowRefreshHeader + .STATE_REFRESHING) { + isLoadingData = true; + if (mFootView instanceof LoadingMoreFooter) { + ((LoadingMoreFooter) mFootView).setState(LoadingMoreFooter.STATE_LOADING); + } else { + mFootView.setVisibility(View.VISIBLE); + } + mLoadingListener.onLoadMore(); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mLastY == -1) { + mLastY = ev.getRawY(); + } + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + mLastY = ev.getRawY(); + break; + case MotionEvent.ACTION_MOVE: + final float deltaY = ev.getRawY() - mLastY; + mLastY = ev.getRawY(); + if (isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) { + mRefreshHeader.onMove(deltaY / DRAG_RATE); + if (mRefreshHeader.getVisibleHeight() > 0 && mRefreshHeader.getState() < ArrowRefreshHeader + .STATE_REFRESHING) { + return false; + } + } + break; + default: + mLastY = -1; // reset + if (isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) { + if (mRefreshHeader.releaseAction()) { + if (mLoadingListener != null) { + mLoadingListener.onRefresh(); + } + } + } + break; + } + return super.onTouchEvent(ev); + } + + private int findMax(int[] lastPositions) { + int max = lastPositions[0]; + for (int value : lastPositions) { + if (value > max) { + max = value; + } + } + return max; + } + + private boolean isOnTop() { + if (mRefreshHeader.getParent() != null) { + return true; + } else { + return false; + } + } + + private class DataObserver extends AdapterDataObserver { + @Override + public void onChanged() { + if (mWrapAdapter != null) { + mWrapAdapter.notifyDataSetChanged(); + } + if (mWrapAdapter != null && mEmptyView != null) { + int emptyCount = 1 + mWrapAdapter.getHeadersCount(); + if (loadingMoreEnabled) { + emptyCount++; + } + if (mWrapAdapter.getItemCount() == emptyCount) { + mEmptyView.setVisibility(View.VISIBLE); + XRecyclerView.this.setVisibility(GONE); + } else { + + mEmptyView.setVisibility(GONE); + XRecyclerView.this.setVisibility(View.VISIBLE); + } + } + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeInserted(positionStart, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeChanged(positionStart, itemCount); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + mWrapAdapter.notifyItemRangeChanged(positionStart, itemCount, payload); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + mWrapAdapter.notifyItemRangeRemoved(positionStart, itemCount); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + mWrapAdapter.notifyItemMoved(fromPosition, toPosition); + } + } + + ; + + private class WrapAdapter extends Adapter { + + private Adapter adapter; + + public WrapAdapter(Adapter adapter) { + this.adapter = adapter; + } + + public Adapter getOriginalAdapter() { + return this.adapter; + } + + public boolean isHeader(int position) { + return position >= 1 && position < mHeaderViews.size() + 1; + } + + public boolean isFooter(int position) { + if (loadingMoreEnabled) { + return position == getItemCount() - 1; + } else { + return false; + } + } + + public boolean isRefreshHeader(int position) { + return position == 0; + } + + public int getHeadersCount() { + return mHeaderViews.size(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == TYPE_REFRESH_HEADER) { + return new SimpleViewHolder(mRefreshHeader); + } else if (isHeaderType(viewType)) { + return new SimpleViewHolder(getHeaderViewByType(viewType)); + } else if (viewType == TYPE_FOOTER) { + return new SimpleViewHolder(mFootView); + } + return adapter.onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + if (isHeader(position) || isRefreshHeader(position)) { + return; + } + int adjPosition = position - (getHeadersCount() + 1); + int adapterCount; + if (adapter != null) { + adapterCount = adapter.getItemCount(); + if (adjPosition < adapterCount) { + adapter.onBindViewHolder(holder, adjPosition); + } + } + } + + // some times we need to override this + @Override + public void onBindViewHolder(ViewHolder holder, int position, List payloads) { + if (isHeader(position) || isRefreshHeader(position)) { + return; + } + int adjPosition = position - (getHeadersCount() + 1); + int adapterCount; + if (adapter != null) { + adapterCount = adapter.getItemCount(); + if (adjPosition < adapterCount) { + if (payloads.isEmpty()) { + adapter.onBindViewHolder(holder, adjPosition); + } else { + adapter.onBindViewHolder(holder, adjPosition, payloads); + } + } + } + } + + @Override + public int getItemCount() { + if (loadingMoreEnabled) { + if (adapter != null) { + return getHeadersCount() + adapter.getItemCount() + 2; + } else { + return getHeadersCount() + 2; + } + } else { + if (adapter != null) { + return getHeadersCount() + adapter.getItemCount() + 1; + } else { + return getHeadersCount() + 1; + } + } + } + + @Override + public int getItemViewType(int position) { + int adjPosition = position - (getHeadersCount() + 1); + if (isRefreshHeader(position)) { + return TYPE_REFRESH_HEADER; + } + if (isHeader(position)) { + position = position - 1; + return sHeaderTypes.get(position); + } + if (isFooter(position)) { + return TYPE_FOOTER; + } + int adapterCount; + if (adapter != null) { + adapterCount = adapter.getItemCount(); + if (adjPosition < adapterCount) { + int type = adapter.getItemViewType(adjPosition); + if (isReservedItemViewType(type)) { + throw new IllegalStateException("XRecyclerView require itemViewType in adapter should be less " + + "than 10000 "); + } + return type; + } + } + return 0; + } + + @Override + public long getItemId(int position) { + if (adapter != null && position >= getHeadersCount() + 1) { + int adjPosition = position - (getHeadersCount() + 1); + if (adjPosition < adapter.getItemCount()) { + return adapter.getItemId(adjPosition); + } + } + return -1; + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + LayoutManager manager = recyclerView.getLayoutManager(); + if (manager instanceof GridLayoutManager) { + final GridLayoutManager gridManager = ((GridLayoutManager) manager); + gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return (isHeader(position) || isFooter(position) || isRefreshHeader(position)) + ? gridManager.getSpanCount() : 1; + } + }); + } + adapter.onAttachedToRecyclerView(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + adapter.onDetachedFromRecyclerView(recyclerView); + } + + @Override + public void onViewAttachedToWindow(ViewHolder holder) { + super.onViewAttachedToWindow(holder); + ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + if (lp != null + && lp instanceof StaggeredGridLayoutManager.LayoutParams + && (isHeader(holder.getLayoutPosition()) || isRefreshHeader(holder.getLayoutPosition()) || isFooter + (holder.getLayoutPosition()))) { + StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp; + p.setFullSpan(true); + } + adapter.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(ViewHolder holder) { + adapter.onViewDetachedFromWindow(holder); + } + + @Override + public void onViewRecycled(ViewHolder holder) { + adapter.onViewRecycled(holder); + } + + @Override + public boolean onFailedToRecycleView(ViewHolder holder) { + return adapter.onFailedToRecycleView(holder); + } + + @Override + public void unregisterAdapterDataObserver(AdapterDataObserver observer) { + adapter.unregisterAdapterDataObserver(observer); + } + + @Override + public void registerAdapterDataObserver(AdapterDataObserver observer) { + adapter.registerAdapterDataObserver(observer); + } + + private class SimpleViewHolder extends ViewHolder { + public SimpleViewHolder(View itemView) { + super(itemView); + } + } + } + + public void setLoadingListener(LoadingListener listener) { + mLoadingListener = listener; + } + + public interface LoadingListener { + + void onRefresh(); + + void onLoadMore(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + //解决和CollapsingToolbarLayout冲突的问题 + AppBarLayout appBarLayout = null; + ViewParent p = getParent(); + while (p != null) { + if (p instanceof CoordinatorLayout) { + break; + } + p = p.getParent(); + } + if (p instanceof CoordinatorLayout) { + CoordinatorLayout coordinatorLayout = (CoordinatorLayout) p; + final int childCount = coordinatorLayout.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = coordinatorLayout.getChildAt(i); + if (child instanceof AppBarLayout) { + appBarLayout = (AppBarLayout) child; + break; + } + } + if (appBarLayout != null) { + appBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() { + @Override + public void onStateChanged(AppBarLayout appBarLayout, AppBarStateChangeListener.State state) { + appbarState = state; + } + }); + } + } + } + + public class DividerItemDecoration extends ItemDecoration { + + private Drawable mDivider; + private int mOrientation; + + /** + * Sole constructor. Takes in a {@link Drawable} to be used as the interior + * divider. + * + * @param divider A divider {@code Drawable} to be drawn on the RecyclerView + */ + public DividerItemDecoration(Drawable divider) { + mDivider = divider; + } + + /** + * Draws horizontal or vertical dividers onto the parent RecyclerView. + * + * @param canvas The {@link Canvas} onto which dividers will be drawn + * @param parent The RecyclerView onto which dividers are being added + * @param state The current RecyclerView.State of the RecyclerView + */ + @Override + public void onDraw(Canvas canvas, RecyclerView parent, State state) { + if (mOrientation == LinearLayoutManager.HORIZONTAL) { + drawHorizontalDividers(canvas, parent); + } else if (mOrientation == LinearLayoutManager.VERTICAL) { + drawVerticalDividers(canvas, parent); + } + } + + /** + * Determines the size and location of offsets between items in the parent + * RecyclerView. + * + * @param outRect The {@link Rect} of offsets to be added around the child + * view + * @param view The child view to be decorated with an offset + * @param parent The RecyclerView onto which dividers are being added + * @param state The current RecyclerView.State of the RecyclerView + */ + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) { + super.getItemOffsets(outRect, view, parent, state); + + if (parent.getChildAdapterPosition(view) <= mWrapAdapter.getHeadersCount() + 1) { + return; + } + mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation(); + if (mOrientation == LinearLayoutManager.HORIZONTAL) { + outRect.left = mDivider.getIntrinsicWidth(); + } else if (mOrientation == LinearLayoutManager.VERTICAL) { + outRect.top = mDivider.getIntrinsicHeight(); + } + } + + /** + * Adds dividers to a RecyclerView with a LinearLayoutManager or its + * subclass oriented horizontally. + * + * @param canvas The {@link Canvas} onto which horizontal dividers will be + * drawn + * @param parent The RecyclerView onto which horizontal dividers are being + * added + */ + private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) { + int parentTop = parent.getPaddingTop(); + int parentBottom = parent.getHeight() - parent.getPaddingBottom(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + + LayoutParams params = (LayoutParams) child.getLayoutParams(); + + int parentLeft = child.getRight() + params.rightMargin; + int parentRight = parentLeft + mDivider.getIntrinsicWidth(); + + mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom); + mDivider.draw(canvas); + } + } + + /** + * Adds dividers to a RecyclerView with a LinearLayoutManager or its + * subclass oriented vertically. + * + * @param canvas The {@link Canvas} onto which vertical dividers will be + * drawn + * @param parent The RecyclerView onto which vertical dividers are being + * added + */ + private void drawVerticalDividers(Canvas canvas, RecyclerView parent) { + int parentLeft = parent.getPaddingLeft(); + int parentRight = parent.getWidth() - parent.getPaddingRight(); + + int childCount = parent.getChildCount(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + + LayoutParams params = (LayoutParams) child.getLayoutParams(); + + int parentTop = child.getBottom() + params.bottomMargin; + int parentBottom = parentTop + mDivider.getIntrinsicHeight(); + + mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom); + mDivider.draw(canvas); + } + } + } +} \ No newline at end of file diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java new file mode 100644 index 0000000..b70fb76 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/AVLoadingIndicatorView.java @@ -0,0 +1,365 @@ +package com.hzecool.widget.xRecyclerView.progressindicator; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.os.Build; +import android.support.annotation.IntDef; +import android.util.AttributeSet; +import android.view.View; + +import com.hzecool.widget.R; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallBeatIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallClipRotateIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallClipRotateMultipleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallClipRotatePulseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallGridBeatIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallGridPulseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallPulseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallPulseRiseIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallPulseSyncIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallRotateIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleMultipleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleRippleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallScaleRippleMultipleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallSpinFadeLoaderIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallTrianglePathIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallZigZagDeflectIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BallZigZagIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.BaseIndicatorController; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.CubeTransitionIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScaleIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScalePartyIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScalePulseOutIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineScalePulseOutRapidIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.LineSpinFadeLoaderIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.PacmanIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.SemiCircleSpinIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.SquareSpinIndicator; +import com.hzecool.widget.xRecyclerView.progressindicator.indicator.TriangleSkewSpinIndicator; + + +/** + * Created by Jack on 2015/10/15 + * + .BallPulse, + .BallGridPulse, + .BallClipRotate, + .BallClipRotatePulse, + .SquareSpin, + .BallClipRotateMultiple, + .BallPulseRise, + .BallRotate, + .CubeTransition, + .BallZigZag, + .BallZigZagDeflect, + .BallTrianglePath, + .BallScale, + .LineScale, + .LineScaleParty, + .BallScaleMultiple, + .BallPulseSync, + .BallBeat, + .LineScalePulseOut, + .LineScalePulseOutRapid, + .BallScaleRipple, + .BallScaleRippleMultiple, + .BallSpinFadeLoader, + .LineSpinFadeLoader, + .TriangleSkewSpin, + .Pacman, + .BallGridBeat, + .SemiCircleSpin + * + */ +public class AVLoadingIndicatorView extends View{ + //indicators + public static final int BallPulse=0; + public static final int BallGridPulse=1; + public static final int BallClipRotate=2; + public static final int BallClipRotatePulse=3; + public static final int SquareSpin=4; + public static final int BallClipRotateMultiple=5; + public static final int BallPulseRise=6; + public static final int BallRotate=7; + public static final int CubeTransition=8; + public static final int BallZigZag=9; + public static final int BallZigZagDeflect=10; + public static final int BallTrianglePath=11; + public static final int BallScale=12; + public static final int LineScale=13; + public static final int LineScaleParty=14; + public static final int BallScaleMultiple=15; + public static final int BallPulseSync=16; + public static final int BallBeat=17; + public static final int LineScalePulseOut=18; + public static final int LineScalePulseOutRapid=19; + public static final int BallScaleRipple=20; + public static final int BallScaleRippleMultiple=21; + public static final int BallSpinFadeLoader=22; + public static final int LineSpinFadeLoader=23; + public static final int TriangleSkewSpin=24; + public static final int Pacman=25; + public static final int BallGridBeat=26; + public static final int SemiCircleSpin=27; + + + @IntDef(flag = true, + value = { + BallPulse, + BallGridPulse, + BallClipRotate, + BallClipRotatePulse, + SquareSpin, + BallClipRotateMultiple, + BallPulseRise, + BallRotate, + CubeTransition, + BallZigZag, + BallZigZagDeflect, + BallTrianglePath, + BallScale, + LineScale, + LineScaleParty, + BallScaleMultiple, + BallPulseSync, + BallBeat, + LineScalePulseOut, + LineScalePulseOutRapid, + BallScaleRipple, + BallScaleRippleMultiple, + BallSpinFadeLoader, + LineSpinFadeLoader, + TriangleSkewSpin, + Pacman, + BallGridBeat, + SemiCircleSpin + }) + public @interface Indicator{} + + //Sizes (with defaults in DP) + public static final int DEFAULT_SIZE=30; + + //attrs + int mIndicatorId; + int mIndicatorColor; + + Paint mPaint; + + BaseIndicatorController mIndicatorController; + + private boolean mHasAnimation; + + public AVLoadingIndicatorView(Context context) { + super(context); + init(null, 0); + } + + public AVLoadingIndicatorView(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs, 0); + } + + public AVLoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(attrs, defStyleAttr); + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public AVLoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(attrs, defStyleAttr); + } + + private void init(AttributeSet attrs, int defStyle) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.AVLoadingIndicatorView); + mIndicatorId=a.getInt(R.styleable.AVLoadingIndicatorView_indicator, BallPulse); + mIndicatorColor=a.getColor(R.styleable.AVLoadingIndicatorView_indicator_color, Color.WHITE); + a.recycle(); + mPaint=new Paint(); + mPaint.setColor(mIndicatorColor); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setAntiAlias(true); + applyIndicator(); + } + + public void setIndicatorId(int indicatorId){ + mIndicatorId = indicatorId; + applyIndicator(); + } + + public void setIndicatorColor(int color){ + mIndicatorColor = color; + mPaint.setColor(mIndicatorColor); + this.invalidate(); + } + + private void applyIndicator(){ + switch (mIndicatorId){ + case BallPulse: + mIndicatorController=new BallPulseIndicator(); + break; + case BallGridPulse: + mIndicatorController=new BallGridPulseIndicator(); + break; + case BallClipRotate: + mIndicatorController=new BallClipRotateIndicator(); + break; + case BallClipRotatePulse: + mIndicatorController=new BallClipRotatePulseIndicator(); + break; + case SquareSpin: + mIndicatorController=new SquareSpinIndicator(); + break; + case BallClipRotateMultiple: + mIndicatorController=new BallClipRotateMultipleIndicator(); + break; + case BallPulseRise: + mIndicatorController=new BallPulseRiseIndicator(); + break; + case BallRotate: + mIndicatorController=new BallRotateIndicator(); + break; + case CubeTransition: + mIndicatorController=new CubeTransitionIndicator(); + break; + case BallZigZag: + mIndicatorController=new BallZigZagIndicator(); + break; + case BallZigZagDeflect: + mIndicatorController=new BallZigZagDeflectIndicator(); + break; + case BallTrianglePath: + mIndicatorController=new BallTrianglePathIndicator(); + break; + case BallScale: + mIndicatorController=new BallScaleIndicator(); + break; + case LineScale: + mIndicatorController=new LineScaleIndicator(); + break; + case LineScaleParty: + mIndicatorController=new LineScalePartyIndicator(); + break; + case BallScaleMultiple: + mIndicatorController=new BallScaleMultipleIndicator(); + break; + case BallPulseSync: + mIndicatorController=new BallPulseSyncIndicator(); + break; + case BallBeat: + mIndicatorController=new BallBeatIndicator(); + break; + case LineScalePulseOut: + mIndicatorController=new LineScalePulseOutIndicator(); + break; + case LineScalePulseOutRapid: + mIndicatorController=new LineScalePulseOutRapidIndicator(); + break; + case BallScaleRipple: + mIndicatorController=new BallScaleRippleIndicator(); + break; + case BallScaleRippleMultiple: + mIndicatorController=new BallScaleRippleMultipleIndicator(); + break; + case BallSpinFadeLoader: + mIndicatorController=new BallSpinFadeLoaderIndicator(); + break; + case LineSpinFadeLoader: + mIndicatorController=new LineSpinFadeLoaderIndicator(); + break; + case TriangleSkewSpin: + mIndicatorController=new TriangleSkewSpinIndicator(); + break; + case Pacman: + mIndicatorController=new PacmanIndicator(); + break; + case BallGridBeat: + mIndicatorController=new BallGridBeatIndicator(); + break; + case SemiCircleSpin: + mIndicatorController=new SemiCircleSpinIndicator(); + break; + } + mIndicatorController.setTarget(this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = measureDimension(dp2px(DEFAULT_SIZE), widthMeasureSpec); + int height = measureDimension(dp2px(DEFAULT_SIZE), heightMeasureSpec); + setMeasuredDimension(width, height); + } + + private int measureDimension(int defaultSize,int measureSpec){ + int result = defaultSize; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + if (specMode == MeasureSpec.EXACTLY) { + result = specSize; + } else if (specMode == MeasureSpec.AT_MOST) { + result = Math.min(defaultSize, specSize); + } else { + result = defaultSize; + } + return result; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawIndicator(canvas); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!mHasAnimation){ + mHasAnimation=true; + applyAnimation(); + } + } + + @Override + public void setVisibility(int v) { + if (getVisibility() != v) { + super.setVisibility(v); + if (v == GONE || v == INVISIBLE) { + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.END); + } else { + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.CANCEL); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START); + } + + void drawIndicator(Canvas canvas){ + mIndicatorController.draw(canvas, mPaint); + } + + void applyAnimation(){ + mIndicatorController.initAnimation(); + } + + private int dp2px(int dpValue) { + return (int) getContext().getResources().getDisplayMetrics().density * dpValue; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java new file mode 100644 index 0000000..1191b9b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallBeatIndicator.java @@ -0,0 +1,82 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallBeatIndicator extends BaseIndicatorController { + + public static final float SCALE = 1.0f; + + public static final int ALPHA = 255; + + private float[] scaleFloats = new float[]{SCALE, + SCALE, + SCALE}; + + int[] alphas = new int[]{ALPHA, + ALPHA, + ALPHA,}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing = 4; + float radius = (getWidth() - circleSpacing * 2) / 6; + float x = getWidth() / 2 - (radius * 2 + circleSpacing); + float y = getHeight() / 2; + for (int i = 0; i < 3; i++) { + canvas.save(); + float translateX = x + (radius * 2) * i + circleSpacing * i; + canvas.translate(translateX, y); + canvas.scale(scaleFloats[i], scaleFloats[i]); + paint.setAlpha(alphas[i]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators = new ArrayList<>(); + int[] delays = new int[]{350, 0, 350}; + for (int i = 0; i < 3; i++) { + final int index = i; + ValueAnimator scaleAnim = ValueAnimator.ofFloat(1, 0.75f, 1); + scaleAnim.setDuration(700); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim = ValueAnimator.ofInt(255, 51, 255); + alphaAnim.setDuration(700); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java new file mode 100644 index 0000000..d491163 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateIndicator.java @@ -0,0 +1,65 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallClipRotateIndicator extends BaseIndicatorController { + + float scaleFloat=1,degrees; + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + + float circleSpacing=12; + float x = (getWidth()) / 2; + float y=(getHeight()) / 2; + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.rotate(degrees); + RectF rectF=new RectF(-x+circleSpacing,-y+circleSpacing,0+x-circleSpacing,0+y-circleSpacing); + canvas.drawArc(rectF, -45, 270, false, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.6f,0.5f,1); + scaleAnim.setDuration(750); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0,180,360); + rotateAnim.setDuration(750); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java new file mode 100644 index 0000000..67fcfed --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotateMultipleIndicator.java @@ -0,0 +1,85 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/17. + */ +public class BallClipRotateMultipleIndicator extends BaseIndicatorController{ + + float scaleFloat=1,degrees; + + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStrokeWidth(3); + paint.setStyle(Paint.Style.STROKE); + + float circleSpacing=12; + float x=getWidth()/2; + float y=getHeight()/2; + + canvas.save(); + + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.rotate(degrees); + + //draw two big arc + float[] bStartAngles=new float[]{135,-45}; + for (int i = 0; i < 2; i++) { + RectF rectF=new RectF(-x+circleSpacing,-y+circleSpacing,x-circleSpacing,y-circleSpacing); + canvas.drawArc(rectF, bStartAngles[i], 90, false, paint); + } + + canvas.restore(); + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.rotate(-degrees); + //draw two small arc + float[] sStartAngles=new float[]{225,45}; + for (int i = 0; i < 2; i++) { + RectF rectF=new RectF(-x/1.8f+circleSpacing,-y/1.8f+circleSpacing,x/1.8f-circleSpacing,y/1.8f-circleSpacing); + canvas.drawArc(rectF, sStartAngles[i], 90, false, paint); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.6f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0, 180,360); + rotateAnim.setDuration(1000); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java new file mode 100644 index 0000000..96a3322 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallClipRotatePulseIndicator.java @@ -0,0 +1,94 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallClipRotatePulseIndicator extends BaseIndicatorController { + + float scaleFloat1,scaleFloat2,degrees; + + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=12; + float x=getWidth()/2; + float y=getHeight()/2; + + //draw fill circle + canvas.save(); + canvas.translate(x, y); + canvas.scale(scaleFloat1, scaleFloat1); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(0, 0, x / 2.5f, paint); + + canvas.restore(); + + canvas.translate(x, y); + canvas.scale(scaleFloat2, scaleFloat2); + canvas.rotate(degrees); + + paint.setStrokeWidth(3); + paint.setStyle(Paint.Style.STROKE); + + //draw two arc + float[] startAngles=new float[]{225,45}; + for (int i = 0; i < 2; i++) { + RectF rectF=new RectF(-x+circleSpacing,-y+circleSpacing,x-circleSpacing,y-circleSpacing); + canvas.drawArc(rectF, startAngles[i], 90, false, paint); + } + } + + @Override + public List createAnimation() { + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.3f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat1 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator scaleAnim2=ValueAnimator.ofFloat(1,0.6f,1); + scaleAnim2.setDuration(1000); + scaleAnim2.setRepeatCount(-1); + scaleAnim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat2 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim2.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0, 180,360); + rotateAnim.setDuration(1000); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + List animators=new ArrayList<>(); + animators.add(scaleAnim); + animators.add(scaleAnim2); + animators.add(rotateAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java new file mode 100644 index 0000000..1d55eec --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridBeatIndicator.java @@ -0,0 +1,76 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class BallGridBeatIndicator extends BaseIndicatorController { + + public static final int ALPHA=255; + + int[] alphas=new int[]{ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*4)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + float y = getWidth()/ 2-(radius*2+circleSpacing); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + canvas.save(); + float translateX=x+(radius*2)*j+circleSpacing*j; + float translateY=y+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, translateY); + paint.setAlpha(alphas[3 * i + j]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + + int[] durations={960, 930, 1190, 1130, 1340, 940, 1200, 820, 1190}; + int[] delays= {360, 400, 680, 410, 710, -150, -120, 10, 320}; + + for (int i = 0; i < 9; i++) { + final int index=i; + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 168,255); + alphaAnim.setDuration(durations[i]); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(alphaAnim); + } + return animators; + } + + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java new file mode 100644 index 0000000..09b3219 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallGridPulseIndicator.java @@ -0,0 +1,103 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallGridPulseIndicator extends BaseIndicatorController{ + + public static final int ALPHA=255; + + public static final float SCALE=1.0f; + + int[] alphas=new int[]{ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA}; + + float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE}; + + + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*4)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + float y = getWidth()/ 2-(radius*2+circleSpacing); + + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + canvas.save(); + float translateX=x+(radius*2)*j+circleSpacing*j; + float translateY=y+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, translateY); + canvas.scale(scaleFloats[3 * i + j], scaleFloats[3 * i + j]); + paint.setAlpha(alphas[3 * i + j]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + int[] durations={720, 1020, 1280, 1420, 1450, 1180, 870, 1450, 1060}; + int[] delays= {-60, 250, -170, 480, 310, 30, 460, 780, 450}; + + for (int i = 0; i < 9; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.5f,1); + scaleAnim.setDuration(durations[i]); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 210, 122, 255); + alphaAnim.setDuration(durations[i]); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java new file mode 100644 index 0000000..fb7705f --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseIndicator.java @@ -0,0 +1,68 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class BallPulseIndicator extends BaseIndicatorController{ + + public static final float SCALE=1.0f; + + //scale x ,y + private float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE}; + + + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(Math.min(getWidth(),getHeight())-circleSpacing*2)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + float y=getHeight() / 2; + for (int i = 0; i < 3; i++) { + canvas.save(); + float translateX=x+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, y); + canvas.scale(scaleFloats[i], scaleFloats[i]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + int[] delays=new int[]{120,240,360}; + for (int i = 0; i < 3; i++) { + final int index=i; + + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.3f,1); + + scaleAnim.setDuration(750); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java new file mode 100644 index 0000000..598a615 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseRiseIndicator.java @@ -0,0 +1,41 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/17. + */ +public class BallPulseRiseIndicator extends BaseIndicatorController{ + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + canvas.drawCircle(getWidth()/4,radius*2,radius,paint); + canvas.drawCircle(getWidth()*3/4,radius*2,radius,paint); + + canvas.drawCircle(radius,getHeight()-2*radius,radius,paint); + canvas.drawCircle(getWidth()/2,getHeight()-2*radius,radius,paint); + canvas.drawCircle(getWidth()-radius,getHeight()-2*radius,radius,paint); + } + + @Override + public List createAnimation() { + PropertyValuesHolder rotation6=PropertyValuesHolder.ofFloat("rotationX",0,360); + ObjectAnimator animator=ObjectAnimator.ofPropertyValuesHolder(getTarget(), rotation6); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(-1); + animator.setDuration(1500); + animator.start(); + List animators=new ArrayList<>(); + animators.add(animator); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java new file mode 100644 index 0000000..7b74eea --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallPulseSyncIndicator.java @@ -0,0 +1,57 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallPulseSyncIndicator extends BaseIndicatorController { + + float[] translateYFloats=new float[3]; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*2)/6; + float x = getWidth()/ 2-(radius*2+circleSpacing); + for (int i = 0; i < 3; i++) { + canvas.save(); + float translateX=x+(radius*2)*i+circleSpacing*i; + canvas.translate(translateX, translateYFloats[i]); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float circleSpacing=4; + float radius=(getWidth()-circleSpacing*2)/6; + int[] delays=new int[]{70,140,210}; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(getHeight()/2,getHeight()/2-radius*2,getHeight()/2); + scaleAnim.setDuration(600); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java new file mode 100644 index 0000000..2c46631 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallRotateIndicator.java @@ -0,0 +1,71 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/17. + */ +public class BallRotateIndicator extends BaseIndicatorController{ + + float scaleFloat=0.5f; + + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + float x = getWidth()/ 2; + float y=getHeight()/2; + + canvas.save(); + canvas.translate(x - radius * 2 - radius, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + + canvas.save(); + canvas.translate(x, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.drawCircle(0, 0, radius, paint); + canvas.restore(); + + canvas.save(); + canvas.translate(x + radius * 2 + radius, y); + canvas.scale(scaleFloat, scaleFloat); + canvas.drawCircle(0,0,radius, paint); + canvas.restore(); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0.5f,1,0.5f); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ObjectAnimator rotateAnim=ObjectAnimator.ofFloat(getTarget(),"rotation",0,180,360); + rotateAnim.setDuration(1000); + rotateAnim.setRepeatCount(-1); + rotateAnim.start(); + + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java new file mode 100644 index 0000000..2865b09 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleIndicator.java @@ -0,0 +1,63 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleIndicator extends BaseIndicatorController { + + float scale=1; + int alpha=255; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + paint.setAlpha(alpha); + canvas.scale(scale,scale,getWidth()/2,getHeight()/2); + paint.setAlpha(alpha); + canvas.drawCircle(getWidth()/2,getHeight()/2,getWidth()/2-circleSpacing,paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scale = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 0); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alpha = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java new file mode 100644 index 0000000..9af4551 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleMultipleIndicator.java @@ -0,0 +1,70 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleMultipleIndicator extends BaseIndicatorController { + + float[] scaleFloats=new float[]{1,1,1}; + int[] alphaInts=new int[]{255,255,255}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float circleSpacing=4; + for (int i = 0; i < 3; i++) { + paint.setAlpha(alphaInts[i]); + canvas.scale(scaleFloats[i],scaleFloats[i],getWidth()/2,getHeight()/2); + canvas.drawCircle(getWidth()/2,getHeight()/2,getWidth()/2-circleSpacing,paint); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{0, 200, 400}; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255,0); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphaInts[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + alphaAnim.start(); + + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java new file mode 100644 index 0000000..47008ba --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleIndicator.java @@ -0,0 +1,59 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleRippleIndicator extends BallScaleIndicator { + + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + super.draw(canvas, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scale = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(0, 255); + alphaAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alpha = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + + animators.add(scaleAnim); + animators.add(alphaAnim); + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java new file mode 100644 index 0000000..9abbea7 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallScaleRippleMultipleIndicator.java @@ -0,0 +1,65 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallScaleRippleMultipleIndicator extends BallScaleMultipleIndicator{ + + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(3); + super.draw(canvas, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{0, 200, 400}; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(0,1); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(0,255); + scaleAnim.setInterpolator(new LinearInterpolator()); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphaInts[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.setStartDelay(delays[i]); + alphaAnim.start(); + + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java new file mode 100644 index 0000000..f23ab8f --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallSpinFadeLoaderIndicator.java @@ -0,0 +1,116 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class BallSpinFadeLoaderIndicator extends BaseIndicatorController { + + public static final float SCALE=1.0f; + + public static final int ALPHA=255; + + float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE, + SCALE}; + + int[] alphas=new int[]{ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA, + ALPHA}; + + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + for (int i = 0; i < 8; i++) { + canvas.save(); + Point point=circleAt(getWidth(),getHeight(),getWidth()/2-radius,i*(Math.PI/4)); + canvas.translate(point.x,point.y); + canvas.scale(scaleFloats[i],scaleFloats[i]); + paint.setAlpha(alphas[i]); + canvas.drawCircle(0,0,radius,paint); + canvas.restore(); + } + } + + /** + * 圆O的圆心为(a,b),半径为R,点A与到X轴的为角α. + *则点A的坐标为(a+R*cosα,b+R*sinα) + * @param width + * @param height + * @param radius + * @param angle + * @return + */ + Point circleAt(int width,int height,float radius,double angle){ + float x= (float) (width/2+radius*(Math.cos(angle))); + float y= (float) (height/2+radius*(Math.sin(angle))); + return new Point(x,y); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + int[] delays= {0, 120, 240, 360, 480, 600, 720, 780, 840}; + for (int i = 0; i < 8; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.4f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255, 77, 255); + alphaAnim.setDuration(1000); + alphaAnim.setRepeatCount(-1); + alphaAnim.setStartDelay(delays[i]); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alphas[index] = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + animators.add(scaleAnim); + animators.add(alphaAnim); + } + return animators; + } + + final class Point{ + public float x; + public float y; + + public Point(float x, float y){ + this.x=x; + this.y=y; + } + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java new file mode 100644 index 0000000..7a66938 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallTrianglePathIndicator.java @@ -0,0 +1,82 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallTrianglePathIndicator extends BaseIndicatorController { + + float[] translateX=new float[3],translateY=new float[3]; + + @Override + public void draw(Canvas canvas, Paint paint) { + paint.setStrokeWidth(3); + paint.setStyle(Paint.Style.STROKE); + for (int i = 0; i < 3; i++) { + canvas.save(); + canvas.translate(translateX[i], translateY[i]); + canvas.drawCircle(0, 0, getWidth() / 10, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/5; + float startY=getWidth()/5; + for (int i = 0; i < 3; i++) { + final int index=i; + ValueAnimator translateXAnim=ValueAnimator.ofFloat(getWidth()/2,getWidth()-startX,startX,getWidth()/2); + if (i==1){ + translateXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,getWidth()/2,getWidth()-startX); + }else if (i==2){ + translateXAnim=ValueAnimator.ofFloat(startX,getWidth()/2,getWidth()-startX,startX); + } + ValueAnimator translateYAnim=ValueAnimator.ofFloat(startY,getHeight()-startY,getHeight()-startY,startY); + if (i==1){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,startY,getHeight()-startY); + }else if (i==2){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,startY,getHeight()-startY,getHeight()-startY); + } + + translateXAnim.setDuration(2000); + translateXAnim.setInterpolator(new LinearInterpolator()); + translateXAnim.setRepeatCount(-1); + translateXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateXAnim.start(); + + translateYAnim.setDuration(2000); + translateYAnim.setInterpolator(new LinearInterpolator()); + translateYAnim.setRepeatCount(-1); + translateYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateYAnim.start(); + + animators.add(translateXAnim); + animators.add(translateYAnim); + } + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java new file mode 100644 index 0000000..a7fe02a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagDeflectIndicator.java @@ -0,0 +1,65 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallZigZagDeflectIndicator extends BallZigZagIndicator { + + + + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/6; + float startY=getWidth()/6; + for (int i = 0; i < 2; i++) { + final int index=i; + ValueAnimator translateXAnim=ValueAnimator.ofFloat(startX,getWidth()-startX,startX,getWidth()-startX,startX); + if (i==1){ + translateXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,getWidth()-startX,startX,getWidth()-startX); + } + ValueAnimator translateYAnim=ValueAnimator.ofFloat(startY,startY,getHeight()-startY,getHeight()-startY,startY); + if (i==1){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,startY,startY,getHeight()-startY); + } + + translateXAnim.setDuration(2000); + translateXAnim.setInterpolator(new LinearInterpolator()); + translateXAnim.setRepeatCount(-1); + translateXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateXAnim.start(); + + translateYAnim.setDuration(2000); + translateYAnim.setInterpolator(new LinearInterpolator()); + translateYAnim.setRepeatCount(-1); + translateYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY [index]= (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateYAnim.start(); + + animators.add(translateXAnim); + animators.add(translateYAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java new file mode 100644 index 0000000..5b4f63d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BallZigZagIndicator.java @@ -0,0 +1,75 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class BallZigZagIndicator extends BaseIndicatorController { + + float[] translateX=new float[2],translateY=new float[2]; + + + @Override + public void draw(Canvas canvas, Paint paint) { + for (int i = 0; i < 2; i++) { + canvas.save(); + canvas.translate(translateX[i], translateY[i]); + canvas.drawCircle(0, 0, getWidth() / 10, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/6; + float startY=getWidth()/6; + for (int i = 0; i < 2; i++) { + final int index=i; + ValueAnimator translateXAnim=ValueAnimator.ofFloat(startX,getWidth()-startX,getWidth()/2,startX); + if (i==1){ + translateXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,getWidth()/2,getWidth()-startX); + } + ValueAnimator translateYAnim=ValueAnimator.ofFloat(startY,startY,getHeight()/2,startY); + if (i==1){ + translateYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,getHeight()/2,getHeight()-startY); + } + + translateXAnim.setDuration(1000); + translateXAnim.setInterpolator(new LinearInterpolator()); + translateXAnim.setRepeatCount(-1); + translateXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateXAnim.start(); + + translateYAnim.setDuration(1000); + translateYAnim.setInterpolator(new LinearInterpolator()); + translateYAnim.setRepeatCount(-1); + translateYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translateYAnim.start(); + animators.add(translateXAnim); + animators.add(translateYAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java new file mode 100644 index 0000000..93dd4db --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/BaseIndicatorController.java @@ -0,0 +1,100 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.view.View; + +import java.util.List; + +/** + * Created by Jack on 2015/10/15. + */ +public abstract class BaseIndicatorController { + + + private View mTarget; + + private List mAnimators; + + + public void setTarget(View target){ + this.mTarget=target; + } + + public View getTarget(){ + return mTarget; + } + + + public int getWidth(){ + return mTarget.getWidth(); + } + + public int getHeight(){ + return mTarget.getHeight(); + } + + public void postInvalidate(){ + mTarget.postInvalidate(); + } + + /** + * draw indicator + * @param canvas + * @param paint + */ + public abstract void draw(Canvas canvas,Paint paint); + + /** + * create animation or animations + */ + public abstract List createAnimation(); + + public void initAnimation(){ + mAnimators=createAnimation(); + } + + /** + * make animation to start or end when target + * view was be Visible or Gone or Invisible. + * make animation to cancel when target view + * be onDetachedFromWindow. + * @param animStatus + */ + public void setAnimationStatus(AnimStatus animStatus){ + if (mAnimators==null){ + return; + } + int count=mAnimators.size(); + for (int i = 0; i < count; i++) { + Animator animator=mAnimators.get(i); + boolean isRunning=animator.isRunning(); + switch (animStatus){ + case START: + if (!isRunning){ + animator.start(); + } + break; + case END: + if (isRunning){ + animator.end(); + } + break; + case CANCEL: + if (isRunning){ + animator.cancel(); + } + break; + } + } + } + + + public enum AnimStatus{ + START,END,CANCEL + } + + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java new file mode 100644 index 0000000..8a0543b --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/CubeTransitionIndicator.java @@ -0,0 +1,110 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/18. + */ +public class CubeTransitionIndicator extends BaseIndicatorController { + + float[] translateX=new float[2],translateY=new float[2]; + float degrees,scaleFloat=1.0f; + + @Override + public void draw(Canvas canvas, Paint paint) { + float rWidth=getWidth()/5; + float rHeight=getHeight()/5; + for (int i = 0; i < 2; i++) { + canvas.save(); + canvas.translate(translateX[i], translateY[i]); + canvas.rotate(degrees); + canvas.scale(scaleFloat,scaleFloat); + RectF rectF=new RectF(-rWidth/2,-rHeight/2,rWidth/2,rHeight/2); + canvas.drawRect(rectF,paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startX=getWidth()/5; + float startY=getHeight()/5; + for (int i = 0; i < 2; i++) { + final int index=i; + translateX[index]=startX; + ValueAnimator translationXAnim=ValueAnimator.ofFloat(startX,getWidth()-startX,getWidth()-startX, startX,startX); + if (i==1){ + translationXAnim=ValueAnimator.ofFloat(getWidth()-startX,startX,startX, getWidth()-startX,getWidth()-startX); + } + translationXAnim.setInterpolator(new LinearInterpolator()); + translationXAnim.setDuration(1600); + translationXAnim.setRepeatCount(-1); + translationXAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translationXAnim.start(); + translateY[index]=startY; + ValueAnimator translationYAnim=ValueAnimator.ofFloat(startY,startY,getHeight()-startY,getHeight()- startY,startY); + if (i==1){ + translationYAnim=ValueAnimator.ofFloat(getHeight()-startY,getHeight()-startY,startY,startY,getHeight()-startY); + } + translationYAnim.setDuration(1600); + translationYAnim.setInterpolator(new LinearInterpolator()); + translationYAnim.setRepeatCount(-1); + translationYAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateY[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translationYAnim.start(); + + animators.add(translationXAnim); + animators.add(translationYAnim); + } + + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.5f,1,0.5f,1); + scaleAnim.setDuration(1600); + scaleAnim.setInterpolator(new LinearInterpolator()); + scaleAnim.setRepeatCount(-1); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloat = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + + ValueAnimator rotateAnim=ValueAnimator.ofFloat(0,180,360,1.5f*360,2*360); + rotateAnim.setDuration(1600); + rotateAnim.setInterpolator(new LinearInterpolator()); + rotateAnim.setRepeatCount(-1); + rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim.start(); + + animators.add(scaleAnim); + animators.add(rotateAnim); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java new file mode 100644 index 0000000..828be12 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScaleIndicator.java @@ -0,0 +1,62 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScaleIndicator extends BaseIndicatorController { + + public static final float SCALE=1.0f; + + float[] scaleYFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE,}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float translateX=getWidth()/11; + float translateY=getHeight()/2; + for (int i = 0; i < 5; i++) { + canvas.save(); + canvas.translate((2 + i * 2) * translateX - translateX / 2, translateY); + canvas.scale(SCALE, scaleYFloats[i]); + RectF rectF=new RectF(-translateX/2,-getHeight()/2.5f,translateX/2,getHeight()/2.5f); + canvas.drawRoundRect(rectF, 5, 5, paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{100,200,300,400,500}; + for (int i = 0; i < 5; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1, 0.4f, 1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java new file mode 100644 index 0000000..6ce067a --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePartyIndicator.java @@ -0,0 +1,64 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScalePartyIndicator extends BaseIndicatorController { + + public static final float SCALE=1.0f; + + float[] scaleFloats=new float[]{SCALE, + SCALE, + SCALE, + SCALE, + SCALE,}; + + @Override + public void draw(Canvas canvas, Paint paint) { + float translateX=getWidth()/9; + float translateY=getHeight()/2; + for (int i = 0; i < 4; i++) { + canvas.save(); + canvas.translate((2 + i * 2) * translateX - translateX / 2, translateY); + canvas.scale(scaleFloats[i], scaleFloats[i]); + RectF rectF=new RectF(-translateX/2,-getHeight()/2.5f,translateX/2,getHeight()/2.5f); + canvas.drawRoundRect(rectF,5,5,paint); + canvas.restore(); + } + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] durations=new long[]{1260, 430, 1010, 730}; + long[] delays=new long[]{770, 290, 280, 740}; + for (int i = 0; i < 4; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.4f,1); + scaleAnim.setDuration(durations[i]); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java new file mode 100644 index 0000000..dacd45d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutIndicator.java @@ -0,0 +1,37 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScalePulseOutIndicator extends LineScaleIndicator { + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{500,250,0,250,500}; + for (int i = 0; i < 5; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.3f,1); + scaleAnim.setDuration(900); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java new file mode 100644 index 0000000..8f7facc --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineScalePulseOutRapidIndicator.java @@ -0,0 +1,37 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/19. + */ +public class LineScalePulseOutRapidIndicator extends LineScaleIndicator { + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + long[] delays=new long[]{400,200,0,200,400}; + for (int i = 0; i < 5; i++) { + final int index=i; + ValueAnimator scaleAnim=ValueAnimator.ofFloat(1,0.4f,1); + scaleAnim.setDuration(1000); + scaleAnim.setRepeatCount(-1); + scaleAnim.setStartDelay(delays[i]); + scaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + scaleYFloats[index] = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + scaleAnim.start(); + animators.add(scaleAnim); + } + return animators; + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java new file mode 100644 index 0000000..246c8ca --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/LineSpinFadeLoaderIndicator.java @@ -0,0 +1,30 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +/** + * Created by Jack on 2015/10/24. + * Email:81813780@qq.com + */ +public class LineSpinFadeLoaderIndicator extends BallSpinFadeLoaderIndicator { + + + @Override + public void draw(Canvas canvas, Paint paint) { + float radius=getWidth()/10; + for (int i = 0; i < 8; i++) { + canvas.save(); + Point point=circleAt(getWidth(),getHeight(),getWidth()/2.5f-radius,i*(Math.PI/4)); + canvas.translate(point.x, point.y); + canvas.scale(scaleFloats[i], scaleFloats[i]); + canvas.rotate(i*45); + paint.setAlpha(alphas[i]); + RectF rectF=new RectF(-radius,-radius/1.5f,1.5f*radius,radius/1.5f); + canvas.drawRoundRect(rectF,5,5,paint); + canvas.restore(); + } + } + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java new file mode 100644 index 0000000..2b161f2 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/PacmanIndicator.java @@ -0,0 +1,119 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class PacmanIndicator extends BaseIndicatorController{ + + private float translateX; + + private int alpha; + + private float degrees1,degrees2; + + @Override + public void draw(Canvas canvas, Paint paint) { + drawPacman(canvas,paint); + drawCircle(canvas,paint); + } + + private void drawPacman(Canvas canvas,Paint paint){ + float x=getWidth()/2; + float y=getHeight()/2; + + canvas.save(); + + canvas.translate(x, y); + canvas.rotate(degrees1); + paint.setAlpha(255); + RectF rectF1=new RectF(-x/1.7f,-y/1.7f,x/1.7f,y/1.7f); + canvas.drawArc(rectF1, 0, 270, true, paint); + + canvas.restore(); + + canvas.save(); + canvas.translate(x, y); + canvas.rotate(degrees2); + paint.setAlpha(255); + RectF rectF2=new RectF(-x/1.7f,-y/1.7f,x/1.7f,y/1.7f); + canvas.drawArc(rectF2,90,270,true,paint); + canvas.restore(); + } + + + private void drawCircle(Canvas canvas, Paint paint) { + float radius=getWidth()/11; + paint.setAlpha(alpha); + canvas.drawCircle(translateX, getHeight() / 2, radius, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + float startT=getWidth()/11; + ValueAnimator translationAnim=ValueAnimator.ofFloat(getWidth()-startT,getWidth()/2); + translationAnim.setDuration(650); + translationAnim.setInterpolator(new LinearInterpolator()); + translationAnim.setRepeatCount(-1); + translationAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translateX = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + translationAnim.start(); + + ValueAnimator alphaAnim=ValueAnimator.ofInt(255,122); + alphaAnim.setDuration(650); + alphaAnim.setRepeatCount(-1); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + alpha = (int) animation.getAnimatedValue(); + postInvalidate(); + } + }); + alphaAnim.start(); + + ValueAnimator rotateAnim1=ValueAnimator.ofFloat(0, 45, 0); + rotateAnim1.setDuration(650); + rotateAnim1.setRepeatCount(-1); + rotateAnim1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees1 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim1.start(); + + ValueAnimator rotateAnim2=ValueAnimator.ofFloat(0,-45,0); + rotateAnim2.setDuration(650); + rotateAnim2.setRepeatCount(-1); + rotateAnim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + degrees2 = (float) animation.getAnimatedValue(); + postInvalidate(); + } + }); + rotateAnim2.start(); + + animators.add(translationAnim); + animators.add(alphaAnim); + animators.add(rotateAnim1); + animators.add(rotateAnim2); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java new file mode 100644 index 0000000..8e6e613 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SemiCircleSpinIndicator.java @@ -0,0 +1,36 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class SemiCircleSpinIndicator extends BaseIndicatorController { + + + @Override + public void draw(Canvas canvas, Paint paint) { + RectF rectF=new RectF(0,0,getWidth(),getHeight()); + canvas.drawArc(rectF,-60,120,false,paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + ObjectAnimator rotateAnim=ObjectAnimator.ofFloat(getTarget(),"rotation",0,180,360); + rotateAnim.setDuration(600); + rotateAnim.setRepeatCount(-1); + rotateAnim.start(); + animators.add(rotateAnim); + return animators; + } + + +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java new file mode 100644 index 0000000..b7567f0 --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/SquareSpinIndicator.java @@ -0,0 +1,37 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/16. + */ +public class SquareSpinIndicator extends BaseIndicatorController { + + @Override + public void draw(Canvas canvas, Paint paint) { + canvas.drawRect(new RectF(getWidth()/5,getHeight()/5,getWidth()*4/5,getHeight()*4/5),paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + PropertyValuesHolder rotation5=PropertyValuesHolder.ofFloat("rotationX",0,180,180,0,0); + PropertyValuesHolder rotation6=PropertyValuesHolder.ofFloat("rotationY",0,0,180,180,0); + ObjectAnimator animator=ObjectAnimator.ofPropertyValuesHolder(getTarget(), rotation6,rotation5); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(-1); + animator.setDuration(2500); + animator.start(); + animators.add(animator); + return animators; + } +} diff --git a/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java new file mode 100644 index 0000000..3b3ff1d --- /dev/null +++ b/widget/src/main/java/com/hzecool/widget/xRecyclerView/progressindicator/indicator/TriangleSkewSpinIndicator.java @@ -0,0 +1,45 @@ +package com.hzecool.widget.xRecyclerView.progressindicator.indicator; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.view.animation.LinearInterpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by Jack on 2015/10/20. + */ +public class TriangleSkewSpinIndicator extends BaseIndicatorController { + + @Override + public void draw(Canvas canvas, Paint paint) { + Path path=new Path(); + path.moveTo(getWidth()/5,getHeight()*4/5); + path.lineTo(getWidth()*4/5, getHeight()*4/5); + path.lineTo(getWidth()/2,getHeight()/5); + path.close(); + canvas.drawPath(path, paint); + } + + @Override + public List createAnimation() { + List animators=new ArrayList<>(); + PropertyValuesHolder rotation5=PropertyValuesHolder.ofFloat("rotationX",0,180,180,0,0); + PropertyValuesHolder rotation6=PropertyValuesHolder.ofFloat("rotationY",0,0,180,180,0); + + ObjectAnimator animator=ObjectAnimator.ofPropertyValuesHolder(getTarget(), rotation6,rotation5); + animator.setInterpolator(new LinearInterpolator()); + animator.setRepeatCount(-1); + animator.setDuration(2500); + animator.start(); + + animators.add(animator); + return animators; + } + +} diff --git a/widget/src/main/res/anim/action_sheet_dialog_enter.xml b/widget/src/main/res/anim/action_sheet_dialog_enter.xml new file mode 100644 index 0000000..a3cb29a --- /dev/null +++ b/widget/src/main/res/anim/action_sheet_dialog_enter.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/action_sheet_dialog_exit.xml b/widget/src/main/res/anim/action_sheet_dialog_exit.xml new file mode 100644 index 0000000..c64b482 --- /dev/null +++ b/widget/src/main/res/anim/action_sheet_dialog_exit.xml @@ -0,0 +1,30 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/decelerate_cubic.xml b/widget/src/main/res/anim/decelerate_cubic.xml new file mode 100644 index 0000000..58432b6 --- /dev/null +++ b/widget/src/main/res/anim/decelerate_cubic.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/widget/src/main/res/anim/dialog_enter_anim.xml b/widget/src/main/res/anim/dialog_enter_anim.xml new file mode 100644 index 0000000..b97db24 --- /dev/null +++ b/widget/src/main/res/anim/dialog_enter_anim.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/dialog_exit_anim.xml b/widget/src/main/res/anim/dialog_exit_anim.xml new file mode 100644 index 0000000..fd09a5b --- /dev/null +++ b/widget/src/main/res/anim/dialog_exit_anim.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/hot_pop_enter.xml b/widget/src/main/res/anim/hot_pop_enter.xml new file mode 100644 index 0000000..9437732 --- /dev/null +++ b/widget/src/main/res/anim/hot_pop_enter.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/widget/src/main/res/anim/hot_pop_out.xml b/widget/src/main/res/anim/hot_pop_out.xml new file mode 100644 index 0000000..367bcfb --- /dev/null +++ b/widget/src/main/res/anim/hot_pop_out.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/popup_enter.xml b/widget/src/main/res/anim/popup_enter.xml new file mode 100644 index 0000000..130b9cb --- /dev/null +++ b/widget/src/main/res/anim/popup_enter.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/popup_exit.xml b/widget/src/main/res/anim/popup_exit.xml new file mode 100644 index 0000000..02e9649 --- /dev/null +++ b/widget/src/main/res/anim/popup_exit.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svfade_in_center.xml b/widget/src/main/res/anim/svfade_in_center.xml new file mode 100644 index 0000000..8b162db --- /dev/null +++ b/widget/src/main/res/anim/svfade_in_center.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svfade_out_center.xml b/widget/src/main/res/anim/svfade_out_center.xml new file mode 100644 index 0000000..bda8de2 --- /dev/null +++ b/widget/src/main/res/anim/svfade_out_center.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_in_bottom.xml b/widget/src/main/res/anim/svslide_in_bottom.xml new file mode 100644 index 0000000..2d923d8 --- /dev/null +++ b/widget/src/main/res/anim/svslide_in_bottom.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_in_top.xml b/widget/src/main/res/anim/svslide_in_top.xml new file mode 100644 index 0000000..18a6b56 --- /dev/null +++ b/widget/src/main/res/anim/svslide_in_top.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_out_bottom.xml b/widget/src/main/res/anim/svslide_out_bottom.xml new file mode 100644 index 0000000..c496975 --- /dev/null +++ b/widget/src/main/res/anim/svslide_out_bottom.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/anim/svslide_out_top.xml b/widget/src/main/res/anim/svslide_out_top.xml new file mode 100644 index 0000000..07d3830 --- /dev/null +++ b/widget/src/main/res/anim/svslide_out_top.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/color/mis_default_text_color.xml b/widget/src/main/res/color/mis_default_text_color.xml new file mode 100644 index 0000000..bdbd36c --- /dev/null +++ b/widget/src/main/res/color/mis_default_text_color.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/color/mis_folder_text_color.xml b/widget/src/main/res/color/mis_folder_text_color.xml new file mode 100644 index 0000000..0a07caf --- /dev/null +++ b/widget/src/main/res/color/mis_folder_text_color.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-hdpi/pickup.png b/widget/src/main/res/drawable-hdpi/pickup.png new file mode 100644 index 0000000..b15d66a Binary files /dev/null and b/widget/src/main/res/drawable-hdpi/pickup.png differ diff --git a/widget/src/main/res/drawable-hdpi/rgb.png b/widget/src/main/res/drawable-hdpi/rgb.png new file mode 100644 index 0000000..f70d9d9 Binary files /dev/null and b/widget/src/main/res/drawable-hdpi/rgb.png differ diff --git a/widget/src/main/res/drawable-xhdpi/divider_bg.9.png b/widget/src/main/res/drawable-xhdpi/divider_bg.9.png new file mode 100644 index 0000000..d43bd14 Binary files /dev/null and b/widget/src/main/res/drawable-xhdpi/divider_bg.9.png differ diff --git a/widget/src/main/res/drawable-xhdpi/down.png b/widget/src/main/res/drawable-xhdpi/down.png new file mode 100644 index 0000000..b24c7e8 Binary files /dev/null and b/widget/src/main/res/drawable-xhdpi/down.png differ diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selected.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selected.xml new file mode 100644 index 0000000..65d97c8 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selected.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml new file mode 100644 index 0000000..f91e7d7 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selected_dark.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector.xml new file mode 100644 index 0000000..ead9d21 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml new file mode 100644 index 0000000..564e141 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml new file mode 100644 index 0000000..732eea4 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml new file mode 100644 index 0000000..732eea4 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_selector_ripple_dark.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_btn_shape.xml b/widget/src/main/res/drawable-xhdpi/md_btn_shape.xml new file mode 100644 index 0000000..a0060c7 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_btn_shape.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_item_selected.xml b/widget/src/main/res/drawable-xhdpi/md_item_selected.xml new file mode 100644 index 0000000..10bf840 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_item_selected.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml b/widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml new file mode 100644 index 0000000..b925200 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_item_selected_dark.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_nav_back.xml b/widget/src/main/res/drawable-xhdpi/md_nav_back.xml new file mode 100644 index 0000000..a95494b --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_nav_back.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_selector.xml b/widget/src/main/res/drawable-xhdpi/md_selector.xml new file mode 100644 index 0000000..df3322b --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_selector.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_selector_dark.xml b/widget/src/main/res/drawable-xhdpi/md_selector_dark.xml new file mode 100644 index 0000000..8c599e9 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_selector_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/md_transparent.xml b/widget/src/main/res/drawable-xhdpi/md_transparent.xml new file mode 100644 index 0000000..261a644 --- /dev/null +++ b/widget/src/main/res/drawable-xhdpi/md_transparent.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable-xhdpi/up.png b/widget/src/main/res/drawable-xhdpi/up.png new file mode 100644 index 0000000..e11e1df Binary files /dev/null and b/widget/src/main/res/drawable-xhdpi/up.png differ diff --git a/widget/src/main/res/drawable/bg_overlay_gradient.xml b/widget/src/main/res/drawable/bg_overlay_gradient.xml new file mode 100644 index 0000000..8c4060f --- /dev/null +++ b/widget/src/main/res/drawable/bg_overlay_gradient.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/bg_square_voice.xml b/widget/src/main/res/drawable/bg_square_voice.xml new file mode 100644 index 0000000..d3e07e8 --- /dev/null +++ b/widget/src/main/res/drawable/bg_square_voice.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/widget/src/main/res/drawable/bg_svprogresshuddefault.xml b/widget/src/main/res/drawable/bg_svprogresshuddefault.xml new file mode 100644 index 0000000..2bcad62 --- /dev/null +++ b/widget/src/main/res/drawable/bg_svprogresshuddefault.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/color_cursor.xml b/widget/src/main/res/drawable/color_cursor.xml new file mode 100644 index 0000000..d7bb3b3 --- /dev/null +++ b/widget/src/main/res/drawable/color_cursor.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/mis_action_btn.xml b/widget/src/main/res/drawable/mis_action_btn.xml new file mode 100644 index 0000000..dcd2cbb --- /dev/null +++ b/widget/src/main/res/drawable/mis_action_btn.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/progressbar.xml b/widget/src/main/res/drawable/progressbar.xml new file mode 100644 index 0000000..da267ee --- /dev/null +++ b/widget/src/main/res/drawable/progressbar.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/progressloading.xml b/widget/src/main/res/drawable/progressloading.xml new file mode 100644 index 0000000..2bdaeb6 --- /dev/null +++ b/widget/src/main/res/drawable/progressloading.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/radios.xml b/widget/src/main/res/drawable/radios.xml new file mode 100644 index 0000000..5471d56 --- /dev/null +++ b/widget/src/main/res/drawable/radios.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/rectangle.xml b/widget/src/main/res/drawable/rectangle.xml new file mode 100644 index 0000000..9b39d49 --- /dev/null +++ b/widget/src/main/res/drawable/rectangle.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/rectangle_pop_bg.xml b/widget/src/main/res/drawable/rectangle_pop_bg.xml new file mode 100644 index 0000000..c25c9d7 --- /dev/null +++ b/widget/src/main/res/drawable/rectangle_pop_bg.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/selector_btn_back_gray.xml b/widget/src/main/res/drawable/selector_btn_back_gray.xml new file mode 100644 index 0000000..16f32be --- /dev/null +++ b/widget/src/main/res/drawable/selector_btn_back_gray.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/drawable/top_radio.xml b/widget/src/main/res/drawable/top_radio.xml new file mode 100644 index 0000000..6a883bf --- /dev/null +++ b/widget/src/main/res/drawable/top_radio.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml b/widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml new file mode 100644 index 0000000..aec2f16 --- /dev/null +++ b/widget/src/main/res/layout-ldrtl/md_listitem_multichoice.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml b/widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml new file mode 100644 index 0000000..270550d --- /dev/null +++ b/widget/src/main/res/layout-ldrtl/md_listitem_singlechoice.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/md_stub_progress.xml b/widget/src/main/res/layout-v14/md_stub_progress.xml new file mode 100644 index 0000000..a469811 --- /dev/null +++ b/widget/src/main/res/layout-v14/md_stub_progress.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml new file mode 100644 index 0000000..1e780c1 --- /dev/null +++ b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml new file mode 100644 index 0000000..a4ef9b2 --- /dev/null +++ b/widget/src/main/res/layout-v14/md_stub_progress_indeterminate_horizontal.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/widget/src/main/res/layout-v14/mis_fragment_multi_image.xml b/widget/src/main/res/layout-v14/mis_fragment_multi_image.xml new file mode 100644 index 0000000..0847d14 --- /dev/null +++ b/widget/src/main/res/layout-v14/mis_fragment_multi_image.xml @@ -0,0 +1,47 @@ + + + + + + + +