├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── scopes │ └── scope_settings.xml ├── encodings.xml ├── vcs.xml ├── modules.xml ├── gradle.xml ├── compiler.xml └── misc.xml ├── sample ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── drawable-hdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-mdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── styles.xml │ │ │ └── strings.xml │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── fragment_main.xml │ │ └── values-w820dp │ │ │ └── dimens.xml │ │ ├── java │ │ └── com │ │ │ └── martinappl │ │ │ └── components │ │ │ └── sample │ │ │ ├── MainActivity.java │ │ │ ├── Data.java │ │ │ ├── adapter │ │ │ └── SampleImageAdapter.java │ │ │ └── MainFragment.java │ │ └── AndroidManifest.xml ├── proguard-rules.txt ├── build.gradle └── sample.iml ├── library ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── drawable-mdpi │ │ │ └── ico_delete_asset.png │ │ ├── drawable-xhdpi │ │ │ └── ico_delete_asset.png │ │ └── values │ │ │ └── attrs.xml │ │ └── java │ │ └── com │ │ └── martinappl │ │ └── components │ │ ├── ui │ │ └── containers │ │ │ ├── interfaces │ │ │ ├── IRemoveFromAdapter.java │ │ │ ├── IViewObserver.java │ │ │ └── IRemovableItemsAdapterComponent.java │ │ │ ├── contentbands │ │ │ ├── TileBase.java │ │ │ ├── EndlessContentBand.java │ │ │ └── BasicContentBand.java │ │ │ ├── HorizontalListWithRemovableItems.java │ │ │ ├── HorizontalList.java │ │ │ └── EndlessLoopAdapterContainer.java │ │ └── general │ │ ├── ToolBox.java │ │ └── Validate.java ├── gradle.properties ├── proguard-rules.txt ├── build.gradle └── library.iml ├── settings.gradle ├── .gitignore ├── sample.apk ├── Screenshot_01.gif ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── MAComponents.iml ├── README.md ├── gradle.properties ├── gradlew.bat └── gradlew /.idea/.name: -------------------------------------------------------------------------------- 1 | MAComponents -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library', ':sample' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /sample.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/sample.apk -------------------------------------------------------------------------------- /Screenshot_01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/Screenshot_01.gif -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=FeatureCoverFlow Library 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar 4 | POM_DEVELOPER_EMAIL="gn00747254@gmail.com" -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-mdpi/ico_delete_asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/library/src/main/res/drawable-mdpi/ico_delete_asset.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/ico_delete_asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/HEAD/library/src/main/res/drawable-xhdpi/ico_delete_asset.png -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/interfaces/IRemoveFromAdapter.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers.interfaces; 2 | 3 | 4 | public interface IRemoveFromAdapter{ 5 | void removeItemFromAdapter(int position); 6 | } 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sample 5 | Hello world! 6 | Settings 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 01 17:02:52 CST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip 7 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/interfaces/IViewObserver.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers.interfaces; 2 | 3 | import android.view.View; 4 | 5 | public interface IViewObserver { 6 | /** 7 | * @param v View which is getting removed 8 | * @param position View position in adapter 9 | */ 10 | void onViewRemovedFromParent(View v, int position); 11 | } 12 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers.interfaces; 2 | 3 | import android.view.View; 4 | 5 | public interface IRemovableItemsAdapterComponent { 6 | /** 7 | * Called when item is removed from component by user clicking on remove button 8 | * @return true, if you removed item from adapter manually in this step 9 | */ 10 | boolean onItemRemove(int position, View view, Object item); 11 | } 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /MAComponents.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /library/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 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 | #} -------------------------------------------------------------------------------- /sample/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the ProGuard 5 | # include property in project.properties. 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 | #} -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:0.11.+' 7 | } 8 | } 9 | apply plugin: 'android' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | android { 16 | compileSdkVersion 20 17 | buildToolsVersion "20.0.0" 18 | 19 | defaultConfig { 20 | minSdkVersion 11 21 | targetSdkVersion 20 22 | versionCode 1 23 | versionName "1.0" 24 | } 25 | buildTypes { 26 | release { 27 | runProguard false 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | compile fileTree(dir: 'libs', include: ['*.jar']) 35 | compile project(":library") 36 | compile "com.squareup.picasso:picasso:+" 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/java/com/martinappl/components/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.sample; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.os.Build; 12 | 13 | 14 | 15 | public class MainActivity extends Activity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | if (savedInstanceState == null) { 22 | getFragmentManager().beginTransaction() 23 | .replace(R.id.container, new MainFragment()) 24 | .commit(); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:0.11.+' 7 | } 8 | } 9 | apply plugin: 'android-library' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | android { 16 | compileSdkVersion 20 17 | buildToolsVersion '20.0.0' 18 | 19 | defaultConfig { 20 | minSdkVersion 11 21 | targetSdkVersion 20 22 | versionCode 1 23 | versionName "1.0" 24 | } 25 | buildTypes { 26 | release { 27 | runProguard false 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | compile fileTree(dir: 'libs', include: ['*.jar']) 35 | compile 'com.android.support:support-v4:20.+' 36 | } 37 | 38 | apply from: 'https://raw.githubusercontent.com/shamanland/gradle-mvn-push/master/gradle-mvn-push.gradle' 39 | 40 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Android 19 | 20 | 21 | Android Lint 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/java/com/martinappl/components/sample/Data.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.sample; 2 | 3 | public final class Data { 4 | static final String BASE = "http://i.imgur.com/"; 5 | static final String EXT = ".jpg"; 6 | public static final String[] URLS = { 7 | BASE + "CqmBjo5" + EXT, BASE + "zkaAooq" + EXT, BASE + "0gqnEaY" + EXT, 8 | BASE + "9gbQ7YR" + EXT, BASE + "aFhEEby" + EXT, BASE + "0E2tgV7" + EXT, 9 | BASE + "P5JLfjk" + EXT, BASE + "nz67a4F" + EXT, BASE + "dFH34N5" + EXT, 10 | BASE + "FI49ftb" + EXT, BASE + "DvpvklR" + EXT, BASE + "DNKnbG8" + EXT, 11 | BASE + "yAdbrLp" + EXT, BASE + "55w5Km7" + EXT, BASE + "NIwNTMR" + EXT, 12 | BASE + "DAl0KB8" + EXT, BASE + "xZLIYFV" + EXT, BASE + "HvTyeh3" + EXT, 13 | BASE + "Ig9oHCM" + EXT, BASE + "7GUv9qa" + EXT, BASE + "i5vXmXp" + EXT, 14 | BASE + "glyvuXg" + EXT, BASE + "u6JF6JZ" + EXT, BASE + "ExwR7ap" + EXT, 15 | BASE + "Q54zMKT" + EXT, BASE + "9t6hLbm" + EXT, BASE + "F8n3Ic6" + EXT, 16 | BASE + "P5ZRSvT" + EXT, BASE + "jbemFzr" + EXT, BASE + "8B7haIK" + EXT, 17 | BASE + "aSeTYQr" + EXT, BASE + "OKvWoTh" + EXT, BASE + "zD3gT4Z" + EXT, 18 | BASE + "z77CaIt" + EXT, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /sample/src/main/java/com/martinappl/components/sample/adapter/SampleImageAdapter.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.sample.adapter; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | import android.widget.BaseAdapter; 6 | import android.widget.ImageView; 7 | import com.martinappl.components.sample.Data; 8 | import com.martinappl.components.sample.R; 9 | import com.squareup.picasso.Picasso; 10 | 11 | /** 12 | * Created by SemonCat on 2014/7/1. 13 | */ 14 | public class SampleImageAdapter extends BaseAdapter { 15 | 16 | private static final String TAG = SampleImageAdapter.class.getName(); 17 | 18 | private String[] ImageId = Data.URLS; 19 | 20 | @Override 21 | public int getCount() { 22 | return ImageId.length; 23 | } 24 | 25 | @Override 26 | public String getItem(int position) { 27 | return ImageId[position]; 28 | } 29 | 30 | @Override 31 | public long getItemId(int position) { 32 | return position; 33 | } 34 | 35 | @Override 36 | public View getView(int position, View convertView, ViewGroup parent) { 37 | 38 | ImageView imageView = new ImageView(parent.getContext()); 39 | 40 | Picasso.with(parent.getContext()).setLoggingEnabled(true); 41 | Picasso.with(parent.getContext()).load(getItem(position)).into(imageView); 42 | 43 | return imageView; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sample/src/main/java/com/martinappl/components/sample/MainFragment.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.sample; 2 | 3 | import android.app.Fragment; 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import com.martinappl.components.sample.adapter.SampleImageAdapter; 9 | import com.martinappl.components.ui.containers.FeatureCoverFlow; 10 | 11 | /** 12 | * Created by SemonCat on 2014/7/1. 13 | */ 14 | public class MainFragment extends Fragment { 15 | 16 | private static final String TAG = MainFragment.class.getName(); 17 | 18 | private FeatureCoverFlow mCoverFlow; 19 | 20 | public MainFragment() { 21 | } 22 | 23 | @Override 24 | public void onActivityCreated(Bundle savedInstanceState) { 25 | super.onActivityCreated(savedInstanceState); 26 | 27 | SampleImageAdapter sampleImageAdapter = new SampleImageAdapter(); 28 | mCoverFlow.setAdapter(sampleImageAdapter); 29 | 30 | } 31 | 32 | @Override 33 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 34 | Bundle savedInstanceState) { 35 | View rootView = inflater.inflate(R.layout.fragment_main, container, false); 36 | 37 | mCoverFlow = (FeatureCoverFlow) rootView.findViewById(R.id.CoverFlow); 38 | 39 | return rootView; 40 | } 41 | 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FeatureCoverFlow 2 | 3 | FeatureCoverFlow is a beautiful coverflow and simple to use it. 4 | 5 | All source code port form [MComponents](https://github.com/applm/ma-components). 6 | 7 | I only upload to Maven and didn't motify any code. 8 | 9 | CoverFlow widget

10 | 11 | ![Screenshot](https://raw.githubusercontent.com/SemonCat/FeatureCoverFlow/master/Screenshot_01.gif) 12 | 13 | 14 | ## Sample Application 15 | 16 | You can download sample apk [here](https://github.com/SemonCat/FeatureCoverFlow/raw/master/sample.apk). 17 | 18 | ## Sample Usage 19 | 20 | Add to your build.gradle 21 | 22 | ```groovy 23 | dependencies { 24 | compile "com.github.semoncat.featureCoverFlow:library:+" 25 | } 26 | ``` 27 | 28 | And use it normally like Gallery or Listview or GridView. 29 | 30 | ## License 31 | 32 | Copyright 2011, 2012 Chris Banes 33 | 34 | Licensed under the Apache License, Version 2.0 (the "License"); 35 | you may not use this file except in compliance with the License. 36 | You may obtain a copy of the License at 37 | 38 | http://www.apache.org/licenses/LICENSE-2.0 39 | 40 | Unless required by applicable law or agreed to in writing, software 41 | distributed under the License is distributed on an "AS IS" BASIS, 42 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 43 | See the License for the specific language governing permissions and 44 | limitations under the License. 45 | -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/contentbands/TileBase.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers.contentbands; 2 | 3 | /** 4 | * @author Martin Appl 5 | * 6 | * Base class for Content band datamodel. Extend to add data specific for your tiles and their behavior. 7 | * This class includes data needed for positioning of tiles inside container. 8 | */ 9 | public class TileBase { 10 | 11 | private int id; 12 | 13 | private int x; 14 | private int y; 15 | private int z; 16 | private int width; 17 | private int height; 18 | 19 | 20 | public int getX(){ 21 | return x; 22 | } 23 | 24 | public int getXRight(){ 25 | return getX() + getWidth(); 26 | } 27 | 28 | public int getY(){ 29 | return y; 30 | } 31 | 32 | public int getZ(){ 33 | return z; 34 | } 35 | 36 | public int getWidth(){ 37 | return width; 38 | } 39 | 40 | public int getHeight(){ 41 | return height; 42 | } 43 | 44 | public int getId(){ 45 | return id; 46 | } 47 | 48 | public void setId(String id) { 49 | this.id = Integer.parseInt(id); 50 | } 51 | 52 | public void setId(int id) { 53 | this.id = id; 54 | } 55 | 56 | public void setX(String x) { 57 | this.x = Integer.parseInt(x); 58 | } 59 | 60 | public void setX(int x) { 61 | this.x = x; 62 | } 63 | 64 | public void setY(String y) { 65 | this.y = Integer.parseInt(y); 66 | } 67 | 68 | public void setY(int y) { 69 | this.y = y; 70 | } 71 | 72 | public void setZ(String z) { 73 | this.z = Integer.parseInt(z); 74 | } 75 | 76 | public void setZ(int z) { 77 | this.z = z; 78 | } 79 | 80 | public void setWidth(String width) { 81 | this.width = Integer.parseInt(width); 82 | } 83 | 84 | public void setWidth(int width) { 85 | this.width = width; 86 | } 87 | 88 | public void setHeight(String height) { 89 | this.height = Integer.parseInt(height); 90 | } 91 | 92 | public void setHeight(int height) { 93 | this.height = height; 94 | } 95 | 96 | 97 | 98 | } 99 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 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 19 | # Project-wide Gradle settings. 20 | 21 | # IDE (e.g. Android Studio) users: 22 | # Settings specified in this file will override any Gradle settings 23 | # configured through the IDE. 24 | 25 | # For more details on how to configure your build environment visit 26 | # http://www.gradle.org/docs/current/userguide/build_environment.html 27 | 28 | # Specifies the JVM arguments used for the daemon process. 29 | # The setting is particularly useful for tweaking memory settings. 30 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 31 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 32 | 33 | # When configured, Gradle will run in incubating parallel mode. 34 | # This option should only be used with decoupled projects. More details, visit 35 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 36 | # org.gradle.parallel=true 37 | VERSION_NAME=0.1 38 | VERSION_CODE=1 39 | GROUP=com.github.semoncat.featureCoverFlow 40 | 41 | POM_DESCRIPTION=FeatureCoverFlow 42 | POM_URL=https://github.com/SemonCat/FeatureCoverFlow 43 | POM_SCM_URL=https://github.com/SemonCat/FeatureCoverFlow 44 | POM_SCM_CONNECTION=scm:git@github.com:SemonCat/FeatureCoverFlow.git 45 | POM_SCM_DEV_CONNECTION=scm:git@github.com:SemonCat/FeatureCoverFlow.git 46 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 47 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 48 | POM_LICENCE_DIST=repo 49 | POM_DEVELOPER_ID=semoncat 50 | POM_DEVELOPER_NAME=SemonCat 51 | 52 | ANDROID_BUILD_TARGET_SDK_VERSION=20 53 | ANDROID_BUILD_TOOLS_VERSION=20.0.0 54 | ANDROID_BUILD_SDK_VERSION=20 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /library/library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/contentbands/EndlessContentBand.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers.contentbands; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | 7 | import com.martinappl.components.general.ToolBox; 8 | 9 | /** 10 | * @author Martin Appl 11 | * DSP = device specific pixel 12 | * TODO last poster is disappearing prematurely and reappearing late. Time to time container isn't drawn after Activity initialization. 13 | */ 14 | public class EndlessContentBand extends BasicContentBand { 15 | 16 | public EndlessContentBand(Context context, AttributeSet attrs, int defStyle) { 17 | super(context, attrs, defStyle); 18 | } 19 | 20 | public EndlessContentBand(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | public EndlessContentBand(Context context) { 25 | super(context); 26 | } 27 | 28 | 29 | 30 | /** 31 | * Checks and refills empty area on the left edge of screen 32 | */ 33 | @Override 34 | protected void refillLeftSide(){ 35 | final int leftScreenEdge = getScrollX(); 36 | final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp; 37 | 38 | int dspLeftScreenEdge = pxToDsp(leftScreenEdge); 39 | if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger 40 | 41 | int end = mAdapter.getEnd(); 42 | 43 | if(dspLeftScreenEdge >= dspNextViewsRight || end == 0) return; 44 | 45 | int dspModuloLeftScreenEdge = dspLeftScreenEdge % end; 46 | int dspModuloNextViewsRight = dspNextViewsRight % end; 47 | int dspOffsetLeftScreenEdge = dspLeftScreenEdge / end; 48 | int dspOffsetNextViewsRight = dspNextViewsRight / end; 49 | 50 | if(dspModuloLeftScreenEdge < 0) { 51 | dspModuloLeftScreenEdge += end; 52 | dspOffsetLeftScreenEdge -= 1; 53 | } 54 | if(dspModuloNextViewsRight < 0){ 55 | dspModuloNextViewsRight += end; 56 | dspOffsetNextViewsRight -= 1; 57 | } 58 | 59 | View[] list; 60 | if(dspModuloLeftScreenEdge > dspModuloNextViewsRight){ 61 | View[] list1,list2; 62 | list1 = mAdapter.getViewsByRightSideRange(dspModuloLeftScreenEdge, end); 63 | list2 = mAdapter.getViewsByRightSideRange(0, dspModuloNextViewsRight); 64 | translateLayoutParams(list1, dspOffsetLeftScreenEdge); 65 | translateLayoutParams(list2, dspOffsetNextViewsRight); 66 | 67 | list = ToolBox.concatenateArray(list1,list2); 68 | } 69 | else{ 70 | list = mAdapter.getViewsByRightSideRange(dspModuloLeftScreenEdge, dspModuloNextViewsRight); 71 | translateLayoutParams(list, dspOffsetLeftScreenEdge); 72 | } 73 | 74 | int dspMostLeft = dspNextViewsRight; 75 | LayoutParams lp; 76 | for(int i=0; i < list.length; i++){ 77 | lp = (LayoutParams) list[i].getLayoutParams(); 78 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; 79 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); 80 | } 81 | 82 | if(list.length > 0){ 83 | layoutNewChildren(list); 84 | } 85 | 86 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; 87 | } 88 | 89 | private void translateLayoutParams(View[] list,int offset){ 90 | if(offset == 0 || list.length == 0) return; 91 | 92 | final int end = mAdapter.getEnd(); 93 | LayoutParams lp; 94 | 95 | for(int i=0; i= 0) dspRightScreenEdge++; //to avoid problem with rounding of values 112 | 113 | if(dspNextAddedViewsLeft >= dspRightScreenEdge || end == 0) return; 114 | 115 | int dspModuloRightScreenEdge = dspRightScreenEdge % end; 116 | int dspModuloNextAddedViewsLeft = dspNextAddedViewsLeft % end; 117 | int dspOffsetRightScreenEdge = dspRightScreenEdge / end; 118 | int dspOffsetNextAddedViewsLeft = dspNextAddedViewsLeft / end; 119 | 120 | if(dspModuloRightScreenEdge < 0) { 121 | dspModuloRightScreenEdge += end; 122 | dspOffsetRightScreenEdge -= 1; 123 | } 124 | if(dspModuloNextAddedViewsLeft < 0) { 125 | dspModuloNextAddedViewsLeft += end; 126 | dspOffsetNextAddedViewsLeft -= 1; 127 | } 128 | 129 | View[] list; 130 | if(dspModuloNextAddedViewsLeft > dspModuloRightScreenEdge){ 131 | View[] list1,list2; 132 | list1 = mAdapter.getViewsByLeftSideRange(dspModuloNextAddedViewsLeft, end); 133 | list2 = mAdapter.getViewsByLeftSideRange(0, dspModuloRightScreenEdge); 134 | translateLayoutParams(list1, dspOffsetNextAddedViewsLeft); 135 | translateLayoutParams(list2, dspOffsetRightScreenEdge); 136 | 137 | list = ToolBox.concatenateArray(list1,list2); 138 | } 139 | else{ 140 | list = mAdapter.getViewsByLeftSideRange(dspModuloNextAddedViewsLeft, dspModuloRightScreenEdge); 141 | translateLayoutParams(list, dspOffsetNextAddedViewsLeft); 142 | } 143 | 144 | int dspMostRight = 0; 145 | LayoutParams lp; 146 | for(int i=0; i < list.length; i++){ 147 | lp = (LayoutParams) list[i].getLayoutParams(); 148 | if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight(); 149 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); 150 | } 151 | 152 | if(list.length > 0){ 153 | layoutNewChildren(list); 154 | } 155 | 156 | mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; 157 | } 158 | 159 | public void fling(int velocityX, int velocityY){ 160 | mTouchState = TOUCH_STATE_FLING; 161 | final int x = getScrollX(); 162 | final int y = getScrollY(); 163 | final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; 164 | 165 | mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, 0, bottomInPixels - getHeight()); 166 | 167 | if(velocityX < 0) { 168 | mScrollDirection = DIRECTION_LEFT; 169 | } 170 | else if(velocityX > 0) { 171 | mScrollDirection = DIRECTION_RIGHT; 172 | } 173 | 174 | invalidate(); 175 | } 176 | 177 | @Override 178 | protected void scrollByDelta(int deltaX, int deltaY){ 179 | final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; 180 | final int y = getScrollY() + deltaY; 181 | 182 | if(y < 0 ) deltaY -= y; 183 | else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight()); 184 | 185 | if(deltaX < 0) { 186 | mScrollDirection = DIRECTION_LEFT; 187 | } 188 | else { 189 | mScrollDirection = DIRECTION_RIGHT; 190 | } 191 | 192 | scrollBy(deltaX, deltaY); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/HorizontalListWithRemovableItems.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers; 2 | 3 | import android.animation.Animator; 4 | import android.animation.Animator.AnimatorListener; 5 | import android.animation.AnimatorSet; 6 | import android.animation.ObjectAnimator; 7 | import android.animation.ValueAnimator; 8 | import android.animation.ValueAnimator.AnimatorUpdateListener; 9 | import android.content.Context; 10 | import android.graphics.Canvas; 11 | import android.graphics.Rect; 12 | import android.graphics.drawable.Drawable; 13 | import android.util.AttributeSet; 14 | import android.view.MotionEvent; 15 | import android.view.View; 16 | 17 | import com.martinappl.components.R; 18 | import com.martinappl.components.general.ToolBox; 19 | import com.martinappl.components.ui.containers.interfaces.IRemovableItemsAdapterComponent; 20 | import com.martinappl.components.ui.containers.interfaces.IRemoveFromAdapter; 21 | 22 | 23 | public class HorizontalListWithRemovableItems extends HorizontalList { 24 | private static final int FADE_TIME = 250; 25 | private static final int SLIDE_TIME = 350; 26 | 27 | private Drawable mRemoveItemIconDrawable = getResources().getDrawable(R.drawable.ico_delete_asset); 28 | private Drawable mIconForAnimation; 29 | 30 | private IRemovableItemsAdapterComponent mRemoveListener; 31 | 32 | private int mIconMarginTop = (int) ToolBox.dpToPixels(10, getContext()); 33 | private int mIconMarginRight = (int) ToolBox.dpToPixels(10, getContext()); 34 | private int mIconClickableMarginExtend = (int) ToolBox.dpToPixels(10, getContext()); 35 | 36 | private int mDownX; 37 | private int mDownY; 38 | private boolean isPointerDown; 39 | 40 | private View mContainingView; 41 | private int mContainingViewPosition; 42 | private int mContainingViewIndex; 43 | private Object mData; 44 | 45 | private final Rect mTempRect = new Rect(); 46 | private int mAnimationLastValue; 47 | 48 | private int mAlphaAnimationRunningOnIndex = -1; 49 | 50 | private boolean mEditable; 51 | 52 | public HorizontalListWithRemovableItems(Context context, 53 | AttributeSet attrs, int defStyle) { 54 | super(context, attrs, defStyle); 55 | } 56 | 57 | public HorizontalListWithRemovableItems(Context context, AttributeSet attrs) { 58 | this(context, attrs,0); 59 | } 60 | 61 | public HorizontalListWithRemovableItems(Context context) { 62 | this(context,null); 63 | } 64 | 65 | 66 | @Override 67 | protected void dispatchDraw(Canvas canvas) { 68 | super.dispatchDraw(canvas); 69 | 70 | if(!mEditable) return; 71 | 72 | final int c = getChildCount(); 73 | final int iw = mRemoveItemIconDrawable.getIntrinsicWidth(); 74 | final int ih = mRemoveItemIconDrawable.getIntrinsicHeight(); 75 | 76 | View v; 77 | int r,t; 78 | Drawable d; 79 | for(int i = 0; i < c; i++){ 80 | if(i != mAlphaAnimationRunningOnIndex) d = mRemoveItemIconDrawable; 81 | else d = mIconForAnimation; 82 | 83 | v = getChildAt(i); 84 | r = v.getRight(); 85 | t = v.getTop(); 86 | mTempRect.left = r-iw-mIconMarginRight; 87 | mTempRect.top = t+mIconMarginTop; 88 | mTempRect.right = r-mIconMarginRight; 89 | mTempRect.bottom = t+mIconMarginTop+ih; 90 | d.setBounds(mTempRect); 91 | d.draw(canvas); 92 | } 93 | 94 | } 95 | 96 | 97 | 98 | 99 | @Override 100 | protected View addAndMeasureChild(View child, int layoutMode) { 101 | if(layoutMode == LAYOUT_MODE_TO_BEFORE && mAlphaAnimationRunningOnIndex != -1){ 102 | mAlphaAnimationRunningOnIndex++; 103 | } 104 | 105 | return super.addAndMeasureChild(child, layoutMode); 106 | } 107 | 108 | @Override 109 | public boolean onInterceptTouchEvent(MotionEvent ev) { 110 | if(!mEditable || ev.getActionMasked() != MotionEvent.ACTION_DOWN) return super.onInterceptTouchEvent(ev); 111 | //only down event will get through initial condition 112 | 113 | final int x = (int) ev.getX(); 114 | final int y = (int) ev.getY(); 115 | 116 | final int iw = mRemoveItemIconDrawable.getIntrinsicWidth(); 117 | final int ih = mRemoveItemIconDrawable.getIntrinsicHeight(); 118 | 119 | View v; 120 | int r,t; 121 | final int c = getChildCount(); 122 | for(int i = 0; i < c; i++){ 123 | v = getChildAt(i); 124 | r = v.getRight(); 125 | t = v.getTop(); 126 | mTempRect.left = r-iw-mIconMarginRight - mIconClickableMarginExtend; 127 | mTempRect.top = t+mIconMarginTop - mIconClickableMarginExtend; 128 | mTempRect.right = r-mIconMarginRight + mIconClickableMarginExtend; 129 | mTempRect.bottom = t+mIconMarginTop+ih + mIconClickableMarginExtend; 130 | 131 | if(mTempRect.contains(getScrollX() + x, y)){ 132 | mDownX = x; 133 | mDownY = y; 134 | isPointerDown = true; 135 | 136 | mContainingView = v; 137 | mContainingViewPosition = mFirstItemPosition + i; 138 | mData = mAdapter.getItem(mContainingViewPosition); 139 | mContainingViewIndex = i; 140 | 141 | return true; 142 | } 143 | } 144 | 145 | isPointerDown = false; 146 | 147 | return super.onInterceptTouchEvent(ev); 148 | } 149 | 150 | @Override 151 | public boolean onTouchEvent(MotionEvent ev) { 152 | if(isPointerDown){ 153 | if(ev.getActionMasked() == MotionEvent.ACTION_UP){ 154 | if(ToolBox.getLineLength(ev.getX(), ev.getY(), mDownX, mDownY) < mTouchSlop && mAlphaAnimationRunningOnIndex == -1){ 155 | createRemoveAnimations(mContainingViewIndex).start(); 156 | } 157 | 158 | isPointerDown = false; 159 | } 160 | 161 | return true; 162 | } 163 | else{ 164 | return super.onTouchEvent(ev); 165 | } 166 | } 167 | 168 | // protected void refill(){ 169 | // if(mAdapter == null) return; 170 | // 171 | // final int leftScreenEdge = getScrollX(); 172 | // int rightScreenEdge = leftScreenEdge + getWidth(); 173 | // 174 | // if(mAlphaAnimationRunningOnIndex != -1) rightScreenEdge += mContainingView.getWidth(); 175 | // 176 | // removeNonVisibleViewsLeftToRight(leftScreenEdge); 177 | // removeNonVisibleViewsRightToLeft(rightScreenEdge); 178 | // 179 | // refillLeftToRight(leftScreenEdge, rightScreenEdge); 180 | // refillRightToLeft(leftScreenEdge); 181 | // } 182 | 183 | private void onRemoveAnimationFinished(int position, View view, Object item){ 184 | if(mRemoveListener == null && mAdapter instanceof IRemoveFromAdapter){ 185 | ((IRemoveFromAdapter) mAdapter).removeItemFromAdapter(position); 186 | } 187 | else if(!mRemoveListener.onItemRemove(position,view,item) && mAdapter instanceof IRemoveFromAdapter){ 188 | ((IRemoveFromAdapter) mAdapter).removeItemFromAdapter(position); 189 | } 190 | 191 | 192 | mContainingView = null; 193 | mContainingViewIndex = -1; 194 | mContainingViewPosition = -1; 195 | mData = null; 196 | } 197 | 198 | private Animator createRemoveAnimations(final int removedViewIndex){ 199 | if(mIconForAnimation == null) mIconForAnimation = mRemoveItemIconDrawable.getConstantState().newDrawable(getResources()).mutate(); 200 | mAlphaAnimationRunningOnIndex = removedViewIndex; 201 | isScrollingDisabled = true; 202 | View removed = getChildAt(removedViewIndex); 203 | 204 | ObjectAnimator fader = ObjectAnimator.ofFloat(removed, "alpha", 1f, 0f); 205 | fader.setDuration(FADE_TIME); 206 | fader.addUpdateListener(new AnimatorUpdateListener() { 207 | @Override 208 | public void onAnimationUpdate(ValueAnimator anim) { 209 | mIconForAnimation.setAlpha((int) (255*((Float)anim.getAnimatedValue()))); 210 | invalidate(mIconForAnimation.getBounds()); 211 | } 212 | }); 213 | 214 | 215 | mAnimationLastValue = 0; 216 | final int distance = removed.getWidth(); 217 | final boolean scrollDuringSlide; 218 | if(mRightEdge != NO_VALUE && getScrollX() + distance > mRightEdge - getWidth()) scrollDuringSlide = true; 219 | else scrollDuringSlide = false; 220 | 221 | ValueAnimator slider = ValueAnimator.ofInt(0,-distance); 222 | slider.addUpdateListener(new AnimatorUpdateListener() { 223 | @Override 224 | public void onAnimationUpdate(ValueAnimator anim) { 225 | final int val = (Integer) anim.getAnimatedValue(); 226 | int dx = val - mAnimationLastValue; 227 | mAnimationLastValue = val; 228 | 229 | final int c = getChildCount(); 230 | View v; 231 | for(int i=removedViewIndex+1; i < c; i++){ 232 | v = getChildAt(i); 233 | v.layout(v.getLeft()+dx, v.getTop(), v.getRight()+dx, v.getBottom()); 234 | } 235 | 236 | if(scrollDuringSlide){ 237 | if(getScrollX() + dx < 0) dx = -getScrollX(); 238 | scrollBy(dx, 0); 239 | } 240 | } 241 | }); 242 | slider.setDuration(SLIDE_TIME); 243 | 244 | // View v; 245 | // 246 | // final float distance = -removed.getWidth(); 247 | // final ArrayList anims = new ArrayList(); 248 | // ObjectAnimator slider = null; 249 | // for(int i=removedViewIndex+1; i < getChildCount(); i++){ 250 | // v = getChildAt(i); 251 | // slider = ObjectAnimator.ofFloat(v, "translationX", 0f, distance); 252 | // anims.add(slider); 253 | // } 254 | // if(slider != null) slider.addUpdateListener(new AnimatorUpdateListener() { 255 | // @Override 256 | // public void onAnimationUpdate(ValueAnimator anim) { 257 | // invalidate(); 258 | // } 259 | // }); 260 | // 261 | // AnimatorSet sliderSet = new AnimatorSet(); 262 | // sliderSet.playTogether(anims); 263 | // sliderSet.setDuration(SLIDE_TIME); 264 | 265 | final AnimatorListener listener = new AnimatorListener() { 266 | public void onAnimationStart(Animator arg0) {} 267 | public void onAnimationRepeat(Animator arg0) {} 268 | public void onAnimationCancel(Animator arg0) {} 269 | 270 | public void onAnimationEnd(Animator arg0) { 271 | mAlphaAnimationRunningOnIndex = -1; 272 | isScrollingDisabled = false; 273 | 274 | onRemoveAnimationFinished(mContainingViewPosition, mContainingView, mData); 275 | } 276 | }; 277 | 278 | 279 | AnimatorSet resultSet = new AnimatorSet(); 280 | resultSet.playSequentially(fader,slider); 281 | 282 | resultSet.addListener(listener); 283 | 284 | return resultSet; 285 | } 286 | 287 | 288 | 289 | 290 | /** 291 | * Sets icon for overlay which removes item on click 292 | */ 293 | public void setRemoveItemIcon(int resId){ 294 | mRemoveItemIconDrawable = getResources().getDrawable(resId); 295 | mIconForAnimation = null; 296 | } 297 | 298 | public void setRemoveItemIconMarginTop(int px){ 299 | mIconMarginTop = px; 300 | } 301 | 302 | public void setRemoveItemIconMarginRight(int px){ 303 | mIconMarginRight = px; 304 | } 305 | 306 | /** 307 | * 308 | * @param px 309 | */ 310 | public void setClickableMarginOfIcon(int px){ 311 | mIconClickableMarginExtend = px; 312 | } 313 | 314 | public void setRemoveItemListener(IRemovableItemsAdapterComponent listener){ 315 | mRemoveListener = listener; 316 | } 317 | 318 | public void setEditable(boolean isEditable){ 319 | mEditable = isEditable; 320 | } 321 | 322 | } 323 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/general/ToolBox.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.general; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.lang.reflect.Array; 5 | import java.nio.ByteBuffer; 6 | import java.security.InvalidParameterException; 7 | import java.text.ParseException; 8 | import java.text.SimpleDateFormat; 9 | import java.util.ArrayList; 10 | import java.util.Date; 11 | import java.util.HashSet; 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.Locale; 15 | import java.util.Set; 16 | 17 | import android.content.Context; 18 | import android.content.pm.PackageInfo; 19 | import android.content.pm.PackageManager.NameNotFoundException; 20 | import android.content.res.Resources; 21 | import android.graphics.Bitmap; 22 | import android.graphics.Canvas; 23 | import android.graphics.Color; 24 | import android.graphics.PointF; 25 | import android.util.FloatMath; 26 | import android.util.Log; 27 | import android.util.TypedValue; 28 | import android.view.View; 29 | 30 | 31 | /** 32 | * @author Martin Appl 33 | * Set of handy mostly mathematical, geometric and generic java static methods 34 | */ 35 | public abstract class ToolBox { 36 | //global constants 37 | private static final String TAG = "ToolBox"; 38 | 39 | 40 | 41 | /** 42 | * Get length of line between points a and b. 43 | * @param a point 44 | * @param b point 45 | * @return length 46 | */ 47 | public static float getLineLength(PointF a,PointF b){ 48 | float vx = b.x - a.x; 49 | float vy = b.y - a.y; 50 | return FloatMath.sqrt(vx*vx + vy*vy); 51 | } 52 | /** 53 | * Get length of line between points A and B. 54 | * @param ax point A x coordinate 55 | * @param ay point A y coordinate 56 | * @param bx point B x coordinate 57 | * @param by point B y coordinate 58 | * @return length 59 | */ 60 | public static float getLineLength(float ax,float ay, float bx,float by){ 61 | float vx = bx - ax; 62 | float vy = by - ay; 63 | return FloatMath.sqrt(vx*vx + vy*vy); 64 | } 65 | 66 | /** 67 | * Get length of vector 68 | * @param vx vector x component 69 | * @param vy vector y component 70 | * @return length 71 | */ 72 | public static float getVectorLength(float vx,float vy){ 73 | return FloatMath.sqrt(vx*vx + vy*vy); 74 | } 75 | 76 | public static float getVectorLength(PointF v){ 77 | return FloatMath.sqrt(v.x*v.x + v.y*v.y); 78 | } 79 | 80 | /** 81 | * Compute intersection point of two infinite lines. Each line is specified by two points 82 | * @param a1 line a 83 | * @param a2 line a 84 | * @param b1 line b 85 | * @param b2 line b 86 | * @return Intersection point 87 | */ 88 | public static PointF getLinesIntersection(PointF a1,PointF a2,PointF b1,PointF b2){ 89 | return getLinesIntersection(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y); 90 | } 91 | 92 | /** 93 | * Compute intersection point of two infinite lines. Each line is specified by two points 94 | * @param x1 line a point 1 95 | * @param y1 line a point 1 96 | * @param x2 line a point 2 97 | * @param y2 line a point 2 98 | * @param x3 line b point 1 99 | * @param y3 line b point 1 100 | * @param x4 line b point 2 101 | * @param y4 line b point 2 102 | * @return Intersection point 103 | */ 104 | public static PointF getLinesIntersection( float x1,float y1, float x2, float y2, 105 | float x3,float y3,float x4,float y4) 106 | { 107 | float a1 = y2 - y1; 108 | float b1 = x1 - x2; 109 | float c1 = x2*y1 - x1*y2; //a1*x + b1*y + c1 = 0 is line a 110 | 111 | float a2 = y4-y3; 112 | float b2 = x3-x4; 113 | float c2 = x4*y3 - x3*y4; // a2*x + b2*y + c2 = 0 is line b 114 | 115 | float d = a1*b2 - a2*b1; 116 | if(d == 0) { 117 | throw new InvalidParameterException("Intersection cant be found, lines are paralel."); 118 | } 119 | 120 | float x = (b1*c2 - b2*c1)/d; 121 | float y = (a2*c1 - a1*c2)/d; 122 | return new PointF(x,y); 123 | } 124 | /** 125 | * 126 | * @param num Number to round 127 | * @return value rounded to integer 128 | */ 129 | public static int roundToInt(float num){ 130 | if((num - Math.floor(num)) > 0.5f){ //round up 131 | return (int) Math.ceil(num); 132 | } 133 | else{ //round down 134 | return (int) Math.floor(num); 135 | } 136 | } 137 | 138 | 139 | /** 140 | * Draw the view into a bitmap using drawing cache. 141 | */ 142 | public static Bitmap getViewBitmap(View v) { 143 | v.clearFocus(); 144 | v.setPressed(false); 145 | 146 | boolean willNotCache = v.willNotCacheDrawing(); 147 | v.setWillNotCacheDrawing(false); 148 | 149 | // Reset the drawing cache background color to fully transparent 150 | // for the duration of this operation 151 | int color = v.getDrawingCacheBackgroundColor(); 152 | v.setDrawingCacheBackgroundColor(0); 153 | 154 | if (color != 0) { 155 | v.destroyDrawingCache(); 156 | } 157 | v.buildDrawingCache(); 158 | Bitmap cacheBitmap = v.getDrawingCache(); 159 | if (cacheBitmap == null) { 160 | Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); 161 | return null; 162 | } 163 | 164 | Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); 165 | 166 | // Restore the view 167 | v.destroyDrawingCache(); 168 | v.setWillNotCacheDrawing(willNotCache); 169 | v.setDrawingCacheBackgroundColor(color); 170 | 171 | return bitmap; 172 | } 173 | 174 | public static Bitmap getViewBitmapNoCache(View v,int w,int h){ 175 | Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); 176 | Canvas c = new Canvas(b); 177 | v.layout(0, 0, w, h); 178 | v.draw(c); 179 | return b; 180 | } 181 | 182 | public static Bitmap doInvert(Bitmap src) { 183 | // create new bitmap with the same settings as source bitmap 184 | Bitmap bmOut = Bitmap.createBitmap(src.getWidth(), src.getHeight(), src.getConfig()); 185 | // color info 186 | int A, R, G, B; 187 | int pixelColor; 188 | // image size 189 | int height = src.getHeight(); 190 | int width = src.getWidth(); 191 | 192 | // scan through every pixel 193 | for (int y = 0; y < height; y++) 194 | { 195 | for (int x = 0; x < width; x++) 196 | { 197 | // get one pixel 198 | pixelColor = src.getPixel(x, y); 199 | // saving alpha channel 200 | A = Color.alpha(pixelColor); 201 | // inverting byte for each R/G/B channel 202 | R = 255 - Color.red(pixelColor); 203 | G = 255 - Color.green(pixelColor); 204 | B = 255 - Color.blue(pixelColor); 205 | // set newly-inverted pixel to output image 206 | bmOut.setPixel(x, y, Color.argb(A, R, G, B)); 207 | } 208 | } 209 | 210 | // return final bitmap 211 | return bmOut; 212 | } 213 | 214 | public static float dpToPixels(int dp, Context c){ 215 | final Resources r = c.getResources(); 216 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); 217 | } 218 | 219 | /** 220 | * 221 | * @param center Circle center 222 | * @param radius Circle radius 223 | * @param touchPoint 224 | * @return Point on line from center to touchpoint intersecting circle 225 | */ 226 | public static PointF getCircleIntersection(PointF center,float radius, PointF touchPoint){ 227 | final float r = radius; 228 | 229 | //center must be on (0,0), move second point accordingly 230 | final float x2 = touchPoint.x - center.x; 231 | final float y2 = touchPoint.y - center.y; 232 | 233 | //dx = x2 - x1, x1 is in center => dx = x2-0 234 | final float dx = x2; 235 | final float dy = y2; 236 | 237 | final float dr = FloatMath.sqrt(dx*dx + dy*dy); 238 | 239 | final PointF res1 = new PointF(); 240 | final PointF res2 = new PointF(); 241 | 242 | res1.x = (sgn(dy)*dx*r*dr)/(dr*dr); 243 | res2.x = (-1 * sgn(dy)*dx*r*dr)/(dr*dr); 244 | 245 | res1.y = (Math.abs(dy)*r*dr)/(dr*dr); 246 | res2.y = (-1 * Math.abs(dy)*r*dr)/(dr*dr); 247 | 248 | //move result back to normal coordinates 249 | res1.x = res1.x + center.x; 250 | res1.y = res1.y + center.y; 251 | res2.x = res2.x + center.x; 252 | res2.y = res2.y + center.y; 253 | 254 | //find which of two results is on same side of circle as touchpoint 255 | if(getLineLength(res1, touchPoint) < getLineLength(res2, touchPoint)){ 256 | return res1; 257 | } 258 | else{ 259 | return res2; 260 | } 261 | } 262 | 263 | public static PointF getCircleIntersection(PointF center,float radius, float x, float y){ 264 | return getCircleIntersection(center, radius, new PointF(x, y)); 265 | } 266 | 267 | /** 268 | * Test if point lies inside the circle 269 | * @param center Center of the circle 270 | * @param radius Radius of te circle 271 | * @param x x coordinate of tested point 272 | * @param y y coordinate of tested point 273 | * @return 274 | */ 275 | public static boolean isInsideCircle(PointF center, float radius, float x, float y){ 276 | final float dx = x - center.x; 277 | final float dy = y - center.y; 278 | 279 | if(dx*dx + dy*dy < radius*radius){ 280 | return true; 281 | } 282 | else{ 283 | return false; 284 | } 285 | } 286 | /** 287 | * Test if point lies outside the circle 288 | * @param center Center of the circle 289 | * @param radius Radius of te circle 290 | * @param x x coordinate of tested point 291 | * @param y y coordinate of tested point 292 | * @return 293 | */ 294 | public static boolean isOutsideCircle(PointF center, float radius, float x, float y){ 295 | final float dx = x - center.x; 296 | final float dy = y - center.y; 297 | 298 | if(dx*dx + dy*dy > radius*radius){ 299 | return true; 300 | } 301 | else{ 302 | return false; 303 | } 304 | } 305 | 306 | private static float sgn(float x){ 307 | if(x < 0) return -1; 308 | else return 1; 309 | } 310 | 311 | 312 | public static PointF getNormalizedVector(PointF v){ 313 | float l = getVectorLength(v); 314 | final PointF r = new PointF(); 315 | r.x = v.x / l; 316 | r.y = v.y / l; 317 | return r; 318 | } 319 | 320 | public static float dotProduct(PointF a, PointF b){ 321 | return a.x*b.x + a.y*b.y; 322 | } 323 | 324 | public static float getVectorAngle(float x, float y){ 325 | final float l = getVectorLength(x,y); 326 | final float cos = x/l; 327 | final float sin = y/l; 328 | // final float ac = (float) Math.acos(cos); 329 | final float as = (float) Math.asin(sin); 330 | 331 | if(cos > 0 && sin >= 0){ //quadrant I 332 | return as; 333 | } 334 | else if(sin > 0 && cos <= 0){ //quadrant II 335 | return (float) (Math.PI - as); 336 | } 337 | else if(sin <= 0 && cos < 0){ //quadrant III 338 | return (float) (Math.PI - as); 339 | } 340 | else if(sin < 0 && cos >= 0){// quadrant IV 341 | return (float) (2*Math.PI + as); 342 | } 343 | 344 | return as; 345 | 346 | } 347 | 348 | public static void rgbToHsl(int rgb, float[] hsl) { 349 | float r = ((0x00ff0000 & rgb) >> 16) / 255.f; 350 | float g = ((0x0000ff00 & rgb) >> 8) / 255.f; 351 | float b = ((0x000000ff & rgb)) / 255.f; 352 | float max = Math.max(Math.max(r, g), b); 353 | float min = Math.min(Math.min(r, g), b); 354 | float c = max - min; 355 | 356 | float h_ = 0.f; 357 | if (c == 0) { 358 | h_ = 0; 359 | } else if (max == r) { 360 | h_ = (float)(g-b) / c; 361 | if (h_ < 0) h_ += 6.f; 362 | } else if (max == g) { 363 | h_ = (float)(b-r) / c + 2.f; 364 | } else if (max == b) { 365 | h_ = (float)(r-g) / c + 4.f; 366 | } 367 | float h = 60.f * h_; 368 | 369 | float l = (max + min) * 0.5f; 370 | 371 | float s; 372 | if (c == 0) { 373 | s = 0.f; 374 | } else { 375 | s = c / (1 - Math.abs(2.f * l - 1.f)); 376 | } 377 | 378 | hsl[0] = h; 379 | hsl[1] = s; 380 | hsl[2] = l; 381 | } 382 | 383 | 384 | 385 | // public static void rgbToHsb2(int rgb, float[] hsl) { 386 | // final float r = ((0x00ff0000 & rgb) >> 16) / 255.f; 387 | // final float g = ((0x0000ff00 & rgb) >> 8) / 255.f; 388 | // final float b = ((0x000000ff & rgb)) / 255.f; 389 | // final float max = Math.max(Math.max(r, g), b); 390 | // final float min = Math.min(Math.min(r, g), b); 391 | // 392 | // final float alpha = (2*r - g - b)/2f; 393 | // final double beta = (Math.sqrt(3)/2*(g-b)); 394 | //// final double c2 = Math.sqrt(alpha*alpha + beta*beta); 395 | // final float c = max - min; 396 | // 397 | // final float h = (float) Math.atan2(beta,alpha); 398 | // 399 | // float l = (max + min)/2; 400 | // 401 | // float s; 402 | // if (c == 0) { 403 | // s = 0.f; 404 | // } else { 405 | // s = (float) (c/max); 406 | // } 407 | // 408 | // hsl[0] = h; 409 | // hsl[1] = s; 410 | // hsl[2] = l; 411 | // } 412 | 413 | public static int hslToRgb(float[] hsl) { 414 | float h = hsl[0]; 415 | float s = hsl[1]; 416 | float l = hsl[2]; 417 | 418 | float c = (1 - Math.abs(2.f * l - 1.f)) * s; 419 | float h_ = h / 60.f; 420 | float h_mod2 = h_; 421 | if (h_mod2 >= 4.f) h_mod2 -= 4.f; 422 | else if (h_mod2 >= 2.f) h_mod2 -= 2.f; 423 | 424 | float x = c * (1 - Math.abs(h_mod2 - 1)); 425 | float r_, g_, b_; 426 | if (h_ < 1) { r_ = c; g_ = x; b_ = 0; } 427 | else if (h_ < 2) { r_ = x; g_ = c; b_ = 0; } 428 | else if (h_ < 3) { r_ = 0; g_ = c; b_ = x; } 429 | else if (h_ < 4) { r_ = 0; g_ = x; b_ = c; } 430 | else if (h_ < 5) { r_ = x; g_ = 0; b_ = c; } 431 | else { r_ = c; g_ = 0; b_ = x; } 432 | 433 | float m = l - (0.5f * c); 434 | int r = (int)((r_ + m) * (255.f) + 0.5f); 435 | int g = (int)((g_ + m) * (255.f) + 0.5f); 436 | int b = (int)((b_ + m) * (255.f) + 0.5f); 437 | return r << 16 | g << 8 | b; 438 | } 439 | 440 | 441 | public static String humanReadableByteCount(long bytes, boolean si) { 442 | int unit = si ? 1000 : 1024; 443 | if (bytes < unit) return bytes + " B"; 444 | int exp = (int) (Math.log(bytes) / Math.log(unit)); 445 | String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); 446 | return String.format(Locale.US,"%.1f %sB", bytes / Math.pow(unit, exp), pre); 447 | } 448 | 449 | /** 450 | * @return Application's version code from the {@code PackageManager}. 451 | */ 452 | public static int getAppVersion(Context context) { 453 | try { 454 | PackageInfo packageInfo = context.getPackageManager() 455 | .getPackageInfo(context.getPackageName(), 0); 456 | return packageInfo.versionCode; 457 | } catch (NameNotFoundException e) { 458 | // should never happen 459 | throw new RuntimeException("Could not get package name: " + e); 460 | } 461 | } 462 | 463 | public static String convertStreamToString(java.io.InputStream is) { 464 | java.util.Scanner s = new java.util.Scanner(is,"UTF-8").useDelimiter("\\A"); 465 | String r = s.hasNext() ? s.next() : ""; 466 | s.close(); 467 | return r; 468 | } 469 | 470 | 471 | 472 | public static ArrayList union(List list1, List list2) { 473 | Set set = new HashSet(); 474 | 475 | set.addAll(list1); 476 | set.addAll(list2); 477 | 478 | return new ArrayList(set); 479 | } 480 | 481 | public static ArrayList intersection(List list1, List list2) { 482 | ArrayList list = new ArrayList(); 483 | 484 | for (T t : list1) { 485 | if(list2.contains(t)) { 486 | list.add(t); 487 | } 488 | } 489 | 490 | return list; 491 | } 492 | 493 | public static T[] concatenateArray (T[] A, T[] B) { 494 | int aLen = A.length; 495 | int bLen = B.length; 496 | 497 | @SuppressWarnings("unchecked") 498 | T[] C = (T[]) Array.newInstance(A.getClass().getComponentType(), aLen+bLen); 499 | System.arraycopy(A, 0, C, 0, aLen); 500 | System.arraycopy(B, 0, C, aLen, bLen); 501 | 502 | return C; 503 | } 504 | 505 | public static class ViewCache { 506 | private final LinkedList> mCachedItemViews = new LinkedList>(); 507 | 508 | /** 509 | * Check if list of weak references has any view still in memory to offer for recycling 510 | * @return cached view 511 | */ 512 | public T getCachedView(){ 513 | if (mCachedItemViews.size() != 0) { 514 | T v; 515 | do{ 516 | v = mCachedItemViews.removeFirst().get(); 517 | } 518 | while(v == null && mCachedItemViews.size() != 0); 519 | return v; 520 | } 521 | return null; 522 | } 523 | 524 | public void cacheView(T v){ 525 | WeakReference ref = new WeakReference(v); 526 | mCachedItemViews.addLast(ref); 527 | } 528 | } 529 | 530 | /** 531 | * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation 532 | * @param MAC 533 | * @return 534 | */ 535 | public static byte[] MACtobyteConverter(String MAC) { 536 | // first remove all ":" from MAC address 537 | MAC = MAC.replaceAll(":", ""); 538 | Log.d("ComponentLibrary.ToolBox", "MACtobyteConverter input ="+MAC); 539 | 540 | // now convert to byte array 541 | int len = MAC.length(); 542 | byte[] data = new byte[len / 2]; 543 | for (int i = 0; i < len; i += 2) { 544 | data[i / 2] = (byte) ((Character.digit(MAC.charAt(i), 16) << 4) 545 | + Character.digit(MAC.charAt(i+1), 16)); 546 | } 547 | return data; 548 | } 549 | 550 | /** 551 | * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation 552 | * @param MAC 553 | * @return 554 | */ 555 | public static byte[] IMEItobyteConverter(String IMEI) { 556 | // now convert to byte array 557 | long imeiInLong; 558 | try { 559 | imeiInLong = Long.parseLong(IMEI); 560 | } 561 | catch (NumberFormatException e) { 562 | Log.w(TAG, "Can't convert IMEI to byte, Illegal number format"); 563 | return null; 564 | } 565 | byte[] data = ByteBuffer.allocate(8).putLong(imeiInLong).array(); 566 | return data; 567 | } 568 | 569 | public static String formatISODate(String isoDate){ 570 | SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",Locale.US); 571 | Date dtIn; 572 | try { 573 | dtIn = inFormat.parse(isoDate); //where dateString is a date in ISO-8601 format 574 | SimpleDateFormat outFormat = new SimpleDateFormat("dd.MM.yyyy",Locale.US); 575 | return outFormat.format(dtIn); 576 | } catch (ParseException e) { 577 | Log.e(TAG, "Parse date error",e); 578 | } catch (NullPointerException e) { 579 | Log.e(TAG, "Parse NullPointerException error",e); 580 | } 581 | return isoDate; 582 | } 583 | } 584 | 585 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/general/Validate.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.general; 2 | 3 | /* 4 | * Licensed to the Apache Software Foundation (ASF) under one or more 5 | * contributor license agreements. See the NOTICE file distributed with 6 | * this work for additional information regarding copyright ownership. 7 | * The ASF licenses this file to You under the Apache License, Version 2.0 8 | * (the "License"); you may not use this file except in compliance with 9 | * the License. You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | import java.util.Collection; 21 | import java.util.Iterator; 22 | import java.util.Map; 23 | 24 | /** 25 | *

This class assists in validating arguments.

26 | * 27 | *

The class is based along the lines of JUnit. If an argument value is 28 | * deemed invalid, an IllegalArgumentException is thrown. For example:

29 | * 30 | *
 31 |  * Validate.isTrue( i > 0, "The value must be greater than zero: ", i);
 32 |  * Validate.notNull( surname, "The surname must not be null");
 33 |  * 
34 | * 35 | * @author Apache Software Foundation 36 | * @author Ola Berg 37 | * @author Gary Gregory 38 | * @author Norm Deane 39 | * @since 2.0 40 | * @version $Id: Validate.java 1057051 2011-01-09 23:15:51Z sebb $ 41 | */ 42 | public class Validate { 43 | // Validate has no dependencies on other classes in Commons Lang at present 44 | 45 | /** 46 | * Constructor. This class should not normally be instantiated. 47 | */ 48 | public Validate() { 49 | super(); 50 | } 51 | 52 | // isTrue 53 | //--------------------------------------------------------------------------------- 54 | /** 55 | *

Validate that the argument condition is true; otherwise 56 | * throwing an exception with the specified message. This method is useful when 57 | * validating according to an arbitrary boolean expression, such as validating an 58 | * object or using your own custom validation expression.

59 | * 60 | *
Validate.isTrue( myObject.isOk(), "The object is not OK: ", myObject);
61 | * 62 | *

For performance reasons, the object value is passed as a separate parameter and 63 | * appended to the exception message only in the case of an error.

64 | * 65 | * @param expression the boolean expression to check 66 | * @param message the exception message if invalid 67 | * @param value the value to append to the message when invalid 68 | * @throws IllegalArgumentException if expression is false 69 | */ 70 | public static void isTrue(boolean expression, String message, Object value) { 71 | if (expression == false) { 72 | throw new IllegalArgumentException(message + value); 73 | } 74 | } 75 | 76 | /** 77 | *

Validate that the argument condition is true; otherwise 78 | * throwing an exception with the specified message. This method is useful when 79 | * validating according to an arbitrary boolean expression, such as validating a 80 | * primitive number or using your own custom validation expression.

81 | * 82 | *
Validate.isTrue(i > 0.0, "The value must be greater than zero: ", i);
83 | * 84 | *

For performance reasons, the long value is passed as a separate parameter and 85 | * appended to the exception message only in the case of an error.

86 | * 87 | * @param expression the boolean expression to check 88 | * @param message the exception message if invalid 89 | * @param value the value to append to the message when invalid 90 | * @throws IllegalArgumentException if expression is false 91 | */ 92 | public static void isTrue(boolean expression, String message, long value) { 93 | if (expression == false) { 94 | throw new IllegalArgumentException(message + value); 95 | } 96 | } 97 | 98 | /** 99 | *

Validate that the argument condition is true; otherwise 100 | * throwing an exception with the specified message. This method is useful when 101 | * validating according to an arbitrary boolean expression, such as validating a 102 | * primitive number or using your own custom validation expression.

103 | * 104 | *
Validate.isTrue(d > 0.0, "The value must be greater than zero: ", d);
105 | * 106 | *

For performance reasons, the double value is passed as a separate parameter and 107 | * appended to the exception message only in the case of an error.

108 | * 109 | * @param expression the boolean expression to check 110 | * @param message the exception message if invalid 111 | * @param value the value to append to the message when invalid 112 | * @throws IllegalArgumentException if expression is false 113 | */ 114 | public static void isTrue(boolean expression, String message, double value) { 115 | if (expression == false) { 116 | throw new IllegalArgumentException(message + value); 117 | } 118 | } 119 | 120 | /** 121 | *

Validate that the argument condition is true; otherwise 122 | * throwing an exception with the specified message. This method is useful when 123 | * validating according to an arbitrary boolean expression, such as validating a 124 | * primitive number or using your own custom validation expression.

125 | * 126 | *
127 |      * Validate.isTrue( (i > 0), "The value must be greater than zero");
128 |      * Validate.isTrue( myObject.isOk(), "The object is not OK");
129 |      * 
130 | * 131 | * @param expression the boolean expression to check 132 | * @param message the exception message if invalid 133 | * @throws IllegalArgumentException if expression is false 134 | */ 135 | public static void isTrue(boolean expression, String message) { 136 | if (expression == false) { 137 | throw new IllegalArgumentException(message); 138 | } 139 | } 140 | 141 | /** 142 | *

Validate that the argument condition is true; otherwise 143 | * throwing an exception. This method is useful when validating according 144 | * to an arbitrary boolean expression, such as validating a 145 | * primitive number or using your own custom validation expression.

146 | * 147 | *
148 |      * Validate.isTrue(i > 0);
149 |      * Validate.isTrue(myObject.isOk());
150 | * 151 | *

The message of the exception is "The validated expression is 152 | * false".

153 | * 154 | * @param expression the boolean expression to check 155 | * @throws IllegalArgumentException if expression is false 156 | */ 157 | public static void isTrue(boolean expression) { 158 | if (expression == false) { 159 | throw new IllegalArgumentException("The validated expression is false"); 160 | } 161 | } 162 | 163 | // notNull 164 | //--------------------------------------------------------------------------------- 165 | 166 | /** 167 | *

Validate that the specified argument is not null; 168 | * otherwise throwing an exception. 169 | * 170 | *

Validate.notNull(myObject);
171 | * 172 | *

The message of the exception is "The validated object is 173 | * null".

174 | * 175 | * @param object the object to check 176 | * @throws IllegalArgumentException if the object is null 177 | */ 178 | public static void notNull(Object object) { 179 | notNull(object, "The validated object is null"); 180 | } 181 | 182 | /** 183 | *

Validate that the specified argument is not null; 184 | * otherwise throwing an exception with the specified message. 185 | * 186 | *

Validate.notNull(myObject, "The object must not be null");
187 | * 188 | * @param object the object to check 189 | * @param message the exception message if invalid 190 | */ 191 | public static void notNull(Object object, String message) { 192 | if (object == null) { 193 | throw new IllegalArgumentException(message); 194 | } 195 | } 196 | 197 | // notEmpty array 198 | //--------------------------------------------------------------------------------- 199 | 200 | /** 201 | *

Validate that the specified argument array is neither null 202 | * nor a length of zero (no elements); otherwise throwing an exception 203 | * with the specified message. 204 | * 205 | *

Validate.notEmpty(myArray, "The array must not be empty");
206 | * 207 | * @param array the array to check 208 | * @param message the exception message if invalid 209 | * @throws IllegalArgumentException if the array is empty 210 | */ 211 | public static void notEmpty(Object[] array, String message) { 212 | if (array == null || array.length == 0) { 213 | throw new IllegalArgumentException(message); 214 | } 215 | } 216 | 217 | /** 218 | *

Validate that the specified argument array is neither null 219 | * nor a length of zero (no elements); otherwise throwing an exception. 220 | * 221 | *

Validate.notEmpty(myArray);
222 | * 223 | *

The message in the exception is "The validated array is 224 | * empty". 225 | * 226 | * @param array the array to check 227 | * @throws IllegalArgumentException if the array is empty 228 | */ 229 | public static void notEmpty(Object[] array) { 230 | notEmpty(array, "The validated array is empty"); 231 | } 232 | 233 | // notEmpty collection 234 | //--------------------------------------------------------------------------------- 235 | 236 | /** 237 | *

Validate that the specified argument collection is neither null 238 | * nor a size of zero (no elements); otherwise throwing an exception 239 | * with the specified message. 240 | * 241 | *

Validate.notEmpty(myCollection, "The collection must not be empty");
242 | * 243 | * @param collection the collection to check 244 | * @param message the exception message if invalid 245 | * @throws IllegalArgumentException if the collection is empty 246 | */ 247 | public static void notEmpty(Collection collection, String message) { 248 | if (collection == null || collection.size() == 0) { 249 | throw new IllegalArgumentException(message); 250 | } 251 | } 252 | 253 | /** 254 | *

Validate that the specified argument collection is neither null 255 | * nor a size of zero (no elements); otherwise throwing an exception. 256 | * 257 | *

Validate.notEmpty(myCollection);
258 | * 259 | *

The message in the exception is "The validated collection is 260 | * empty".

261 | * 262 | * @param collection the collection to check 263 | * @throws IllegalArgumentException if the collection is empty 264 | */ 265 | public static void notEmpty(Collection collection) { 266 | notEmpty(collection, "The validated collection is empty"); 267 | } 268 | 269 | // notEmpty map 270 | //--------------------------------------------------------------------------------- 271 | 272 | /** 273 | *

Validate that the specified argument map is neither null 274 | * nor a size of zero (no elements); otherwise throwing an exception 275 | * with the specified message. 276 | * 277 | *

Validate.notEmpty(myMap, "The map must not be empty");
278 | * 279 | * @param map the map to check 280 | * @param message the exception message if invalid 281 | * @throws IllegalArgumentException if the map is empty 282 | */ 283 | public static void notEmpty(Map map, String message) { 284 | if (map == null || map.size() == 0) { 285 | throw new IllegalArgumentException(message); 286 | } 287 | } 288 | 289 | /** 290 | *

Validate that the specified argument map is neither null 291 | * nor a size of zero (no elements); otherwise throwing an exception. 292 | * 293 | *

Validate.notEmpty(myMap);
294 | * 295 | *

The message in the exception is "The validated map is 296 | * empty".

297 | * 298 | * @param map the map to check 299 | * @throws IllegalArgumentException if the map is empty 300 | * @see #notEmpty(Map, String) 301 | */ 302 | public static void notEmpty(Map map) { 303 | notEmpty(map, "The validated map is empty"); 304 | } 305 | 306 | // notEmpty string 307 | //--------------------------------------------------------------------------------- 308 | 309 | /** 310 | *

Validate that the specified argument string is 311 | * neither null nor a length of zero (no characters); 312 | * otherwise throwing an exception with the specified message. 313 | * 314 | *

Validate.notEmpty(myString, "The string must not be empty");
315 | * 316 | * @param string the string to check 317 | * @param message the exception message if invalid 318 | * @throws IllegalArgumentException if the string is empty 319 | */ 320 | public static void notEmpty(String string, String message) { 321 | if (string == null || string.length() == 0) { 322 | throw new IllegalArgumentException(message); 323 | } 324 | } 325 | 326 | /** 327 | *

Validate that the specified argument string is 328 | * neither null nor a length of zero (no characters); 329 | * otherwise throwing an exception with the specified message. 330 | * 331 | *

Validate.notEmpty(myString);
332 | * 333 | *

The message in the exception is "The validated 334 | * string is empty".

335 | * 336 | * @param string the string to check 337 | * @throws IllegalArgumentException if the string is empty 338 | */ 339 | public static void notEmpty(String string) { 340 | notEmpty(string, "The validated string is empty"); 341 | } 342 | 343 | // notNullElements array 344 | //--------------------------------------------------------------------------------- 345 | 346 | /** 347 | *

Validate that the specified argument array is neither 348 | * null nor contains any elements that are null; 349 | * otherwise throwing an exception with the specified message. 350 | * 351 | *

Validate.noNullElements(myArray, "The array contain null at position %d");
352 | * 353 | *

If the array is null, then the message in the exception 354 | * is "The validated object is null".

355 | * 356 | * @param array the array to check 357 | * @param message the exception message if the collection has null elements 358 | * @throws IllegalArgumentException if the array is null or 359 | * an element in the array is null 360 | */ 361 | public static void noNullElements(Object[] array, String message) { 362 | Validate.notNull(array); 363 | for (int i = 0; i < array.length; i++) { 364 | if (array[i] == null) { 365 | throw new IllegalArgumentException(message); 366 | } 367 | } 368 | } 369 | 370 | /** 371 | *

Validate that the specified argument array is neither 372 | * null nor contains any elements that are null; 373 | * otherwise throwing an exception. 374 | * 375 | *

Validate.noNullElements(myArray);
376 | * 377 | *

If the array is null, then the message in the exception 378 | * is "The validated object is null".

379 | * 380 | *

If the array has a null element, then the message in the 381 | * exception is "The validated array contains null element at index: 382 | * " followed by the index.

383 | * 384 | * @param array the array to check 385 | * @throws IllegalArgumentException if the array is null or 386 | * an element in the array is null 387 | */ 388 | public static void noNullElements(Object[] array) { 389 | Validate.notNull(array); 390 | for (int i = 0; i < array.length; i++) { 391 | if (array[i] == null) { 392 | throw new IllegalArgumentException("The validated array contains null element at index: " + i); 393 | } 394 | } 395 | } 396 | 397 | // notNullElements collection 398 | //--------------------------------------------------------------------------------- 399 | 400 | /** 401 | *

Validate that the specified argument collection is neither 402 | * null nor contains any elements that are null; 403 | * otherwise throwing an exception with the specified message. 404 | * 405 | *

Validate.noNullElements(myCollection, "The collection contains null elements");
406 | * 407 | *

If the collection is null, then the message in the exception 408 | * is "The validated object is null".

409 | * 410 | * 411 | * @param collection the collection to check 412 | * @param message the exception message if the collection has 413 | * @throws IllegalArgumentException if the collection is null or 414 | * an element in the collection is null 415 | */ 416 | public static void noNullElements(Collection collection, String message) { 417 | Validate.notNull(collection); 418 | for (Iterator it = collection.iterator(); it.hasNext();) { 419 | if (it.next() == null) { 420 | throw new IllegalArgumentException(message); 421 | } 422 | } 423 | } 424 | 425 | /** 426 | *

Validate that the specified argument collection is neither 427 | * null nor contains any elements that are null; 428 | * otherwise throwing an exception. 429 | * 430 | *

Validate.noNullElements(myCollection);
431 | * 432 | *

If the collection is null, then the message in the exception 433 | * is "The validated object is null".

434 | * 435 | *

If the collection has a null element, then the message in the 436 | * exception is "The validated collection contains null element at index: 437 | * " followed by the index.

438 | * 439 | * @param collection the collection to check 440 | * @throws IllegalArgumentException if the collection is null or 441 | * an element in the collection is null 442 | */ 443 | public static void noNullElements(Collection collection) { 444 | Validate.notNull(collection); 445 | int i = 0; 446 | for (Iterator it = collection.iterator(); it.hasNext(); i++) { 447 | if (it.next() == null) { 448 | throw new IllegalArgumentException("The validated collection contains null element at index: " + i); 449 | } 450 | } 451 | } 452 | 453 | /** 454 | *

Validate an argument, throwing IllegalArgumentException 455 | * if the argument collection is null or has elements that 456 | * are not of type clazz or a subclass.

457 | * 458 | *
459 |      * Validate.allElementsOfType(collection, String.class, "Collection has invalid elements");
460 |      * 
461 | * 462 | * @param collection the collection to check, not null 463 | * @param clazz the Class which the collection's elements are expected to be, not null 464 | * @param message the exception message if the Collection has elements not of type clazz 465 | * @since 2.1 466 | */ 467 | public static void allElementsOfType(Collection collection, Class clazz, String message) { 468 | Validate.notNull(collection); 469 | Validate.notNull(clazz); 470 | for (Iterator it = collection.iterator(); it.hasNext(); ) { 471 | if (clazz.isInstance(it.next()) == false) { 472 | throw new IllegalArgumentException(message); 473 | } 474 | } 475 | } 476 | 477 | /** 478 | *

479 | * Validate an argument, throwing IllegalArgumentException if the argument collection is 480 | * null or has elements that are not of type clazz or a subclass. 481 | *

482 | * 483 | *
484 |      * Validate.allElementsOfType(collection, String.class);
485 |      * 
486 | * 487 | *

488 | * The message in the exception is 'The validated collection contains an element not of type clazz at index: '. 489 | *

490 | * 491 | * @param collection the collection to check, not null 492 | * @param clazz the Class which the collection's elements are expected to be, not null 493 | * @since 2.1 494 | */ 495 | public static void allElementsOfType(Collection collection, Class clazz) { 496 | Validate.notNull(collection); 497 | Validate.notNull(clazz); 498 | int i = 0; 499 | for (Iterator it = collection.iterator(); it.hasNext(); i++) { 500 | if (clazz.isInstance(it.next()) == false) { 501 | throw new IllegalArgumentException("The validated collection contains an element not of type " 502 | + clazz.getName() + " at index: " + i); 503 | } 504 | } 505 | } 506 | 507 | } -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/HorizontalList.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers; 2 | 3 | 4 | import android.content.Context; 5 | import android.database.DataSetObserver; 6 | import android.graphics.Point; 7 | import android.graphics.Rect; 8 | import android.util.AttributeSet; 9 | import android.view.MotionEvent; 10 | import android.view.VelocityTracker; 11 | import android.view.View; 12 | import android.view.ViewConfiguration; 13 | import android.view.ViewGroup; 14 | import android.widget.Adapter; 15 | import android.widget.Scroller; 16 | 17 | import com.martinappl.components.general.ToolBox; 18 | import com.martinappl.components.ui.containers.interfaces.IViewObserver; 19 | 20 | public class HorizontalList extends ViewGroup { 21 | protected final int NO_VALUE = -11; 22 | 23 | /** User is not touching the list */ 24 | protected static final int TOUCH_STATE_RESTING = 0; 25 | 26 | /** User is scrolling the list */ 27 | protected static final int TOUCH_STATE_SCROLLING = 1; 28 | 29 | /** Fling gesture in progress */ 30 | protected static final int TOUCH_STATE_FLING = 2; 31 | 32 | /** Children added with this layout mode will be added after the last child */ 33 | protected static final int LAYOUT_MODE_AFTER = 0; 34 | 35 | /** Children added with this layout mode will be added before the first child */ 36 | protected static final int LAYOUT_MODE_TO_BEFORE = 1; 37 | 38 | protected int mFirstItemPosition; 39 | protected int mLastItemPosition; 40 | protected boolean isScrollingDisabled = false; 41 | 42 | protected Adapter mAdapter; 43 | protected final ToolBox.ViewCache mCache = new ToolBox.ViewCache(); 44 | private final Scroller mScroller = new Scroller(getContext()); 45 | protected int mTouchSlop; 46 | private int mMinimumVelocity; 47 | private int mMaximumVelocity; 48 | 49 | private int mTouchState = TOUCH_STATE_RESTING; 50 | private float mLastMotionX; 51 | private final Point mDown = new Point(); 52 | private VelocityTracker mVelocityTracker; 53 | private boolean mHandleSelectionOnActionUp = false; 54 | 55 | protected int mRightEdge = NO_VALUE; 56 | private int mDefaultItemWidth = 200; 57 | 58 | protected IViewObserver mViewObserver; 59 | 60 | //listeners 61 | private OnItemClickListener mItemClickListener; 62 | 63 | private final DataSetObserver mDataObserver = new DataSetObserver() { 64 | 65 | @Override 66 | public void onChanged() { 67 | reset(); 68 | invalidate(); 69 | } 70 | 71 | @Override 72 | public void onInvalidated() { 73 | removeAllViews(); 74 | invalidate(); 75 | } 76 | 77 | }; 78 | 79 | /** 80 | * Remove all data, reset to initial state and attempt to refill 81 | * Position of first item on screen in Adapter data set is maintained 82 | */ 83 | private void reset() { 84 | int scroll = getScrollX(); 85 | 86 | int left = 0; 87 | if(getChildCount() != 0){ 88 | left = getChildAt(0).getLeft() - ((MarginLayoutParams)getChildAt(0).getLayoutParams()).leftMargin; 89 | } 90 | 91 | removeAllViewsInLayout(); 92 | mLastItemPosition = mFirstItemPosition; 93 | mRightEdge = NO_VALUE; 94 | scrollTo(left, 0); 95 | 96 | final int leftScreenEdge = getScrollX(); 97 | int rightScreenEdge = leftScreenEdge + getWidth(); 98 | 99 | refillLeftToRight(leftScreenEdge, rightScreenEdge); 100 | refillRightToLeft(leftScreenEdge); 101 | 102 | scrollTo(scroll, 0); 103 | } 104 | 105 | public HorizontalList(Context context) { 106 | this(context, null); 107 | } 108 | 109 | public HorizontalList(Context context, AttributeSet attrs) { 110 | this(context, attrs,0); 111 | } 112 | 113 | public HorizontalList(Context context, AttributeSet attrs, int defStyle) { 114 | super(context, attrs, defStyle); 115 | 116 | final ViewConfiguration configuration = ViewConfiguration.get(context); 117 | mTouchSlop = configuration.getScaledTouchSlop(); 118 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 119 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 120 | } 121 | 122 | public interface OnItemClickListener{ 123 | void onItemClick(View v); 124 | } 125 | 126 | @Override 127 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 128 | refill(); 129 | } 130 | 131 | /** 132 | * Checks and refills empty area on the left 133 | * @return firstItemPosition 134 | */ 135 | protected void refillRightToLeft(final int leftScreenEdge){ 136 | if(getChildCount() == 0) return; 137 | 138 | View child = getChildAt(0); 139 | int childLeft = child.getLeft(); 140 | int lastLeft = childLeft - ((MarginLayoutParams)child.getLayoutParams()).leftMargin; 141 | 142 | while(lastLeft > leftScreenEdge && mFirstItemPosition > 0){ 143 | mFirstItemPosition--; 144 | 145 | child = mAdapter.getView(mFirstItemPosition, mCache.getCachedView(), this); 146 | sanitizeLayoutParams(child); 147 | 148 | addAndMeasureChild(child, LAYOUT_MODE_TO_BEFORE); 149 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 150 | lastLeft = layoutChildToBefore(child, lastLeft, lp); 151 | childLeft = child.getLeft() - ((MarginLayoutParams)child.getLayoutParams()).leftMargin; 152 | 153 | } 154 | return; 155 | } 156 | 157 | /** 158 | * Checks and refills empty area on the right 159 | */ 160 | protected void refillLeftToRight(final int leftScreenEdge, final int rightScreenEdge){ 161 | 162 | View child; 163 | int lastRight; 164 | if(getChildCount() != 0){ 165 | child = getChildAt(getChildCount() - 1); 166 | lastRight = child.getRight() + ((MarginLayoutParams)child.getLayoutParams()).rightMargin; 167 | } 168 | else{ 169 | lastRight = leftScreenEdge; 170 | if(mLastItemPosition == mFirstItemPosition) mLastItemPosition--; 171 | } 172 | 173 | while(lastRight < rightScreenEdge && mLastItemPosition < mAdapter.getCount()-1){ 174 | mLastItemPosition++; 175 | 176 | child = mAdapter.getView(mLastItemPosition, mCache.getCachedView(), this); 177 | sanitizeLayoutParams(child); 178 | 179 | addAndMeasureChild(child, LAYOUT_MODE_AFTER); 180 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 181 | lastRight = layoutChild(child, lastRight, lp); 182 | 183 | if(mLastItemPosition >= mAdapter.getCount()-1) { 184 | mRightEdge = lastRight; 185 | } 186 | } 187 | } 188 | 189 | 190 | /** 191 | * Remove non visible views from left edge of screen 192 | */ 193 | protected void removeNonVisibleViewsLeftToRight(final int leftScreenEdge){ 194 | if(getChildCount() == 0) return; 195 | 196 | // check if we should remove any views in the left 197 | View firstChild = getChildAt(0); 198 | 199 | while (firstChild != null && firstChild.getRight() + ((MarginLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) { 200 | 201 | // remove view 202 | removeViewsInLayout(0, 1); 203 | 204 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition); 205 | mCache.cacheView(firstChild); 206 | 207 | mFirstItemPosition++; 208 | if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; 209 | 210 | // Continue to check the next child only if we have more than 211 | // one child left 212 | if (getChildCount() > 1) { 213 | firstChild = getChildAt(0); 214 | } else { 215 | firstChild = null; 216 | } 217 | } 218 | 219 | } 220 | 221 | /** 222 | * Remove non visible views from right edge of screen 223 | */ 224 | protected void removeNonVisibleViewsRightToLeft(final int rightScreenEdge){ 225 | if(getChildCount() == 0) return; 226 | 227 | // check if we should remove any views in the right 228 | View lastChild = getChildAt(getChildCount() - 1); 229 | while (lastChild != null && lastChild.getLeft() - ((MarginLayoutParams)lastChild.getLayoutParams()).leftMargin > rightScreenEdge) { 230 | // remove the right view 231 | removeViewsInLayout(getChildCount() - 1, 1); 232 | 233 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition); 234 | mCache.cacheView(lastChild); 235 | 236 | mLastItemPosition--; 237 | if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; 238 | 239 | // Continue to check the next child only if we have more than 240 | // one child left 241 | if (getChildCount() > 1) { 242 | lastChild = getChildAt(getChildCount() - 1); 243 | } else { 244 | lastChild = null; 245 | } 246 | } 247 | 248 | } 249 | 250 | protected void refill(){ 251 | if(mAdapter == null) return; 252 | 253 | final int leftScreenEdge = getScrollX(); 254 | int rightScreenEdge = leftScreenEdge + getWidth(); 255 | 256 | removeNonVisibleViewsLeftToRight(leftScreenEdge); 257 | removeNonVisibleViewsRightToLeft(rightScreenEdge); 258 | 259 | refillLeftToRight(leftScreenEdge, rightScreenEdge); 260 | refillRightToLeft(leftScreenEdge); 261 | } 262 | 263 | 264 | 265 | protected void sanitizeLayoutParams(View child){ 266 | MarginLayoutParams lp; 267 | if(child.getLayoutParams() instanceof MarginLayoutParams) lp = (MarginLayoutParams) child.getLayoutParams(); 268 | else if(child.getLayoutParams() != null) lp = new MarginLayoutParams(child.getLayoutParams()); 269 | else lp = new MarginLayoutParams(mDefaultItemWidth,getHeight()); 270 | 271 | if(lp.height == LayoutParams.MATCH_PARENT) lp.height = getHeight(); 272 | if(lp.width == LayoutParams.MATCH_PARENT) lp.width = getWidth(); 273 | 274 | if(lp.height == LayoutParams.WRAP_CONTENT){ 275 | measureUnspecified(child); 276 | lp.height = child.getMeasuredHeight(); 277 | } 278 | if(lp.width == LayoutParams.WRAP_CONTENT){ 279 | measureUnspecified(child); 280 | lp.width = child.getMeasuredWidth(); 281 | } 282 | child.setLayoutParams(lp); 283 | } 284 | 285 | private void measureUnspecified(View child){ 286 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED); 287 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED); 288 | measureChild(child, pwms, phms); 289 | } 290 | 291 | /** 292 | * Adds a view as a child view and takes care of measuring it 293 | * 294 | * @param child The view to add 295 | * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT 296 | * @return child which was actually added to container, subclasses can override to introduce frame views 297 | */ 298 | protected View addAndMeasureChild(final View child, final int layoutMode) { 299 | if(child.getLayoutParams() == null) child.setLayoutParams(new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 300 | 301 | final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; 302 | addViewInLayout(child, index, child.getLayoutParams(), true); 303 | 304 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY); 305 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY); 306 | measureChild(child, pwms, phms); 307 | child.setDrawingCacheEnabled(false); 308 | 309 | return child; 310 | } 311 | 312 | /** 313 | * Layout children from right to left 314 | */ 315 | protected int layoutChildToBefore(View v, int right , MarginLayoutParams lp){ 316 | final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; 317 | layoutChild(v, left, lp); 318 | return left; 319 | } 320 | 321 | /** 322 | * @param topline Y coordinate of topline 323 | * @param left X coordinate where should we start layout 324 | */ 325 | protected int layoutChild(View v, int left, MarginLayoutParams lp){ 326 | int l,t,r,b; 327 | l = left + lp.leftMargin; 328 | t = lp.topMargin; 329 | r = l + v.getMeasuredWidth(); 330 | b = t + v.getMeasuredHeight(); 331 | 332 | v.layout(l, t, r, b); 333 | return r + lp.rightMargin; 334 | } 335 | 336 | 337 | @Override 338 | public boolean onInterceptTouchEvent(MotionEvent ev) { 339 | 340 | /* 341 | * This method JUST determines whether we want to intercept the motion. 342 | * If we return true, onTouchEvent will be called and we do the actual 343 | * scrolling there. 344 | */ 345 | 346 | 347 | /* 348 | * Shortcut the most recurring case: the user is in the dragging 349 | * state and he is moving his finger. We want to intercept this 350 | * motion. 351 | */ 352 | final int action = ev.getAction(); 353 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { 354 | return true; 355 | } 356 | 357 | final float x = ev.getX(); 358 | final float y = ev.getY(); 359 | switch (action) { 360 | case MotionEvent.ACTION_MOVE: 361 | /* 362 | * not dragging, otherwise the shortcut would have caught it. Check 363 | * whether the user has moved far enough from his original down touch. 364 | */ 365 | 366 | /* 367 | * Locally do absolute value. mLastMotionX is set to the x value 368 | * of the down event. 369 | */ 370 | final int xDiff = (int) Math.abs(x - mLastMotionX); 371 | 372 | final int touchSlop = mTouchSlop; 373 | final boolean xMoved = xDiff > touchSlop; 374 | 375 | if (xMoved) { 376 | // Scroll if the user moved far enough along the axis 377 | mTouchState = TOUCH_STATE_SCROLLING; 378 | mHandleSelectionOnActionUp = false; 379 | enableChildrenCache(); 380 | cancelLongPress(); 381 | } 382 | 383 | break; 384 | 385 | case MotionEvent.ACTION_DOWN: 386 | // Remember location of down touch 387 | mLastMotionX = x; 388 | 389 | mDown.x = (int) x; 390 | mDown.y = (int) y; 391 | 392 | /* 393 | * If being flinged and user touches the screen, initiate drag; 394 | * otherwise don't. mScroller.isFinished should be false when 395 | * being flinged. 396 | */ 397 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; 398 | //if he had normal click in rested state, remember for action up check 399 | if(mTouchState == TOUCH_STATE_RESTING){ 400 | mHandleSelectionOnActionUp = true; 401 | } 402 | break; 403 | 404 | case MotionEvent.ACTION_CANCEL: 405 | mDown.x = -1; 406 | mDown.y = -1; 407 | break; 408 | case MotionEvent.ACTION_UP: 409 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates 410 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 411 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); 412 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); 413 | } 414 | // Release the drag 415 | mHandleSelectionOnActionUp = false; 416 | mDown.x = -1; 417 | mDown.y = -1; 418 | 419 | mTouchState = TOUCH_STATE_RESTING; 420 | clearChildrenCache(); 421 | break; 422 | } 423 | 424 | return mTouchState == TOUCH_STATE_SCROLLING; 425 | 426 | } 427 | 428 | @Override 429 | public boolean onTouchEvent(MotionEvent event) { 430 | if (mVelocityTracker == null) { 431 | mVelocityTracker = VelocityTracker.obtain(); 432 | } 433 | mVelocityTracker.addMovement(event); 434 | 435 | final int action = event.getAction(); 436 | final float x = event.getX(); 437 | final float y = event.getY(); 438 | 439 | switch (action) { 440 | case MotionEvent.ACTION_DOWN: 441 | /* 442 | * If being flinged and user touches, stop the fling. isFinished 443 | * will be false if being flinged. 444 | */ 445 | if (!mScroller.isFinished()) { 446 | mScroller.forceFinished(true); 447 | } 448 | 449 | // Remember where the motion event started 450 | mLastMotionX = x; 451 | 452 | break; 453 | case MotionEvent.ACTION_MOVE: 454 | 455 | if (mTouchState == TOUCH_STATE_SCROLLING) { 456 | // Scroll to follow the motion event 457 | final int deltaX = (int) (mLastMotionX - x); 458 | mLastMotionX = x; 459 | 460 | scrollByDelta(deltaX); 461 | } 462 | else{ 463 | final int xDiff = (int) Math.abs(x - mLastMotionX); 464 | 465 | final int touchSlop = mTouchSlop; 466 | final boolean xMoved = xDiff > touchSlop; 467 | 468 | 469 | if (xMoved) { 470 | // Scroll if the user moved far enough along the axis 471 | mTouchState = TOUCH_STATE_SCROLLING; 472 | enableChildrenCache(); 473 | cancelLongPress(); 474 | } 475 | } 476 | break; 477 | case MotionEvent.ACTION_UP: 478 | 479 | //this must be here, in case no child view returns true, 480 | //events will propagate back here and on intercept touch event wont be called again 481 | //in case of no parent it propagates here, in case of parent it usually propagates to on cancel 482 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 483 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); 484 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); 485 | mHandleSelectionOnActionUp = false; 486 | } 487 | 488 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates 489 | if (mTouchState == TOUCH_STATE_SCROLLING) { 490 | 491 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 492 | int initialXVelocity = (int) mVelocityTracker.getXVelocity(); 493 | int initialYVelocity = (int) mVelocityTracker.getYVelocity(); 494 | 495 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { 496 | fling(-initialXVelocity, -initialYVelocity); 497 | } 498 | else{ 499 | // Release the drag 500 | clearChildrenCache(); 501 | mTouchState = TOUCH_STATE_RESTING; 502 | 503 | mDown.x = -1; 504 | mDown.y = -1; 505 | } 506 | 507 | if (mVelocityTracker != null) { 508 | mVelocityTracker.recycle(); 509 | mVelocityTracker = null; 510 | } 511 | 512 | break; 513 | } 514 | 515 | // Release the drag 516 | clearChildrenCache(); 517 | mTouchState = TOUCH_STATE_RESTING; 518 | 519 | mDown.x = -1; 520 | mDown.y = -1; 521 | 522 | break; 523 | case MotionEvent.ACTION_CANCEL: 524 | mTouchState = TOUCH_STATE_RESTING; 525 | } 526 | 527 | return true; 528 | } 529 | 530 | @Override 531 | public void computeScroll() { 532 | if(mRightEdge != NO_VALUE && mScroller.getFinalX() > mRightEdge - getWidth() + 1){ 533 | mScroller.setFinalX(mRightEdge - getWidth() + 1); 534 | } 535 | 536 | if(mRightEdge != NO_VALUE && getScrollX() > mRightEdge - getWidth()) { 537 | if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0); 538 | else scrollTo(0, 0); 539 | return; 540 | } 541 | 542 | if (mScroller.computeScrollOffset()) { 543 | if(mScroller.getFinalX() == mScroller.getCurrX()){ 544 | mScroller.abortAnimation(); 545 | mTouchState = TOUCH_STATE_RESTING; 546 | clearChildrenCache(); 547 | } 548 | else{ 549 | final int x = mScroller.getCurrX(); 550 | scrollTo(x, 0); 551 | 552 | postInvalidate(); 553 | } 554 | } 555 | else if(mTouchState == TOUCH_STATE_FLING){ 556 | mTouchState = TOUCH_STATE_RESTING; 557 | clearChildrenCache(); 558 | } 559 | 560 | refill(); 561 | } 562 | 563 | public void fling(int velocityX, int velocityY){ 564 | if(isScrollingDisabled) return; 565 | 566 | mTouchState = TOUCH_STATE_FLING; 567 | final int x = getScrollX(); 568 | final int y = getScrollY(); 569 | 570 | final int rightInPixels; 571 | if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE; 572 | else rightInPixels = mRightEdge; 573 | 574 | mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth() + 1,0,0); 575 | 576 | invalidate(); 577 | } 578 | 579 | protected void scrollByDelta(int deltaX){ 580 | if(isScrollingDisabled) return; 581 | 582 | final int rightInPixels; 583 | if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE; 584 | else { 585 | rightInPixels = mRightEdge; 586 | if(getScrollX() > mRightEdge - getWidth()) { 587 | if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0); 588 | else scrollTo(0, 0); 589 | return; 590 | } 591 | } 592 | 593 | final int x = getScrollX() + deltaX; 594 | 595 | if(x < 0 ) deltaX -= x; 596 | else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth()); 597 | 598 | scrollBy(deltaX, 0); 599 | } 600 | 601 | protected void handleClick(Point p){ 602 | final int c = getChildCount(); 603 | View v; 604 | final Rect r = new Rect(); 605 | for(int i=0; i < c; i++){ 606 | v = getChildAt(i); 607 | v.getHitRect(r); 608 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ 609 | if(mItemClickListener != null) mItemClickListener.onItemClick(v); 610 | } 611 | } 612 | } 613 | 614 | 615 | public void setAdapter(Adapter adapter) { 616 | if(mAdapter != null) { 617 | mAdapter.unregisterDataSetObserver(mDataObserver); 618 | } 619 | mAdapter = adapter; 620 | mAdapter.registerDataSetObserver(mDataObserver); 621 | reset(); 622 | } 623 | 624 | private void enableChildrenCache() { 625 | setChildrenDrawnWithCacheEnabled(true); 626 | setChildrenDrawingCacheEnabled(true); 627 | } 628 | 629 | private void clearChildrenCache() { 630 | setChildrenDrawnWithCacheEnabled(false); 631 | } 632 | 633 | @Override 634 | protected MarginLayoutParams generateDefaultLayoutParams() { 635 | return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 636 | } 637 | 638 | @Override 639 | protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 640 | return new MarginLayoutParams(p); 641 | } 642 | 643 | @Override 644 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 645 | return p instanceof MarginLayoutParams; 646 | } 647 | 648 | public void setDefaultItemWidth(int width){ 649 | //MTODO add xml attributes 650 | mDefaultItemWidth = width; 651 | } 652 | 653 | /** 654 | * Set listener which will fire if item in container is clicked 655 | */ 656 | public void setOnItemClickListener(OnItemClickListener itemClickListener) { 657 | this.mItemClickListener = itemClickListener; 658 | } 659 | 660 | public void setViewObserver(IViewObserver viewObserver) { 661 | this.mViewObserver = viewObserver; 662 | } 663 | 664 | } 665 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/contentbands/BasicContentBand.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers.contentbands; 2 | 3 | import java.lang.ref.WeakReference; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | 11 | import android.content.Context; 12 | import android.content.res.TypedArray; 13 | import android.graphics.Point; 14 | import android.graphics.Rect; 15 | import android.util.AttributeSet; 16 | import android.view.MotionEvent; 17 | import android.view.VelocityTracker; 18 | import android.view.View; 19 | import android.view.ViewConfiguration; 20 | import android.view.ViewGroup; 21 | import android.widget.Scroller; 22 | 23 | import com.martinappl.components.R; 24 | import com.martinappl.components.general.ToolBox; 25 | import com.martinappl.components.general.Validate; 26 | 27 | 28 | /** 29 | * @author Martin Appl 30 | * 31 | * Horizontally scrollable container with boundaries on the ends, which places Views on coordinates specified 32 | * by tile objects. Data binding is specified by adapter interface. Use abstract adapter which has already implemented 33 | * algorithms for searching views in requested ranges. You only need to implement getViewForTile method where you map 34 | * Tile objects from dataset to corresponding View objects, which get displayed. Position on screen is described by LayoutParams object. 35 | * Method getLayoutParamsForTile helps generate layout params from data objects. If you don't set Layout params in getViewForTile, this 36 | * methods is called automatically afterwards. 37 | * 38 | * DSP = device specific pixel 39 | */ 40 | public class BasicContentBand extends ViewGroup { 41 | //CONSTANTS 42 | // private static final String LOG_TAG = "Basic_ContentBand_Component"; 43 | private static final int NO_VALUE = -11; 44 | private static final int DSP_DEFAULT = 10; 45 | 46 | /** User is not touching the list */ 47 | protected static final int TOUCH_STATE_RESTING = 0; 48 | 49 | /** User is scrolling the list */ 50 | protected static final int TOUCH_STATE_SCROLLING = 1; 51 | 52 | /** Fling gesture in progress */ 53 | protected static final int TOUCH_STATE_FLING = 2; 54 | 55 | /** 56 | * In this mode we have pixel size of DSP specified, if dspHeight is bigger than window, content band can be scrolled vertically. 57 | */ 58 | public static final int GRID_MODE_FIXED_SIZE = 0; 59 | /** 60 | * In this mode is pixel size of DSP calculated dynamically, based on widget height in pixels and value of dspHeight which is fixed 61 | * and taken from adapters getBottom method 62 | */ 63 | public static final int GRID_MODE_DYNAMIC_SIZE = 1; 64 | 65 | //to which direction on X axis are window coordinates sliding 66 | protected static final int DIRECTION_RIGHT = 0; 67 | protected static final int DIRECTION_LEFT = 1; 68 | 69 | 70 | //VARIABLES 71 | protected Adapter mAdapter; 72 | private int mGridMode = GRID_MODE_DYNAMIC_SIZE; 73 | /**How many normal pixels corresponds to one DSP pixel*/ 74 | private int mDspPixelRatio = DSP_DEFAULT; 75 | private int mDspHeight = NO_VALUE; 76 | protected int mDspHeightModulo; 77 | //refilling 78 | protected int mCurrentlyLayoutedViewsLeftEdgeDsp; 79 | protected int mCurrentlyLayoutedViewsRightEdgeDsp; 80 | private final ArrayList mTempViewArray = new ArrayList(); 81 | //touch, scrolling 82 | protected int mTouchState = TOUCH_STATE_RESTING; 83 | private float mLastMotionX; 84 | private float mLastMotionY; 85 | private final Point mDown = new Point(); 86 | private VelocityTracker mVelocityTracker; 87 | protected final Scroller mScroller; 88 | private boolean mHandleSelectionOnActionUp = false; 89 | protected int mScrollDirection = NO_VALUE; 90 | //constant values 91 | private final int mTouchSlop; 92 | private final int mMinimumVelocity; 93 | private final int mMaximumVelocity; 94 | // private final Rect mTempRect = new Rect(); 95 | 96 | private boolean mIsZOrderEnabled; 97 | private int[] mDrawingOrderArray; 98 | 99 | //listeners 100 | private OnItemClickListener mItemClickListener; 101 | 102 | public BasicContentBand(Context context, AttributeSet attrs, int defStyle) { 103 | super(context, attrs, defStyle); 104 | 105 | final ViewConfiguration configuration = ViewConfiguration.get(context); 106 | mTouchSlop = configuration.getScaledTouchSlop(); 107 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 108 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 109 | mScroller = new Scroller(context); 110 | 111 | if(attrs != null){ 112 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BasicContentBand, defStyle, 0); 113 | 114 | mDspPixelRatio = a.getInteger(R.styleable.BasicContentBand_deviceSpecificPixelSize, mDspPixelRatio); 115 | mGridMode = a.getInteger(R.styleable.BasicContentBand_gridMode, mGridMode); 116 | 117 | a.recycle(); 118 | } 119 | 120 | 121 | } 122 | 123 | public BasicContentBand(Context context, AttributeSet attrs) { 124 | this(context, attrs, 0); 125 | } 126 | 127 | public BasicContentBand(Context context) { 128 | this(context,null); 129 | } 130 | 131 | protected int dspToPx(int dsp){ 132 | return dsp * mDspPixelRatio; 133 | } 134 | 135 | protected int pxToDsp(int px){ 136 | return px / mDspPixelRatio; 137 | } 138 | 139 | @Override 140 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 141 | int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 142 | int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 143 | int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 144 | int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 145 | 146 | if(mAdapter != null){ 147 | mDspHeight = mAdapter.getBottom(); 148 | Validate.isTrue(mDspHeight > 0, "Adapter getBottom must return value greater than zero"); 149 | } 150 | else{ 151 | setMeasuredDimension(widthSpecSize, heightSpecSize); 152 | return; 153 | } 154 | 155 | int measuredWidth, measuredHeight; 156 | if(mGridMode == GRID_MODE_FIXED_SIZE){ 157 | /*HEIGHT*/ 158 | measuredHeight = mDspPixelRatio * mDspHeight; 159 | 160 | if(heightSpecMode == MeasureSpec.AT_MOST){ 161 | if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize; 162 | } 163 | else if(heightSpecMode == MeasureSpec.EXACTLY){ 164 | measuredHeight = heightSpecSize; 165 | } 166 | 167 | /*WIDTH*/ 168 | measuredWidth = widthSpecSize; 169 | if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio; 170 | 171 | if(widthSpecMode == MeasureSpec.AT_MOST){ 172 | if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize; 173 | } 174 | else if(widthSpecMode == MeasureSpec.EXACTLY){ 175 | measuredWidth = widthSpecSize; 176 | } 177 | } 178 | else{ 179 | if (heightSpecMode == MeasureSpec.UNSPECIFIED) { 180 | throw new RuntimeException("Can not have unspecified hight dimension in dynamic grid mode"); 181 | } 182 | /*HEIGHT*/ 183 | measuredHeight = heightSpecSize; 184 | 185 | mDspPixelRatio = measuredHeight / mDspHeight; 186 | mDspHeightModulo = measuredHeight % mDspHeight; 187 | 188 | measuredHeight = mDspPixelRatio * mDspHeight; 189 | 190 | if(heightSpecMode == MeasureSpec.AT_MOST){ 191 | if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize; 192 | else mDspHeightModulo = 0; 193 | } 194 | else if(heightSpecMode == MeasureSpec.EXACTLY){ 195 | measuredHeight = heightSpecSize; 196 | } 197 | 198 | /*WIDTH*/ 199 | measuredWidth = widthSpecSize; 200 | if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio; 201 | 202 | if(widthSpecMode == MeasureSpec.AT_MOST){ 203 | if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize; 204 | } 205 | else if(widthSpecMode == MeasureSpec.EXACTLY){ 206 | measuredWidth = widthSpecSize; 207 | } 208 | 209 | } 210 | 211 | setMeasuredDimension(measuredWidth, measuredHeight); 212 | } 213 | 214 | @Override 215 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 216 | int c = getChildCount(); 217 | 218 | if(c == 0) { 219 | fillEmptyContainer(); 220 | c = getChildCount(); 221 | } 222 | 223 | for(int i=0; i= 0; j--){ //start at the end, because mostly we are searching for view which was added to end in previous iterations 253 | // if(((LayoutParams)getChildAt(j).getLayoutParams()).tileNumber == lp.tileNumber) { 254 | // arr[i] = null; 255 | // nullCounter++; 256 | // break; 257 | // } 258 | // } 259 | // } 260 | // 261 | // final View[] res = new View[arr.length - nullCounter]; 262 | // for(int i=0,j=0; i comparator = new Comparator() { 280 | @Override 281 | public int compare(View lhs, View rhs) { 282 | final LayoutParams l = (LayoutParams) lhs.getLayoutParams(); 283 | final LayoutParams r = (LayoutParams) rhs.getLayoutParams(); 284 | 285 | if(l.z == r.z) return 0; 286 | else if(l.z < r.z) return -1; 287 | else return 1; 288 | } 289 | }; 290 | 291 | Arrays.sort(tempArr, comparator); 292 | mDrawingOrderArray = new int[tempArr.length]; 293 | for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight(); 325 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; 326 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); 327 | } 328 | 329 | if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder(); 330 | 331 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; 332 | mCurrentlyLayoutedViewsRightEdgeDsp= dspMostRight; 333 | } 334 | 335 | /** 336 | * Checks and refills empty area on the left edge of screen 337 | */ 338 | protected void refillLeftSide(){ 339 | if(mAdapter == null) return; 340 | 341 | final int leftScreenEdge = getScrollX(); 342 | final int dspLeftScreenEdge = pxToDsp(leftScreenEdge); 343 | final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp; 344 | 345 | if(dspLeftScreenEdge >= dspNextViewsRight) return; 346 | // Logger.d(LOG_TAG, "from " + dspLeftScreenEdge + ", to " + dspNextViewsRight); 347 | 348 | View[] list = mAdapter.getViewsByRightSideRange(dspLeftScreenEdge, dspNextViewsRight); 349 | // list = filterAlreadyPresentViews(list); 350 | 351 | int dspMostLeft = dspNextViewsRight; 352 | LayoutParams lp; 353 | for(int i=0; i < list.length; i++){ 354 | lp = (LayoutParams) list[i].getLayoutParams(); 355 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; 356 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); 357 | } 358 | 359 | if(list.length > 0){ 360 | layoutNewChildren(list); 361 | } 362 | 363 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; 364 | } 365 | 366 | /** 367 | * Checks and refills empty area on the right 368 | */ 369 | protected void refillRightSide(){ 370 | if(mAdapter == null) return; 371 | 372 | final int rightScreenEdge = getScrollX() + getWidth(); 373 | final int dspNextAddedViewsLeft = mCurrentlyLayoutedViewsRightEdgeDsp; 374 | 375 | int dspRightScreenEdge = pxToDsp(rightScreenEdge) + 1; 376 | if(dspRightScreenEdge > mAdapter.getEnd()) dspRightScreenEdge = mAdapter.getEnd(); 377 | 378 | if(dspNextAddedViewsLeft >= dspRightScreenEdge) return; 379 | 380 | View[] list = mAdapter.getViewsByLeftSideRange(dspNextAddedViewsLeft, dspRightScreenEdge); 381 | // list = filterAlreadyPresentViews(list); 382 | 383 | int dspMostRight = 0; 384 | LayoutParams lp; 385 | for(int i=0; i < list.length; i++){ 386 | lp = (LayoutParams) list[i].getLayoutParams(); 387 | if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight(); 388 | addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); 389 | } 390 | 391 | if(list.length > 0){ 392 | layoutNewChildren(list); 393 | } 394 | 395 | mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; 396 | } 397 | 398 | /** 399 | * Remove non visible views laid out of the screen 400 | */ 401 | private void removeNonVisibleViews(){ 402 | if(getChildCount() == 0) return; 403 | 404 | final int leftScreenEdge = getScrollX(); 405 | final int rightScreenEdge = leftScreenEdge + getWidth(); 406 | 407 | int dspRightScreenEdge = pxToDsp(rightScreenEdge); 408 | if(dspRightScreenEdge >= 0) dspRightScreenEdge++; //to avoid problem with rounding of values 409 | 410 | int dspLeftScreenEdge = pxToDsp(leftScreenEdge); 411 | if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger 412 | 413 | mTempViewArray.clear(); 414 | View v; 415 | for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight(); 433 | if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; 434 | } 435 | 436 | mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; 437 | mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; 438 | 439 | } 440 | 441 | //check if View with specified LayoutParams is currently on screen 442 | private boolean isOnScreen(LayoutParams lp, int dspLeftScreenEdge, int dspRightScreenEdge){ 443 | final int left = lp.dspLeft; 444 | final int right = left + lp.dspWidth; 445 | 446 | if(right > dspLeftScreenEdge && left < dspRightScreenEdge) return true; 447 | else return false; 448 | } 449 | 450 | @Override 451 | public boolean onInterceptTouchEvent(MotionEvent ev) { 452 | 453 | /* 454 | * This method JUST determines whether we want to intercept the motion. 455 | * If we return true, onTouchEvent will be called and we do the actual 456 | * scrolling there. 457 | */ 458 | 459 | 460 | /* 461 | * Shortcut the most recurring case: the user is in the dragging 462 | * state and he is moving his finger. We want to intercept this 463 | * motion. 464 | */ 465 | final int action = ev.getAction(); 466 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { 467 | return true; 468 | } 469 | 470 | final float x = ev.getX(); 471 | final float y = ev.getY(); 472 | switch (action) { 473 | case MotionEvent.ACTION_MOVE: 474 | /* 475 | * not dragging, otherwise the shortcut would have caught it. Check 476 | * whether the user has moved far enough from his original down touch. 477 | */ 478 | 479 | /* 480 | * Locally do absolute value. mLastMotionX is set to the x value 481 | * of the down event. 482 | */ 483 | final int xDiff = (int) Math.abs(x - mLastMotionX); 484 | final int yDiff = (int) Math.abs(y - mLastMotionY); 485 | 486 | final int touchSlop = mTouchSlop; 487 | final boolean xMoved = xDiff > touchSlop; 488 | final boolean yMoved = yDiff > touchSlop; 489 | 490 | 491 | if (xMoved || yMoved) { 492 | // Scroll if the user moved far enough along the axis 493 | mTouchState = TOUCH_STATE_SCROLLING; 494 | mHandleSelectionOnActionUp = false; 495 | enableChildrenCache(); 496 | cancelLongPress(); 497 | } 498 | 499 | break; 500 | 501 | case MotionEvent.ACTION_DOWN: 502 | // Remember location of down touch 503 | mLastMotionX = x; 504 | 505 | mDown.x = (int) x; 506 | mDown.y = (int) y; 507 | 508 | /* 509 | * If being flinged and user touches the screen, initiate drag; 510 | * otherwise don't. mScroller.isFinished should be false when 511 | * being flinged. 512 | */ 513 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; 514 | //if he had normal click in rested state, remember for action up check 515 | if(mTouchState == TOUCH_STATE_RESTING){ 516 | mHandleSelectionOnActionUp = true; 517 | } 518 | break; 519 | 520 | case MotionEvent.ACTION_CANCEL: 521 | mDown.x = -1; 522 | mDown.y = -1; 523 | break; 524 | case MotionEvent.ACTION_UP: 525 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates 526 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 527 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); 528 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); 529 | } 530 | // Release the drag 531 | mHandleSelectionOnActionUp = false; 532 | mDown.x = -1; 533 | mDown.y = -1; 534 | 535 | mTouchState = TOUCH_STATE_RESTING; 536 | clearChildrenCache(); 537 | break; 538 | } 539 | 540 | return mTouchState == TOUCH_STATE_SCROLLING; 541 | 542 | } 543 | 544 | @Override 545 | public boolean onTouchEvent(MotionEvent event) { 546 | if (mVelocityTracker == null) { 547 | mVelocityTracker = VelocityTracker.obtain(); 548 | } 549 | mVelocityTracker.addMovement(event); 550 | 551 | final int action = event.getAction(); 552 | final float x = event.getX(); 553 | final float y = event.getY(); 554 | 555 | switch (action) { 556 | case MotionEvent.ACTION_DOWN: 557 | /* 558 | * If being flinged and user touches, stop the fling. isFinished 559 | * will be false if being flinged. 560 | */ 561 | if (!mScroller.isFinished()) { 562 | mScroller.forceFinished(true); 563 | } 564 | 565 | // Remember where the motion event started 566 | mLastMotionX = x; 567 | mLastMotionY = y; 568 | 569 | break; 570 | case MotionEvent.ACTION_MOVE: 571 | 572 | if (mTouchState == TOUCH_STATE_SCROLLING) { 573 | // Scroll to follow the motion event 574 | final int deltaX = (int) (mLastMotionX - x); 575 | final int deltaY = (int) (mLastMotionY - y); 576 | mLastMotionX = x; 577 | mLastMotionY = y; 578 | 579 | scrollByDelta(deltaX, deltaY); 580 | } 581 | else{ 582 | final int xDiff = (int) Math.abs(x - mLastMotionX); 583 | final int yDiff = (int) Math.abs(y - mLastMotionY); 584 | 585 | final int touchSlop = mTouchSlop; 586 | final boolean xMoved = xDiff > touchSlop; 587 | final boolean yMoved = yDiff > touchSlop; 588 | 589 | 590 | if (xMoved || yMoved) { 591 | // Scroll if the user moved far enough along the axis 592 | mTouchState = TOUCH_STATE_SCROLLING; 593 | enableChildrenCache(); 594 | cancelLongPress(); 595 | } 596 | } 597 | break; 598 | case MotionEvent.ACTION_UP: 599 | 600 | //this must be here, in case no child view returns true, 601 | //events will propagate back here and on intercept touch event wont be called again 602 | //in case of no parent it propagates here, in case of parent it usually propagates to on cancel 603 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 604 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); 605 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); 606 | mHandleSelectionOnActionUp = false; 607 | } 608 | 609 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates 610 | if (mTouchState == TOUCH_STATE_SCROLLING) { 611 | 612 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 613 | int initialXVelocity = (int) mVelocityTracker.getXVelocity(); 614 | int initialYVelocity = (int) mVelocityTracker.getYVelocity(); 615 | 616 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { 617 | fling(-initialXVelocity, -initialYVelocity); 618 | } 619 | else{ 620 | // Release the drag 621 | clearChildrenCache(); 622 | mTouchState = TOUCH_STATE_RESTING; 623 | 624 | mDown.x = -1; 625 | mDown.y = -1; 626 | } 627 | 628 | if (mVelocityTracker != null) { 629 | mVelocityTracker.recycle(); 630 | mVelocityTracker = null; 631 | } 632 | 633 | break; 634 | } 635 | 636 | // Release the drag 637 | clearChildrenCache(); 638 | mTouchState = TOUCH_STATE_RESTING; 639 | 640 | mDown.x = -1; 641 | mDown.y = -1; 642 | 643 | break; 644 | case MotionEvent.ACTION_CANCEL: 645 | mTouchState = TOUCH_STATE_RESTING; 646 | } 647 | 648 | return true; 649 | } 650 | 651 | 652 | @Override 653 | public void computeScroll() { 654 | if (mScroller.computeScrollOffset()) { 655 | if(mScroller.getFinalX() == mScroller.getCurrX()){ 656 | mScroller.abortAnimation(); 657 | mTouchState = TOUCH_STATE_RESTING; 658 | mScrollDirection = NO_VALUE; 659 | clearChildrenCache(); 660 | } 661 | else{ 662 | final int x = mScroller.getCurrX(); 663 | final int y = mScroller.getCurrY(); 664 | scrollTo(x, y); 665 | 666 | postInvalidate(); 667 | } 668 | } 669 | else if(mTouchState == TOUCH_STATE_FLING){ 670 | mTouchState = TOUCH_STATE_RESTING; 671 | mScrollDirection = NO_VALUE; 672 | clearChildrenCache(); 673 | } 674 | 675 | removeNonVisibleViews(); 676 | if(mScrollDirection == DIRECTION_LEFT) refillLeftSide(); 677 | if(mScrollDirection == DIRECTION_RIGHT) refillRightSide(); 678 | 679 | if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder(); 680 | } 681 | 682 | public void fling(int velocityX, int velocityY){ 683 | mTouchState = TOUCH_STATE_FLING; 684 | final int x = getScrollX(); 685 | final int y = getScrollY(); 686 | final int rightInPixels = dspToPx(mAdapter.getEnd()); 687 | final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; 688 | 689 | mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth(),0,bottomInPixels - getHeight()); 690 | 691 | if(velocityX < 0) { 692 | mScrollDirection = DIRECTION_LEFT; 693 | } 694 | else if(velocityX > 0) { 695 | mScrollDirection = DIRECTION_RIGHT; 696 | } 697 | 698 | 699 | invalidate(); 700 | } 701 | 702 | protected void scrollByDelta(int deltaX, int deltaY){ 703 | final int rightInPixels = dspToPx(mAdapter.getEnd()); 704 | final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; 705 | final int x = getScrollX() + deltaX; 706 | final int y = getScrollY() + deltaY; 707 | 708 | if(x < 0 ) deltaX -= x; 709 | else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth()); 710 | 711 | if(y < 0 ) deltaY -= y; 712 | else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight()); 713 | 714 | if(deltaX < 0) { 715 | mScrollDirection = DIRECTION_LEFT; 716 | } 717 | else { 718 | mScrollDirection = DIRECTION_RIGHT; 719 | } 720 | 721 | scrollBy(deltaX, deltaY); 722 | } 723 | 724 | protected void handleClick(Point p){ 725 | final int c = getChildCount(); 726 | View v; 727 | final Rect r = new Rect(); 728 | for(int i=0; i < c; i++){ 729 | v = getChildAt(i); 730 | v.getHitRect(r); 731 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ 732 | if(mItemClickListener != null) mItemClickListener.onItemClick(v); 733 | } 734 | } 735 | } 736 | 737 | /** 738 | * Returns current Adapter with backing data 739 | */ 740 | public Adapter getAdapter() { 741 | return mAdapter; 742 | } 743 | 744 | /** 745 | * Set Adapter with backing data 746 | */ 747 | public void setAdapter(Adapter adapter) { 748 | this.mAdapter = adapter; 749 | requestLayout(); 750 | } 751 | 752 | /** 753 | * Set listener which will fire if item in container is clicked 754 | */ 755 | public void setOnItemClickListener(OnItemClickListener itemClickListener) { 756 | this.mItemClickListener = itemClickListener; 757 | } 758 | 759 | private void enableChildrenCache() { 760 | setChildrenDrawingCacheEnabled(true); 761 | setChildrenDrawnWithCacheEnabled(true); 762 | } 763 | 764 | private void clearChildrenCache() { 765 | setChildrenDrawnWithCacheEnabled(false); 766 | } 767 | 768 | /** 769 | * In GRID_MODE_FIXED_SIZE mode has one dsp dimension set by setDspSize(), If band height is after transformation to normal pixels bigger than 770 | * available space, content becomes scrollable also vertically. 771 | * 772 | * In GRID_MODE_DYNAMIC_SIZE is dsp dimension computed from measured height and band height to always 773 | */ 774 | public void setGridMode(int mode){ 775 | mGridMode = mode; 776 | } 777 | 778 | /** 779 | * Specifies how many normal pixels is in length of one device specific pixel 780 | * This method is significant only in GRID_MODE_FIXED_SIZE mode (use setGridMode) 781 | */ 782 | public void setDspSize(int pixels){ 783 | mDspPixelRatio = pixels; 784 | } 785 | 786 | /** 787 | * Set to true if you want component to work with tile z parameter; 788 | * If you don't have any overlapping view, leave it on default false, because computing 789 | * with z order makes rendering slower. 790 | */ 791 | public void setZOrderEnabled(boolean enable){ 792 | mIsZOrderEnabled = enable; 793 | setChildrenDrawingOrderEnabled(enable); 794 | } 795 | 796 | @Override 797 | protected LayoutParams generateDefaultLayoutParams() { 798 | return new LayoutParams(); 799 | } 800 | 801 | @Override 802 | protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 803 | return new LayoutParams(); 804 | } 805 | 806 | @Override 807 | protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 808 | return p instanceof LayoutParams; 809 | } 810 | 811 | //----------------CONTENT BAND END-------------------------------------------------------------------- 812 | 813 | public interface OnItemClickListener{ 814 | void onItemClick(View v); 815 | } 816 | 817 | public interface Adapter { 818 | 819 | /** 820 | * Return Views which have left edge in device specific coordinates in range from-to, 821 | * @param from inclusive 822 | * @param to exclusive 823 | */ 824 | public abstract View[] getViewsByLeftSideRange(int from, int to); 825 | 826 | /** 827 | * Return Views which have right edge in device specific coordinates in range from-to, 828 | * @param from exclusive 829 | * @param to inclusive 830 | */ 831 | public abstract View[] getViewsByRightSideRange(int from, int to); 832 | 833 | /** 834 | * @return Right Coordinate of last tile in DSP 835 | */ 836 | int getEnd(); 837 | 838 | /** 839 | * @return Bottom Coordinate of tiles on bottom edge in DSP, Must be > 0 840 | * 841 | */ 842 | int getBottom(); 843 | 844 | /** 845 | * @return total number of tiles 846 | */ 847 | public int getCount(); 848 | 849 | /** 850 | * Makes union between View returned by left side and right side ranges 851 | * Needed for initialization of component 852 | */ 853 | public abstract View[] getViewsVisibleInRange(int from, int to); 854 | 855 | /** 856 | * Puts View, which is not needed anymore back to Adapter. View will be used later instead of creating or inflating same view. 857 | */ 858 | public void offerViewForRecycling(View view); 859 | } 860 | 861 | 862 | public static class LayoutParams extends ViewGroup.LayoutParams{ 863 | public int tileId; 864 | public int dspLeft; 865 | public int dspTop; 866 | public int dspWidth; 867 | public int dspHeight; 868 | public int z; 869 | 870 | private int viewgroupIndex; 871 | 872 | public LayoutParams() { 873 | super(NO_VALUE, NO_VALUE); 874 | } 875 | 876 | public int getDspRight(){ 877 | return dspLeft + dspWidth; 878 | } 879 | } 880 | 881 | 882 | 883 | public static abstract class AbstractAdapter implements Adapter{ 884 | private final ViewCache mViewCache = new ViewCache(); 885 | 886 | protected ArrayList mTilesByBegining; 887 | protected ArrayList mTilesByEnd; 888 | // protected SparseArray mTilesByNumber; 889 | protected IDataListener mChangeListener; 890 | 891 | public AbstractAdapter(){} 892 | public AbstractAdapter(ArrayList tiles){ 893 | initWithNewData(tiles); 894 | } 895 | 896 | private final Comparator beginingComparator = new Comparator() { 897 | @Override 898 | public int compare(Tile o1, Tile o2) { 899 | if(o1.getX() == o2.getX()) return 0; 900 | else if(o1.getX() < o2.getX()) return -1; 901 | else return 1; 902 | } 903 | }; 904 | 905 | private final Comparator endComparator = new Comparator() { 906 | @Override 907 | public int compare(Tile o1, Tile o2) { 908 | if(o1.getXRight() == o2.getXRight()) return 0; 909 | else if(o1.getXRight() < o2.getXRight()) return -1; 910 | else return 1; 911 | } 912 | }; 913 | 914 | @SuppressWarnings("unchecked") 915 | @Override 916 | public void offerViewForRecycling(View view){ 917 | mViewCache.cacheView((V) view); 918 | } 919 | 920 | 921 | /** 922 | * Use getLayoutParamsForTile to get correct layout params for Tile data and set them with setLayoutParams before returning View 923 | * @param t Tile data from datamodel 924 | * @param recycled View no more used and returned for recycling. Use together with ViewHolder pattern to avoid performance loss 925 | * in inflating and searching by ids in more complex xml layouts. 926 | * @return View which will be displayed in component using layout data from Tile 927 | * 928 | *
 929 | 		 * 	public ImageView getViewForTile(Tile t, ImageView recycled) { 
 930 | 		 * 		ImageView iw;
 931 | 		 *		if(recycled != null) iw = recycled;
 932 | 		 * 		else iw = new ImageView(MainActivity.this);
 933 | 		 *
 934 | 		 *		iw.setLayoutParams(getLayoutParamsForTile(t));
 935 | 		 *		return iw;
 936 | 		 *	}
 937 | 		 * 
938 | */ 939 | public abstract V getViewForTile(Tile t, V recycled); 940 | 941 | /** 942 | * @return total number of tiles 943 | */ 944 | public int getCount(){ 945 | return mTilesByBegining.size(); 946 | } 947 | 948 | public int getEnd(){ 949 | if(mTilesByEnd.size() > 0)return mTilesByEnd.get(mTilesByEnd.size()-1).getXRight(); 950 | else return 0; 951 | } 952 | 953 | private void checkAndFixLayoutParams(View v, Tile t){ 954 | if(!(v.getLayoutParams() instanceof LayoutParams)) v.setLayoutParams(getLayoutParamsForTile(t)); 955 | } 956 | 957 | @Override 958 | public View[] getViewsByLeftSideRange(int from, int to) { 959 | if(from == to) return new View[0]; 960 | final List list = getTilesWithLeftRange(from, to); 961 | 962 | final View[] arr = new View[list.size()]; 963 | for(int i=0; i < arr.length; i++){ 964 | Tile t = list.get(i); 965 | arr[i] = getViewForTile(t, mViewCache.getCachedView()); 966 | checkAndFixLayoutParams(arr[i], t); 967 | } 968 | 969 | return arr; 970 | } 971 | 972 | @Override 973 | public View[] getViewsByRightSideRange(int from, int to) { 974 | if(from == to) return new View[0]; 975 | final List list = getTilesWithRightRange(from, to); 976 | 977 | final View[] arr = new View[list.size()]; 978 | for(int i=0; i < arr.length; i++){ 979 | Tile t = list.get(i); 980 | arr[i] = getViewForTile(t, mViewCache.getCachedView()); 981 | checkAndFixLayoutParams(arr[i], t); 982 | } 983 | 984 | return arr; 985 | } 986 | 987 | public View[] getViewsVisibleInRange(int from, int to){ 988 | final List listLeft = getTilesWithLeftRange(from, to); 989 | final List listRight = getTilesWithRightRange(from, to); 990 | 991 | ArrayList union = ToolBox.union(listLeft, listRight); 992 | 993 | final View[] arr = new View[union.size()]; 994 | for(int i=0; i < arr.length; i++){ 995 | Tile t = union.get(i); 996 | arr[i] = getViewForTile(t, mViewCache.getCachedView()); 997 | checkAndFixLayoutParams(arr[i], t); 998 | } 999 | 1000 | return arr; 1001 | } 1002 | 1003 | public void setTiles(ArrayList tiles) { 1004 | initWithNewData(tiles); 1005 | if(mChangeListener != null) mChangeListener.onDataSetChanged(); 1006 | } 1007 | 1008 | public void setDataChangeListener(IDataListener listener){ 1009 | mChangeListener = listener; 1010 | } 1011 | 1012 | @SuppressWarnings("unchecked") 1013 | protected void initWithNewData(ArrayList tiles){ 1014 | mTilesByBegining = (ArrayList) tiles.clone(); 1015 | 1016 | Collections.sort(mTilesByBegining, beginingComparator); 1017 | 1018 | mTilesByEnd = (ArrayList) mTilesByBegining.clone(); 1019 | Collections.sort(mTilesByEnd, endComparator); 1020 | } 1021 | 1022 | /** 1023 | * @param from inclusive 1024 | * @param to exclusive 1025 | */ 1026 | public List getTilesWithLeftRange(int from, int to){ 1027 | if(mTilesByBegining.size() == 0) return Collections.emptyList(); 1028 | final int fromIndex = binarySearchLeftEdges(from); 1029 | if(mTilesByBegining.get(fromIndex).getX() > to) return Collections.emptyList(); 1030 | 1031 | int i = fromIndex; 1032 | Tile t = mTilesByBegining.get(i); 1033 | while(t.getX() < to){ 1034 | i++; 1035 | if(i < mTilesByBegining.size())t = mTilesByBegining.get(i); 1036 | else break; 1037 | } 1038 | 1039 | return mTilesByBegining.subList(fromIndex, i); 1040 | } 1041 | 1042 | /** 1043 | * 1044 | * @param from exclusive 1045 | * @param to inclusive 1046 | */ 1047 | public List getTilesWithRightRange(int from, int to){ 1048 | if(mTilesByEnd.size() == 0) return Collections.emptyList(); 1049 | 1050 | final int fromIndex = binarySearchRightEdges(from + 1); //from is exclusive 1051 | final int fromRight = mTilesByEnd.get(fromIndex).getXRight(); 1052 | 1053 | if(fromRight > to) return Collections.emptyList(); 1054 | 1055 | int i = fromIndex; 1056 | Tile t = mTilesByEnd.get(i); 1057 | while(t.getXRight() <= to){ 1058 | i++; 1059 | if(i < mTilesByEnd.size()) t = mTilesByEnd.get(i); 1060 | else break; 1061 | } 1062 | 1063 | return mTilesByEnd.subList(fromIndex, i); 1064 | } 1065 | 1066 | /** Continues to split same values until it rests on first of them 1067 | * returns first tile with left equal than value or greater 1068 | */ 1069 | private int binarySearchLeftEdges(int value){ 1070 | int lo = 0; 1071 | int hi = mTilesByBegining.size() - 1; 1072 | int mid = 0; 1073 | Tile t = null; 1074 | while (lo <= hi) { 1075 | // Key is in a[lo..hi] or not present. 1076 | mid = lo + (hi - lo) / 2; 1077 | t = mTilesByBegining.get(mid); 1078 | 1079 | if (value > t.getX()) lo = mid + 1; 1080 | else hi = mid - 1; 1081 | 1082 | } 1083 | 1084 | while(t != null && t.getX() < value && mid < mTilesByBegining.size()-1){ 1085 | mid++; 1086 | t = mTilesByBegining.get(mid); 1087 | } 1088 | 1089 | return mid; 1090 | } 1091 | 1092 | /** Continues to split same values until it rests on first of them 1093 | * returns first tile with right equal than value or greater 1094 | */ 1095 | private int binarySearchRightEdges(int value){ 1096 | int lo = 0; 1097 | int hi = mTilesByEnd.size() - 1; 1098 | int mid = 0; 1099 | Tile t = null; 1100 | while (lo <= hi) { 1101 | // Key is in a[lo..hi] or not present. 1102 | mid = lo + (hi - lo) / 2; 1103 | t = mTilesByEnd.get(mid); 1104 | 1105 | final int r = t.getXRight(); 1106 | if (value > r) lo = mid + 1; 1107 | else hi = mid - 1; 1108 | } 1109 | 1110 | while(t != null && t.getXRight() < value && mid < mTilesByEnd.size()-1){ 1111 | mid++; 1112 | t = mTilesByEnd.get(mid); 1113 | } 1114 | 1115 | return mid; 1116 | } 1117 | 1118 | 1119 | 1120 | /** 1121 | * Use this in getViewForTile implementation to provide correctly initialized layout params for component 1122 | * @param t Tile data from datamodel 1123 | * @return ContendBand layout params 1124 | */ 1125 | public LayoutParams getLayoutParamsForTile(Tile t){ 1126 | LayoutParams lp = new LayoutParams(); 1127 | lp.tileId = t.getId(); 1128 | lp.dspLeft = t.getX(); 1129 | lp.dspTop = t.getY(); 1130 | lp.dspWidth = t.getWidth(); 1131 | lp.dspHeight = t.getHeight(); 1132 | lp.z = t.getZ(); 1133 | return lp; 1134 | } 1135 | 1136 | 1137 | interface IDataListener { 1138 | void onDataSetChanged(); 1139 | } 1140 | 1141 | } 1142 | 1143 | private static class ViewCache { 1144 | final LinkedList> mCachedItemViews = new LinkedList>(); 1145 | 1146 | /** 1147 | * Check if list of weak references has any view still in memory to offer for recycling 1148 | * @return cached view 1149 | */ 1150 | T getCachedView(){ 1151 | if (mCachedItemViews.size() != 0) { 1152 | T v; 1153 | do{ 1154 | v = mCachedItemViews.removeFirst().get(); 1155 | } 1156 | while(v == null && mCachedItemViews.size() != 0); 1157 | return v; 1158 | } 1159 | return null; 1160 | } 1161 | 1162 | void cacheView(T v){ 1163 | WeakReference ref = new WeakReference(v); 1164 | mCachedItemViews.addLast(ref); 1165 | } 1166 | } 1167 | 1168 | } 1169 | -------------------------------------------------------------------------------- /library/src/main/java/com/martinappl/components/ui/containers/EndlessLoopAdapterContainer.java: -------------------------------------------------------------------------------- 1 | package com.martinappl.components.ui.containers; 2 | 3 | 4 | import java.lang.ref.WeakReference; 5 | import java.util.LinkedList; 6 | 7 | import android.content.Context; 8 | import android.content.res.TypedArray; 9 | import android.database.DataSetObserver; 10 | import android.graphics.Point; 11 | import android.graphics.Rect; 12 | import android.util.AttributeSet; 13 | import android.util.Log; 14 | import android.view.KeyEvent; 15 | import android.view.MotionEvent; 16 | import android.view.VelocityTracker; 17 | import android.view.View; 18 | import android.view.ViewConfiguration; 19 | import android.view.ViewDebug.CapturedViewProperty; 20 | import android.widget.Adapter; 21 | import android.widget.AdapterView; 22 | import android.widget.Scroller; 23 | 24 | import com.martinappl.components.R; 25 | import com.martinappl.components.general.ToolBox; 26 | import com.martinappl.components.ui.containers.interfaces.IViewObserver; 27 | 28 | /** 29 | * 30 | * @author Martin Appl 31 | * 32 | * Endless loop with items filling from adapter. Currently only horizontal orientation is implemented 33 | * View recycling in adapter is supported. You are encouraged to recycle view in adapter if possible 34 | * 35 | */ 36 | public class EndlessLoopAdapterContainer extends AdapterView { 37 | /** Children added with this layout mode will be added after the last child */ 38 | protected static final int LAYOUT_MODE_AFTER = 0; 39 | 40 | /** Children added with this layout mode will be added before the first child */ 41 | protected static final int LAYOUT_MODE_TO_BEFORE = 1; 42 | 43 | protected static final int SCROLLING_DURATION = 500; 44 | 45 | 46 | 47 | /** The adapter providing data for container */ 48 | protected Adapter mAdapter; 49 | 50 | /** The adaptor position of the first visible item */ 51 | protected int mFirstItemPosition; 52 | 53 | /** The adaptor position of the last visible item */ 54 | protected int mLastItemPosition; 55 | 56 | /** The adaptor position of selected item */ 57 | protected int mSelectedPosition = INVALID_POSITION; 58 | 59 | /** Left of current most left child*/ 60 | protected int mLeftChildEdge; 61 | 62 | /** User is not touching the list */ 63 | protected static final int TOUCH_STATE_RESTING = 1; 64 | 65 | /** User is scrolling the list */ 66 | protected static final int TOUCH_STATE_SCROLLING = 2; 67 | 68 | /** Fling gesture in progress */ 69 | protected static final int TOUCH_STATE_FLING = 3; 70 | 71 | /** Aligning in progress */ 72 | protected static final int TOUCH_STATE_ALIGN = 4; 73 | 74 | protected static final int TOUCH_STATE_DISTANCE_SCROLL = 5; 75 | 76 | /** A list of cached (re-usable) item views */ 77 | protected final LinkedList> mCachedItemViews = new LinkedList>(); 78 | 79 | /** If there is not enough items to fill adapter, this value is set to true and scrolling is disabled. Since all items from adapter are on screen*/ 80 | protected boolean isSrollingDisabled = false; 81 | 82 | /** Whether content should be repeated when there is not enough items to fill container */ 83 | protected boolean shouldRepeat = true; 84 | 85 | /** Position to scroll adapter only if is in endless mode. This is done after layout if we find out we are endless, we must relayout*/ 86 | protected int mScrollPositionIfEndless = -1; 87 | 88 | private IViewObserver mViewObserver; 89 | 90 | 91 | protected int mTouchState = TOUCH_STATE_RESTING; 92 | 93 | protected final Scroller mScroller = new Scroller(getContext()); 94 | private VelocityTracker mVelocityTracker; 95 | private boolean mDataChanged; 96 | 97 | private int mTouchSlop; 98 | private int mMinimumVelocity; 99 | private int mMaximumVelocity; 100 | 101 | private boolean mAllowLongPress; 102 | private float mLastMotionX; 103 | private float mLastMotionY; 104 | // private long mDownTime; 105 | 106 | private final Point mDown = new Point(); 107 | private boolean mHandleSelectionOnActionUp = false; 108 | private boolean mInterceptTouchEvents; 109 | // private boolean mCancelInIntercept; 110 | 111 | protected OnItemClickListener mOnItemClickListener; 112 | protected OnItemSelectedListener mOnItemSelectedListener; 113 | 114 | public EndlessLoopAdapterContainer(Context context, AttributeSet attrs, 115 | int defStyle) { 116 | super(context, attrs, defStyle); 117 | 118 | final ViewConfiguration configuration = ViewConfiguration.get(context); 119 | mTouchSlop = configuration.getScaledTouchSlop(); 120 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 121 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 122 | 123 | //init params from xml 124 | if(attrs != null){ 125 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EndlessLoopAdapterContainer, defStyle, 0); 126 | 127 | shouldRepeat = a.getBoolean(R.styleable.EndlessLoopAdapterContainer_shouldRepeat, false); 128 | 129 | a.recycle(); 130 | } 131 | } 132 | 133 | public EndlessLoopAdapterContainer(Context context, AttributeSet attrs) { 134 | this(context, attrs,0); 135 | 136 | } 137 | 138 | public EndlessLoopAdapterContainer(Context context) { 139 | this(context,null); 140 | } 141 | 142 | private final DataSetObserver fDataObserver = new DataSetObserver() { 143 | 144 | @Override 145 | public void onChanged() { 146 | synchronized(this){ 147 | mDataChanged = true; 148 | } 149 | invalidate(); 150 | } 151 | 152 | @Override 153 | public void onInvalidated() { 154 | mAdapter = null; 155 | } 156 | }; 157 | 158 | 159 | /** 160 | * Params describing position of child view in container 161 | * in HORIZONTAL mode TOP,CENTER,BOTTOM are active in VERTICAL mode LEFT,CENTER,RIGHT are active 162 | * @author Martin Appl 163 | * 164 | */ 165 | public static class LoopLayoutParams extends MarginLayoutParams{ 166 | public static final int TOP = 0; 167 | public static final int CENTER = 1; 168 | public static final int BOTTOM = 2; 169 | public static final int LEFT = 3; 170 | public static final int RIGHT = 4; 171 | 172 | public int position; 173 | // public int actualWidth; 174 | // public int actualHeight; 175 | 176 | public LoopLayoutParams(int w, int h) { 177 | super(w, h); 178 | position = CENTER; 179 | } 180 | 181 | public LoopLayoutParams(int w, int h,int pos){ 182 | super(w, h); 183 | position = pos; 184 | } 185 | 186 | public LoopLayoutParams(android.view.ViewGroup.LayoutParams lp) { 187 | super(lp); 188 | 189 | if(lp!=null && lp instanceof MarginLayoutParams){ 190 | MarginLayoutParams mp = (MarginLayoutParams) lp; 191 | leftMargin = mp.leftMargin; 192 | rightMargin = mp.rightMargin; 193 | topMargin = mp.topMargin; 194 | bottomMargin = mp.bottomMargin; 195 | } 196 | 197 | position = CENTER; 198 | } 199 | 200 | 201 | } 202 | 203 | protected LoopLayoutParams createLayoutParams(int w, int h){ 204 | return new LoopLayoutParams(w, h); 205 | } 206 | 207 | protected LoopLayoutParams createLayoutParams(int w, int h,int pos){ 208 | return new LoopLayoutParams(w, h, pos); 209 | } 210 | 211 | protected LoopLayoutParams createLayoutParams(android.view.ViewGroup.LayoutParams lp){ 212 | return new LoopLayoutParams(lp); 213 | } 214 | 215 | 216 | public boolean isRepeatable() { 217 | return shouldRepeat; 218 | } 219 | 220 | public boolean isEndlessRightNow(){ 221 | return !isSrollingDisabled; 222 | } 223 | 224 | public void setShouldRepeat(boolean shouldRepeat) { 225 | this.shouldRepeat = shouldRepeat; 226 | } 227 | 228 | /** 229 | * Sets position in adapter of first shown item in container 230 | * @param position 231 | */ 232 | public void scrollToPosition(int position){ 233 | if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count"); 234 | 235 | reset(); 236 | refillInternal(position-1, position); 237 | invalidate(); 238 | } 239 | 240 | public void scrollToPositionIfEndless(int position){ 241 | if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count"); 242 | 243 | if(isEndlessRightNow() && getChildCount() != 0){ 244 | scrollToPosition(position); 245 | } 246 | else{ 247 | mScrollPositionIfEndless = position; 248 | } 249 | } 250 | 251 | /** 252 | * Returns position to which will container scroll on next relayout 253 | * @return scroll position on next layout or -1 if it will scroll nowhere 254 | */ 255 | public int getScrollPositionIfEndless(){ 256 | return mScrollPositionIfEndless; 257 | } 258 | 259 | /** 260 | * Get index of currently first item in adapter 261 | * @return 262 | */ 263 | public int getScrollPosition(){ 264 | return mFirstItemPosition; 265 | } 266 | 267 | /** 268 | * Return offset by which is edge off first item moved off screen. 269 | * You can persist it and insert to setFirstItemOffset() to restore exact scroll position 270 | * 271 | * @return offset of first item, or 0 if there is not enough items to fill container and scrolling is disabled 272 | */ 273 | public int getFirstItemOffset(){ 274 | if(isSrollingDisabled) return 0; 275 | else return getScrollX() - mLeftChildEdge; 276 | } 277 | 278 | /** 279 | * Negative number. Offset by which is left edge of first item moved off screen. 280 | * @param offset 281 | */ 282 | public void setFirstItemOffset(int offset){ 283 | scrollTo(offset, 0); 284 | } 285 | 286 | @Override 287 | public Adapter getAdapter() { 288 | return mAdapter; 289 | } 290 | 291 | @Override 292 | public void setAdapter(Adapter adapter) { 293 | if(mAdapter != null) { 294 | mAdapter.unregisterDataSetObserver(fDataObserver); 295 | } 296 | mAdapter = adapter; 297 | mAdapter.registerDataSetObserver(fDataObserver); 298 | 299 | if(adapter instanceof IViewObserver){ 300 | setViewObserver((IViewObserver) adapter); 301 | } 302 | 303 | reset(); 304 | refill(); 305 | invalidate(); 306 | } 307 | 308 | @Override 309 | public View getSelectedView() { 310 | if(mSelectedPosition == INVALID_POSITION) return null; 311 | 312 | final int index; 313 | if(mFirstItemPosition > mSelectedPosition){ 314 | index = mSelectedPosition + mAdapter.getCount() - mFirstItemPosition; 315 | } 316 | else{ 317 | index = mSelectedPosition - mFirstItemPosition; 318 | } 319 | if(index < 0 || index >= getChildCount()) return null; 320 | 321 | return getChildAt(index); 322 | } 323 | 324 | 325 | /** 326 | * Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect 327 | */ 328 | @Override 329 | public void setSelection(int position) { 330 | if(mAdapter == null) throw new IllegalStateException("You are trying to set selection on widget without adapter"); 331 | if(mAdapter.getCount() == 0 && position == 0) position = -1; 332 | if(position < -1 || position > mAdapter.getCount()-1) 333 | throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect"); 334 | 335 | View v = getSelectedView(); 336 | if(v != null) v.setSelected(false); 337 | 338 | 339 | final int oldPos = mSelectedPosition; 340 | mSelectedPosition = position; 341 | 342 | if(position == -1){ 343 | if(mOnItemSelectedListener != null) mOnItemSelectedListener.onNothingSelected(this); 344 | return; 345 | } 346 | 347 | v = getSelectedView(); 348 | if(v != null) v.setSelected(true); 349 | 350 | if(oldPos != mSelectedPosition && mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, mSelectedPosition, getSelectedItemId()); 351 | } 352 | 353 | 354 | private void reset() { 355 | scrollTo(0, 0); 356 | removeAllViewsInLayout(); 357 | mFirstItemPosition = 0; 358 | mLastItemPosition = -1; 359 | mLeftChildEdge = 0; 360 | } 361 | 362 | 363 | @Override 364 | public void computeScroll() { 365 | // if we don't have an adapter, we don't need to do anything 366 | if (mAdapter == null) { 367 | return; 368 | } 369 | if(mAdapter.getCount() == 0){ 370 | return; 371 | } 372 | 373 | if (mScroller.computeScrollOffset()) { 374 | if(mScroller.getFinalX() == mScroller.getCurrX()){ 375 | mScroller.abortAnimation(); 376 | mTouchState = TOUCH_STATE_RESTING; 377 | if(!checkScrollPosition()) 378 | clearChildrenCache(); 379 | return; 380 | } 381 | 382 | int x = mScroller.getCurrX(); 383 | scrollTo(x, 0); 384 | 385 | postInvalidate(); 386 | } 387 | else if(mTouchState == TOUCH_STATE_FLING || mTouchState == TOUCH_STATE_DISTANCE_SCROLL){ 388 | mTouchState = TOUCH_STATE_RESTING; 389 | if(!checkScrollPosition()) 390 | clearChildrenCache(); 391 | } 392 | 393 | if(mDataChanged){ 394 | removeAllViewsInLayout(); 395 | refillOnChange(mFirstItemPosition); 396 | return; 397 | } 398 | 399 | relayout(); 400 | removeNonVisibleViews(); 401 | refillRight(); 402 | refillLeft(); 403 | 404 | } 405 | 406 | /** 407 | * 408 | * @param velocityY The initial velocity in the Y direction. Positive 409 | * numbers mean that the finger/cursor is moving down the screen, 410 | * which means we want to scroll towards the top. 411 | * @param velocityX The initial velocity in the X direction. Positive 412 | * numbers mean that the finger/cursor is moving right the screen, 413 | * which means we want to scroll towards the top. 414 | */ 415 | public void fling(int velocityX, int velocityY){ 416 | mTouchState = TOUCH_STATE_FLING; 417 | final int x = getScrollX(); 418 | final int y = getScrollY(); 419 | 420 | mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, Integer.MIN_VALUE,Integer.MAX_VALUE); 421 | 422 | invalidate(); 423 | } 424 | 425 | /** 426 | * Scroll widget by given distance in pixels 427 | * @param dx 428 | */ 429 | public void scroll(int dx){ 430 | mScroller.startScroll(getScrollX(), 0, dx, 0, SCROLLING_DURATION); 431 | mTouchState = TOUCH_STATE_DISTANCE_SCROLL; 432 | invalidate(); 433 | } 434 | 435 | @Override 436 | protected void onLayout(boolean changed, int left, int top, int right, 437 | int bottom) { 438 | super.onLayout(changed, left, top, right, bottom); 439 | 440 | // if we don't have an adapter, we don't need to do anything 441 | if (mAdapter == null) { 442 | return; 443 | } 444 | 445 | refillInternal(mLastItemPosition,mFirstItemPosition); 446 | } 447 | 448 | /** 449 | * Method for actualizing content after data change in adapter. It is expected container was emptied before 450 | * @param firstItemPosition 451 | */ 452 | protected void refillOnChange(int firstItemPosition){ 453 | refillInternal(firstItemPosition-1, firstItemPosition); 454 | } 455 | 456 | 457 | protected void refillInternal(final int lastItemPos,final int firstItemPos){ 458 | // if we don't have an adapter, we don't need to do anything 459 | if (mAdapter == null) { 460 | return; 461 | } 462 | if(mAdapter.getCount() == 0){ 463 | return; 464 | } 465 | 466 | if(getChildCount() == 0){ 467 | fillFirstTime(lastItemPos, firstItemPos); 468 | } 469 | else{ 470 | relayout(); 471 | removeNonVisibleViews(); 472 | refillRight(); 473 | refillLeft(); 474 | } 475 | } 476 | 477 | /** 478 | * Check if container visible area is filled and refill empty areas 479 | */ 480 | private void refill(){ 481 | scrollTo(0, 0); 482 | refillInternal(-1, 0); 483 | } 484 | 485 | // protected void measureChild(View child, LoopLayoutParams params){ 486 | // //prepare spec for measurement 487 | // final int specW, specH; 488 | // 489 | // specW = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), 0, params.width); 490 | // specH = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED), 0, params.height); 491 | // 492 | ////// final boolean useMeasuredW, useMeasuredH; 493 | //// if(params.height >= 0){ 494 | //// specH = MeasureSpec.EXACTLY | params.height; 495 | ////// useMeasuredH = false; 496 | //// } 497 | //// else{ 498 | //// if(params.height == LayoutParams.MATCH_PARENT){ 499 | //// specH = MeasureSpec.EXACTLY | getHeight(); 500 | //// params.height = getHeight(); 501 | ////// useMeasuredH = false; 502 | //// }else{ 503 | //// specH = MeasureSpec.AT_MOST | getHeight(); 504 | ////// useMeasuredH = true; 505 | //// } 506 | //// } 507 | //// 508 | //// if(params.width >= 0){ 509 | //// specW = MeasureSpec.EXACTLY | params.width; 510 | ////// useMeasuredW = false; 511 | //// } 512 | //// else{ 513 | //// if(params.width == LayoutParams.MATCH_PARENT){ 514 | //// specW = MeasureSpec.EXACTLY | getWidth(); 515 | //// params.width = getWidth(); 516 | ////// useMeasuredW = false; 517 | //// }else{ 518 | //// specW = MeasureSpec.UNSPECIFIED; 519 | ////// useMeasuredW = true; 520 | //// } 521 | //// } 522 | // 523 | // //measure 524 | // child.measure(specW, specH); 525 | // //put measured values into layout params from where they will be used in layout. 526 | // //Use measured values only if exact values was not specified in layout params. 527 | //// if(useMeasuredH) params.actualHeight = child.getMeasuredHeight(); 528 | //// else params.actualHeight = params.height; 529 | //// 530 | //// if(useMeasuredW) params.actualWidth = child.getMeasuredWidth(); 531 | //// else params.actualWidth = params.width; 532 | // } 533 | 534 | protected void measureChild(View child){ 535 | final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY); 536 | final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY); 537 | measureChild(child, pwms, phms); 538 | } 539 | 540 | private void relayout(){ 541 | final int c = getChildCount(); 542 | int left = mLeftChildEdge; 543 | 544 | View child; 545 | LoopLayoutParams lp; 546 | for(int i = 0; i < c; i++){ 547 | child = getChildAt(i); 548 | lp = (LoopLayoutParams) child.getLayoutParams(); 549 | measureChild(child); 550 | 551 | left = layoutChildHorizontal(child, left, lp); 552 | } 553 | 554 | } 555 | 556 | 557 | protected void fillFirstTime(final int lastItemPos,final int firstItemPos){ 558 | final int leftScreenEdge = 0; 559 | final int rightScreenEdge = leftScreenEdge + getWidth(); 560 | 561 | int right; 562 | int left; 563 | View child; 564 | 565 | boolean isRepeatingNow = false; 566 | 567 | //scrolling is enabled until we find out we don't have enough items 568 | isSrollingDisabled = false; 569 | 570 | mLastItemPosition = lastItemPos; 571 | mFirstItemPosition = firstItemPos; 572 | mLeftChildEdge = 0; 573 | right = mLeftChildEdge; 574 | left = mLeftChildEdge; 575 | 576 | while(right < rightScreenEdge){ 577 | mLastItemPosition++; 578 | 579 | if(isRepeatingNow && mLastItemPosition >= firstItemPos) return; 580 | 581 | if(mLastItemPosition >= mAdapter.getCount()){ 582 | if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0; 583 | else{ 584 | if(firstItemPos > 0){ 585 | mLastItemPosition = 0; 586 | isRepeatingNow = true; 587 | } 588 | else if(!shouldRepeat){ 589 | mLastItemPosition--; 590 | isSrollingDisabled = true; 591 | final int w = right-mLeftChildEdge; 592 | final int dx = (getWidth() - w)/2; 593 | scrollTo(-dx, 0); 594 | return; 595 | } 596 | 597 | } 598 | } 599 | 600 | if(mLastItemPosition >= mAdapter.getCount() ){ 601 | Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()"); 602 | return; 603 | } 604 | 605 | child = mAdapter.getView(mLastItemPosition, getCachedView(), this); 606 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); 607 | left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams()); 608 | right = child.getRight(); 609 | 610 | //if selected view is going to screen, set selected state on him 611 | if(mLastItemPosition == mSelectedPosition){ 612 | child.setSelected(true); 613 | } 614 | 615 | } 616 | 617 | if(mScrollPositionIfEndless > 0){ 618 | final int p = mScrollPositionIfEndless; 619 | mScrollPositionIfEndless = -1; 620 | removeAllViewsInLayout(); 621 | refillOnChange(p); 622 | } 623 | } 624 | 625 | 626 | /** 627 | * Checks and refills empty area on the right 628 | */ 629 | protected void refillRight(){ 630 | if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch 631 | if(getChildCount() == 0) return; 632 | 633 | final int leftScreenEdge = getScrollX(); 634 | final int rightScreenEdge = leftScreenEdge + getWidth(); 635 | 636 | View child = getChildAt(getChildCount() - 1); 637 | int right = child.getRight(); 638 | int currLayoutLeft = right + ((LoopLayoutParams)child.getLayoutParams()).rightMargin; 639 | while(right < rightScreenEdge){ 640 | mLastItemPosition++; 641 | if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0; 642 | 643 | child = mAdapter.getView(mLastItemPosition, getCachedView(), this); 644 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); 645 | currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams()); 646 | right = child.getRight(); 647 | 648 | //if selected view is going to screen, set selected state on him 649 | if(mLastItemPosition == mSelectedPosition){ 650 | child.setSelected(true); 651 | } 652 | } 653 | } 654 | 655 | /** 656 | * Checks and refills empty area on the left 657 | */ 658 | protected void refillLeft(){ 659 | if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override first init to scrolling disabled by falling to this branch 660 | if(getChildCount() == 0) return; 661 | 662 | final int leftScreenEdge = getScrollX(); 663 | 664 | View child = getChildAt(0); 665 | int childLeft = child.getLeft(); 666 | int currLayoutRight = childLeft - ((LoopLayoutParams)child.getLayoutParams()).leftMargin; 667 | while(currLayoutRight > leftScreenEdge){ 668 | mFirstItemPosition--; 669 | if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; 670 | 671 | child = mAdapter.getView(mFirstItemPosition, getCachedView(), this); 672 | child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); 673 | currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); 674 | childLeft = child.getLeft() - ((LoopLayoutParams)child.getLayoutParams()).leftMargin; 675 | //update left edge of children in container 676 | mLeftChildEdge = childLeft; 677 | 678 | //if selected view is going to screen, set selected state on him 679 | if(mFirstItemPosition == mSelectedPosition){ 680 | child.setSelected(true); 681 | } 682 | } 683 | } 684 | 685 | // /** 686 | // * Checks and refills empty area on the left 687 | // */ 688 | // protected void refillLeft(){ 689 | // if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch 690 | // final int leftScreenEdge = getScrollX(); 691 | // 692 | // View child = getChildAt(0); 693 | // int currLayoutRight = child.getRight(); 694 | // while(currLayoutRight > leftScreenEdge){ 695 | // mFirstItemPosition--; 696 | // if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; 697 | // 698 | // child = mAdapter.getView(mFirstItemPosition, getCachedView(mFirstItemPosition), this); 699 | // child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); 700 | // currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); 701 | // 702 | // //update left edge of children in container 703 | // mLeftChildEdge = child.getLeft(); 704 | // 705 | // //if selected view is going to screen, set selected state on him 706 | // if(mFirstItemPosition == mSelectedPosition){ 707 | // child.setSelected(true); 708 | // } 709 | // } 710 | // } 711 | 712 | /** 713 | * Removes view that are outside of the visible part of the list. Will not 714 | * remove all views. 715 | */ 716 | protected void removeNonVisibleViews() { 717 | if(getChildCount() == 0) return; 718 | 719 | final int leftScreenEdge = getScrollX(); 720 | final int rightScreenEdge = leftScreenEdge + getWidth(); 721 | 722 | // check if we should remove any views in the left 723 | View firstChild = getChildAt(0); 724 | final int leftedge = firstChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin; 725 | if(leftedge != mLeftChildEdge) throw new IllegalStateException("firstChild.getLeft() != mLeftChildEdge"); 726 | while (firstChild != null && firstChild.getRight() + ((LoopLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) { 727 | //if selected view is going off screen, remove selected state 728 | firstChild.setSelected(false); 729 | 730 | // remove view 731 | removeViewInLayout(firstChild); 732 | 733 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition); 734 | WeakReference ref = new WeakReference(firstChild); 735 | mCachedItemViews.addLast(ref); 736 | 737 | mFirstItemPosition++; 738 | if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; 739 | 740 | // update left item position 741 | mLeftChildEdge = getChildAt(0).getLeft() - ((LoopLayoutParams)getChildAt(0).getLayoutParams()).leftMargin; 742 | 743 | // Continue to check the next child only if we have more than 744 | // one child left 745 | if (getChildCount() > 1) { 746 | firstChild = getChildAt(0); 747 | } else { 748 | firstChild = null; 749 | } 750 | } 751 | 752 | // check if we should remove any views in the right 753 | View lastChild = getChildAt(getChildCount() - 1); 754 | while (lastChild != null && firstChild!=null && lastChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin > rightScreenEdge) { 755 | //if selected view is going off screen, remove selected state 756 | lastChild.setSelected(false); 757 | 758 | // remove the right view 759 | removeViewInLayout(lastChild); 760 | 761 | if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition); 762 | WeakReference ref = new WeakReference(lastChild); 763 | mCachedItemViews.addLast(ref); 764 | 765 | mLastItemPosition--; 766 | if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; 767 | 768 | // Continue to check the next child only if we have more than 769 | // one child left 770 | if (getChildCount() > 1) { 771 | lastChild = getChildAt(getChildCount() - 1); 772 | } else { 773 | lastChild = null; 774 | } 775 | } 776 | } 777 | 778 | 779 | /** 780 | * Adds a view as a child view and takes care of measuring it 781 | * 782 | * @param child The view to add 783 | * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT 784 | * @return child which was actually added to container, subclasses can override to introduce frame views 785 | */ 786 | protected View addAndMeasureChildHorizontal(final View child, final int layoutMode) { 787 | LayoutParams lp = child.getLayoutParams(); 788 | LoopLayoutParams params; 789 | if (lp == null) { 790 | params = createLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 791 | } 792 | else{ 793 | if(lp!=null && lp instanceof LoopLayoutParams) params = (LoopLayoutParams) lp; 794 | else params = createLayoutParams(lp); 795 | } 796 | final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; 797 | addViewInLayout(child, index, params, true); 798 | 799 | measureChild(child); 800 | child.setDrawingCacheEnabled(true); 801 | 802 | return child; 803 | } 804 | 805 | 806 | 807 | /** 808 | * Layouts children from left to right 809 | * @param left positon for left edge in parent container 810 | * @param lp layout params 811 | * @return new left 812 | */ 813 | protected int layoutChildHorizontal(View v,int left, LoopLayoutParams lp){ 814 | int l,t,r,b; 815 | 816 | switch(lp.position){ 817 | case LoopLayoutParams.TOP: 818 | l = left + lp.leftMargin; 819 | t = lp.topMargin; 820 | r = l + v.getMeasuredWidth(); 821 | b = t + v.getMeasuredHeight(); 822 | break; 823 | case LoopLayoutParams.BOTTOM: 824 | b = getHeight() - lp.bottomMargin; 825 | t = b - v.getMeasuredHeight(); 826 | l = left + lp.leftMargin; 827 | r = l + v.getMeasuredWidth(); 828 | break; 829 | case LoopLayoutParams.CENTER: 830 | l = left + lp.leftMargin; 831 | r = l + v.getMeasuredWidth(); 832 | final int x = (getHeight() - v.getMeasuredHeight())/2; 833 | t = x; 834 | b = t + v.getMeasuredHeight(); 835 | break; 836 | default: 837 | throw new RuntimeException("Only TOP,BOTTOM,CENTER are alowed in horizontal orientation"); 838 | } 839 | 840 | 841 | v.layout(l, t, r, b); 842 | return r + lp.rightMargin; 843 | } 844 | 845 | /** 846 | * Layout children from right to left 847 | */ 848 | protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){ 849 | final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; 850 | layoutChildHorizontal(v, left, lp); 851 | return left; 852 | } 853 | 854 | /** 855 | * Allows to make scroll alignments 856 | * @return true if invalidate() was issued, and container is going to scroll 857 | */ 858 | protected boolean checkScrollPosition(){ 859 | return false; 860 | } 861 | 862 | @Override 863 | public boolean onInterceptTouchEvent(MotionEvent ev) { 864 | 865 | /* 866 | * This method JUST determines whether we want to intercept the motion. 867 | * If we return true, onTouchEvent will be called and we do the actual 868 | * scrolling there. 869 | */ 870 | 871 | 872 | /* 873 | * Shortcut the most recurring case: the user is in the dragging 874 | * state and he is moving his finger. We want to intercept this 875 | * motion. 876 | */ 877 | final int action = ev.getAction(); 878 | if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { 879 | return true; 880 | } 881 | 882 | final float x = ev.getX(); 883 | final float y = ev.getY(); 884 | switch (action) { 885 | case MotionEvent.ACTION_MOVE: 886 | //if we have scrolling disabled, we don't do anything 887 | if(!shouldRepeat && isSrollingDisabled) return false; 888 | 889 | /* 890 | * not dragging, otherwise the shortcut would have caught it. Check 891 | * whether the user has moved far enough from his original down touch. 892 | */ 893 | 894 | /* 895 | * Locally do absolute value. mLastMotionX is set to the x value 896 | * of the down event. 897 | */ 898 | final int xDiff = (int) Math.abs(x - mLastMotionX); 899 | final int yDiff = (int) Math.abs(y - mLastMotionY); 900 | 901 | final int touchSlop = mTouchSlop; 902 | final boolean xMoved = xDiff > touchSlop; 903 | final boolean yMoved = yDiff > touchSlop; 904 | 905 | if (xMoved) { 906 | 907 | // Scroll if the user moved far enough along the X axis 908 | mTouchState = TOUCH_STATE_SCROLLING; 909 | mHandleSelectionOnActionUp = false; 910 | enableChildrenCache(); 911 | 912 | // Either way, cancel any pending longpress 913 | if (mAllowLongPress) { 914 | mAllowLongPress = false; 915 | // Try canceling the long press. It could also have been scheduled 916 | // by a distant descendant, so use the mAllowLongPress flag to block 917 | // everything 918 | cancelLongPress(); 919 | } 920 | } 921 | if(yMoved){ 922 | mHandleSelectionOnActionUp = false; 923 | if (mAllowLongPress) { 924 | mAllowLongPress = false; 925 | cancelLongPress(); 926 | } 927 | } 928 | break; 929 | 930 | case MotionEvent.ACTION_DOWN: 931 | // Remember location of down touch 932 | mLastMotionX = x; 933 | mLastMotionY = y; 934 | mAllowLongPress = true; 935 | // mCancelInIntercept = false; 936 | 937 | mDown.x = (int) x; 938 | mDown.y = (int) y; 939 | 940 | /* 941 | * If being flinged and user touches the screen, initiate drag; 942 | * otherwise don't. mScroller.isFinished should be false when 943 | * being flinged. 944 | */ 945 | mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; 946 | //if he had normal click in rested state, remember for action up check 947 | if(mTouchState == TOUCH_STATE_RESTING){ 948 | mHandleSelectionOnActionUp = true; 949 | } 950 | break; 951 | 952 | case MotionEvent.ACTION_CANCEL: 953 | mDown.x = -1; 954 | mDown.y = -1; 955 | // mCancelInIntercept = true; 956 | break; 957 | case MotionEvent.ACTION_UP: 958 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates 959 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 960 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); 961 | if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); 962 | } 963 | // Release the drag 964 | mAllowLongPress = false; 965 | mHandleSelectionOnActionUp = false; 966 | mDown.x = -1; 967 | mDown.y = -1; 968 | if(mTouchState == TOUCH_STATE_SCROLLING){ 969 | if(checkScrollPosition()){ 970 | break; 971 | } 972 | } 973 | mTouchState = TOUCH_STATE_RESTING; 974 | clearChildrenCache(); 975 | break; 976 | } 977 | 978 | mInterceptTouchEvents = mTouchState == TOUCH_STATE_SCROLLING; 979 | return mInterceptTouchEvents; 980 | 981 | } 982 | 983 | // /** 984 | // * Allow subclasses to override this to always intercept events 985 | // * @return 986 | // */ 987 | // protected boolean interceptEvents(){ 988 | // /* 989 | // * The only time we want to intercept motion events is if we are in the 990 | // * drag mode. 991 | // */ 992 | // return mTouchState == TOUCH_STATE_SCROLLING; 993 | // } 994 | 995 | protected void handleClick(Point p){ 996 | final int c = getChildCount(); 997 | View v; 998 | final Rect r = new Rect(); 999 | for(int i=0; i < c; i++){ 1000 | v = getChildAt(i); 1001 | v.getHitRect(r); 1002 | if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ 1003 | final View old = getSelectedView(); 1004 | if(old != null) old.setSelected(false); 1005 | 1006 | int position = mFirstItemPosition + i; 1007 | if(position >= mAdapter.getCount()) position = position - mAdapter.getCount(); 1008 | 1009 | 1010 | mSelectedPosition = position; 1011 | v.setSelected(true); 1012 | 1013 | if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position)); 1014 | if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position)); 1015 | 1016 | break; 1017 | } 1018 | } 1019 | } 1020 | 1021 | 1022 | @Override 1023 | public boolean onTouchEvent(MotionEvent event) { 1024 | // if we don't have an adapter, we don't need to do anything 1025 | if (mAdapter == null) { 1026 | return false; 1027 | } 1028 | 1029 | 1030 | 1031 | if (mVelocityTracker == null) { 1032 | mVelocityTracker = VelocityTracker.obtain(); 1033 | } 1034 | mVelocityTracker.addMovement(event); 1035 | 1036 | final int action = event.getAction(); 1037 | final float x = event.getX(); 1038 | final float y = event.getY(); 1039 | 1040 | switch (action) { 1041 | case MotionEvent.ACTION_DOWN: 1042 | /* 1043 | * If being flinged and user touches, stop the fling. isFinished 1044 | * will be false if being flinged. 1045 | */ 1046 | if (!mScroller.isFinished()) { 1047 | mScroller.forceFinished(true); 1048 | } 1049 | 1050 | // Remember where the motion event started 1051 | mLastMotionX = x; 1052 | mLastMotionY = y; 1053 | 1054 | break; 1055 | case MotionEvent.ACTION_MOVE: 1056 | //if we have scrolling disabled, we don't do anything 1057 | if(!shouldRepeat && isSrollingDisabled) return false; 1058 | 1059 | if (mTouchState == TOUCH_STATE_SCROLLING) { 1060 | // Scroll to follow the motion event 1061 | final int deltaX = (int) (mLastMotionX - x); 1062 | mLastMotionX = x; 1063 | mLastMotionY = y; 1064 | 1065 | int sx = getScrollX() + deltaX; 1066 | 1067 | scrollTo(sx, 0); 1068 | 1069 | } 1070 | else{ 1071 | final int xDiff = (int) Math.abs(x - mLastMotionX); 1072 | 1073 | final int touchSlop = mTouchSlop; 1074 | final boolean xMoved = xDiff > touchSlop; 1075 | 1076 | 1077 | if (xMoved) { 1078 | 1079 | // Scroll if the user moved far enough along the X axis 1080 | mTouchState = TOUCH_STATE_SCROLLING; 1081 | enableChildrenCache(); 1082 | 1083 | // Either way, cancel any pending longpress 1084 | if (mAllowLongPress) { 1085 | mAllowLongPress = false; 1086 | // Try canceling the long press. It could also have been scheduled 1087 | // by a distant descendant, so use the mAllowLongPress flag to block 1088 | // everything 1089 | cancelLongPress(); 1090 | } 1091 | } 1092 | } 1093 | break; 1094 | case MotionEvent.ACTION_UP: 1095 | 1096 | //this must be here, in case no child view returns true, 1097 | //events will propagate back here and on intercept touch event wont be called again 1098 | //in case of no parent it propagates here, in case of parent it usualy propagates to on cancel 1099 | if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 1100 | final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); 1101 | if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); 1102 | mHandleSelectionOnActionUp = false; 1103 | } 1104 | 1105 | //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates 1106 | if (mTouchState == TOUCH_STATE_SCROLLING) { 1107 | 1108 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 1109 | int initialXVelocity = (int) mVelocityTracker.getXVelocity(); 1110 | int initialYVelocity = (int) mVelocityTracker.getYVelocity(); 1111 | 1112 | if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { 1113 | fling(-initialXVelocity, -initialYVelocity); 1114 | } 1115 | else{ 1116 | // Release the drag 1117 | clearChildrenCache(); 1118 | mTouchState = TOUCH_STATE_RESTING; 1119 | checkScrollPosition(); 1120 | mAllowLongPress = false; 1121 | 1122 | mDown.x = -1; 1123 | mDown.y = -1; 1124 | } 1125 | 1126 | if (mVelocityTracker != null) { 1127 | mVelocityTracker.recycle(); 1128 | mVelocityTracker = null; 1129 | } 1130 | 1131 | break; 1132 | } 1133 | 1134 | // Release the drag 1135 | clearChildrenCache(); 1136 | mTouchState = TOUCH_STATE_RESTING; 1137 | mAllowLongPress = false; 1138 | 1139 | mDown.x = -1; 1140 | mDown.y = -1; 1141 | 1142 | break; 1143 | case MotionEvent.ACTION_CANCEL: 1144 | 1145 | //this must be here, in case no child view returns true, 1146 | //events will propagate back here and on intercept touch event wont be called again 1147 | //instead we get cancel here, since we stated we shouldn't intercept events and propagate them to children 1148 | //but events propagated back here, because no child was interested 1149 | // if(!mInterceptTouchEvents && mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ 1150 | // handleClick(mDown); 1151 | // mHandleSelectionOnActionUp = false; 1152 | // } 1153 | 1154 | mAllowLongPress = false; 1155 | 1156 | mDown.x = -1; 1157 | mDown.y = -1; 1158 | 1159 | if(mTouchState == TOUCH_STATE_SCROLLING){ 1160 | if(checkScrollPosition()){ 1161 | break; 1162 | } 1163 | } 1164 | 1165 | mTouchState = TOUCH_STATE_RESTING; 1166 | } 1167 | 1168 | return true; 1169 | } 1170 | 1171 | @Override 1172 | public boolean onKeyDown(int keyCode, KeyEvent event) { 1173 | switch (keyCode) { 1174 | case KeyEvent.KEYCODE_DPAD_LEFT: 1175 | checkScrollFocusLeft(); 1176 | break; 1177 | case KeyEvent.KEYCODE_DPAD_RIGHT: 1178 | checkScrollFocusRight(); 1179 | break; 1180 | default: 1181 | break; 1182 | } 1183 | 1184 | return super.onKeyDown(keyCode, event); 1185 | } 1186 | 1187 | /** 1188 | * Moves with scroll window if focus hits one view before end of screen 1189 | */ 1190 | private void checkScrollFocusLeft(){ 1191 | final View focused = getFocusedChild(); 1192 | if(getChildCount() >= 2 ){ 1193 | View second = getChildAt(1); 1194 | View first = getChildAt(0); 1195 | 1196 | if(focused == second){ 1197 | scroll(-first.getWidth()); 1198 | } 1199 | } 1200 | } 1201 | 1202 | private void checkScrollFocusRight(){ 1203 | final View focused = getFocusedChild(); 1204 | if(getChildCount() >= 2 ){ 1205 | View last = getChildAt(getChildCount()-1); 1206 | View lastButOne = getChildAt(getChildCount()-2); 1207 | 1208 | if(focused == lastButOne){ 1209 | scroll(last.getWidth()); 1210 | } 1211 | } 1212 | } 1213 | 1214 | /** 1215 | * Check if list of weak references has any view still in memory to offer for recyclation 1216 | * @return cached view 1217 | */ 1218 | protected View getCachedView(){ 1219 | if (mCachedItemViews.size() != 0) { 1220 | View v; 1221 | do{ 1222 | v = mCachedItemViews.removeFirst().get(); 1223 | } 1224 | while(v == null && mCachedItemViews.size() != 0); 1225 | return v; 1226 | } 1227 | return null; 1228 | } 1229 | 1230 | protected void enableChildrenCache() { 1231 | setChildrenDrawnWithCacheEnabled(true); 1232 | setChildrenDrawingCacheEnabled(true); 1233 | } 1234 | 1235 | protected void clearChildrenCache() { 1236 | setChildrenDrawnWithCacheEnabled(false); 1237 | } 1238 | 1239 | @Override 1240 | public void setOnItemClickListener( 1241 | android.widget.AdapterView.OnItemClickListener listener) { 1242 | mOnItemClickListener = listener; 1243 | } 1244 | 1245 | @Override 1246 | public void setOnItemSelectedListener( 1247 | android.widget.AdapterView.OnItemSelectedListener listener) { 1248 | mOnItemSelectedListener = listener; 1249 | } 1250 | 1251 | @Override 1252 | @CapturedViewProperty 1253 | public int getSelectedItemPosition() { 1254 | return mSelectedPosition; 1255 | } 1256 | 1257 | /** 1258 | * Only set value for selection position field, no gui updates are done 1259 | * for setting selection with gui updates and callback calls use setSelection 1260 | * @param position 1261 | */ 1262 | public void setSeletedItemPosition(int position){ 1263 | if(mAdapter.getCount() == 0 && position == 0) position = -1; 1264 | if(position < -1 || position > mAdapter.getCount()-1) 1265 | throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect"); 1266 | 1267 | mSelectedPosition = position; 1268 | } 1269 | 1270 | @Override 1271 | @CapturedViewProperty 1272 | public long getSelectedItemId() { 1273 | return mAdapter.getItemId(mSelectedPosition); 1274 | } 1275 | 1276 | @Override 1277 | public Object getSelectedItem() { 1278 | return getSelectedView(); 1279 | } 1280 | 1281 | @Override 1282 | @CapturedViewProperty 1283 | public int getCount() { 1284 | if(mAdapter != null) return mAdapter.getCount(); 1285 | else return 0; 1286 | } 1287 | 1288 | @Override 1289 | public int getPositionForView(View view) { 1290 | final int c = getChildCount(); 1291 | View v; 1292 | for(int i = 0; i < c; i++){ 1293 | v = getChildAt(i); 1294 | if(v == view) return mFirstItemPosition + i; 1295 | } 1296 | return INVALID_POSITION; 1297 | } 1298 | 1299 | @Override 1300 | public int getFirstVisiblePosition() { 1301 | return mFirstItemPosition; 1302 | } 1303 | 1304 | @Override 1305 | public int getLastVisiblePosition() { 1306 | return mLastItemPosition; 1307 | } 1308 | 1309 | @Override 1310 | public Object getItemAtPosition(int position) { 1311 | final int index; 1312 | if(mFirstItemPosition > position){ 1313 | index = position + mAdapter.getCount() - mFirstItemPosition; 1314 | } 1315 | else{ 1316 | index = position - mFirstItemPosition; 1317 | } 1318 | if(index < 0 || index >= getChildCount()) return null; 1319 | 1320 | return getChildAt(index); 1321 | } 1322 | 1323 | @Override 1324 | public long getItemIdAtPosition(int position) { 1325 | return mAdapter.getItemId(position); 1326 | } 1327 | 1328 | @Override 1329 | public boolean performItemClick(View view, int position, long id) { 1330 | throw new UnsupportedOperationException(); 1331 | } 1332 | 1333 | 1334 | public void setViewObserver(IViewObserver viewObserver) { 1335 | this.mViewObserver = viewObserver; 1336 | } 1337 | 1338 | 1339 | } 1340 | 1341 | 1342 | --------------------------------------------------------------------------------