├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-mdpi │ │ │ │ └── action_bar.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── dimens.xml │ │ │ │ └── styles.xml │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ ├── menu │ │ │ │ └── menu_main.xml │ │ │ └── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── list_item.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── net │ │ │ └── kyu_mu │ │ │ └── pigeonholeview │ │ │ └── example │ │ │ ├── MyDataList.java │ │ │ ├── MyData.java │ │ │ ├── SelectActionDialogFragment.java │ │ │ └── MainActivity.java │ └── androidTest │ │ └── java │ │ └── net │ │ └── kyu_mu │ │ └── pigeonholeview │ │ └── example │ │ └── ApplicationTest.java ├── proguard-rules.pro ├── build.gradle └── app.iml ├── pigeonholeview ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable-hdpi │ │ │ │ ├── drop_area.9.png │ │ │ │ ├── placeholder.9.png │ │ │ │ ├── swap_candidate.9.png │ │ │ │ └── drop_area_highlight.9.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── drop_area.9.png │ │ │ │ ├── placeholder.9.png │ │ │ │ ├── swap_candidate.9.png │ │ │ │ └── drop_area_highlight.9.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── drop_area.9.png │ │ │ │ ├── placeholder.9.png │ │ │ │ ├── swap_candidate.9.png │ │ │ │ └── drop_area_highlight.9.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── drop_area.9.png │ │ │ │ ├── placeholder.9.png │ │ │ │ ├── swap_candidate.9.png │ │ │ │ └── drop_area_highlight.9.png │ │ │ ├── values-ja │ │ │ │ └── strings.xml │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ └── attrs.xml │ │ │ └── layout │ │ │ │ └── drop_area.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── net │ │ │ └── kyu_mu │ │ │ └── pigeonholeview │ │ │ └── PigeonholeView.java │ └── androidTest │ │ └── java │ │ └── net │ │ └── kyu_mu │ │ └── pigeonholeview │ │ └── ApplicationTest.java ├── proguard-rules.pro ├── build.gradle └── pigeonholeview.iml ├── settings.gradle ├── images ├── edit.png ├── normal.png └── parameters.png ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── vcs.xml ├── modules.xml ├── runConfigurations.xml ├── gradle.xml ├── misc.xml └── codeStyles │ └── Project.xml ├── gradle.properties ├── PigeonholeView.iml ├── gradlew.bat ├── gradlew ├── LICENSE └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /pigeonholeview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':pigeonholeview' 2 | -------------------------------------------------------------------------------- /images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/images/edit.png -------------------------------------------------------------------------------- /images/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/images/normal.png -------------------------------------------------------------------------------- /images/parameters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/images/parameters.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/action_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/app/src/main/res/drawable-mdpi/action_bar.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PigeonholeView 3 | 4 | Add 5 | 6 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-hdpi/drop_area.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-hdpi/drop_area.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-hdpi/placeholder.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-hdpi/placeholder.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-mdpi/drop_area.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-mdpi/drop_area.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-mdpi/placeholder.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-mdpi/placeholder.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xhdpi/drop_area.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xhdpi/drop_area.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xxhdpi/drop_area.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xxhdpi/drop_area.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xhdpi/placeholder.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xhdpi/placeholder.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xxhdpi/placeholder.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xxhdpi/placeholder.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ここにドロップして編集 4 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-hdpi/swap_candidate.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-hdpi/swap_candidate.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-mdpi/swap_candidate.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-mdpi/swap_candidate.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xhdpi/swap_candidate.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xhdpi/swap_candidate.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xxhdpi/swap_candidate.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xxhdpi/swap_candidate.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-hdpi/drop_area_highlight.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-hdpi/drop_area_highlight.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-mdpi/drop_area_highlight.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-mdpi/drop_area_highlight.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xhdpi/drop_area_highlight.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xhdpi/drop_area_highlight.9.png -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/drawable-xxhdpi/drop_area_highlight.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iizukanao/PigeonholeView/HEAD/pigeonholeview/src/main/res/drawable-xxhdpi/drop_area_highlight.9.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PigeonholeView 3 | Drop here to edit 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Sep 22 07:36:30 JST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /pigeonholeview/src/androidTest/java/net/kyu_mu/pigeonholeview/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/net/kyu_mu/pigeonholeview/example/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview.example; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/res/layout/drop_area.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/android-sdk-mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /pigeonholeview/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/android-sdk-mac_x86/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion "28.0.2" 6 | 7 | defaultConfig { 8 | applicationId "net.kyu_mu.pigeonholeview.example" 9 | minSdkVersion 14 10 | targetSdkVersion 27 11 | versionCode 1 12 | versionName "1.0.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | implementation "com.android.support:appcompat-v7:27.1.1" 25 | implementation project(':pigeonholeview') 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /PigeonholeView.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/net/kyu_mu/pigeonholeview/example/MyDataList.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview.example; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.util.ArrayList; 7 | 8 | /** 9 | * Created by nao on 3/6/15. 10 | */ 11 | public class MyDataList extends ArrayList implements Parcelable { 12 | public MyDataList() { 13 | super(); 14 | } 15 | 16 | @Override 17 | public int describeContents() { 18 | return 0; 19 | } 20 | 21 | @Override 22 | public void writeToParcel(Parcel dest, int flags) { 23 | dest.writeList(this); 24 | } 25 | 26 | public static final Creator CREATOR = new Creator() { 27 | @Override 28 | public MyDataList createFromParcel(Parcel src) { 29 | return new MyDataList(src); 30 | } 31 | 32 | @Override 33 | public MyDataList[] newArray(int size) { 34 | return new MyDataList[size]; 35 | } 36 | }; 37 | 38 | private MyDataList(Parcel src) { 39 | src.readList(this, null); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/net/kyu_mu/pigeonholeview/example/MyData.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview.example; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | /** 7 | * Created by nao on 3/6/15. 8 | */ 9 | public class MyData implements Parcelable { 10 | private String name; 11 | 12 | // NOTE: In general, you should not save resource id to storage 13 | // since its value may change upon each compilation. Instead, 14 | // use context.getResources().getResourceName(resourceId) 15 | private int imageResourceId; 16 | 17 | private int viewPosition; 18 | 19 | public MyData(String name, int imageResourceId, int viewPosition) { 20 | this.name = name; 21 | this.imageResourceId = imageResourceId; 22 | this.viewPosition = viewPosition; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public void setName(String name) { 30 | this.name = name; 31 | } 32 | 33 | public int getImageResourceId() { 34 | return imageResourceId; 35 | } 36 | 37 | public void setImageResourceId(int imageResourceId) { 38 | this.imageResourceId = imageResourceId; 39 | } 40 | 41 | public int getViewPosition() { 42 | return viewPosition; 43 | } 44 | 45 | public void setViewPosition(int viewPosition) { 46 | this.viewPosition = viewPosition; 47 | } 48 | 49 | // Methods for Parcelable 50 | 51 | @Override 52 | public int describeContents() { 53 | return 0; 54 | } 55 | 56 | @Override 57 | public void writeToParcel(Parcel dest, int flags) { 58 | dest.writeString(name); 59 | dest.writeInt(imageResourceId); 60 | dest.writeInt(viewPosition); 61 | } 62 | 63 | public static final Creator CREATOR = new Creator() { 64 | @Override 65 | public MyData createFromParcel(Parcel src) { 66 | return new MyData(src); 67 | } 68 | 69 | @Override 70 | public MyData[] newArray(int size) { 71 | return new MyData[size]; 72 | } 73 | }; 74 | 75 | public MyData(Parcel src) { 76 | name = src.readString(); 77 | imageResourceId = src.readInt(); 78 | viewPosition = src.readInt(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /pigeonholeview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'com.jfrog.bintray' 4 | 5 | version = "1.0.4" 6 | 7 | android { 8 | compileSdkVersion 27 9 | buildToolsVersion "28.0.2" 10 | 11 | defaultConfig { 12 | minSdkVersion 4 13 | targetSdkVersion 27 14 | versionCode 1 15 | versionName version 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | def siteUrl = 'https://github.com/iizukanao/PigeonholeView' 26 | def gitUrl = 'https://github.com/iizukanao/PigeonholeView.git' 27 | def projectName = 'PigeonholeView' 28 | def projectDescription = 'Grid-based reorderable view like Android home screen' 29 | group = "net.kyu_mu" 30 | 31 | install { 32 | repositories.mavenInstaller { 33 | pom { 34 | project { 35 | packaging 'aar' 36 | 37 | name projectName 38 | description projectDescription 39 | url siteUrl 40 | 41 | licenses { 42 | license { 43 | name 'The Apache Software License, Version 2.0' 44 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 45 | } 46 | } 47 | developers { 48 | developer { 49 | id 'iizukanao' 50 | name 'Nao Iizuka' 51 | email 'iizuka@kyu-mu.net' 52 | } 53 | } 54 | scm { 55 | connection gitUrl 56 | developerConnection gitUrl 57 | url siteUrl 58 | 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | dependencies { 66 | implementation fileTree(dir: 'libs', include: ['*.jar']) 67 | implementation "com.android.support:support-v4:27.1.1" 68 | } 69 | 70 | task sourcesJar(type: Jar) { 71 | from android.sourceSets.main.java.srcDirs 72 | classifier = 'sources' 73 | } 74 | 75 | task javadoc(type: Javadoc) { 76 | source = android.sourceSets.main.java.srcDirs 77 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 78 | } 79 | 80 | task javadocJar(type: Jar, dependsOn: javadoc) { 81 | classifier = 'javadoc' 82 | from javadoc.destinationDir 83 | } 84 | artifacts { 85 | archives javadocJar 86 | archives sourcesJar 87 | } 88 | 89 | Properties properties = new Properties() 90 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 91 | 92 | bintray { 93 | user = properties.getProperty("bintray.user") 94 | key = properties.getProperty("bintray.apikey") 95 | 96 | configurations = ['archives'] 97 | pkg { 98 | repo = "maven" 99 | name = projectName 100 | desc = projectDescription 101 | websiteUrl = siteUrl 102 | vcsUrl = gitUrl 103 | licenses = ["Apache-2.0"] 104 | publish = true 105 | version { 106 | gpg { 107 | sign = true 108 | passphrase = properties.getProperty("bintray.gpgPassphrase") 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/net/kyu_mu/pigeonholeview/example/SelectActionDialogFragment.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview.example; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.os.Bundle; 9 | import android.support.annotation.NonNull; 10 | import android.support.v4.app.DialogFragment; 11 | 12 | /** 13 | * Created by nao on 3/6/15. 14 | */ 15 | public class SelectActionDialogFragment extends DialogFragment { 16 | public static final String TAG = SelectActionDialogFragment.class.getSimpleName(); 17 | 18 | private boolean isFiredEvent; 19 | 20 | public interface SelectActionDialogFragmentListener { 21 | public void onChooseEdit(); 22 | public void onChooseDelete(); 23 | public void onChooseCancel(); 24 | } 25 | 26 | private SelectActionDialogFragmentListener SelectActionDialogFragmentListener; 27 | 28 | @Override 29 | public void onAttach(Context context) { 30 | super.onAttach(context); 31 | 32 | isFiredEvent = false; 33 | 34 | if (context instanceof Activity) { 35 | Activity activity = (Activity) context; 36 | // Verify that the host activity implements the callback interface 37 | try { 38 | SelectActionDialogFragmentListener = (SelectActionDialogFragmentListener) activity; 39 | } catch (ClassCastException e) { 40 | // The activity doesn't implement the interface, throw exception 41 | throw new ClassCastException(activity.toString() 42 | + " must implement SelectActionDialogFragmentListener"); 43 | } 44 | } 45 | } 46 | 47 | @NonNull 48 | @Override 49 | public Dialog onCreateDialog(Bundle savedInstanceState) { 50 | // Use the Builder class for convenient dialog construction 51 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 52 | 53 | final CharSequence[] items = { 54 | "Edit", 55 | "Delete", 56 | "Cancel", 57 | }; 58 | 59 | builder.setTitle("Example actions"); 60 | builder.setItems(items, new DialogInterface.OnClickListener() { 61 | @Override 62 | public void onClick(DialogInterface dialog, int item) { 63 | switch (item) { 64 | case 0: { // Edit 65 | dialog.dismiss(); 66 | isFiredEvent = true; 67 | if (SelectActionDialogFragmentListener != null) { 68 | SelectActionDialogFragmentListener.onChooseEdit(); 69 | } 70 | break; 71 | } 72 | case 1: { // Delete 73 | dialog.dismiss(); 74 | isFiredEvent = true; 75 | if (SelectActionDialogFragmentListener != null) { 76 | SelectActionDialogFragmentListener.onChooseDelete(); 77 | } 78 | break; 79 | } 80 | case 2: { // Cancel 81 | dialog.dismiss(); 82 | isFiredEvent = true; 83 | if (SelectActionDialogFragmentListener != null) { 84 | SelectActionDialogFragmentListener.onChooseCancel(); 85 | } 86 | break; 87 | } 88 | } 89 | } 90 | }); 91 | 92 | return builder.create(); 93 | } 94 | 95 | @Override 96 | public void onDismiss(DialogInterface dialog) { 97 | super.onDismiss(dialog); 98 | if (!isFiredEvent && SelectActionDialogFragmentListener != null) { 99 | SelectActionDialogFragmentListener.onChooseCancel(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/src/main/java/net/kyu_mu/pigeonholeview/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview.example; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.ActionBar; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.LayoutInflater; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | import android.widget.Toast; 13 | 14 | import net.kyu_mu.pigeonholeview.PigeonholeView; 15 | 16 | import java.util.Iterator; 17 | 18 | 19 | public class MainActivity extends AppCompatActivity implements PigeonholeView.PigeonholeViewListener, SelectActionDialogFragment.SelectActionDialogFragmentListener { 20 | public static final String TAG = MainActivity.class.getSimpleName(); 21 | 22 | private PigeonholeView pigeonholeView; 23 | private MyDataList myDataList; 24 | private MyData editingMyData; 25 | private boolean isEditable; 26 | 27 | /** 28 | * Save UI state 29 | * 30 | * @param outState 31 | */ 32 | @Override 33 | protected void onSaveInstanceState(Bundle outState) { 34 | outState.putParcelable("editingMyData", editingMyData); 35 | outState.putParcelable("myDataList", myDataList); 36 | outState.putBoolean("isEditable", isEditable); 37 | super.onSaveInstanceState(outState); 38 | } 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | setContentView(R.layout.activity_main); 44 | 45 | if (savedInstanceState != null) { 46 | editingMyData = savedInstanceState.getParcelable("editingMyData"); 47 | myDataList = savedInstanceState.getParcelable("myDataList"); 48 | isEditable = savedInstanceState.getBoolean("isEditable"); 49 | } else { 50 | myDataList = new MyDataList(); 51 | myDataList.add(new MyData("Button 1", android.R.drawable.ic_menu_myplaces, 0)); 52 | myDataList.add(new MyData("Button 2", android.R.drawable.ic_menu_rotate, 1)); 53 | myDataList.add(new MyData("Button 3", android.R.drawable.ic_menu_mapmode, 2)); 54 | isEditable = true; 55 | } 56 | 57 | pigeonholeView = findViewById(R.id.example_pigeonhole_view); 58 | 59 | PigeonholeView.DataProvider provider = new PigeonholeView.DataProvider() { 60 | @Override 61 | public int getViewPosition(MyData item) { 62 | return item.getViewPosition(); 63 | } 64 | 65 | @Override 66 | public void setViewPosition(MyData item, int viewPosition) { 67 | item.setViewPosition(viewPosition); 68 | } 69 | 70 | @Override 71 | public View getView(View existingView, MyData item) { 72 | if (existingView == null) { 73 | LayoutInflater inflater = LayoutInflater.from(MainActivity.this); 74 | existingView = inflater.inflate(R.layout.list_item, pigeonholeView, false); 75 | } 76 | ImageView imageView = (ImageView) existingView.findViewById(R.id.item_image); 77 | imageView.setImageResource(item.getImageResourceId()); 78 | TextView nameTextView = (TextView) existingView.findViewById(R.id.item_name); 79 | nameTextView.setText(item.getName()); 80 | return existingView; 81 | } 82 | 83 | @Override 84 | public Iterator iterator() { 85 | return myDataList.iterator(); 86 | } 87 | }; 88 | pigeonholeView.setDataProvider(provider); 89 | 90 | pigeonholeView.setListener(this); 91 | pigeonholeView.setOnCellClickListener(new PigeonholeView.OnCellClickListener() { 92 | @Override 93 | public void onClick(PigeonholeView.CellData cellData) { 94 | MyData myData = cellData.getObject(); 95 | Toast.makeText(MainActivity.this, myData.getName() + " is clicked", Toast.LENGTH_SHORT).show(); 96 | } 97 | }); 98 | 99 | updateEditMode(); 100 | } 101 | 102 | private void updateEditMode() { 103 | pigeonholeView.setEditable(isEditable); 104 | } 105 | 106 | @Override 107 | public boolean onCreateOptionsMenu(Menu menu) { 108 | // Inflate the menu; this adds items to the action bar if it is present. 109 | getMenuInflater().inflate(R.menu.menu_main, menu); 110 | menu.findItem(R.id.action_toggle_edit).setChecked(isEditable); 111 | return true; 112 | } 113 | 114 | @Override 115 | public boolean onOptionsItemSelected(MenuItem item) { 116 | // Handle action bar item clicks here. The action bar will 117 | // automatically handle clicks on the Home/Up button, so long 118 | // as you specify a parent activity in AndroidManifest.xml. 119 | int id = item.getItemId(); 120 | 121 | //noinspection SimplifiableIfStatement 122 | if (id == R.id.action_add) { // Add a new button to PigeonholeView 123 | if (pigeonholeView.isFull()) { 124 | Toast.makeText(this, "Can't add more buttons", Toast.LENGTH_SHORT).show(); 125 | } else { 126 | MyData newData = new MyData("New Button", android.R.drawable.ic_menu_zoom, 0); 127 | myDataList.add(newData); 128 | pigeonholeView.addObject(newData); 129 | } 130 | return true; 131 | } else if (id == R.id.action_toggle_edit) { // Toggle edit mode 132 | isEditable = !item.isChecked(); 133 | item.setChecked(isEditable); 134 | updateEditMode(); 135 | return true; 136 | } 137 | 138 | return super.onOptionsItemSelected(item); 139 | } 140 | 141 | private void hideActionBar() { 142 | ActionBar actionBar = getSupportActionBar(); 143 | if (actionBar != null) { 144 | actionBar.hide(); 145 | } 146 | } 147 | 148 | private void showActionBar() { 149 | ActionBar actionBar = getSupportActionBar(); 150 | if (actionBar != null) { 151 | actionBar.show(); 152 | } 153 | } 154 | 155 | /** 156 | * Called when the dragging has started. 157 | */ 158 | @Override 159 | public void onDragStart() { 160 | hideActionBar(); 161 | } 162 | 163 | /** 164 | * Called when the dragging has ended. 165 | */ 166 | @Override 167 | public void onDragEnd() { 168 | showActionBar(); 169 | } 170 | 171 | /** 172 | * Called when the user dropped a button to the drop area. 173 | * 174 | * @param myData 175 | */ 176 | @Override 177 | public void onEditObject(MyData myData) { 178 | editingMyData = myData; 179 | 180 | // In general, you might want to start an activity here. 181 | SelectActionDialogFragment dialog = new SelectActionDialogFragment(); 182 | dialog.show(getSupportFragmentManager(), "SelectActionDialogFragment"); 183 | } 184 | 185 | /** 186 | * Called when reordering has happened in the PigeonholeView. 187 | * You should save new view positions to a storage here. 188 | */ 189 | @Override 190 | public void onReorder() { 191 | // TODO: Save myDataList to storage. 192 | } 193 | 194 | /** 195 | * Called when the user clicked "Edit" in the dialog. 196 | */ 197 | @Override 198 | public void onChooseEdit() { 199 | if (editingMyData != null) { 200 | editingMyData.setName(""); 201 | editingMyData.setImageResourceId(android.R.drawable.ic_menu_edit); 202 | pigeonholeView.updateEditingObject(); 203 | } 204 | } 205 | 206 | /** 207 | * Called when the user clicked "Delete" in the dialog. 208 | */ 209 | @Override 210 | public void onChooseDelete() { 211 | if (editingMyData != null) { 212 | myDataList.remove(editingMyData); 213 | pigeonholeView.deleteEditingObject(); 214 | } 215 | } 216 | 217 | /** 218 | * Called when the user clicked "Cancel" in the dialog. 219 | */ 220 | @Override 221 | public void onChooseCancel() { 222 | pigeonholeView.cancelEdit(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /pigeonholeview/pigeonholeview.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Grid-based reorderable view like Android home screen 2 | 3 | ![Screenshot](images/normal.png) 4 | ![Edit mode](images/edit.png) 5 | 6 | ## Download 7 | 8 | If you are using Android Studio, add the following line to the `dependencies` section in your module-level build.gradle. 9 | 10 | implementation 'net.kyu_mu:pigeonholeview:1.0.4' 11 | 12 | ## Usage 13 | 14 | An example app is available at [app](app) directory. To run the example app, open this top-level directory in Android Studio and press the "Run" button. 15 | 16 | The instructions below are a complete guide to integrate PigeonholeView in your app. 17 | 18 | First, define a class that holds the data for a single cell. Include an `int` instance variable which stores a view position in PigeonholeView. 19 | 20 | MyData.java: 21 | 22 | /** 23 | * Data for a single cell. 24 | */ 25 | public class MyData { 26 | private String name; 27 | private int imageResourceId; 28 | private int viewPosition; // view position in PigeonholeView 29 | 30 | public MyData(String name, int imageResourceId, int viewPosition) { 31 | this.name = name; 32 | this.imageResourceId = imageResourceId; 33 | this.viewPosition = viewPosition; 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | 40 | public void setName(String name) { 41 | this.name = name; 42 | } 43 | 44 | public int getImageResourceId() { 45 | return imageResourceId; 46 | } 47 | 48 | public void setImageResourceId(int imageResourceId) { 49 | this.imageResourceId = imageResourceId; 50 | } 51 | 52 | public int getViewPosition() { 53 | return viewPosition; 54 | } 55 | 56 | public void setViewPosition(int viewPosition) { 57 | this.viewPosition = viewPosition; 58 | } 59 | } 60 | 61 | Then, create an Activity that contains PigeonholeView. In the example below, all parameters prefixed with `custom:` are optional. In order to use `custom:` attributes, you have to add `xmlns:custom="http://schemas.android.com/apk/res-auto"` to the root element. For details of the attributes, see [XML Attributes](#xml-attributes). 62 | 63 | activity_main.xml: 64 | 65 | 71 | 72 | 83 | 84 | 85 | 86 | Include v7 appcompat Android Support Library in module-level build.gradle. 87 | 88 | dependencies { 89 | compile 'net.kyu_mu:pigeonholeview:1.0.4' 90 | compile "com.android.support:appcompat-v7:27.1.1" 91 | } 92 | 93 | Edit values/styles.xml and add a custom style (CustomActionBarTheme) that enables ActionBar overlay: 94 | 95 | 96 | 97 | 102 | 103 | 104 | 105 | Edit AndroidManifest.xml and apply CustomActionBarTheme to the Activity using `android:theme` attribute. 106 | 107 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Set up the view in the code. 119 | 120 | MainActivity.java: 121 | 122 | public class MainActivity extends AppCompatActivity implements 123 | PigeonholeView.PigeonholeViewListener { 124 | 125 | private PigeonholeView pigeonholeView; 126 | private ArrayList myDataList; 127 | 128 | @Override 129 | protected void onCreate(Bundle savedInstanceState) { 130 | super.onCreate(savedInstanceState); 131 | setContentView(R.layout.activity_main); 132 | 133 | // Test data 134 | myDataList = new ArrayList<>(); 135 | myDataList.add(new MyData("Button 1", android.R.drawable.ic_menu_myplaces, 0)); 136 | myDataList.add(new MyData("Button 2", android.R.drawable.ic_menu_rotate, 1)); 137 | myDataList.add(new MyData("Button 3", android.R.drawable.ic_menu_mapmode, 2)); 138 | 139 | pigeonholeView = (PigeonholeView) findViewById(R.id.my_pigeonhole_view); 140 | 141 | // This is the bridge between PigeonholeView and your data 142 | pigeonholeView.setDataProvider(new PigeonholeView.DataProvider() { 143 | /** 144 | * Return the view position for the item. 145 | */ 146 | @Override 147 | public int getViewPosition(MyData item) { 148 | return item.getViewPosition(); 149 | } 150 | 151 | /** 152 | * Store the view position for the item. 153 | */ 154 | @Override 155 | public void setViewPosition(MyData item, int viewPosition) { 156 | item.setViewPosition(viewPosition); 157 | 158 | // You don't need to persist the view positions here. Instead, 159 | // save them in onReorder() to minimize the overhead. 160 | } 161 | 162 | /** 163 | * Return the view for the item. 164 | */ 165 | @Override 166 | public View getView(View cellView, MyData item) { 167 | // Reuse the existing view if cellView is not null. 168 | // Otherwise, instantiate a view. 169 | if (cellView == null) { 170 | LayoutInflater inflater = LayoutInflater.from(MainActivity.this); 171 | cellView = inflater.inflate(R.layout.list_item, pigeonholeView, false); 172 | } 173 | 174 | // Update the image 175 | ImageView imageView = (ImageView) cellView.findViewById(R.id.item_image); 176 | imageView.setImageResource(item.getImageResourceId()); 177 | 178 | // Update the name 179 | TextView nameTextView = (TextView) cellView.findViewById(R.id.item_name); 180 | nameTextView.setText(item.getName()); 181 | 182 | return cellView; 183 | } 184 | 185 | /** 186 | * Return the iterator for the list of cells. 187 | */ 188 | @Override 189 | public Iterator iterator() { 190 | return myDataList.iterator(); 191 | } 192 | }); 193 | 194 | pigeonholeView.setOnCellClickListener(new PigeonholeView.OnCellClickListener() { 195 | @Override 196 | public void onClick(PigeonholeView.CellData cellData) { 197 | // Retrieve the target data 198 | MyData myData = cellData.getObject(); 199 | 200 | Toast.makeText(MainActivity.this, myData.getName() + " is clicked", Toast.LENGTH_SHORT).show(); 201 | } 202 | }); 203 | 204 | // Receive events from PigeonholeView 205 | pigeonholeView.setListener(this); 206 | } 207 | 208 | private void hideActionBar() { 209 | ActionBar actionBar = getSupportActionBar(); 210 | if (actionBar != null) { 211 | actionBar.hide(); 212 | } 213 | } 214 | 215 | private void showActionBar() { 216 | ActionBar actionBar = getSupportActionBar(); 217 | if (actionBar != null) { 218 | actionBar.show(); 219 | } 220 | } 221 | 222 | /** 223 | * Called when the dragging has started. 224 | */ 225 | @Override 226 | public void onDragStart() { 227 | hideActionBar(); 228 | } 229 | 230 | /** 231 | * Called when the dragging has ended. 232 | */ 233 | @Override 234 | public void onDragEnd() { 235 | showActionBar(); 236 | } 237 | 238 | /** 239 | * Called when the user dropped a button to the drop area. 240 | */ 241 | @Override 242 | public void onEditObject(MyData myData) { 243 | // To update the cell data: 244 | myData.setName("Updated"); 245 | myData.setImageResourceId(android.R.drawable.ic_menu_edit); 246 | pigeonholeView.updateEditingObject(); 247 | 248 | // // To delete the cell: 249 | // myDataList.remove(myData); 250 | // pigeonholeView.deleteEditingObject(); 251 | 252 | // // To cancel edit: 253 | // pigeonholeView.cancelEdit(); 254 | } 255 | 256 | /** 257 | * Called when reordering has happened in the PigeonholeView. 258 | */ 259 | @Override 260 | public void onReorder() { 261 | // You should persist the view positions here. 262 | } 263 | } 264 | 265 | Put [list_item.xml](app/src/main/res/layout/list_item.xml) in res/layout. This is the layout for a single cell. 266 | 267 | Now you can run your app and see PigeonholeView in action. 268 | 269 | ### Adding a button dynamically 270 | 271 | If you call `pigeonholeView.addObject()` with a MyData instance, smallest-avialable view position will be assigned to it. 272 | 273 | MyData newData = new MyData("New Button", android.R.drawable.ic_menu_zoom, 0); 274 | myDataList.add(newData); 275 | pigeonholeView.addObject(newData); 276 | 277 | To check whether the PigeonholeView is full: 278 | 279 | if (pigeonholeView.isFull()) { 280 | Toast.makeText(this, "Can't add more buttons", Toast.LENGTH_SHORT).show(); 281 | } else { 282 | // Add a button 283 | } 284 | 285 | ### Changing edit mode 286 | 287 | To disable edit mode dynamically: 288 | 289 | pigeonholeView.setEditable(false); 290 | 291 | To enable edit mode dynamically: 292 | 293 | pigeonholeView.setEditable(true); 294 | 295 | To disable edit mode in xml, add `custom:editable="false"` attribute to PigeonholeView element. Edit mode is enabled by default. 296 | 297 | ### XML attributes 298 | 299 | | Attribute | Description | Type (default value) | 300 | | ----------------------------------- | ----------- | -------------------- | 301 | | `custom:editable` | If true, long click is enabled | boolean (true) | 302 | | `custom:cellWidth` | Width of a cell | dimension (80px) | 303 | | `custom:cellHeight` | Height of a cell | dimension (90px) | 304 | | `custom:topSpaceHeight` | (See the image below) | dimension (0px) | 305 | | `custom:dropAreaTopPadding` | (See the image below) | dimension (20px) | 306 | | `custom:dropAreaText` | Text for drop area | string (@string/drop_area__text) | 307 | | `custom:dragStartAnimationDuration` | Duration of drag start animation in milliseconds | integer (200) | 308 | 309 | ![View parameters](images/parameters.png) 310 | 311 | ## License 312 | 313 | Copyright 2015 Nao Iizuka 314 | 315 | Licensed under the Apache License, Version 2.0 (the "License"); 316 | you may not use this file except in compliance with the License. 317 | You may obtain a copy of the License at 318 | 319 | http://www.apache.org/licenses/LICENSE-2.0 320 | 321 | Unless required by applicable law or agreed to in writing, software 322 | distributed under the License is distributed on an "AS IS" BASIS, 323 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 324 | See the License for the specific language governing permissions and 325 | limitations under the License. 326 | -------------------------------------------------------------------------------- /pigeonholeview/src/main/java/net/kyu_mu/pigeonholeview/PigeonholeView.java: -------------------------------------------------------------------------------- 1 | package net.kyu_mu.pigeonholeview; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorSet; 5 | import android.animation.ObjectAnimator; 6 | import android.content.Context; 7 | import android.content.res.TypedArray; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.Point; 11 | import android.os.Build; 12 | import android.os.Bundle; 13 | import android.os.Parcelable; 14 | import android.support.annotation.NonNull; 15 | import android.support.v4.view.MotionEventCompat; 16 | import android.util.AttributeSet; 17 | import android.util.Log; 18 | import android.util.SparseArray; 19 | import android.view.LayoutInflater; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.widget.ImageView; 24 | import android.widget.TextView; 25 | 26 | import java.util.Iterator; 27 | 28 | /** 29 | * Grid-based reorderable view like Android home screen. 30 | * 31 | * Copyright (C) 2015 Nao Iizuka 32 | */ 33 | public class PigeonholeView extends ViewGroup { 34 | public static final String TAG = PigeonholeView.class.getSimpleName(); 35 | 36 | public static final int POSITION_INVALID = -1; 37 | public static final int POSITION_DROP_AREA = -2; 38 | 39 | private Context context; 40 | private int numColumns; 41 | private int numRows; 42 | private long dragStartAnimationDuration; 43 | private float topSpaceHeight; // usually this is equal to actionBarSize 44 | private float editDropAreaTopPadding; 45 | private String editDropAreaText; 46 | private float editDropAreaBottomPadding = 0; 47 | private boolean editable; 48 | private float cellWidth; 49 | private float cellHeight; 50 | private float widthPerCell; 51 | private float heightPerCell; 52 | private CellData hoverCellData; 53 | private ImageView dropTargetView; 54 | private ImageView swapTargetView; 55 | private Paint textPaint; 56 | private SparseArray> cellMap; 57 | private int activePointerId = -1; // MotionEvent.INVALID_POINTER_ID; 58 | private float lastTouchX; 59 | private float lastTouchY; 60 | private float posX; 61 | private float posY; 62 | private boolean isDragging = false; 63 | private PigeonholeViewListener listener; 64 | private int editingPosition = POSITION_INVALID; 65 | private CellData swapCandidateCellData; 66 | private int currentHoverPosition; 67 | private float paddingLeft; 68 | private float paddingTop; 69 | private float paddingRight; 70 | private float paddingBottom; 71 | private OnCellClickListener onCellClickListener; 72 | private View editDropAreaView; 73 | private DataProvider provider; 74 | 75 | public interface DataProvider { 76 | public int getViewPosition(T item); 77 | 78 | public void setViewPosition(T item, int viewPosition); 79 | 80 | public View getView(View existingView, T item); 81 | 82 | public Iterator iterator(); 83 | } 84 | 85 | public interface OnCellClickListener { 86 | void onClick(CellData cellData); 87 | } 88 | 89 | public interface PigeonholeViewListener { 90 | void onDragStart(); 91 | 92 | void onDragEnd(); 93 | 94 | void onEditObject(T object); 95 | 96 | void onReorder(); 97 | } 98 | 99 | public static class CellData { 100 | private View view; 101 | private int position; 102 | private T object; 103 | 104 | public CellData(View view, int position, T object) { 105 | this.view = view; 106 | this.position = position; 107 | this.object = object; 108 | } 109 | 110 | public CellData() { 111 | } 112 | 113 | public View getView() { 114 | return view; 115 | } 116 | 117 | public void setView(View view) { 118 | this.view = view; 119 | } 120 | 121 | public int getPosition() { 122 | return position; 123 | } 124 | 125 | public void setPosition(int position) { 126 | this.position = position; 127 | } 128 | 129 | public T getObject() { 130 | return object; 131 | } 132 | 133 | public void setObject(T object) { 134 | this.object = object; 135 | } 136 | } 137 | 138 | public PigeonholeView(Context context) { 139 | super(context); 140 | init(context); 141 | } 142 | 143 | public PigeonholeView(Context context, AttributeSet attrs) { 144 | super(context, attrs); 145 | 146 | TypedArray a = context.getTheme().obtainStyledAttributes( 147 | attrs, R.styleable.PigeonholeView, 0, 0 148 | ); 149 | 150 | // If numColumns or numRows is zero, it may cause "divide by zero" exception in getXYForPosition(). 151 | // So initializing these variables here is necessary. 152 | this.numColumns = 1; 153 | this.numRows = 1; 154 | 155 | try { 156 | this.dragStartAnimationDuration = a.getInteger(R.styleable.PigeonholeView_dragStartAnimationDuration, 200); 157 | this.editable = a.getBoolean(R.styleable.PigeonholeView_editable, true); 158 | 159 | // a.getDimension() will return a number of pixels 160 | this.topSpaceHeight = a.getDimension(R.styleable.PigeonholeView_topSpaceHeight, 0); 161 | this.cellWidth = a.getDimension(R.styleable.PigeonholeView_cellWidth, 80); 162 | this.cellHeight = a.getDimension(R.styleable.PigeonholeView_cellHeight, 90); 163 | this.editDropAreaTopPadding = a.getDimension(R.styleable.PigeonholeView_dropAreaTopPadding, 20); 164 | this.editDropAreaText = a.getString(R.styleable.PigeonholeView_dropAreaText); 165 | } finally { 166 | a.recycle(); 167 | } 168 | 169 | init(context); 170 | } 171 | 172 | public PigeonholeViewListener getListener() { 173 | return listener; 174 | } 175 | 176 | /** 177 | * Sets PigeonholeViewListener for this instance. 178 | * 179 | * @param listener The callback that will run 180 | */ 181 | public void setListener(PigeonholeViewListener listener) { 182 | this.listener = listener; 183 | } 184 | 185 | /** 186 | * Returns the number of columns. 187 | * 188 | * @return The number of columns 189 | */ 190 | public int getNumColumns() { 191 | return numColumns; 192 | } 193 | 194 | /** 195 | * Sets the number of columns. 196 | * 197 | * @param numColumns The number of columns 198 | */ 199 | public void setNumColumns(int numColumns) { 200 | this.numColumns = numColumns; 201 | invalidate(); 202 | requestLayout(); 203 | } 204 | 205 | /** 206 | * Returns the number of rows. 207 | * 208 | * @return The number of rows 209 | */ 210 | public int getNumRows() { 211 | return numRows; 212 | } 213 | 214 | /** 215 | * Sets the number of rows. 216 | * 217 | * @param numRows The number of rows 218 | */ 219 | public void setNumRows(int numRows) { 220 | this.numRows = numRows; 221 | invalidate(); 222 | requestLayout(); 223 | } 224 | 225 | /** 226 | * Returns the maximum view position for this view. 227 | * 228 | * @return The maximum view position 229 | */ 230 | public int getMaxPosition() { 231 | return this.numColumns * this.numRows - 1; 232 | } 233 | 234 | @Override 235 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 236 | int w = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); 237 | int h = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom(); 238 | 239 | // Call measure() on every child views. Otherwise those views are invisible. 240 | if (cellMap != null) { 241 | for (int i = 0, l = cellMap.size(); i < l; i++) { 242 | CellData cellData = cellMap.valueAt(i); 243 | cellData.getView().measure((int) this.widthPerCell, (int) this.heightPerCell); 244 | } 245 | } 246 | editDropAreaView.measure( 247 | (int) (this.widthPerCell * this.numColumns), 248 | (int) (topSpaceHeight - editDropAreaTopPadding - editDropAreaBottomPadding) 249 | ); 250 | 251 | setMeasuredDimension(w, h); 252 | } 253 | 254 | @Override 255 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 256 | super.onSizeChanged(w, h, oldw, oldh); 257 | 258 | layoutComponents(w, h); 259 | } 260 | 261 | @Override 262 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 263 | // This method must be overridden. 264 | // Do not call the superclass method here. 265 | } 266 | 267 | private void layoutComponents(int w, int h) { 268 | paddingLeft = paddingRight = w * .03f; 269 | paddingTop = h * .03f + topSpaceHeight; 270 | paddingBottom = h * .03f; 271 | 272 | float ww = (float) w - paddingLeft - paddingRight; 273 | float hh = (float) h - paddingTop - paddingBottom; 274 | 275 | this.numColumns = (int) (ww / this.cellWidth); 276 | this.numRows = (int) (hh / this.cellHeight); 277 | 278 | float horizontalReminder = ww - this.numColumns * this.cellWidth; 279 | paddingLeft += horizontalReminder / 2.0f; 280 | float verticalReminder = hh - this.numRows * this.cellHeight; 281 | paddingTop += verticalReminder / 2.0f; 282 | 283 | this.widthPerCell = this.cellWidth; 284 | this.heightPerCell = this.cellHeight; 285 | 286 | dropTargetView.layout(0, 0, (int) this.widthPerCell, (int) this.heightPerCell); 287 | swapTargetView.layout(0, 0, (int) this.widthPerCell, (int) this.heightPerCell); 288 | editDropAreaView.layout( 289 | (int) paddingLeft, 290 | (int) editDropAreaTopPadding, 291 | (int) (paddingLeft + this.widthPerCell * this.numColumns), 292 | (int) (topSpaceHeight - editDropAreaBottomPadding) 293 | ); 294 | 295 | if (cellMap != null) { 296 | int maxPosition = getMaxPosition(); 297 | 298 | // SparseArray version 299 | for (int i = 0, l = cellMap.size(); i < l; i++) { 300 | int position = cellMap.keyAt(i); 301 | if (position <= maxPosition) { 302 | CellData cellData = cellMap.valueAt(i); 303 | if (cellData == hoverCellData) { 304 | continue; 305 | } 306 | int row = position / numColumns; 307 | int col = position % numColumns; 308 | cellData.getView().layout((int) (paddingLeft + col * widthPerCell), 309 | (int) (paddingTop + row * heightPerCell), 310 | (int) (paddingLeft + (col + 1) * widthPerCell), 311 | (int) (paddingTop + (row + 1) * heightPerCell)); 312 | } 313 | } 314 | } 315 | } 316 | 317 | /** 318 | * Returns whether x,y is inside the drop area or not. 319 | * 320 | * @param x Pixels along the x-axis 321 | * @param y Pixels along the y-axis 322 | * @return True if x,y is inside the drop area. Otherwise false. 323 | */ 324 | private boolean isInDropArea(float x, float y) { 325 | return x >= paddingLeft 326 | && x <= paddingLeft + widthPerCell * numColumns 327 | && y >= editDropAreaTopPadding 328 | && y <= topSpaceHeight - editDropAreaBottomPadding; 329 | } 330 | 331 | /** 332 | * Returns the view position for x,y. 333 | * 334 | * @param x Pixels along the x-axis 335 | * @param y Pixels along the y-axis 336 | * @return The view position for x,y 337 | */ 338 | private int getPositionForXY(float x, float y) { 339 | if (isInDropArea(x, y)) { 340 | return POSITION_DROP_AREA; 341 | } 342 | if (x < paddingLeft || y < paddingTop) { 343 | return POSITION_INVALID; 344 | } 345 | int row = (int) ((y - paddingTop) / this.heightPerCell); 346 | int col = (int) ((x - paddingLeft) / this.widthPerCell); 347 | int position = row * this.numColumns + col; 348 | if (col >= this.numColumns || position >= this.numColumns * this.numRows) { 349 | position = POSITION_INVALID; 350 | } 351 | return position; 352 | } 353 | 354 | /** 355 | * Returns Point(col, row) for x,y. 356 | * 357 | * @param x Pixels along the x-axis 358 | * @param y Pixels along the y-axis 359 | * @return Point(col, row) for x,y 360 | */ 361 | private Point getColRowForXY(float x, float y) { 362 | int position = getPositionForXY(x, y); 363 | if (position == POSITION_INVALID || position == POSITION_DROP_AREA) { 364 | return null; 365 | } 366 | int row = position / this.numColumns; 367 | int col = position % this.numColumns; 368 | return new Point(col, row); 369 | } 370 | 371 | /** 372 | * Returns Point(x-pixels, y-pixels) for the view position. 373 | * 374 | * @param position The view position 375 | * @return Point(x-pixels, y-pixels) 376 | */ 377 | private Point getXYForPosition(int position) { 378 | int row = position / this.numColumns; 379 | int col = position % this.numColumns; 380 | int x = (int) (paddingLeft + col * this.widthPerCell); 381 | int y = (int) (paddingTop + row * this.heightPerCell); 382 | return new Point(x, y); 383 | } 384 | 385 | /** 386 | * Returns Point(x-pixels, y-pixels) for the column and row. 387 | * 388 | * @param col Column index (starts from zero) 389 | * @param row Row index (starts from zero) 390 | * @return Point(x-pixels, y-pixels) for the column and row 391 | */ 392 | private Point getXYForColRow(int col, int row) { 393 | int x = (int) (paddingLeft + col * this.widthPerCell); 394 | int y = (int) (paddingTop + row * this.heightPerCell); 395 | return new Point(x, y); 396 | } 397 | 398 | /** 399 | * Sets DataProvider for this view. 400 | * 401 | * @param provider A DataProvider 402 | */ 403 | public void setDataProvider(DataProvider provider) { 404 | this.provider = provider; 405 | setupViews(); 406 | } 407 | 408 | private void enableEditing() { 409 | if (cellMap != null) { 410 | // SparseArray version 411 | for (int i = 0, l = cellMap.size(); i < l; i++) { 412 | CellData cellData = cellMap.valueAt(i); 413 | View view = cellData.getView(); 414 | view.setLongClickable(true); 415 | } 416 | } 417 | } 418 | 419 | private void disableEditing() { 420 | cancelEdit(); 421 | 422 | if (cellMap != null) { 423 | // SparseArray version 424 | for (int i = 0, l = cellMap.size(); i < l; i++) { 425 | CellData cellData = cellMap.valueAt(i); 426 | View view = cellData.getView(); 427 | view.setLongClickable(false); 428 | } 429 | } 430 | } 431 | 432 | /** 433 | * Enable or disable edit mode. 434 | * 435 | * @param set To enable edit mode, true. To disable, false. 436 | */ 437 | public void setEditable(boolean set) { 438 | if (editable != set) { 439 | editable = set; 440 | if (editable) { // Enable editing 441 | enableEditing(); 442 | } else { // Disable editing 443 | disableEditing(); 444 | } 445 | } 446 | } 447 | 448 | /** 449 | * Creates and adds cell views using DataProvider. 450 | */ 451 | private void setupViews() { 452 | cellMap = new SparseArray<>(); 453 | Iterator iter = this.provider.iterator(); 454 | boolean isAltered = false; 455 | while (iter.hasNext()) { 456 | T item = iter.next(); 457 | View cellView = this.provider.getView(null, item); 458 | int position = this.provider.getViewPosition(item); 459 | if (cellView != null && position != POSITION_INVALID) { 460 | // check validity of position 461 | if (cellMap.get(position) != null) { 462 | int altPosition = getMinimumVacantPosition(); 463 | if (altPosition == POSITION_INVALID || altPosition == POSITION_DROP_AREA) { 464 | Log.e(TAG, "No available position for overlapping position: " + position); 465 | continue; 466 | } 467 | Log.w(TAG, "Assigned new position " + altPosition + " for overlapped position " + position); 468 | this.provider.setViewPosition(item, altPosition); 469 | position = altPosition; 470 | isAltered = true; 471 | } 472 | 473 | addView(cellView); 474 | cellView.setClickable(true); 475 | final CellData cellData = new CellData<>(cellView, position, item); 476 | cellView.setOnClickListener(new OnClickListener() { 477 | @Override 478 | public void onClick(View view) { 479 | if (onCellClickListener != null) { 480 | onCellClickListener.onClick(cellData); 481 | } 482 | } 483 | }); 484 | cellView.setOnLongClickListener(new OnLongClickListener() { 485 | @Override 486 | public boolean onLongClick(View view) { 487 | if (isDragging) { 488 | return false; 489 | } else { 490 | startDrag(cellData); 491 | return true; 492 | } 493 | } 494 | }); 495 | if (!editable) { 496 | cellView.setLongClickable(false); 497 | } 498 | cellMap.put(cellData.getPosition(), cellData); 499 | } 500 | } 501 | if (isAltered) { 502 | if (listener != null) { 503 | listener.onReorder(); 504 | } 505 | } 506 | } 507 | 508 | private int getMinimumVacantPositionWithoutUpperLimit() { 509 | for (int i = 0; ; i++) { 510 | if (cellMap.get(i) == null) { 511 | return i; 512 | } 513 | } 514 | } 515 | 516 | private int getMinimumVacantPosition() { 517 | int maxPosition = this.numColumns * this.numRows - 1; 518 | for (int i = 0; i <= maxPosition; i++) { 519 | if (cellMap.get(i) == null) { 520 | return i; 521 | } 522 | } 523 | return POSITION_INVALID; 524 | } 525 | 526 | /** 527 | * Checks whether the matrix is full. Cells cannot be added 528 | * if the matrix is full. 529 | * 530 | * @return True if the matrix is full. Otherwise false. 531 | */ 532 | public boolean isFull() { 533 | return getMinimumVacantPosition() == POSITION_INVALID; 534 | } 535 | 536 | /** 537 | * Adds a cell to this view. You can use this method to dynamically 538 | * add a cell. The view position will be automatically assigned. 539 | * If there is no view position available, it assigns invisible 540 | * (above the maximum) view position. 541 | * 542 | * @param object Object representing a cell 543 | */ 544 | public void addObject(T object) { 545 | int position = getMinimumVacantPosition(); 546 | if (position == -1) { 547 | position = getMinimumVacantPositionWithoutUpperLimit(); 548 | Log.w(TAG, "Assigning a position above upper limit: " + position); 549 | } 550 | if (this.provider == null) { 551 | throw new IllegalStateException("DataProvider is null"); 552 | } 553 | this.provider.setViewPosition(object, position); 554 | if (listener != null) { 555 | listener.onReorder(); 556 | } 557 | 558 | View cellView = this.provider.getView(null, object); 559 | if (cellView != null) { 560 | cellView.setClickable(true); 561 | final CellData cellData = new CellData<>(cellView, position, object); 562 | cellView.setOnClickListener(new OnClickListener() { 563 | @Override 564 | public void onClick(View view) { 565 | if (onCellClickListener != null) { 566 | onCellClickListener.onClick(cellData); 567 | } 568 | } 569 | }); 570 | cellView.setOnLongClickListener(new OnLongClickListener() { 571 | @Override 572 | public boolean onLongClick(View view) { 573 | if (isDragging) { 574 | return false; 575 | } else { 576 | startDrag(cellData); 577 | return true; 578 | } 579 | } 580 | }); 581 | if (!editable) { 582 | cellView.setLongClickable(false); 583 | } 584 | cellMap.put(position, cellData); 585 | addView(cellView); 586 | if (position <= getMaxPosition()) { 587 | Point cellPoint = getXYForPosition(position); 588 | cellView.measure((int) widthPerCell, (int) heightPerCell); 589 | cellView.layout( 590 | cellPoint.x, 591 | cellPoint.y, 592 | (int) (cellPoint.x + widthPerCell), 593 | (int) (cellPoint.y + heightPerCell) 594 | ); 595 | invalidate(); 596 | } 597 | } 598 | } 599 | 600 | /** 601 | * Removes a cell from this view. 602 | * 603 | * @param cellData CellData that will be removed 604 | */ 605 | private void deleteCell(CellData cellData) { 606 | if (cellData == null) { 607 | Log.e(TAG, "deleteCell: cellData is null"); 608 | return; 609 | } 610 | cellMap.remove(cellData.getPosition()); 611 | 612 | if (listener != null) { 613 | listener.onReorder(); 614 | } 615 | 616 | final View cellView = cellData.getView(); 617 | 618 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 619 | AnimatorSet animatorSet = new AnimatorSet(); 620 | 621 | // shrink 622 | ObjectAnimator animScaleX = ObjectAnimator.ofFloat(cellView, "scaleX", 0f); 623 | ObjectAnimator animScaleY = ObjectAnimator.ofFloat(cellView, "scaleY", 0f); 624 | 625 | // fade out 626 | ObjectAnimator animFadeOut = ObjectAnimator.ofFloat(cellView, "alpha", 1f, 0f); 627 | animatorSet.playTogether(animScaleX, animScaleY, animFadeOut); 628 | animatorSet.addListener(new Animator.AnimatorListener() { 629 | @Override 630 | public void onAnimationStart(Animator animator) { 631 | } 632 | 633 | @Override 634 | public void onAnimationEnd(Animator animator) { 635 | removeView(cellView); 636 | } 637 | 638 | @Override 639 | public void onAnimationCancel(Animator animator) { 640 | } 641 | 642 | @Override 643 | public void onAnimationRepeat(Animator animator) { 644 | } 645 | }); 646 | animatorSet.start(); 647 | } else { 648 | removeView(cellView); 649 | } 650 | } 651 | 652 | private void deleteHoveringObject() { 653 | deleteCell(hoverCellData); 654 | } 655 | 656 | private void editHoveringObject() { 657 | if (hoverCellData != null) { 658 | editingPosition = hoverCellData.getPosition(); 659 | T cellInfo = hoverCellData.getObject(); 660 | if (listener != null) { 661 | listener.onEditObject(cellInfo); 662 | } 663 | } 664 | } 665 | 666 | /** 667 | * Removes the currently editing cell from this view. 668 | */ 669 | public void deleteEditingObject() { 670 | CellData cellData = cellMap.get(editingPosition); 671 | if (cellData != null) { 672 | deleteCell(cellData); 673 | } 674 | 675 | if (listener != null) { 676 | listener.onDragEnd(); 677 | } 678 | } 679 | 680 | private void putBackEditingCellView() { 681 | if (editingPosition != POSITION_INVALID) { 682 | CellData cellData = cellMap.get(editingPosition); 683 | if (cellData != null) { 684 | View cellView = cellData.getView(); 685 | 686 | Point destXY = getXYForPosition(editingPosition); 687 | if (destXY != null) { 688 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 689 | cellView.setScaleX(1.0f); 690 | cellView.setScaleY(1.0f); 691 | cellView.setX(destXY.x); 692 | cellView.setY(destXY.y); 693 | 694 | // Fade in the cell 695 | AnimatorSet animatorSet = new AnimatorSet(); 696 | ObjectAnimator animFadeIn = ObjectAnimator.ofFloat(cellView, "alpha", 0f, 1f); 697 | animatorSet.play(animFadeIn); 698 | animatorSet.start(); 699 | } else { 700 | cellView.layout( 701 | destXY.x, 702 | destXY.y, 703 | (int) (destXY.x + widthPerCell), 704 | (int) (destXY.y + heightPerCell) 705 | ); 706 | } 707 | invalidate(); 708 | } 709 | } 710 | editingPosition = POSITION_INVALID; 711 | } 712 | } 713 | 714 | /** 715 | * Cancels the ongoing edit. 716 | */ 717 | public void cancelEdit() { 718 | putBackEditingCellView(); 719 | 720 | if (listener != null) { 721 | listener.onDragEnd(); 722 | } 723 | } 724 | 725 | @Override 726 | public Parcelable onSaveInstanceState() { 727 | Bundle bundle = new Bundle(); 728 | bundle.putInt("editingPosition", editingPosition); 729 | bundle.putParcelable("superState", super.onSaveInstanceState()); 730 | return bundle; 731 | } 732 | 733 | @Override 734 | public void onRestoreInstanceState(Parcelable state) { 735 | if (state instanceof Bundle) { 736 | Bundle bundle = (Bundle) state; 737 | editingPosition = bundle.getInt("editingPosition", POSITION_INVALID); 738 | Parcelable superState = bundle.getParcelable("superState"); 739 | super.onRestoreInstanceState(superState); 740 | } else { 741 | Log.e(TAG, "state is not an instance of Bundle"); 742 | } 743 | } 744 | 745 | /** 746 | * Updates the view using DataProvider. 747 | */ 748 | public void notifyDataSetChanged() { 749 | invalidate(); 750 | } 751 | 752 | /** 753 | * Updates the view for the cell which is being edited. 754 | */ 755 | public void updateEditingObject() { 756 | if (editingPosition != POSITION_INVALID) { 757 | CellData cellData = cellMap.get(editingPosition); 758 | if (cellData != null) { 759 | if (this.provider == null) { 760 | throw new IllegalStateException("DataProvider is null"); 761 | } 762 | 763 | // Update contents of the view 764 | this.provider.getView(cellData.getView(), cellData.getObject()); 765 | 766 | invalidate(); 767 | } else { 768 | Log.e(TAG, "No data at editingPosition"); 769 | } 770 | } 771 | 772 | putBackEditingCellView(); 773 | 774 | if (listener != null) { 775 | listener.onDragEnd(); 776 | } 777 | } 778 | 779 | /** 780 | * Moves the cell to the new position. 781 | * 782 | * @param cellData CellData that will be moved 783 | * @param newPosition The new position for the cell 784 | */ 785 | private void moveCell(CellData cellData, int newPosition) { 786 | Point newPoint = getXYForPosition(newPosition); 787 | if (newPoint == null) { 788 | Log.e(TAG, "moveCell: Invalid position is specified: " + newPosition); 789 | return; 790 | } 791 | 792 | // Move the cell to newPosition 793 | View cellView = cellData.getView(); 794 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 795 | AnimatorSet animatorSet = new AnimatorSet(); 796 | ObjectAnimator animX = ObjectAnimator.ofFloat(cellView, "x", newPoint.x); 797 | ObjectAnimator animY = ObjectAnimator.ofFloat(cellView, "y", newPoint.y); 798 | animatorSet.playTogether(animX, animY); 799 | animatorSet.start(); 800 | } else { 801 | cellView.layout( 802 | newPoint.x, 803 | newPoint.y, 804 | (int) (newPoint.x + widthPerCell), 805 | (int) (newPoint.y + heightPerCell) 806 | ); 807 | } 808 | 809 | int oldPosition = cellData.getPosition(); 810 | cellData.setPosition(newPosition); 811 | cellMap.remove(oldPosition); 812 | cellMap.put(newPosition, cellData); 813 | 814 | T object = cellData.getObject(); 815 | if (this.provider == null) { 816 | throw new IllegalStateException("DataProvider is null"); 817 | } 818 | this.provider.setViewPosition(object, newPosition); 819 | } 820 | 821 | private void cancelMove() { 822 | if (hoverCellData == null) { 823 | Log.e(TAG, "cancelMove: hoverCellData is null"); 824 | return; 825 | } 826 | 827 | // Move and back to normal scale 828 | Point targetPoint = getXYForPosition(hoverCellData.getPosition()); 829 | View cellView = hoverCellData.getView(); 830 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 831 | AnimatorSet animatorSet = new AnimatorSet(); 832 | ObjectAnimator animX = ObjectAnimator.ofFloat(cellView, "x", targetPoint.x); 833 | ObjectAnimator animY = ObjectAnimator.ofFloat(cellView, "y", targetPoint.y); 834 | ObjectAnimator animScaleX = ObjectAnimator.ofFloat(cellView, "scaleX", 1.0f); 835 | ObjectAnimator animScaleY = ObjectAnimator.ofFloat(cellView, "scaleY", 1.0f); 836 | animatorSet.playTogether(animX, animY, animScaleX, animScaleY); 837 | animatorSet.start(); 838 | } else { 839 | cellView.layout( 840 | targetPoint.x, 841 | targetPoint.y, 842 | (int) (targetPoint.x + widthPerCell), 843 | (int) (targetPoint.y + heightPerCell) 844 | ); 845 | } 846 | 847 | swapTargetView.setVisibility(View.GONE); 848 | } 849 | 850 | private void endDrag(float x, float y) { 851 | if (hoverCellData == null) { 852 | Log.e(TAG, "endDrag: hoverCellData is null"); 853 | return; 854 | } 855 | int dropPosition = getPositionForXY(x, y); 856 | boolean isAltered = false; 857 | if (dropPosition == POSITION_INVALID) { // Cancel move 858 | cancelMove(); 859 | if (listener != null) { 860 | listener.onDragEnd(); 861 | } 862 | } else if (dropPosition == POSITION_DROP_AREA) { // Edit object 863 | editHoveringObject(); 864 | } else { // Move 865 | Point dropPoint = getColRowForXY(x, y); 866 | Point dropXY = getXYForColRow(dropPoint.x, dropPoint.y); 867 | View cellView = hoverCellData.getView(); 868 | 869 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 870 | // Move and shrink 871 | AnimatorSet animatorSet = new AnimatorSet(); 872 | ObjectAnimator animX = ObjectAnimator.ofFloat(cellView, "x", dropXY.x); 873 | ObjectAnimator animY = ObjectAnimator.ofFloat(cellView, "y", dropXY.y); 874 | ObjectAnimator animScaleX = ObjectAnimator.ofFloat(cellView, "scaleX", 1.0f); 875 | ObjectAnimator animScaleY = ObjectAnimator.ofFloat(cellView, "scaleY", 1.0f); 876 | animatorSet.playTogether(animX, animY, animScaleX, animScaleY); 877 | animatorSet.start(); 878 | } else { 879 | cellView.layout( 880 | dropXY.x, 881 | dropXY.y, 882 | (int) (dropXY.x + widthPerCell), 883 | (int) (dropXY.y + heightPerCell) 884 | ); 885 | } 886 | 887 | int oldPosition = hoverCellData.getPosition(); 888 | int newPosition = getPositionForXY(x, y); 889 | if (newPosition != oldPosition) { 890 | hoverCellData.setPosition(newPosition); 891 | cellMap.remove(oldPosition); 892 | if (swapCandidateCellData != null && swapCandidateCellData != hoverCellData) { 893 | moveCell(swapCandidateCellData, oldPosition); 894 | swapTargetView.setVisibility(View.GONE); 895 | } 896 | cellMap.put(newPosition, hoverCellData); 897 | // Moved cell from oldPosition to newPosition 898 | 899 | T object = hoverCellData.getObject(); 900 | if (this.provider == null) { 901 | throw new IllegalStateException("DataProvider is null"); 902 | } 903 | this.provider.setViewPosition(object, newPosition); 904 | 905 | isAltered = true; 906 | } 907 | if (listener != null) { 908 | listener.onDragEnd(); 909 | } 910 | } 911 | dropTargetView.setVisibility(View.GONE); 912 | 913 | if (isAltered) { 914 | if (listener != null) { 915 | listener.onReorder(); 916 | } 917 | } 918 | } 919 | 920 | /** 921 | * Starts dragging the cell. 922 | * 923 | * @param cellData CellData that is started dragging 924 | */ 925 | private void startDrag(CellData cellData) { 926 | if (isDragging) { // Another cell is being dragged 927 | return; 928 | } 929 | 930 | if (listener != null) { 931 | listener.onDragStart(); 932 | } 933 | 934 | View cellView = cellData.getView(); 935 | hoverCellData = cellData; 936 | 937 | float newX; 938 | float newY; 939 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 940 | newX = cellView.getX(); 941 | newY = cellView.getY(); 942 | } else { 943 | newX = cellView.getLeft(); 944 | newY = cellView.getTop(); 945 | } 946 | 947 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 948 | // Zoom 949 | AnimatorSet animatorSet = new AnimatorSet(); 950 | ObjectAnimator animX = ObjectAnimator.ofFloat(cellView, "x", newX); 951 | ObjectAnimator animY = ObjectAnimator.ofFloat(cellView, "y", newY); 952 | ObjectAnimator animScaleX = ObjectAnimator.ofFloat(cellView, "scaleX", 1.25f); 953 | ObjectAnimator animScaleY = ObjectAnimator.ofFloat(cellView, "scaleY", 1.25f); 954 | animatorSet.setDuration(dragStartAnimationDuration); 955 | animatorSet.playTogether(animX, animY, animScaleX, animScaleY); 956 | animatorSet.start(); 957 | } else { 958 | cellView.layout( 959 | (int) newX, 960 | (int) newY, 961 | (int) (newX + widthPerCell), 962 | (int) (newY + heightPerCell) 963 | ); 964 | } 965 | 966 | posX = newX; 967 | posY = newY; 968 | currentHoverPosition = cellData.getPosition(); 969 | isDragging = true; 970 | cellView.bringToFront(); 971 | 972 | invalidate(); 973 | } 974 | 975 | @Override 976 | public boolean onInterceptTouchEvent(MotionEvent ev) { 977 | final int action = MotionEventCompat.getActionMasked(ev); 978 | switch (action) { 979 | case MotionEvent.ACTION_DOWN: { 980 | try { 981 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 982 | final float x = MotionEventCompat.getX(ev, pointerIndex); 983 | final float y = MotionEventCompat.getY(ev, pointerIndex); 984 | 985 | // Remember where we started (for dragging) 986 | lastTouchX = x; 987 | lastTouchY = y; 988 | // Save the ID of this pointer (for dragging) 989 | activePointerId = MotionEventCompat.getPointerId(ev, 0); 990 | } catch (IllegalArgumentException ex) { // pointerIndex out of range 991 | Log.e(TAG, "IllegalArgumentException: " + ex.getMessage()); 992 | return false; 993 | } 994 | break; 995 | } 996 | case MotionEvent.ACTION_UP: { 997 | onMouseActionUp(ev); 998 | break; 999 | } 1000 | case MotionEvent.ACTION_CANCEL: { 1001 | cancelDrag(); 1002 | break; 1003 | } 1004 | } 1005 | 1006 | return isDragging; 1007 | } 1008 | 1009 | private void resetDragState() { 1010 | hoverCellData = null; 1011 | swapCandidateCellData = null; 1012 | currentHoverPosition = POSITION_INVALID; 1013 | activePointerId = -1; // MotionEvent.INVALID_POINTER_ID; 1014 | } 1015 | 1016 | private void cancelDrag() { 1017 | if (isDragging) { 1018 | cancelMove(); 1019 | if (listener != null) { 1020 | listener.onDragEnd(); 1021 | } 1022 | dropTargetView.setVisibility(View.GONE); 1023 | isDragging = false; 1024 | } 1025 | resetDragState(); 1026 | } 1027 | 1028 | private void onMouseActionUp(MotionEvent ev) { 1029 | if (isDragging) { 1030 | isDragging = false; 1031 | try { 1032 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1033 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1034 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1035 | endDrag(x, y); 1036 | } catch (IllegalArgumentException ex) { // pointerIndex out of range 1037 | Log.e(TAG, "IllegalArgumentException: " + ex.getMessage()); 1038 | } 1039 | } 1040 | resetDragState(); 1041 | } 1042 | 1043 | private void cancelSwapCandidate() { 1044 | if (swapCandidateCellData == null) { 1045 | return; 1046 | } 1047 | if (swapCandidateCellData == hoverCellData) { 1048 | return; 1049 | } 1050 | 1051 | View swapCandidateView = swapCandidateCellData.getView(); 1052 | Point targetPoint = getXYForPosition(swapCandidateCellData.getPosition()); 1053 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 1054 | // Move the swap candidate cell back to its original position 1055 | AnimatorSet animatorSet = new AnimatorSet(); 1056 | ObjectAnimator animX = ObjectAnimator.ofFloat(swapCandidateView, "x", targetPoint.x); 1057 | ObjectAnimator animY = ObjectAnimator.ofFloat(swapCandidateView, "y", targetPoint.y); 1058 | animatorSet.playTogether(animX, animY); 1059 | animatorSet.start(); 1060 | } else { 1061 | swapCandidateView.layout( 1062 | targetPoint.x, 1063 | targetPoint.y, 1064 | (int) (targetPoint.x + widthPerCell), 1065 | (int) (targetPoint.y + heightPerCell) 1066 | ); 1067 | } 1068 | 1069 | swapTargetView.setVisibility(View.GONE); 1070 | } 1071 | 1072 | private void swapCandidateEffect() { 1073 | if (swapCandidateCellData == null || hoverCellData == null) { 1074 | return; 1075 | } 1076 | if (swapCandidateCellData == hoverCellData) { 1077 | return; 1078 | } 1079 | 1080 | // Move the swap candidate cell to the original position of hovering cell 1081 | View swapCandidateView = swapCandidateCellData.getView(); 1082 | swapCandidateView.bringToFront(); 1083 | hoverCellData.getView().bringToFront(); 1084 | Point targetPoint = getXYForPosition(hoverCellData.getPosition()); 1085 | 1086 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 1087 | swapTargetView.setX(targetPoint.x); 1088 | swapTargetView.setY(targetPoint.y); 1089 | } else { 1090 | swapTargetView.layout( 1091 | targetPoint.x, 1092 | targetPoint.y, 1093 | (int) (targetPoint.x + widthPerCell), 1094 | (int) (targetPoint.y + heightPerCell) 1095 | ); 1096 | } 1097 | swapTargetView.setVisibility(View.VISIBLE); 1098 | 1099 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 1100 | AnimatorSet animatorSet = new AnimatorSet(); 1101 | ObjectAnimator animX = ObjectAnimator.ofFloat(swapCandidateView, "x", targetPoint.x); 1102 | ObjectAnimator animY = ObjectAnimator.ofFloat(swapCandidateView, "y", targetPoint.y); 1103 | animatorSet.playTogether(animX, animY); 1104 | animatorSet.start(); 1105 | } else { 1106 | swapCandidateView.layout( 1107 | targetPoint.x, 1108 | targetPoint.y, 1109 | (int) (targetPoint.x + widthPerCell), 1110 | (int) (targetPoint.y + heightPerCell) 1111 | ); 1112 | } 1113 | } 1114 | 1115 | @Override 1116 | public boolean onTouchEvent(@NonNull MotionEvent ev) { 1117 | final int action = MotionEventCompat.getActionMasked(ev); 1118 | switch (action) { 1119 | case MotionEvent.ACTION_DOWN: { 1120 | try { 1121 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 1122 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1123 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1124 | 1125 | // Remember where we started (for dragging) 1126 | lastTouchX = x; 1127 | lastTouchY = y; 1128 | // Save the ID of this pointer (for dragging) 1129 | activePointerId = MotionEventCompat.getPointerId(ev, 0); 1130 | } catch (IllegalArgumentException ex) { // pointerIndex out of range 1131 | Log.e(TAG, "IllegalArgumentException: " + ex.getMessage()); 1132 | return false; 1133 | } 1134 | break; 1135 | } 1136 | case MotionEvent.ACTION_MOVE: { 1137 | if (activePointerId != MotionEvent.INVALID_POINTER_ID) { 1138 | try { 1139 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 1140 | final float x = MotionEventCompat.getX(ev, pointerIndex); 1141 | final float y = MotionEventCompat.getY(ev, pointerIndex); 1142 | 1143 | // Calculate the distance moved 1144 | final float dx = x - lastTouchX; 1145 | final float dy = y - lastTouchY; 1146 | 1147 | posX += dx; 1148 | posY += dy; 1149 | if (hoverCellData != null) { // User is dragging a cell 1150 | View cellView = hoverCellData.getView(); 1151 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 1152 | cellView.setX(posX); 1153 | cellView.setY(posY); 1154 | } else { 1155 | cellView.layout((int) posX, (int) posY, (int) (posX + widthPerCell), (int) (posY + heightPerCell)); 1156 | } 1157 | int hoverPosition = getPositionForXY(x, y); 1158 | if (hoverPosition != currentHoverPosition) { 1159 | cancelSwapCandidate(); 1160 | swapCandidateCellData = null; 1161 | } 1162 | if (hoverPosition == POSITION_DROP_AREA) { 1163 | editDropAreaView.setBackgroundResource(R.drawable.drop_area_highlight); 1164 | } else { 1165 | editDropAreaView.setBackgroundResource(R.drawable.drop_area); 1166 | } 1167 | Point hoverPoint = getColRowForXY(x, y); 1168 | if (hoverPoint != null) { 1169 | // Show the drop target 1170 | Point hoverXY = getXYForColRow(hoverPoint.x, hoverPoint.y); 1171 | dropTargetView.setVisibility(View.VISIBLE); 1172 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 1173 | dropTargetView.setX(hoverXY.x); 1174 | dropTargetView.setY(hoverXY.y); 1175 | } else { 1176 | dropTargetView.layout( 1177 | hoverXY.x, 1178 | hoverXY.y, 1179 | (int) (hoverXY.x + widthPerCell), 1180 | (int) (hoverXY.y + heightPerCell) 1181 | ); 1182 | } 1183 | 1184 | CellData dropTargetCellData = cellMap.get(hoverPosition); 1185 | if (dropTargetCellData != null) { 1186 | if (hoverPosition != currentHoverPosition) { 1187 | // Move the drop target cell 1188 | swapCandidateCellData = dropTargetCellData; 1189 | swapCandidateEffect(); 1190 | } 1191 | } 1192 | } else { 1193 | // Hide the drop target 1194 | dropTargetView.setVisibility(View.GONE); 1195 | } 1196 | currentHoverPosition = hoverPosition; 1197 | } 1198 | 1199 | invalidate(); // TODO: Is this necessary? 1200 | 1201 | // Remember this touch position for the next move event 1202 | lastTouchX = x; 1203 | lastTouchY = y; 1204 | } catch (IllegalArgumentException ex) { // pointerIndex out of range 1205 | Log.e(TAG, "IllegalArgumentException: " + ex.getMessage()); 1206 | return false; 1207 | } 1208 | // } else { 1209 | // // First pointer down -> second pointer down -> first pointer up -> second pointer drag -> (here) 1210 | } 1211 | break; 1212 | } 1213 | case MotionEvent.ACTION_UP: { 1214 | onMouseActionUp(ev); 1215 | break; 1216 | } 1217 | case MotionEvent.ACTION_CANCEL: { 1218 | cancelDrag(); 1219 | break; 1220 | } 1221 | case MotionEvent.ACTION_POINTER_UP: { 1222 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 1223 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 1224 | 1225 | if (pointerId == activePointerId) { 1226 | onMouseActionUp(ev); 1227 | } 1228 | break; 1229 | } 1230 | } 1231 | return true; 1232 | } 1233 | 1234 | /** 1235 | * Initialize this view. 1236 | * 1237 | * @param context 1238 | */ 1239 | private void init(Context context) { 1240 | this.context = context; 1241 | 1242 | textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 1243 | textPaint.setColor(Color.BLACK); 1244 | textPaint.setTextSize(60); 1245 | 1246 | dropTargetView = new ImageView(context); 1247 | dropTargetView.setImageResource(R.drawable.placeholder); 1248 | dropTargetView.setScaleType(ImageView.ScaleType.FIT_XY); 1249 | dropTargetView.setVisibility(View.GONE); 1250 | addView(dropTargetView); 1251 | 1252 | swapTargetView = new ImageView(context); 1253 | swapTargetView.setImageResource(R.drawable.swap_candidate); 1254 | swapTargetView.setScaleType(ImageView.ScaleType.FIT_XY); 1255 | swapTargetView.setVisibility(View.GONE); 1256 | addView(swapTargetView); 1257 | 1258 | LayoutInflater inflater = LayoutInflater.from(context); 1259 | 1260 | // Drop area 1261 | editDropAreaView = inflater.inflate(R.layout.drop_area, this, false); 1262 | addView(editDropAreaView); 1263 | 1264 | if (editDropAreaText != null) { // Use custom text for drop area 1265 | TextView dropAreaTextView = (TextView) editDropAreaView.findViewById(R.id.drop_area__label); 1266 | dropAreaTextView.setText(editDropAreaText); 1267 | } 1268 | } 1269 | 1270 | public OnCellClickListener getOnCellClickListener() { 1271 | return onCellClickListener; 1272 | } 1273 | 1274 | /** 1275 | * Set the listener that will be called when user clicks a cell 1276 | * 1277 | * @param onCellClickListener The callback that will run 1278 | */ 1279 | public void setOnCellClickListener(OnCellClickListener onCellClickListener) { 1280 | this.onCellClickListener = onCellClickListener; 1281 | } 1282 | } 1283 | --------------------------------------------------------------------------------