├── 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 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
7 |
8 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | generateDebugSources
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | generateDebugSources
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
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 | 
4 | 
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 | 
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 |
--------------------------------------------------------------------------------