diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d45f3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Created by .ignore support plugin (hsz.mobi) +### Android template +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + + diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b521b91 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +CoverFlow \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..217af47 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..a3ec22a --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/.idea/libraries/appcompat_v7_21_0_3.xml b/.idea/libraries/appcompat_v7_21_0_3.xml new file mode 100644 index 0000000..751a9d9 --- /dev/null +++ b/.idea/libraries/appcompat_v7_21_0_3.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_annotations_21_0_3.xml b/.idea/libraries/support_annotations_21_0_3.xml new file mode 100644 index 0000000..7bade0b --- /dev/null +++ b/.idea/libraries/support_annotations_21_0_3.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/support_v4_21_0_3.xml b/.idea/libraries/support_v4_21_0_3.xml new file mode 100644 index 0000000..d35337a --- /dev/null +++ b/.idea/libraries/support_v4_21_0_3.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..90572ee --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + Android + + + Android Lint + + + Java language level migration aids + + + + + Abstraction issues + + + + + + + + + + + + + 1.7 + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..fbd2bf3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..276779e --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/CoverFlow.iml b/CoverFlow.iml new file mode 100644 index 0000000..037608e --- /dev/null +++ b/CoverFlow.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/MAComponents/.classpath b/MAComponents/.classpath deleted file mode 100644 index 26bdfa6..0000000 --- a/MAComponents/.classpath +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/MAComponents/.gitignore b/MAComponents/.gitignore deleted file mode 100644 index a02fbbe..0000000 --- a/MAComponents/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/gen -/bin diff --git a/MAComponents/.project b/MAComponents/.project deleted file mode 100644 index 0e2148c..0000000 --- a/MAComponents/.project +++ /dev/null @@ -1,33 +0,0 @@ - - - MAComponents - - - - - - com.android.ide.eclipse.adt.ResourceManagerBuilder - - - - - com.android.ide.eclipse.adt.PreCompilerBuilder - - - - - org.eclipse.jdt.core.javabuilder - - - - - com.android.ide.eclipse.adt.ApkBuilder - - - - - - com.android.ide.eclipse.adt.AndroidNature - org.eclipse.jdt.core.javanature - - diff --git a/MAComponents/AndroidManifest.xml b/MAComponents/AndroidManifest.xml deleted file mode 100644 index e3f11da..0000000 --- a/MAComponents/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/MAComponents/libs/android-support-v4.jar b/MAComponents/libs/android-support-v4.jar deleted file mode 100644 index 9056828..0000000 Binary files a/MAComponents/libs/android-support-v4.jar and /dev/null differ diff --git a/MAComponents/project.properties b/MAComponents/project.properties deleted file mode 100644 index 91d2b02..0000000 --- a/MAComponents/project.properties +++ /dev/null @@ -1,15 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-19 -android.library=true diff --git a/MAComponents/res/values/strings.xml b/MAComponents/res/values/strings.xml deleted file mode 100644 index f2a4bb7..0000000 --- a/MAComponents/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - MAComponents - - diff --git a/MAComponents/res/values/styles.xml b/MAComponents/res/values/styles.xml deleted file mode 100644 index 4ea9326..0000000 --- a/MAComponents/res/values/styles.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - diff --git a/README.md b/README.md index b356278..f1ca363 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,90 @@ -MComponents +[![Project Status: Suspended - Initial development has started, but there has not yet been a stable, usable release; work has been stopped for the time being but the author(s) intend on resuming work.](http://www.repostatus.org/badges/0.1.0/suspended.svg)](http://www.repostatus.org/#suspended) + +Coverflow ============= +### Description +Android CoverFlow widget with demo. +Forked from [applm/ma-components](https://github.com/applm/ma-components). + +### Screenshot +![Demo](art/screenshot.png) + + +### Code Samples +For example, in your layout: + +```xml + +``` + + +then in your Activity: + +```java + mCoverFlow = (FeatureCoverFlow) findViewById(R.id.coverflow); + mCoverFlow.setAdapter(mAdapter); + + mCoverFlow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + //TODO CoverFlow item clicked + } + }); + + mCoverFlow.setOnScrollPositionListener(new FeatureCoverFlow.OnScrollPositionListener() { + @Override + public void onScrolledToPosition(int position) { + //TODO CoverFlow stopped to position + } + + @Override + public void onScrolling() { + //TODO CoverFlow began scrolling + } + }); +``` + +### Quick Start + +CoverFlow is pushed to Maven Central as a AAR, so you just need to add the following dependency to your `build.gradle`: + + dependencies { + compile 'com.github.moondroid.coverflow:library:1.0' + } + + +### License + +``` +The MIT License (MIT) + +Copyright (c) 2014 Marco Granatiero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -Android library with some Widgets and handy pieces of code I have developed. - -

Library contains: -

-CoverFlow widget

+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000..e26ef78 --- /dev/null +++ b/app/app.iml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..a9cacde --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "it.moondroid.coverflowdemo" + minSdkVersion 15 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.3' + compile project(':lib') +} diff --git a/MAComponents/proguard-project.txt b/app/proguard-rules.pro similarity index 62% rename from MAComponents/proguard-project.txt rename to app/proguard-rules.pro index f2fe155..c58a4fe 100644 --- a/MAComponents/proguard-project.txt +++ b/app/proguard-rules.pro @@ -1,11 +1,8 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. +# in F:/ANDROID/DEVELOPMENT/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/app/src/androidTest/java/it/moondroid/coverflowdemo/ApplicationTest.java b/app/src/androidTest/java/it/moondroid/coverflowdemo/ApplicationTest.java new file mode 100644 index 0000000..d2fdac9 --- /dev/null +++ b/app/src/androidTest/java/it/moondroid/coverflowdemo/ApplicationTest.java @@ -0,0 +1,13 @@ +package it.moondroid.coverflowdemo; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..67c07de --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/it/moondroid/coverflowdemo/CoverFlowActivity.java b/app/src/main/java/it/moondroid/coverflowdemo/CoverFlowActivity.java new file mode 100644 index 0000000..1c366d8 --- /dev/null +++ b/app/src/main/java/it/moondroid/coverflowdemo/CoverFlowActivity.java @@ -0,0 +1,103 @@ +package it.moondroid.coverflowdemo; + +import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.TextSwitcher; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewSwitcher; + +import java.util.ArrayList; + +import it.moondroid.coverflow.components.ui.containers.FeatureCoverFlow; + + +public class CoverFlowActivity extends ActionBarActivity { + + private FeatureCoverFlow mCoverFlow; + private CoverFlowAdapter mAdapter; + private ArrayList mData = new ArrayList<>(0); + private TextSwitcher mTitle; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_coverflow); + + mData.add(new GameEntity(R.drawable.image_1, R.string.title1)); + mData.add(new GameEntity(R.drawable.image_2, R.string.title2)); + mData.add(new GameEntity(R.drawable.image_3, R.string.title3)); + mData.add(new GameEntity(R.drawable.image_4, R.string.title4)); + + mTitle = (TextSwitcher) findViewById(R.id.title); + mTitle.setFactory(new ViewSwitcher.ViewFactory() { + @Override + public View makeView() { + LayoutInflater inflater = LayoutInflater.from(CoverFlowActivity.this); + TextView textView = (TextView) inflater.inflate(R.layout.item_title, null); + return textView; + } + }); + Animation in = AnimationUtils.loadAnimation(this, R.anim.slide_in_top); + Animation out = AnimationUtils.loadAnimation(this, R.anim.slide_out_bottom); + mTitle.setInAnimation(in); + mTitle.setOutAnimation(out); + + mAdapter = new CoverFlowAdapter(this); + mAdapter.setData(mData); + mCoverFlow = (FeatureCoverFlow) findViewById(R.id.coverflow); + mCoverFlow.setAdapter(mAdapter); + + mCoverFlow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Toast.makeText(CoverFlowActivity.this, + getResources().getString(mData.get(position).titleResId), + Toast.LENGTH_SHORT).show(); + } + }); + + mCoverFlow.setOnScrollPositionListener(new FeatureCoverFlow.OnScrollPositionListener() { + @Override + public void onScrolledToPosition(int position) { + mTitle.setText(getResources().getString(mData.get(position).titleResId)); + } + + @Override + public void onScrolling() { + mTitle.setText(""); + } + }); + + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_coverflow_activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/it/moondroid/coverflowdemo/CoverFlowAdapter.java b/app/src/main/java/it/moondroid/coverflowdemo/CoverFlowAdapter.java new file mode 100644 index 0000000..c71a4e8 --- /dev/null +++ b/app/src/main/java/it/moondroid/coverflowdemo/CoverFlowAdapter.java @@ -0,0 +1,70 @@ +package it.moondroid.coverflowdemo; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; + +public class CoverFlowAdapter extends BaseAdapter { + + private ArrayList mData = new ArrayList<>(0); + private Context mContext; + + public CoverFlowAdapter(Context context) { + mContext = context; + } + + public void setData(ArrayList data) { + mData = data; + } + + @Override + public int getCount() { + return mData.size(); + } + + @Override + public Object getItem(int pos) { + return mData.get(pos); + } + + @Override + public long getItemId(int pos) { + return pos; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + + View rowView = convertView; + + if (rowView == null) { + LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + rowView = inflater.inflate(R.layout.item_coverflow, null); + + ViewHolder viewHolder = new ViewHolder(); + viewHolder.text = (TextView) rowView.findViewById(R.id.label); + viewHolder.image = (ImageView) rowView + .findViewById(R.id.image); + rowView.setTag(viewHolder); + } + + ViewHolder holder = (ViewHolder) rowView.getTag(); + + holder.image.setImageResource(mData.get(position).imageResId); + holder.text.setText(mData.get(position).titleResId); + + return rowView; + } + + + static class ViewHolder { + public TextView text; + public ImageView image; + } +} diff --git a/app/src/main/java/it/moondroid/coverflowdemo/GameEntity.java b/app/src/main/java/it/moondroid/coverflowdemo/GameEntity.java new file mode 100644 index 0000000..ad65f88 --- /dev/null +++ b/app/src/main/java/it/moondroid/coverflowdemo/GameEntity.java @@ -0,0 +1,14 @@ +package it.moondroid.coverflowdemo; + +/** + * Created by marco.granatiero on 03/02/2015. + */ +public class GameEntity { + public int imageResId; + public int titleResId; + + public GameEntity (int imageResId, int titleResId){ + this.imageResId = imageResId; + this.titleResId = titleResId; + } +} diff --git a/app/src/main/res/anim/slide_in_top.xml b/app/src/main/res/anim/slide_in_top.xml new file mode 100644 index 0000000..e3b021f --- /dev/null +++ b/app/src/main/res/anim/slide_in_top.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_bottom.xml b/app/src/main/res/anim/slide_out_bottom.xml new file mode 100644 index 0000000..3e64d54 --- /dev/null +++ b/app/src/main/res/anim/slide_out_bottom.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-nodpi/image_1.jpg b/app/src/main/res/drawable-nodpi/image_1.jpg new file mode 100644 index 0000000..64d6668 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/image_1.jpg differ diff --git a/app/src/main/res/drawable-nodpi/image_2.jpg b/app/src/main/res/drawable-nodpi/image_2.jpg new file mode 100644 index 0000000..ae1f193 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/image_2.jpg differ diff --git a/app/src/main/res/drawable-nodpi/image_3.jpg b/app/src/main/res/drawable-nodpi/image_3.jpg new file mode 100644 index 0000000..e7ac1f3 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/image_3.jpg differ diff --git a/app/src/main/res/drawable-nodpi/image_4.jpg b/app/src/main/res/drawable-nodpi/image_4.jpg new file mode 100644 index 0000000..2daae50 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/image_4.jpg differ diff --git a/app/src/main/res/drawable-nodpi/slider_bg.9.png b/app/src/main/res/drawable-nodpi/slider_bg.9.png new file mode 100644 index 0000000..ea1c01b Binary files /dev/null and b/app/src/main/res/drawable-nodpi/slider_bg.9.png differ diff --git a/app/src/main/res/drawable/cover_selector.xml b/app/src/main/res/drawable/cover_selector.xml new file mode 100644 index 0000000..491f58e --- /dev/null +++ b/app/src/main/res/drawable/cover_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/label_background.xml b/app/src/main/res/drawable/label_background.xml new file mode 100644 index 0000000..fd9338e --- /dev/null +++ b/app/src/main/res/drawable/label_background.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_coverflow.xml b/app/src/main/res/layout/activity_coverflow.xml new file mode 100644 index 0000000..f84ce64 --- /dev/null +++ b/app/src/main/res/layout/activity_coverflow.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_coverflow.xml b/app/src/main/res/layout/item_coverflow.xml new file mode 100644 index 0000000..e9c6f9b --- /dev/null +++ b/app/src/main/res/layout/item_coverflow.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_title.xml b/app/src/main/res/layout/item_title.xml new file mode 100644 index 0000000..85b9db2 --- /dev/null +++ b/app/src/main/res/layout/item_title.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_coverflow_activity.xml b/app/src/main/res/menu/menu_coverflow_activity.xml new file mode 100644 index 0000000..3cd3b1c --- /dev/null +++ b/app/src/main/res/menu/menu_coverflow_activity.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cde69bc Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c133a0c Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..bfa42f0 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..324e72c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..63fc816 --- /dev/null +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..c1da005 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,8 @@ + + + 16dp + 16dp + + 120dp + 180dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..06674c7 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + CoverFlow Demo + + Hello world! + Settings + + God of War Ascension + Assassin\'s Creed III + Hitman Absolution + Dishonored + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..766ab99 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/art/screenshot.png b/art/screenshot.png new file mode 100644 index 0000000..4626a94 Binary files /dev/null and b/art/screenshot.png differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..1f26e6f --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +def isReleaseBuild() { + return version.contains("SNAPSHOT") == false +} + +allprojects { + + version = VERSION_NAME + group = GROUP + + repositories { + jcenter() + } +} + +apply plugin: 'android-reporting' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..140d971 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Settings specified in this file will override any Gradle settings +# configured through the IDE. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +VERSION_NAME=1.0 +VERSION_CODE=1 +GROUP=com.github.moondroid.coverflow + +POM_DESCRIPTION=Android CoverFlow widget +POM_URL=https://github.com/moondroid/CoverFlow +POM_SCM_URL=https://github.com/moondroid/CoverFlow +POM_SCM_CONNECTION=scm:git@github.com:moondroid/CoverFlow.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:moondroid/CoverFlow.git +POM_LICENCE_NAME=The MIT License (MIT) +POM_LICENCE_URL=http://opensource.org/licenses/mit-license.php +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=moondroid +POM_DEVELOPER_NAME=Marco Granatiero \ No newline at end of file diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..425838c --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.3' +} + +apply from: '../maven_push.gradle' \ No newline at end of file diff --git a/lib/gradle.properties b/lib/gradle.properties new file mode 100644 index 0000000..eca4295 --- /dev/null +++ b/lib/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=CoverFlow Library +POM_ARTIFACT_ID=library +POM_PACKAGING=aar \ No newline at end of file diff --git a/lib/lib.iml b/lib/lib.iml new file mode 100644 index 0000000..c5cc800 --- /dev/null +++ b/lib/lib.iml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/proguard-rules.pro b/lib/proguard-rules.pro new file mode 100644 index 0000000..c58a4fe --- /dev/null +++ b/lib/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in F:/ANDROID/DEVELOPMENT/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/lib/src/androidTest/java/it/moondroid/coverflow/ApplicationTest.java b/lib/src/androidTest/java/it/moondroid/coverflow/ApplicationTest.java new file mode 100644 index 0000000..3a388bc --- /dev/null +++ b/lib/src/androidTest/java/it/moondroid/coverflow/ApplicationTest.java @@ -0,0 +1,13 @@ +package it.moondroid.coverflow; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..92bd2a4 --- /dev/null +++ b/lib/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/MAComponents/src/com/martinappl/components/general/ToolBox.java b/lib/src/main/java/it/moondroid/coverflow/components/general/ToolBox.java similarity index 96% rename from MAComponents/src/com/martinappl/components/general/ToolBox.java rename to lib/src/main/java/it/moondroid/coverflow/components/general/ToolBox.java index 9bd7b70..584c3d7 100644 --- a/MAComponents/src/com/martinappl/components/general/ToolBox.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/general/ToolBox.java @@ -1,584 +1,584 @@ -package com.martinappl.components.general; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Array; -import java.nio.ByteBuffer; -import java.security.InvalidParameterException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.PointF; -import android.util.FloatMath; -import android.util.Log; -import android.util.TypedValue; -import android.view.View; - - -/** - * @author Martin Appl - * Set of handy mostly mathematical, geometric and generic java static methods - */ -public abstract class ToolBox { - //global constants - private static final String TAG = "ToolBox"; - - - - /** - * Get length of line between points a and b. - * @param a point - * @param b point - * @return length - */ - public static float getLineLength(PointF a,PointF b){ - float vx = b.x - a.x; - float vy = b.y - a.y; - return FloatMath.sqrt(vx*vx + vy*vy); - } - /** - * Get length of line between points A and B. - * @param ax point A x coordinate - * @param ay point A y coordinate - * @param bx point B x coordinate - * @param by point B y coordinate - * @return length - */ - public static float getLineLength(float ax,float ay, float bx,float by){ - float vx = bx - ax; - float vy = by - ay; - return FloatMath.sqrt(vx*vx + vy*vy); - } - - /** - * Get length of vector - * @param vx vector x component - * @param vy vector y component - * @return length - */ - public static float getVectorLength(float vx,float vy){ - return FloatMath.sqrt(vx*vx + vy*vy); - } - - public static float getVectorLength(PointF v){ - return FloatMath.sqrt(v.x*v.x + v.y*v.y); - } - - /** - * Compute intersection point of two infinite lines. Each line is specified by two points - * @param a1 line a - * @param a2 line a - * @param b1 line b - * @param b2 line b - * @return Intersection point - */ - public static PointF getLinesIntersection(PointF a1,PointF a2,PointF b1,PointF b2){ - return getLinesIntersection(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y); - } - - /** - * Compute intersection point of two infinite lines. Each line is specified by two points - * @param x1 line a point 1 - * @param y1 line a point 1 - * @param x2 line a point 2 - * @param y2 line a point 2 - * @param x3 line b point 1 - * @param y3 line b point 1 - * @param x4 line b point 2 - * @param y4 line b point 2 - * @return Intersection point - */ - public static PointF getLinesIntersection( float x1,float y1, float x2, float y2, - float x3,float y3,float x4,float y4) - { - float a1 = y2 - y1; - float b1 = x1 - x2; - float c1 = x2*y1 - x1*y2; //a1*x + b1*y + c1 = 0 is line a - - float a2 = y4-y3; - float b2 = x3-x4; - float c2 = x4*y3 - x3*y4; // a2*x + b2*y + c2 = 0 is line b - - float d = a1*b2 - a2*b1; - if(d == 0) { - throw new InvalidParameterException("Intersection cant be found, lines are paralel."); - } - - float x = (b1*c2 - b2*c1)/d; - float y = (a2*c1 - a1*c2)/d; - return new PointF(x,y); - } - /** - * - * @param num Number to round - * @return value rounded to integer - */ - public static int roundToInt(float num){ - if((num - Math.floor(num)) > 0.5f){ //round up - return (int) Math.ceil(num); - } - else{ //round down - return (int) Math.floor(num); - } - } - - - /** - * Draw the view into a bitmap using drawing cache. - */ - public static Bitmap getViewBitmap(View v) { - v.clearFocus(); - v.setPressed(false); - - boolean willNotCache = v.willNotCacheDrawing(); - v.setWillNotCacheDrawing(false); - - // Reset the drawing cache background color to fully transparent - // for the duration of this operation - int color = v.getDrawingCacheBackgroundColor(); - v.setDrawingCacheBackgroundColor(0); - - if (color != 0) { - v.destroyDrawingCache(); - } - v.buildDrawingCache(); - Bitmap cacheBitmap = v.getDrawingCache(); - if (cacheBitmap == null) { - Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); - return null; - } - - Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); - - // Restore the view - v.destroyDrawingCache(); - v.setWillNotCacheDrawing(willNotCache); - v.setDrawingCacheBackgroundColor(color); - - return bitmap; - } - - public static Bitmap getViewBitmapNoCache(View v,int w,int h){ - Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - Canvas c = new Canvas(b); - v.layout(0, 0, w, h); - v.draw(c); - return b; - } - - public static Bitmap doInvert(Bitmap src) { - // create new bitmap with the same settings as source bitmap - Bitmap bmOut = Bitmap.createBitmap(src.getWidth(), src.getHeight(), src.getConfig()); - // color info - int A, R, G, B; - int pixelColor; - // image size - int height = src.getHeight(); - int width = src.getWidth(); - - // scan through every pixel - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - // get one pixel - pixelColor = src.getPixel(x, y); - // saving alpha channel - A = Color.alpha(pixelColor); - // inverting byte for each R/G/B channel - R = 255 - Color.red(pixelColor); - G = 255 - Color.green(pixelColor); - B = 255 - Color.blue(pixelColor); - // set newly-inverted pixel to output image - bmOut.setPixel(x, y, Color.argb(A, R, G, B)); - } - } - - // return final bitmap - return bmOut; - } - - public static float dpToPixels(int dp, Context c){ - final Resources r = c.getResources(); - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); - } - - /** - * - * @param center Circle center - * @param radius Circle radius - * @param touchPoint - * @return Point on line from center to touchpoint intersecting circle - */ - public static PointF getCircleIntersection(PointF center,float radius, PointF touchPoint){ - final float r = radius; - - //center must be on (0,0), move second point accordingly - final float x2 = touchPoint.x - center.x; - final float y2 = touchPoint.y - center.y; - - //dx = x2 - x1, x1 is in center => dx = x2-0 - final float dx = x2; - final float dy = y2; - - final float dr = FloatMath.sqrt(dx*dx + dy*dy); - - final PointF res1 = new PointF(); - final PointF res2 = new PointF(); - - res1.x = (sgn(dy)*dx*r*dr)/(dr*dr); - res2.x = (-1 * sgn(dy)*dx*r*dr)/(dr*dr); - - res1.y = (Math.abs(dy)*r*dr)/(dr*dr); - res2.y = (-1 * Math.abs(dy)*r*dr)/(dr*dr); - - //move result back to normal coordinates - res1.x = res1.x + center.x; - res1.y = res1.y + center.y; - res2.x = res2.x + center.x; - res2.y = res2.y + center.y; - - //find which of two results is on same side of circle as touchpoint - if(getLineLength(res1, touchPoint) < getLineLength(res2, touchPoint)){ - return res1; - } - else{ - return res2; - } - } - - public static PointF getCircleIntersection(PointF center,float radius, float x, float y){ - return getCircleIntersection(center, radius, new PointF(x, y)); - } - - /** - * Test if point lies inside the circle - * @param center Center of the circle - * @param radius Radius of te circle - * @param x x coordinate of tested point - * @param y y coordinate of tested point - * @return - */ - public static boolean isInsideCircle(PointF center, float radius, float x, float y){ - final float dx = x - center.x; - final float dy = y - center.y; - - if(dx*dx + dy*dy < radius*radius){ - return true; - } - else{ - return false; - } - } - /** - * Test if point lies outside the circle - * @param center Center of the circle - * @param radius Radius of te circle - * @param x x coordinate of tested point - * @param y y coordinate of tested point - * @return - */ - public static boolean isOutsideCircle(PointF center, float radius, float x, float y){ - final float dx = x - center.x; - final float dy = y - center.y; - - if(dx*dx + dy*dy > radius*radius){ - return true; - } - else{ - return false; - } - } - - private static float sgn(float x){ - if(x < 0) return -1; - else return 1; - } - - - public static PointF getNormalizedVector(PointF v){ - float l = getVectorLength(v); - final PointF r = new PointF(); - r.x = v.x / l; - r.y = v.y / l; - return r; - } - - public static float dotProduct(PointF a, PointF b){ - return a.x*b.x + a.y*b.y; - } - - public static float getVectorAngle(float x, float y){ - final float l = getVectorLength(x,y); - final float cos = x/l; - final float sin = y/l; -// final float ac = (float) Math.acos(cos); - final float as = (float) Math.asin(sin); - - if(cos > 0 && sin >= 0){ //quadrant I - return as; - } - else if(sin > 0 && cos <= 0){ //quadrant II - return (float) (Math.PI - as); - } - else if(sin <= 0 && cos < 0){ //quadrant III - return (float) (Math.PI - as); - } - else if(sin < 0 && cos >= 0){// quadrant IV - return (float) (2*Math.PI + as); - } - - return as; - - } - - public static void rgbToHsl(int rgb, float[] hsl) { - float r = ((0x00ff0000 & rgb) >> 16) / 255.f; - float g = ((0x0000ff00 & rgb) >> 8) / 255.f; - float b = ((0x000000ff & rgb)) / 255.f; - float max = Math.max(Math.max(r, g), b); - float min = Math.min(Math.min(r, g), b); - float c = max - min; - - float h_ = 0.f; - if (c == 0) { - h_ = 0; - } else if (max == r) { - h_ = (float)(g-b) / c; - if (h_ < 0) h_ += 6.f; - } else if (max == g) { - h_ = (float)(b-r) / c + 2.f; - } else if (max == b) { - h_ = (float)(r-g) / c + 4.f; - } - float h = 60.f * h_; - - float l = (max + min) * 0.5f; - - float s; - if (c == 0) { - s = 0.f; - } else { - s = c / (1 - Math.abs(2.f * l - 1.f)); - } - - hsl[0] = h; - hsl[1] = s; - hsl[2] = l; - } - - - -// public static void rgbToHsb2(int rgb, float[] hsl) { -// final float r = ((0x00ff0000 & rgb) >> 16) / 255.f; -// final float g = ((0x0000ff00 & rgb) >> 8) / 255.f; -// final float b = ((0x000000ff & rgb)) / 255.f; -// final float max = Math.max(Math.max(r, g), b); -// final float min = Math.min(Math.min(r, g), b); -// -// final float alpha = (2*r - g - b)/2f; -// final double beta = (Math.sqrt(3)/2*(g-b)); -//// final double c2 = Math.sqrt(alpha*alpha + beta*beta); -// final float c = max - min; -// -// final float h = (float) Math.atan2(beta,alpha); -// -// float l = (max + min)/2; -// -// float s; -// if (c == 0) { -// s = 0.f; -// } else { -// s = (float) (c/max); -// } -// -// hsl[0] = h; -// hsl[1] = s; -// hsl[2] = l; -// } - - public static int hslToRgb(float[] hsl) { - float h = hsl[0]; - float s = hsl[1]; - float l = hsl[2]; - - float c = (1 - Math.abs(2.f * l - 1.f)) * s; - float h_ = h / 60.f; - float h_mod2 = h_; - if (h_mod2 >= 4.f) h_mod2 -= 4.f; - else if (h_mod2 >= 2.f) h_mod2 -= 2.f; - - float x = c * (1 - Math.abs(h_mod2 - 1)); - float r_, g_, b_; - if (h_ < 1) { r_ = c; g_ = x; b_ = 0; } - else if (h_ < 2) { r_ = x; g_ = c; b_ = 0; } - else if (h_ < 3) { r_ = 0; g_ = c; b_ = x; } - else if (h_ < 4) { r_ = 0; g_ = x; b_ = c; } - else if (h_ < 5) { r_ = x; g_ = 0; b_ = c; } - else { r_ = c; g_ = 0; b_ = x; } - - float m = l - (0.5f * c); - int r = (int)((r_ + m) * (255.f) + 0.5f); - int g = (int)((g_ + m) * (255.f) + 0.5f); - int b = (int)((b_ + m) * (255.f) + 0.5f); - return r << 16 | g << 8 | b; - } - - - public static String humanReadableByteCount(long bytes, boolean si) { - int unit = si ? 1000 : 1024; - if (bytes < unit) return bytes + " B"; - int exp = (int) (Math.log(bytes) / Math.log(unit)); - String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); - return String.format(Locale.US,"%.1f %sB", bytes / Math.pow(unit, exp), pre); - } - - /** - * @return Application's version code from the {@code PackageManager}. - */ - public static int getAppVersion(Context context) { - try { - PackageInfo packageInfo = context.getPackageManager() - .getPackageInfo(context.getPackageName(), 0); - return packageInfo.versionCode; - } catch (NameNotFoundException e) { - // should never happen - throw new RuntimeException("Could not get package name: " + e); - } - } - - public static String convertStreamToString(java.io.InputStream is) { - java.util.Scanner s = new java.util.Scanner(is,"UTF-8").useDelimiter("\\A"); - String r = s.hasNext() ? s.next() : ""; - s.close(); - return r; - } - - - - public static ArrayList union(List list1, List list2) { - Set set = new HashSet(); - - set.addAll(list1); - set.addAll(list2); - - return new ArrayList(set); - } - - public static ArrayList intersection(List list1, List list2) { - ArrayList list = new ArrayList(); - - for (T t : list1) { - if(list2.contains(t)) { - list.add(t); - } - } - - return list; - } - - public static T[] concatenateArray (T[] A, T[] B) { - int aLen = A.length; - int bLen = B.length; - - @SuppressWarnings("unchecked") - T[] C = (T[]) Array.newInstance(A.getClass().getComponentType(), aLen+bLen); - System.arraycopy(A, 0, C, 0, aLen); - System.arraycopy(B, 0, C, aLen, bLen); - - return C; - } - - public static class ViewCache { - private final LinkedList> mCachedItemViews = new LinkedList>(); - - /** - * Check if list of weak references has any view still in memory to offer for recycling - * @return cached view - */ - public T getCachedView(){ - if (mCachedItemViews.size() != 0) { - T v; - do{ - v = mCachedItemViews.removeFirst().get(); - } - while(v == null && mCachedItemViews.size() != 0); - return v; - } - return null; - } - - public void cacheView(T v){ - WeakReference ref = new WeakReference(v); - mCachedItemViews.addLast(ref); - } - } - - /** - * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation - * @param MAC - * @return - */ - public static byte[] MACtobyteConverter(String MAC) { - // first remove all ":" from MAC address - MAC = MAC.replaceAll(":", ""); - Log.d("ComponentLibrary.ToolBox", "MACtobyteConverter input ="+MAC); - - // now convert to byte array - int len = MAC.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(MAC.charAt(i), 16) << 4) - + Character.digit(MAC.charAt(i+1), 16)); - } - return data; - } - - /** - * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation - * @param MAC - * @return - */ - public static byte[] IMEItobyteConverter(String IMEI) { - // now convert to byte array - long imeiInLong; - try { - imeiInLong = Long.parseLong(IMEI); - } - catch (NumberFormatException e) { - Log.w(TAG, "Can't convert IMEI to byte, Illegal number format"); - return null; - } - byte[] data = ByteBuffer.allocate(8).putLong(imeiInLong).array(); - return data; - } - - public static String formatISODate(String isoDate){ - SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",Locale.US); - Date dtIn; - try { - dtIn = inFormat.parse(isoDate); //where dateString is a date in ISO-8601 format - SimpleDateFormat outFormat = new SimpleDateFormat("dd.MM.yyyy",Locale.US); - return outFormat.format(dtIn); - } catch (ParseException e) { - Log.e(TAG, "Parse date error",e); - } catch (NullPointerException e) { - Log.e(TAG, "Parse NullPointerException error",e); - } - return isoDate; - } -} - +package it.moondroid.coverflow.components.general; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.security.InvalidParameterException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PointF; +import android.util.FloatMath; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; + + +/** + * @author Martin Appl + * Set of handy mostly mathematical, geometric and generic java static methods + */ +public abstract class ToolBox { + //global constants + private static final String TAG = "ToolBox"; + + + + /** + * Get length of line between points a and b. + * @param a point + * @param b point + * @return length + */ + public static float getLineLength(PointF a,PointF b){ + float vx = b.x - a.x; + float vy = b.y - a.y; + return FloatMath.sqrt(vx*vx + vy*vy); + } + /** + * Get length of line between points A and B. + * @param ax point A x coordinate + * @param ay point A y coordinate + * @param bx point B x coordinate + * @param by point B y coordinate + * @return length + */ + public static float getLineLength(float ax,float ay, float bx,float by){ + float vx = bx - ax; + float vy = by - ay; + return FloatMath.sqrt(vx*vx + vy*vy); + } + + /** + * Get length of vector + * @param vx vector x component + * @param vy vector y component + * @return length + */ + public static float getVectorLength(float vx,float vy){ + return FloatMath.sqrt(vx*vx + vy*vy); + } + + public static float getVectorLength(PointF v){ + return FloatMath.sqrt(v.x*v.x + v.y*v.y); + } + + /** + * Compute intersection point of two infinite lines. Each line is specified by two points + * @param a1 line a + * @param a2 line a + * @param b1 line b + * @param b2 line b + * @return Intersection point + */ + public static PointF getLinesIntersection(PointF a1,PointF a2,PointF b1,PointF b2){ + return getLinesIntersection(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y); + } + + /** + * Compute intersection point of two infinite lines. Each line is specified by two points + * @param x1 line a point 1 + * @param y1 line a point 1 + * @param x2 line a point 2 + * @param y2 line a point 2 + * @param x3 line b point 1 + * @param y3 line b point 1 + * @param x4 line b point 2 + * @param y4 line b point 2 + * @return Intersection point + */ + public static PointF getLinesIntersection( float x1,float y1, float x2, float y2, + float x3,float y3,float x4,float y4) + { + float a1 = y2 - y1; + float b1 = x1 - x2; + float c1 = x2*y1 - x1*y2; //a1*x + b1*y + c1 = 0 is line a + + float a2 = y4-y3; + float b2 = x3-x4; + float c2 = x4*y3 - x3*y4; // a2*x + b2*y + c2 = 0 is line b + + float d = a1*b2 - a2*b1; + if(d == 0) { + throw new InvalidParameterException("Intersection cant be found, lines are paralel."); + } + + float x = (b1*c2 - b2*c1)/d; + float y = (a2*c1 - a1*c2)/d; + return new PointF(x,y); + } + /** + * + * @param num Number to round + * @return value rounded to integer + */ + public static int roundToInt(float num){ + if((num - Math.floor(num)) > 0.5f){ //round up + return (int) Math.ceil(num); + } + else{ //round down + return (int) Math.floor(num); + } + } + + + /** + * Draw the view into a bitmap using drawing cache. + */ + public static Bitmap getViewBitmap(View v) { + v.clearFocus(); + v.setPressed(false); + + boolean willNotCache = v.willNotCacheDrawing(); + v.setWillNotCacheDrawing(false); + + // Reset the drawing cache background color to fully transparent + // for the duration of this operation + int color = v.getDrawingCacheBackgroundColor(); + v.setDrawingCacheBackgroundColor(0); + + if (color != 0) { + v.destroyDrawingCache(); + } + v.buildDrawingCache(); + Bitmap cacheBitmap = v.getDrawingCache(); + if (cacheBitmap == null) { + Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); + return null; + } + + Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); + + // Restore the view + v.destroyDrawingCache(); + v.setWillNotCacheDrawing(willNotCache); + v.setDrawingCacheBackgroundColor(color); + + return bitmap; + } + + public static Bitmap getViewBitmapNoCache(View v,int w,int h){ + Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(b); + v.layout(0, 0, w, h); + v.draw(c); + return b; + } + + public static Bitmap doInvert(Bitmap src) { + // create new bitmap with the same settings as source bitmap + Bitmap bmOut = Bitmap.createBitmap(src.getWidth(), src.getHeight(), src.getConfig()); + // color info + int A, R, G, B; + int pixelColor; + // image size + int height = src.getHeight(); + int width = src.getWidth(); + + // scan through every pixel + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + // get one pixel + pixelColor = src.getPixel(x, y); + // saving alpha channel + A = Color.alpha(pixelColor); + // inverting byte for each R/G/B channel + R = 255 - Color.red(pixelColor); + G = 255 - Color.green(pixelColor); + B = 255 - Color.blue(pixelColor); + // set newly-inverted pixel to output image + bmOut.setPixel(x, y, Color.argb(A, R, G, B)); + } + } + + // return final bitmap + return bmOut; + } + + public static float dpToPixels(int dp, Context c){ + final Resources r = c.getResources(); + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()); + } + + /** + * + * @param center Circle center + * @param radius Circle radius + * @param touchPoint + * @return Point on line from center to touchpoint intersecting circle + */ + public static PointF getCircleIntersection(PointF center,float radius, PointF touchPoint){ + final float r = radius; + + //center must be on (0,0), move second point accordingly + final float x2 = touchPoint.x - center.x; + final float y2 = touchPoint.y - center.y; + + //dx = x2 - x1, x1 is in center => dx = x2-0 + final float dx = x2; + final float dy = y2; + + final float dr = FloatMath.sqrt(dx*dx + dy*dy); + + final PointF res1 = new PointF(); + final PointF res2 = new PointF(); + + res1.x = (sgn(dy)*dx*r*dr)/(dr*dr); + res2.x = (-1 * sgn(dy)*dx*r*dr)/(dr*dr); + + res1.y = (Math.abs(dy)*r*dr)/(dr*dr); + res2.y = (-1 * Math.abs(dy)*r*dr)/(dr*dr); + + //move result back to normal coordinates + res1.x = res1.x + center.x; + res1.y = res1.y + center.y; + res2.x = res2.x + center.x; + res2.y = res2.y + center.y; + + //find which of two results is on same side of circle as touchpoint + if(getLineLength(res1, touchPoint) < getLineLength(res2, touchPoint)){ + return res1; + } + else{ + return res2; + } + } + + public static PointF getCircleIntersection(PointF center,float radius, float x, float y){ + return getCircleIntersection(center, radius, new PointF(x, y)); + } + + /** + * Test if point lies inside the circle + * @param center Center of the circle + * @param radius Radius of te circle + * @param x x coordinate of tested point + * @param y y coordinate of tested point + * @return + */ + public static boolean isInsideCircle(PointF center, float radius, float x, float y){ + final float dx = x - center.x; + final float dy = y - center.y; + + if(dx*dx + dy*dy < radius*radius){ + return true; + } + else{ + return false; + } + } + /** + * Test if point lies outside the circle + * @param center Center of the circle + * @param radius Radius of te circle + * @param x x coordinate of tested point + * @param y y coordinate of tested point + * @return + */ + public static boolean isOutsideCircle(PointF center, float radius, float x, float y){ + final float dx = x - center.x; + final float dy = y - center.y; + + if(dx*dx + dy*dy > radius*radius){ + return true; + } + else{ + return false; + } + } + + private static float sgn(float x){ + if(x < 0) return -1; + else return 1; + } + + + public static PointF getNormalizedVector(PointF v){ + float l = getVectorLength(v); + final PointF r = new PointF(); + r.x = v.x / l; + r.y = v.y / l; + return r; + } + + public static float dotProduct(PointF a, PointF b){ + return a.x*b.x + a.y*b.y; + } + + public static float getVectorAngle(float x, float y){ + final float l = getVectorLength(x,y); + final float cos = x/l; + final float sin = y/l; +// final float ac = (float) Math.acos(cos); + final float as = (float) Math.asin(sin); + + if(cos > 0 && sin >= 0){ //quadrant I + return as; + } + else if(sin > 0 && cos <= 0){ //quadrant II + return (float) (Math.PI - as); + } + else if(sin <= 0 && cos < 0){ //quadrant III + return (float) (Math.PI - as); + } + else if(sin < 0 && cos >= 0){// quadrant IV + return (float) (2*Math.PI + as); + } + + return as; + + } + + public static void rgbToHsl(int rgb, float[] hsl) { + float r = ((0x00ff0000 & rgb) >> 16) / 255.f; + float g = ((0x0000ff00 & rgb) >> 8) / 255.f; + float b = ((0x000000ff & rgb)) / 255.f; + float max = Math.max(Math.max(r, g), b); + float min = Math.min(Math.min(r, g), b); + float c = max - min; + + float h_ = 0.f; + if (c == 0) { + h_ = 0; + } else if (max == r) { + h_ = (float)(g-b) / c; + if (h_ < 0) h_ += 6.f; + } else if (max == g) { + h_ = (float)(b-r) / c + 2.f; + } else if (max == b) { + h_ = (float)(r-g) / c + 4.f; + } + float h = 60.f * h_; + + float l = (max + min) * 0.5f; + + float s; + if (c == 0) { + s = 0.f; + } else { + s = c / (1 - Math.abs(2.f * l - 1.f)); + } + + hsl[0] = h; + hsl[1] = s; + hsl[2] = l; + } + + + +// public static void rgbToHsb2(int rgb, float[] hsl) { +// final float r = ((0x00ff0000 & rgb) >> 16) / 255.f; +// final float g = ((0x0000ff00 & rgb) >> 8) / 255.f; +// final float b = ((0x000000ff & rgb)) / 255.f; +// final float max = Math.max(Math.max(r, g), b); +// final float min = Math.min(Math.min(r, g), b); +// +// final float alpha = (2*r - g - b)/2f; +// final double beta = (Math.sqrt(3)/2*(g-b)); +//// final double c2 = Math.sqrt(alpha*alpha + beta*beta); +// final float c = max - min; +// +// final float h = (float) Math.atan2(beta,alpha); +// +// float l = (max + min)/2; +// +// float s; +// if (c == 0) { +// s = 0.f; +// } else { +// s = (float) (c/max); +// } +// +// hsl[0] = h; +// hsl[1] = s; +// hsl[2] = l; +// } + + public static int hslToRgb(float[] hsl) { + float h = hsl[0]; + float s = hsl[1]; + float l = hsl[2]; + + float c = (1 - Math.abs(2.f * l - 1.f)) * s; + float h_ = h / 60.f; + float h_mod2 = h_; + if (h_mod2 >= 4.f) h_mod2 -= 4.f; + else if (h_mod2 >= 2.f) h_mod2 -= 2.f; + + float x = c * (1 - Math.abs(h_mod2 - 1)); + float r_, g_, b_; + if (h_ < 1) { r_ = c; g_ = x; b_ = 0; } + else if (h_ < 2) { r_ = x; g_ = c; b_ = 0; } + else if (h_ < 3) { r_ = 0; g_ = c; b_ = x; } + else if (h_ < 4) { r_ = 0; g_ = x; b_ = c; } + else if (h_ < 5) { r_ = x; g_ = 0; b_ = c; } + else { r_ = c; g_ = 0; b_ = x; } + + float m = l - (0.5f * c); + int r = (int)((r_ + m) * (255.f) + 0.5f); + int g = (int)((g_ + m) * (255.f) + 0.5f); + int b = (int)((b_ + m) * (255.f) + 0.5f); + return r << 16 | g << 8 | b; + } + + + public static String humanReadableByteCount(long bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < unit) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i"); + return String.format(Locale.US,"%.1f %sB", bytes / Math.pow(unit, exp), pre); + } + + /** + * @return Application's version code from the {@code PackageManager}. + */ + public static int getAppVersion(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager() + .getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionCode; + } catch (NameNotFoundException e) { + // should never happen + throw new RuntimeException("Could not get package name: " + e); + } + } + + public static String convertStreamToString(java.io.InputStream is) { + java.util.Scanner s = new java.util.Scanner(is,"UTF-8").useDelimiter("\\A"); + String r = s.hasNext() ? s.next() : ""; + s.close(); + return r; + } + + + + public static ArrayList union(List list1, List list2) { + Set set = new HashSet(); + + set.addAll(list1); + set.addAll(list2); + + return new ArrayList(set); + } + + public static ArrayList intersection(List list1, List list2) { + ArrayList list = new ArrayList(); + + for (T t : list1) { + if(list2.contains(t)) { + list.add(t); + } + } + + return list; + } + + public static T[] concatenateArray (T[] A, T[] B) { + int aLen = A.length; + int bLen = B.length; + + @SuppressWarnings("unchecked") + T[] C = (T[]) Array.newInstance(A.getClass().getComponentType(), aLen+bLen); + System.arraycopy(A, 0, C, 0, aLen); + System.arraycopy(B, 0, C, aLen, bLen); + + return C; + } + + public static class ViewCache { + private final LinkedList> mCachedItemViews = new LinkedList>(); + + /** + * Check if list of weak references has any view still in memory to offer for recycling + * @return cached view + */ + public T getCachedView(){ + if (mCachedItemViews.size() != 0) { + T v; + do{ + v = mCachedItemViews.removeFirst().get(); + } + while(v == null && mCachedItemViews.size() != 0); + return v; + } + return null; + } + + public void cacheView(T v){ + WeakReference ref = new WeakReference(v); + mCachedItemViews.addLast(ref); + } + } + + /** + * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation + * @param MAC + * @return + */ + public static byte[] MACtobyteConverter(String MAC) { + // first remove all ":" from MAC address + MAC = MAC.replaceAll(":", ""); + Log.d("ComponentLibrary.ToolBox", "MACtobyteConverter input ="+MAC); + + // now convert to byte array + int len = MAC.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(MAC.charAt(i), 16) << 4) + + Character.digit(MAC.charAt(i+1), 16)); + } + return data; + } + + /** + * Transforms MAC address in 00:11:22:33:44:55 format to byte array representation + * @param MAC + * @return + */ + public static byte[] IMEItobyteConverter(String IMEI) { + // now convert to byte array + long imeiInLong; + try { + imeiInLong = Long.parseLong(IMEI); + } + catch (NumberFormatException e) { + Log.w(TAG, "Can't convert IMEI to byte, Illegal number format"); + return null; + } + byte[] data = ByteBuffer.allocate(8).putLong(imeiInLong).array(); + return data; + } + + public static String formatISODate(String isoDate){ + SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",Locale.US); + Date dtIn; + try { + dtIn = inFormat.parse(isoDate); //where dateString is a date in ISO-8601 format + SimpleDateFormat outFormat = new SimpleDateFormat("dd.MM.yyyy",Locale.US); + return outFormat.format(dtIn); + } catch (ParseException e) { + Log.e(TAG, "Parse date error",e); + } catch (NullPointerException e) { + Log.e(TAG, "Parse NullPointerException error",e); + } + return isoDate; + } +} + diff --git a/MAComponents/src/com/martinappl/components/general/Validate.java b/lib/src/main/java/it/moondroid/coverflow/components/general/Validate.java similarity index 97% rename from MAComponents/src/com/martinappl/components/general/Validate.java rename to lib/src/main/java/it/moondroid/coverflow/components/general/Validate.java index 26cfc6c..b0b4d98 100644 --- a/MAComponents/src/com/martinappl/components/general/Validate.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/general/Validate.java @@ -1,507 +1,507 @@ -package com.martinappl.components.general; - -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; - -/** - *

This class assists in validating arguments.

- * - *

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

- * - *
- * Validate.isTrue( i > 0, "The value must be greater than zero: ", i);
- * Validate.notNull( surname, "The surname must not be null");
- * 
- * - * @author Apache Software Foundation - * @author Ola Berg - * @author Gary Gregory - * @author Norm Deane - * @since 2.0 - * @version $Id: Validate.java 1057051 2011-01-09 23:15:51Z sebb $ - */ -public class Validate { - // Validate has no dependencies on other classes in Commons Lang at present - - /** - * Constructor. This class should not normally be instantiated. - */ - public Validate() { - super(); - } - - // isTrue - //--------------------------------------------------------------------------------- - /** - *

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

- * - *
Validate.isTrue( myObject.isOk(), "The object is not OK: ", myObject);
- * - *

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

- * - * @param expression the boolean expression to check - * @param message the exception message if invalid - * @param value the value to append to the message when invalid - * @throws IllegalArgumentException if expression is false - */ - public static void isTrue(boolean expression, String message, Object value) { - if (expression == false) { - throw new IllegalArgumentException(message + value); - } - } - - /** - *

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

- * - *
Validate.isTrue(i > 0.0, "The value must be greater than zero: ", i);
- * - *

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

- * - * @param expression the boolean expression to check - * @param message the exception message if invalid - * @param value the value to append to the message when invalid - * @throws IllegalArgumentException if expression is false - */ - public static void isTrue(boolean expression, String message, long value) { - if (expression == false) { - throw new IllegalArgumentException(message + value); - } - } - - /** - *

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

- * - *
Validate.isTrue(d > 0.0, "The value must be greater than zero: ", d);
- * - *

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

- * - * @param expression the boolean expression to check - * @param message the exception message if invalid - * @param value the value to append to the message when invalid - * @throws IllegalArgumentException if expression is false - */ - public static void isTrue(boolean expression, String message, double value) { - if (expression == false) { - throw new IllegalArgumentException(message + value); - } - } - - /** - *

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

- * - *
-     * Validate.isTrue( (i > 0), "The value must be greater than zero");
-     * Validate.isTrue( myObject.isOk(), "The object is not OK");
-     * 
- * - * @param expression the boolean expression to check - * @param message the exception message if invalid - * @throws IllegalArgumentException if expression is false - */ - public static void isTrue(boolean expression, String message) { - if (expression == false) { - throw new IllegalArgumentException(message); - } - } - - /** - *

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

- * - *
-     * Validate.isTrue(i > 0);
-     * Validate.isTrue(myObject.isOk());
- * - *

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

- * - * @param expression the boolean expression to check - * @throws IllegalArgumentException if expression is false - */ - public static void isTrue(boolean expression) { - if (expression == false) { - throw new IllegalArgumentException("The validated expression is false"); - } - } - - // notNull - //--------------------------------------------------------------------------------- - - /** - *

Validate that the specified argument is not null; - * otherwise throwing an exception. - * - *

Validate.notNull(myObject);
- * - *

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

- * - * @param object the object to check - * @throws IllegalArgumentException if the object is null - */ - public static void notNull(Object object) { - notNull(object, "The validated object is null"); - } - - /** - *

Validate that the specified argument is not null; - * otherwise throwing an exception with the specified message. - * - *

Validate.notNull(myObject, "The object must not be null");
- * - * @param object the object to check - * @param message the exception message if invalid - */ - public static void notNull(Object object, String message) { - if (object == null) { - throw new IllegalArgumentException(message); - } - } - - // notEmpty array - //--------------------------------------------------------------------------------- - - /** - *

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

Validate.notEmpty(myArray, "The array must not be empty");
- * - * @param array the array to check - * @param message the exception message if invalid - * @throws IllegalArgumentException if the array is empty - */ - public static void notEmpty(Object[] array, String message) { - if (array == null || array.length == 0) { - throw new IllegalArgumentException(message); - } - } - - /** - *

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

Validate.notEmpty(myArray);
- * - *

The message in the exception is "The validated array is - * empty". - * - * @param array the array to check - * @throws IllegalArgumentException if the array is empty - */ - public static void notEmpty(Object[] array) { - notEmpty(array, "The validated array is empty"); - } - - // notEmpty collection - //--------------------------------------------------------------------------------- - - /** - *

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

Validate.notEmpty(myCollection, "The collection must not be empty");
- * - * @param collection the collection to check - * @param message the exception message if invalid - * @throws IllegalArgumentException if the collection is empty - */ - public static void notEmpty(Collection collection, String message) { - if (collection == null || collection.size() == 0) { - throw new IllegalArgumentException(message); - } - } - - /** - *

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

Validate.notEmpty(myCollection);
- * - *

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

- * - * @param collection the collection to check - * @throws IllegalArgumentException if the collection is empty - */ - public static void notEmpty(Collection collection) { - notEmpty(collection, "The validated collection is empty"); - } - - // notEmpty map - //--------------------------------------------------------------------------------- - - /** - *

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

Validate.notEmpty(myMap, "The map must not be empty");
- * - * @param map the map to check - * @param message the exception message if invalid - * @throws IllegalArgumentException if the map is empty - */ - public static void notEmpty(Map map, String message) { - if (map == null || map.size() == 0) { - throw new IllegalArgumentException(message); - } - } - - /** - *

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

Validate.notEmpty(myMap);
- * - *

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

- * - * @param map the map to check - * @throws IllegalArgumentException if the map is empty - * @see #notEmpty(Map, String) - */ - public static void notEmpty(Map map) { - notEmpty(map, "The validated map is empty"); - } - - // notEmpty string - //--------------------------------------------------------------------------------- - - /** - *

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

Validate.notEmpty(myString, "The string must not be empty");
- * - * @param string the string to check - * @param message the exception message if invalid - * @throws IllegalArgumentException if the string is empty - */ - public static void notEmpty(String string, String message) { - if (string == null || string.length() == 0) { - throw new IllegalArgumentException(message); - } - } - - /** - *

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

Validate.notEmpty(myString);
- * - *

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

- * - * @param string the string to check - * @throws IllegalArgumentException if the string is empty - */ - public static void notEmpty(String string) { - notEmpty(string, "The validated string is empty"); - } - - // notNullElements array - //--------------------------------------------------------------------------------- - - /** - *

Validate that the specified argument array is neither - * null nor contains any elements that are null; - * otherwise throwing an exception with the specified message. - * - *

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

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

- * - * @param array the array to check - * @param message the exception message if the collection has null elements - * @throws IllegalArgumentException if the array is null or - * an element in the array is null - */ - public static void noNullElements(Object[] array, String message) { - Validate.notNull(array); - for (int i = 0; i < array.length; i++) { - if (array[i] == null) { - throw new IllegalArgumentException(message); - } - } - } - - /** - *

Validate that the specified argument array is neither - * null nor contains any elements that are null; - * otherwise throwing an exception. - * - *

Validate.noNullElements(myArray);
- * - *

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

- * - *

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

- * - * @param array the array to check - * @throws IllegalArgumentException if the array is null or - * an element in the array is null - */ - public static void noNullElements(Object[] array) { - Validate.notNull(array); - for (int i = 0; i < array.length; i++) { - if (array[i] == null) { - throw new IllegalArgumentException("The validated array contains null element at index: " + i); - } - } - } - - // notNullElements collection - //--------------------------------------------------------------------------------- - - /** - *

Validate that the specified argument collection is neither - * null nor contains any elements that are null; - * otherwise throwing an exception with the specified message. - * - *

Validate.noNullElements(myCollection, "The collection contains null elements");
- * - *

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

- * - * - * @param collection the collection to check - * @param message the exception message if the collection has - * @throws IllegalArgumentException if the collection is null or - * an element in the collection is null - */ - public static void noNullElements(Collection collection, String message) { - Validate.notNull(collection); - for (Iterator it = collection.iterator(); it.hasNext();) { - if (it.next() == null) { - throw new IllegalArgumentException(message); - } - } - } - - /** - *

Validate that the specified argument collection is neither - * null nor contains any elements that are null; - * otherwise throwing an exception. - * - *

Validate.noNullElements(myCollection);
- * - *

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

- * - *

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

- * - * @param collection the collection to check - * @throws IllegalArgumentException if the collection is null or - * an element in the collection is null - */ - public static void noNullElements(Collection collection) { - Validate.notNull(collection); - int i = 0; - for (Iterator it = collection.iterator(); it.hasNext(); i++) { - if (it.next() == null) { - throw new IllegalArgumentException("The validated collection contains null element at index: " + i); - } - } - } - - /** - *

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

- * - *
-     * Validate.allElementsOfType(collection, String.class, "Collection has invalid elements");
-     * 
- * - * @param collection the collection to check, not null - * @param clazz the Class which the collection's elements are expected to be, not null - * @param message the exception message if the Collection has elements not of type clazz - * @since 2.1 - */ - public static void allElementsOfType(Collection collection, Class clazz, String message) { - Validate.notNull(collection); - Validate.notNull(clazz); - for (Iterator it = collection.iterator(); it.hasNext(); ) { - if (clazz.isInstance(it.next()) == false) { - throw new IllegalArgumentException(message); - } - } - } - - /** - *

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

- * - *
-     * Validate.allElementsOfType(collection, String.class);
-     * 
- * - *

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

- * - * @param collection the collection to check, not null - * @param clazz the Class which the collection's elements are expected to be, not null - * @since 2.1 - */ - public static void allElementsOfType(Collection collection, Class clazz) { - Validate.notNull(collection); - Validate.notNull(clazz); - int i = 0; - for (Iterator it = collection.iterator(); it.hasNext(); i++) { - if (clazz.isInstance(it.next()) == false) { - throw new IllegalArgumentException("The validated collection contains an element not of type " - + clazz.getName() + " at index: " + i); - } - } - } - +package it.moondroid.coverflow.components.general; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + *

This class assists in validating arguments.

+ * + *

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

+ * + *
+ * Validate.isTrue( i > 0, "The value must be greater than zero: ", i);
+ * Validate.notNull( surname, "The surname must not be null");
+ * 
+ * + * @author Apache Software Foundation + * @author Ola Berg + * @author Gary Gregory + * @author Norm Deane + * @since 2.0 + * @version $Id: Validate.java 1057051 2011-01-09 23:15:51Z sebb $ + */ +public class Validate { + // Validate has no dependencies on other classes in Commons Lang at present + + /** + * Constructor. This class should not normally be instantiated. + */ + public Validate() { + super(); + } + + // isTrue + //--------------------------------------------------------------------------------- + /** + *

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

+ * + *
Validate.isTrue( myObject.isOk(), "The object is not OK: ", myObject);
+ * + *

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

+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message, Object value) { + if (expression == false) { + throw new IllegalArgumentException(message + value); + } + } + + /** + *

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

+ * + *
Validate.isTrue(i > 0.0, "The value must be greater than zero: ", i);
+ * + *

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

+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message, long value) { + if (expression == false) { + throw new IllegalArgumentException(message + value); + } + } + + /** + *

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

+ * + *
Validate.isTrue(d > 0.0, "The value must be greater than zero: ", d);
+ * + *

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

+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @param value the value to append to the message when invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message, double value) { + if (expression == false) { + throw new IllegalArgumentException(message + value); + } + } + + /** + *

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

+ * + *
+     * Validate.isTrue( (i > 0), "The value must be greater than zero");
+     * Validate.isTrue( myObject.isOk(), "The object is not OK");
+     * 
+ * + * @param expression the boolean expression to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message) { + if (expression == false) { + throw new IllegalArgumentException(message); + } + } + + /** + *

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

+ * + *
+     * Validate.isTrue(i > 0);
+     * Validate.isTrue(myObject.isOk());
+ * + *

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

+ * + * @param expression the boolean expression to check + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression) { + if (expression == false) { + throw new IllegalArgumentException("The validated expression is false"); + } + } + + // notNull + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument is not null; + * otherwise throwing an exception. + * + *

Validate.notNull(myObject);
+ * + *

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

+ * + * @param object the object to check + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(Object object) { + notNull(object, "The validated object is null"); + } + + /** + *

Validate that the specified argument is not null; + * otherwise throwing an exception with the specified message. + * + *

Validate.notNull(myObject, "The object must not be null");
+ * + * @param object the object to check + * @param message the exception message if invalid + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + // notEmpty array + //--------------------------------------------------------------------------------- + + /** + *

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

Validate.notEmpty(myArray, "The array must not be empty");
+ * + * @param array the array to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the array is empty + */ + public static void notEmpty(Object[] array, String message) { + if (array == null || array.length == 0) { + throw new IllegalArgumentException(message); + } + } + + /** + *

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

Validate.notEmpty(myArray);
+ * + *

The message in the exception is "The validated array is + * empty". + * + * @param array the array to check + * @throws IllegalArgumentException if the array is empty + */ + public static void notEmpty(Object[] array) { + notEmpty(array, "The validated array is empty"); + } + + // notEmpty collection + //--------------------------------------------------------------------------------- + + /** + *

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

Validate.notEmpty(myCollection, "The collection must not be empty");
+ * + * @param collection the collection to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the collection is empty + */ + public static void notEmpty(Collection collection, String message) { + if (collection == null || collection.size() == 0) { + throw new IllegalArgumentException(message); + } + } + + /** + *

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

Validate.notEmpty(myCollection);
+ * + *

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

+ * + * @param collection the collection to check + * @throws IllegalArgumentException if the collection is empty + */ + public static void notEmpty(Collection collection) { + notEmpty(collection, "The validated collection is empty"); + } + + // notEmpty map + //--------------------------------------------------------------------------------- + + /** + *

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

Validate.notEmpty(myMap, "The map must not be empty");
+ * + * @param map the map to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the map is empty + */ + public static void notEmpty(Map map, String message) { + if (map == null || map.size() == 0) { + throw new IllegalArgumentException(message); + } + } + + /** + *

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

Validate.notEmpty(myMap);
+ * + *

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

+ * + * @param map the map to check + * @throws IllegalArgumentException if the map is empty + * @see #notEmpty(java.util.Map, String) + */ + public static void notEmpty(Map map) { + notEmpty(map, "The validated map is empty"); + } + + // notEmpty string + //--------------------------------------------------------------------------------- + + /** + *

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

Validate.notEmpty(myString, "The string must not be empty");
+ * + * @param string the string to check + * @param message the exception message if invalid + * @throws IllegalArgumentException if the string is empty + */ + public static void notEmpty(String string, String message) { + if (string == null || string.length() == 0) { + throw new IllegalArgumentException(message); + } + } + + /** + *

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

Validate.notEmpty(myString);
+ * + *

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

+ * + * @param string the string to check + * @throws IllegalArgumentException if the string is empty + */ + public static void notEmpty(String string) { + notEmpty(string, "The validated string is empty"); + } + + // notNullElements array + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument array is neither + * null nor contains any elements that are null; + * otherwise throwing an exception with the specified message. + * + *

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

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

+ * + * @param array the array to check + * @param message the exception message if the collection has null elements + * @throws IllegalArgumentException if the array is null or + * an element in the array is null + */ + public static void noNullElements(Object[] array, String message) { + Validate.notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException(message); + } + } + } + + /** + *

Validate that the specified argument array is neither + * null nor contains any elements that are null; + * otherwise throwing an exception. + * + *

Validate.noNullElements(myArray);
+ * + *

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

+ * + *

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

+ * + * @param array the array to check + * @throws IllegalArgumentException if the array is null or + * an element in the array is null + */ + public static void noNullElements(Object[] array) { + Validate.notNull(array); + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException("The validated array contains null element at index: " + i); + } + } + } + + // notNullElements collection + //--------------------------------------------------------------------------------- + + /** + *

Validate that the specified argument collection is neither + * null nor contains any elements that are null; + * otherwise throwing an exception with the specified message. + * + *

Validate.noNullElements(myCollection, "The collection contains null elements");
+ * + *

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

+ * + * + * @param collection the collection to check + * @param message the exception message if the collection has + * @throws IllegalArgumentException if the collection is null or + * an element in the collection is null + */ + public static void noNullElements(Collection collection, String message) { + Validate.notNull(collection); + for (Iterator it = collection.iterator(); it.hasNext();) { + if (it.next() == null) { + throw new IllegalArgumentException(message); + } + } + } + + /** + *

Validate that the specified argument collection is neither + * null nor contains any elements that are null; + * otherwise throwing an exception. + * + *

Validate.noNullElements(myCollection);
+ * + *

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

+ * + *

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

+ * + * @param collection the collection to check + * @throws IllegalArgumentException if the collection is null or + * an element in the collection is null + */ + public static void noNullElements(Collection collection) { + Validate.notNull(collection); + int i = 0; + for (Iterator it = collection.iterator(); it.hasNext(); i++) { + if (it.next() == null) { + throw new IllegalArgumentException("The validated collection contains null element at index: " + i); + } + } + } + + /** + *

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

+ * + *
+     * Validate.allElementsOfType(collection, String.class, "Collection has invalid elements");
+     * 
+ * + * @param collection the collection to check, not null + * @param clazz the Class which the collection's elements are expected to be, not null + * @param message the exception message if the Collection has elements not of type clazz + * @since 2.1 + */ + public static void allElementsOfType(Collection collection, Class clazz, String message) { + Validate.notNull(collection); + Validate.notNull(clazz); + for (Iterator it = collection.iterator(); it.hasNext(); ) { + if (clazz.isInstance(it.next()) == false) { + throw new IllegalArgumentException(message); + } + } + } + + /** + *

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

+ * + *
+     * Validate.allElementsOfType(collection, String.class);
+     * 
+ * + *

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

+ * + * @param collection the collection to check, not null + * @param clazz the Class which the collection's elements are expected to be, not null + * @since 2.1 + */ + public static void allElementsOfType(Collection collection, Class clazz) { + Validate.notNull(collection); + Validate.notNull(clazz); + int i = 0; + for (Iterator it = collection.iterator(); it.hasNext(); i++) { + if (clazz.isInstance(it.next()) == false) { + throw new IllegalArgumentException("The validated collection contains an element not of type " + + clazz.getName() + " at index: " + i); + } + } + } + } \ No newline at end of file diff --git a/MAComponents/src/com/martinappl/components/ui/containers/EndlessLoopAdapterContainer.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/EndlessLoopAdapterContainer.java similarity index 95% rename from MAComponents/src/com/martinappl/components/ui/containers/EndlessLoopAdapterContainer.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/EndlessLoopAdapterContainer.java index 58c931b..d9cedc6 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/EndlessLoopAdapterContainer.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/EndlessLoopAdapterContainer.java @@ -1,1345 +1,1347 @@ -package com.martinappl.components.ui.containers; - - -import java.lang.ref.WeakReference; -import java.util.LinkedList; - -import android.content.Context; -import android.content.res.TypedArray; -import android.database.DataSetObserver; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewDebug.CapturedViewProperty; -import android.widget.Adapter; -import android.widget.AdapterView; -import android.widget.Scroller; - -import com.martinappl.components.R; -import com.martinappl.components.general.ToolBox; -import com.martinappl.components.general.Validate; -import com.martinappl.components.ui.containers.interfaces.IViewObserver; - -/** - * - * @author Martin Appl - * - * Endless loop with items filling from adapter. Currently only horizontal orientation is implemented - * View recycling in adapter is supported. You are encouraged to recycle view in adapter if possible - * - */ -public class EndlessLoopAdapterContainer extends AdapterView { - /** Children added with this layout mode will be added after the last child */ - protected static final int LAYOUT_MODE_AFTER = 0; - - /** Children added with this layout mode will be added before the first child */ - protected static final int LAYOUT_MODE_TO_BEFORE = 1; - - protected static final int SCROLLING_DURATION = 500; - - - - /** The adapter providing data for container */ - protected Adapter mAdapter; - - /** The adaptor position of the first visible item */ - protected int mFirstItemPosition; - - /** The adaptor position of the last visible item */ - protected int mLastItemPosition; - - /** The adaptor position of selected item */ - protected int mSelectedPosition = INVALID_POSITION; - - /** Left of current most left child*/ - protected int mLeftChildEdge; - - /** User is not touching the list */ - protected static final int TOUCH_STATE_RESTING = 1; - - /** User is scrolling the list */ - protected static final int TOUCH_STATE_SCROLLING = 2; - - /** Fling gesture in progress */ - protected static final int TOUCH_STATE_FLING = 3; - - /** Aligning in progress */ - protected static final int TOUCH_STATE_ALIGN = 4; - - protected static final int TOUCH_STATE_DISTANCE_SCROLL = 5; - - /** A list of cached (re-usable) item views */ - protected final LinkedList> mCachedItemViews = new LinkedList>(); - - /** 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*/ - protected boolean isSrollingDisabled = false; - - /** Whether content should be repeated when there is not enough items to fill container */ - protected boolean shouldRepeat = true; - - /** 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*/ - protected int mScrollPositionIfEndless = -1; - - private IViewObserver mViewObserver; - - - protected int mTouchState = TOUCH_STATE_RESTING; - - protected final Scroller mScroller = new Scroller(getContext()); - private VelocityTracker mVelocityTracker; - private boolean mDataChanged; - - private int mTouchSlop; - private int mMinimumVelocity; - private int mMaximumVelocity; - - private boolean mAllowLongPress; - private float mLastMotionX; - private float mLastMotionY; -// private long mDownTime; - - private final Point mDown = new Point(); - private boolean mHandleSelectionOnActionUp = false; - private boolean mInterceptTouchEvents; -// private boolean mCancelInIntercept; - - protected OnItemClickListener mOnItemClickListener; - protected OnItemSelectedListener mOnItemSelectedListener; - - public EndlessLoopAdapterContainer(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - - final ViewConfiguration configuration = ViewConfiguration.get(context); - mTouchSlop = configuration.getScaledTouchSlop(); - mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); - mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); - - //init params from xml - if(attrs != null){ - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EndlessLoopAdapterContainer, defStyle, 0); - - shouldRepeat = a.getBoolean(R.styleable.EndlessLoopAdapterContainer_shouldRepeat, true); - - a.recycle(); - } - } - - public EndlessLoopAdapterContainer(Context context, AttributeSet attrs) { - this(context, attrs,0); - - } - - public EndlessLoopAdapterContainer(Context context) { - this(context,null); - } - - private final DataSetObserver fDataObserver = new DataSetObserver() { - - @Override - public void onChanged() { - synchronized(this){ - mDataChanged = true; - } - invalidate(); - } - - @Override - public void onInvalidated() { - mAdapter = null; - } - }; - - - /** - * Params describing position of child view in container - * in HORIZONTAL mode TOP,CENTER,BOTTOM are active in VERTICAL mode LEFT,CENTER,RIGHT are active - * @author Martin Appl - * - */ - public static class LoopLayoutParams extends MarginLayoutParams{ - public static final int TOP = 0; - public static final int CENTER = 1; - public static final int BOTTOM = 2; - public static final int LEFT = 3; - public static final int RIGHT = 4; - - public int position; -// public int actualWidth; -// public int actualHeight; - - public LoopLayoutParams(int w, int h) { - super(w, h); - position = CENTER; - } - - public LoopLayoutParams(int w, int h,int pos){ - super(w, h); - position = pos; - } - - public LoopLayoutParams(android.view.ViewGroup.LayoutParams lp) { - super(lp); - - if(lp!=null && lp instanceof MarginLayoutParams){ - MarginLayoutParams mp = (MarginLayoutParams) lp; - leftMargin = mp.leftMargin; - rightMargin = mp.rightMargin; - topMargin = mp.topMargin; - bottomMargin = mp.bottomMargin; - } - - position = CENTER; - } - - - } - - protected LoopLayoutParams createLayoutParams(int w, int h){ - return new LoopLayoutParams(w, h); - } - - protected LoopLayoutParams createLayoutParams(int w, int h,int pos){ - return new LoopLayoutParams(w, h, pos); - } - - protected LoopLayoutParams createLayoutParams(android.view.ViewGroup.LayoutParams lp){ - return new LoopLayoutParams(lp); - } - - - public boolean isRepeatable() { - return shouldRepeat; - } - - public boolean isEndlessRightNow(){ - return !isSrollingDisabled; - } - - public void setShouldRepeat(boolean shouldRepeat) { - this.shouldRepeat = shouldRepeat; - } - - /** - * Sets position in adapter of first shown item in container - * @param position - */ - public void scrollToPosition(int position){ - if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count"); - - reset(); - refillInternal(position-1, position); - invalidate(); - } - - public void scrollToPositionIfEndless(int position){ - if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count"); - - if(isEndlessRightNow() && getChildCount() != 0){ - scrollToPosition(position); - } - else{ - mScrollPositionIfEndless = position; - } - } - - /** - * Returns position to which will container scroll on next relayout - * @return scroll position on next layout or -1 if it will scroll nowhere - */ - public int getScrollPositionIfEndless(){ - return mScrollPositionIfEndless; - } - - /** - * Get index of currently first item in adapter - * @return - */ - public int getScrollPosition(){ - return mFirstItemPosition; - } - - /** - * Return offset by which is edge off first item moved off screen. - * You can persist it and insert to setFirstItemOffset() to restore exact scroll position - * - * @return offset of first item, or 0 if there is not enough items to fill container and scrolling is disabled - */ - public int getFirstItemOffset(){ - if(isSrollingDisabled) return 0; - else return getScrollX() - mLeftChildEdge; - } - - /** - * Negative number. Offset by which is left edge of first item moved off screen. - * @param offset - */ - public void setFirstItemOffset(int offset){ - scrollTo(offset, 0); - } - - @Override - public Adapter getAdapter() { - return mAdapter; - } - - @Override - public void setAdapter(Adapter adapter) { - if(mAdapter != null) { - mAdapter.unregisterDataSetObserver(fDataObserver); - } - mAdapter = adapter; - mAdapter.registerDataSetObserver(fDataObserver); - - if(adapter instanceof IViewObserver){ - setViewObserver((IViewObserver) adapter); - } - - reset(); - refill(); - invalidate(); - } - - @Override - public View getSelectedView() { - if(mSelectedPosition == INVALID_POSITION) return null; - - final int index; - if(mFirstItemPosition > mSelectedPosition){ - index = mSelectedPosition + mAdapter.getCount() - mFirstItemPosition; - } - else{ - index = mSelectedPosition - mFirstItemPosition; - } - if(index < 0 || index >= getChildCount()) return null; - - return getChildAt(index); - } - - - /** - * Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect - */ - @Override - public void setSelection(int position) { - if(mAdapter == null) throw new IllegalStateException("You are trying to set selection on widget without adapter"); - if(mAdapter.getCount() == 0 && position == 0) position = -1; - if(position < -1 || position > mAdapter.getCount()-1) - throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect"); - - View v = getSelectedView(); - if(v != null) v.setSelected(false); - - - final int oldPos = mSelectedPosition; - mSelectedPosition = position; - - if(position == -1){ - if(mOnItemSelectedListener != null) mOnItemSelectedListener.onNothingSelected(this); - return; - } - - v = getSelectedView(); - if(v != null) v.setSelected(true); - - if(oldPos != mSelectedPosition && mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, mSelectedPosition, getSelectedItemId()); - } - - - private void reset() { - scrollTo(0, 0); - removeAllViewsInLayout(); - mFirstItemPosition = 0; - mLastItemPosition = -1; - mLeftChildEdge = 0; - } - - - @Override - public void computeScroll() { - // if we don't have an adapter, we don't need to do anything - if (mAdapter == null) { - return; - } - if(mAdapter.getCount() == 0){ - return; - } - - if (mScroller.computeScrollOffset()) { - if(mScroller.getFinalX() == mScroller.getCurrX()){ - mScroller.abortAnimation(); - mTouchState = TOUCH_STATE_RESTING; - if(!checkScrollPosition()) - clearChildrenCache(); - return; - } - - int x = mScroller.getCurrX(); - scrollTo(x, 0); - - postInvalidate(); - } - else if(mTouchState == TOUCH_STATE_FLING || mTouchState == TOUCH_STATE_DISTANCE_SCROLL){ - mTouchState = TOUCH_STATE_RESTING; - if(!checkScrollPosition()) - clearChildrenCache(); - } - - if(mDataChanged){ - removeAllViewsInLayout(); - refillOnChange(mFirstItemPosition); - return; - } - - relayout(); - removeNonVisibleViews(); - refillRight(); - refillLeft(); - - } - - /** - * - * @param velocityY The initial velocity in the Y direction. Positive - * numbers mean that the finger/cursor is moving down the screen, - * which means we want to scroll towards the top. - * @param velocityX The initial velocity in the X direction. Positive - * numbers mean that the finger/cursor is moving right the screen, - * which means we want to scroll towards the top. - */ - public void fling(int velocityX, int velocityY){ - mTouchState = TOUCH_STATE_FLING; - final int x = getScrollX(); - final int y = getScrollY(); - - mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, Integer.MIN_VALUE,Integer.MAX_VALUE); - - invalidate(); - } - - /** - * Scroll widget by given distance in pixels - * @param dx - */ - public void scroll(int dx){ - mScroller.startScroll(getScrollX(), 0, dx, 0, SCROLLING_DURATION); - mTouchState = TOUCH_STATE_DISTANCE_SCROLL; - invalidate(); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, - int bottom) { - super.onLayout(changed, left, top, right, bottom); - - // if we don't have an adapter, we don't need to do anything - if (mAdapter == null) { - return; - } - - refillInternal(mLastItemPosition,mFirstItemPosition); - } - - /** - * Method for actualizing content after data change in adapter. It is expected container was emptied before - * @param firstItemPosition - */ - protected void refillOnChange(int firstItemPosition){ - refillInternal(firstItemPosition-1, firstItemPosition); - } - - - protected void refillInternal(final int lastItemPos,final int firstItemPos){ - // if we don't have an adapter, we don't need to do anything - if (mAdapter == null) { - return; - } - if(mAdapter.getCount() == 0){ - return; - } - - if(getChildCount() == 0){ - fillFirstTime(lastItemPos, firstItemPos); - } - else{ - relayout(); - removeNonVisibleViews(); - refillRight(); - refillLeft(); - } - } - - /** - * Check if container visible area is filled and refill empty areas - */ - private void refill(){ - scrollTo(0, 0); - refillInternal(-1, 0); - } - -// protected void measureChild(View child, LoopLayoutParams params){ -// //prepare spec for measurement -// final int specW, specH; -// -// specW = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), 0, params.width); -// specH = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED), 0, params.height); -// -////// final boolean useMeasuredW, useMeasuredH; -//// if(params.height >= 0){ -//// specH = MeasureSpec.EXACTLY | params.height; -////// useMeasuredH = false; -//// } -//// else{ -//// if(params.height == LayoutParams.MATCH_PARENT){ -//// specH = MeasureSpec.EXACTLY | getHeight(); -//// params.height = getHeight(); -////// useMeasuredH = false; -//// }else{ -//// specH = MeasureSpec.AT_MOST | getHeight(); -////// useMeasuredH = true; -//// } -//// } -//// -//// if(params.width >= 0){ -//// specW = MeasureSpec.EXACTLY | params.width; -////// useMeasuredW = false; -//// } -//// else{ -//// if(params.width == LayoutParams.MATCH_PARENT){ -//// specW = MeasureSpec.EXACTLY | getWidth(); -//// params.width = getWidth(); -////// useMeasuredW = false; -//// }else{ -//// specW = MeasureSpec.UNSPECIFIED; -////// useMeasuredW = true; -//// } -//// } -// -// //measure -// child.measure(specW, specH); -// //put measured values into layout params from where they will be used in layout. -// //Use measured values only if exact values was not specified in layout params. -//// if(useMeasuredH) params.actualHeight = child.getMeasuredHeight(); -//// else params.actualHeight = params.height; -//// -//// if(useMeasuredW) params.actualWidth = child.getMeasuredWidth(); -//// else params.actualWidth = params.width; -// } - - protected void measureChild(View child){ - final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY); - final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY); - measureChild(child, pwms, phms); - } - - private void relayout(){ - final int c = getChildCount(); - int left = mLeftChildEdge; - - View child; - LoopLayoutParams lp; - for(int i = 0; i < c; i++){ - child = getChildAt(i); - lp = (LoopLayoutParams) child.getLayoutParams(); - measureChild(child); - - left = layoutChildHorizontal(child, left, lp); - } - - } - - - protected void fillFirstTime(final int lastItemPos,final int firstItemPos){ - final int leftScreenEdge = 0; - final int rightScreenEdge = leftScreenEdge + getWidth(); - - int right; - int left; - View child; - - boolean isRepeatingNow = false; - - //scrolling is enabled until we find out we don't have enough items - isSrollingDisabled = false; - - mLastItemPosition = lastItemPos; - mFirstItemPosition = firstItemPos; - mLeftChildEdge = 0; - right = mLeftChildEdge; - left = mLeftChildEdge; - - while(right < rightScreenEdge){ - mLastItemPosition++; - - if(isRepeatingNow && mLastItemPosition >= firstItemPos) return; - - if(mLastItemPosition >= mAdapter.getCount()){ - if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0; - else{ - if(firstItemPos > 0){ - mLastItemPosition = 0; - isRepeatingNow = true; - } - else if(!shouldRepeat){ - mLastItemPosition--; - isSrollingDisabled = true; - final int w = right-mLeftChildEdge; - final int dx = (getWidth() - w)/2; - scrollTo(-dx, 0); - return; - } - - } - } - - if(mLastItemPosition >= mAdapter.getCount() ){ - Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()"); - return; - } - - child = mAdapter.getView(mLastItemPosition, getCachedView(), this); - Validate.notNull(child,"Your adapter has returned null from getView."); - child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); - left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams()); - right = child.getRight(); - - //if selected view is going to screen, set selected state on him - if(mLastItemPosition == mSelectedPosition){ - child.setSelected(true); - } - - } - - if(mScrollPositionIfEndless > 0){ - final int p = mScrollPositionIfEndless; - mScrollPositionIfEndless = -1; - removeAllViewsInLayout(); - refillOnChange(p); - } - } - - - /** - * Checks and refills empty area on the right - */ - protected void refillRight(){ - if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - final int rightScreenEdge = leftScreenEdge + getWidth(); - - View child = getChildAt(getChildCount() - 1); - int right = child.getRight(); - int currLayoutLeft = right + ((LoopLayoutParams)child.getLayoutParams()).rightMargin; - while(right < rightScreenEdge){ - mLastItemPosition++; - if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0; - - child = mAdapter.getView(mLastItemPosition, getCachedView(), this); - Validate.notNull(child,"Your adapter has returned null from getView."); - child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); - currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams()); - right = child.getRight(); - - //if selected view is going to screen, set selected state on him - if(mLastItemPosition == mSelectedPosition){ - child.setSelected(true); - } - } - } - - /** - * Checks and refills empty area on the left - */ - protected void refillLeft(){ - if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override first init to scrolling disabled by falling to this branch - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - - View child = getChildAt(0); - int childLeft = child.getLeft(); - int currLayoutRight = childLeft - ((LoopLayoutParams)child.getLayoutParams()).leftMargin; - while(currLayoutRight > leftScreenEdge){ - mFirstItemPosition--; - if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; - - child = mAdapter.getView(mFirstItemPosition, getCachedView(), this); - Validate.notNull(child,"Your adapter has returned null from getView."); - child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); - currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); - childLeft = child.getLeft() - ((LoopLayoutParams)child.getLayoutParams()).leftMargin; - //update left edge of children in container - mLeftChildEdge = childLeft; - - //if selected view is going to screen, set selected state on him - if(mFirstItemPosition == mSelectedPosition){ - child.setSelected(true); - } - } - } - -// /** -// * Checks and refills empty area on the left -// */ -// protected void refillLeft(){ -// if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch -// final int leftScreenEdge = getScrollX(); -// -// View child = getChildAt(0); -// int currLayoutRight = child.getRight(); -// while(currLayoutRight > leftScreenEdge){ -// mFirstItemPosition--; -// if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; -// -// child = mAdapter.getView(mFirstItemPosition, getCachedView(mFirstItemPosition), this); -// child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); -// currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); -// -// //update left edge of children in container -// mLeftChildEdge = child.getLeft(); -// -// //if selected view is going to screen, set selected state on him -// if(mFirstItemPosition == mSelectedPosition){ -// child.setSelected(true); -// } -// } -// } - - /** - * Removes view that are outside of the visible part of the list. Will not - * remove all views. - */ - protected void removeNonVisibleViews() { - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - final int rightScreenEdge = leftScreenEdge + getWidth(); - - // check if we should remove any views in the left - View firstChild = getChildAt(0); - final int leftedge = firstChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin; - if(leftedge != mLeftChildEdge) throw new IllegalStateException("firstChild.getLeft() != mLeftChildEdge"); - while (firstChild != null && firstChild.getRight() + ((LoopLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) { - //if selected view is going off screen, remove selected state - firstChild.setSelected(false); - - // remove view - removeViewInLayout(firstChild); - - if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition); - WeakReference ref = new WeakReference(firstChild); - mCachedItemViews.addLast(ref); - - mFirstItemPosition++; - if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; - - // update left item position - mLeftChildEdge = getChildAt(0).getLeft() - ((LoopLayoutParams)getChildAt(0).getLayoutParams()).leftMargin; - - // Continue to check the next child only if we have more than - // one child left - if (getChildCount() > 1) { - firstChild = getChildAt(0); - } else { - firstChild = null; - } - } - - // check if we should remove any views in the right - View lastChild = getChildAt(getChildCount() - 1); - while (lastChild != null && firstChild!=null && lastChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin > rightScreenEdge) { - //if selected view is going off screen, remove selected state - lastChild.setSelected(false); - - // remove the right view - removeViewInLayout(lastChild); - - if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition); - WeakReference ref = new WeakReference(lastChild); - mCachedItemViews.addLast(ref); - - mLastItemPosition--; - if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; - - // Continue to check the next child only if we have more than - // one child left - if (getChildCount() > 1) { - lastChild = getChildAt(getChildCount() - 1); - } else { - lastChild = null; - } - } - } - - - /** - * Adds a view as a child view and takes care of measuring it - * - * @param child The view to add - * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT - * @return child which was actually added to container, subclasses can override to introduce frame views - */ - protected View addAndMeasureChildHorizontal(final View child, final int layoutMode) { - LayoutParams lp = child.getLayoutParams(); - LoopLayoutParams params; - if (lp == null) { - params = createLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - } - else{ - if(lp!=null && lp instanceof LoopLayoutParams) params = (LoopLayoutParams) lp; - else params = createLayoutParams(lp); - } - final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; - addViewInLayout(child, index, params, true); - - measureChild(child); - child.setDrawingCacheEnabled(true); - - return child; - } - - - - /** - * Layouts children from left to right - * @param left positon for left edge in parent container - * @param lp layout params - * @return new left - */ - protected int layoutChildHorizontal(View v,int left, LoopLayoutParams lp){ - int l,t,r,b; - - switch(lp.position){ - case LoopLayoutParams.TOP: - l = left + lp.leftMargin; - t = lp.topMargin; - r = l + v.getMeasuredWidth(); - b = t + v.getMeasuredHeight(); - break; - case LoopLayoutParams.BOTTOM: - b = getHeight() - lp.bottomMargin; - t = b - v.getMeasuredHeight(); - l = left + lp.leftMargin; - r = l + v.getMeasuredWidth(); - break; - case LoopLayoutParams.CENTER: - l = left + lp.leftMargin; - r = l + v.getMeasuredWidth(); - final int x = (getHeight() - v.getMeasuredHeight())/2; - t = x; - b = t + v.getMeasuredHeight(); - break; - default: - throw new RuntimeException("Only TOP,BOTTOM,CENTER are alowed in horizontal orientation"); - } - - - v.layout(l, t, r, b); - return r + lp.rightMargin; - } - - /** - * Layout children from right to left - */ - protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){ - final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; - layoutChildHorizontal(v, left, lp); - return left; - } - - /** - * Allows to make scroll alignments - * @return true if invalidate() was issued, and container is going to scroll - */ - protected boolean checkScrollPosition(){ - return false; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - - /* - * This method JUST determines whether we want to intercept the motion. - * If we return true, onTouchEvent will be called and we do the actual - * scrolling there. - */ - - - /* - * Shortcut the most recurring case: the user is in the dragging - * state and he is moving his finger. We want to intercept this - * motion. - */ - final int action = ev.getAction(); - if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { - return true; - } - - final float x = ev.getX(); - final float y = ev.getY(); - switch (action) { - case MotionEvent.ACTION_MOVE: - //if we have scrolling disabled, we don't do anything - if(!shouldRepeat && isSrollingDisabled) return false; - - /* - * not dragging, otherwise the shortcut would have caught it. Check - * whether the user has moved far enough from his original down touch. - */ - - /* - * Locally do absolute value. mLastMotionX is set to the x value - * of the down event. - */ - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - final boolean xMoved = xDiff > touchSlop; - final boolean yMoved = yDiff > touchSlop; - - if (xMoved) { - - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - mHandleSelectionOnActionUp = false; - enableChildrenCache(); - - // Either way, cancel any pending longpress - if (mAllowLongPress) { - mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - cancelLongPress(); - } - } - if(yMoved){ - mHandleSelectionOnActionUp = false; - if (mAllowLongPress) { - mAllowLongPress = false; - cancelLongPress(); - } - } - break; - - case MotionEvent.ACTION_DOWN: - // Remember location of down touch - mLastMotionX = x; - mLastMotionY = y; - mAllowLongPress = true; -// mCancelInIntercept = false; - - mDown.x = (int) x; - mDown.y = (int) y; - - /* - * If being flinged and user touches the screen, initiate drag; - * otherwise don't. mScroller.isFinished should be false when - * being flinged. - */ - mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; - //if he had normal click in rested state, remember for action up check - if(mTouchState == TOUCH_STATE_RESTING){ - mHandleSelectionOnActionUp = true; - } - break; - - case MotionEvent.ACTION_CANCEL: - mDown.x = -1; - mDown.y = -1; -// mCancelInIntercept = true; - break; - case MotionEvent.ACTION_UP: - //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates - if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ - final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); - if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); - } - // Release the drag - mAllowLongPress = false; - mHandleSelectionOnActionUp = false; - mDown.x = -1; - mDown.y = -1; - if(mTouchState == TOUCH_STATE_SCROLLING){ - if(checkScrollPosition()){ - break; - } - } - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - break; - } - - mInterceptTouchEvents = mTouchState == TOUCH_STATE_SCROLLING; - return mInterceptTouchEvents; - - } - -// /** -// * Allow subclasses to override this to always intercept events -// * @return -// */ -// protected boolean interceptEvents(){ -// /* -// * The only time we want to intercept motion events is if we are in the -// * drag mode. -// */ -// return mTouchState == TOUCH_STATE_SCROLLING; -// } - - protected void handleClick(Point p){ - final int c = getChildCount(); - View v; - final Rect r = new Rect(); - for(int i=0; i < c; i++){ - v = getChildAt(i); - v.getHitRect(r); - if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ - final View old = getSelectedView(); - if(old != null) old.setSelected(false); - - int position = mFirstItemPosition + i; - if(position >= mAdapter.getCount()) position = position - mAdapter.getCount(); - - - mSelectedPosition = position; - v.setSelected(true); - - if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position)); - if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position)); - - break; - } - } - } - - - @Override - public boolean onTouchEvent(MotionEvent event) { - // if we don't have an adapter, we don't need to do anything - if (mAdapter == null) { - return false; - } - - - - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(event); - - final int action = event.getAction(); - final float x = event.getX(); - final float y = event.getY(); - - switch (action) { - case MotionEvent.ACTION_DOWN: - /* - * If being flinged and user touches, stop the fling. isFinished - * will be false if being flinged. - */ - if (!mScroller.isFinished()) { - mScroller.forceFinished(true); - } - - // Remember where the motion event started - mLastMotionX = x; - mLastMotionY = y; - - break; - case MotionEvent.ACTION_MOVE: - //if we have scrolling disabled, we don't do anything - if(!shouldRepeat && isSrollingDisabled) return false; - - if (mTouchState == TOUCH_STATE_SCROLLING) { - // Scroll to follow the motion event - final int deltaX = (int) (mLastMotionX - x); - mLastMotionX = x; - mLastMotionY = y; - - int sx = getScrollX() + deltaX; - - scrollTo(sx, 0); - - } - else{ - final int xDiff = (int) Math.abs(x - mLastMotionX); - - final int touchSlop = mTouchSlop; - final boolean xMoved = xDiff > touchSlop; - - - if (xMoved) { - - // Scroll if the user moved far enough along the X axis - mTouchState = TOUCH_STATE_SCROLLING; - enableChildrenCache(); - - // Either way, cancel any pending longpress - if (mAllowLongPress) { - mAllowLongPress = false; - // Try canceling the long press. It could also have been scheduled - // by a distant descendant, so use the mAllowLongPress flag to block - // everything - cancelLongPress(); - } - } - } - break; - case MotionEvent.ACTION_UP: - - //this must be here, in case no child view returns true, - //events will propagate back here and on intercept touch event wont be called again - //in case of no parent it propagates here, in case of parent it usualy propagates to on cancel - if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ - final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); - if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); - mHandleSelectionOnActionUp = false; - } - - //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates - if (mTouchState == TOUCH_STATE_SCROLLING) { - - mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int initialXVelocity = (int) mVelocityTracker.getXVelocity(); - int initialYVelocity = (int) mVelocityTracker.getYVelocity(); - - if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { - fling(-initialXVelocity, -initialYVelocity); - } - else{ - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_RESTING; - checkScrollPosition(); - mAllowLongPress = false; - - mDown.x = -1; - mDown.y = -1; - } - - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - - break; - } - - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_RESTING; - mAllowLongPress = false; - - mDown.x = -1; - mDown.y = -1; - - break; - case MotionEvent.ACTION_CANCEL: - - //this must be here, in case no child view returns true, - //events will propagate back here and on intercept touch event wont be called again - //instead we get cancel here, since we stated we shouldn't intercept events and propagate them to children - //but events propagated back here, because no child was interested -// if(!mInterceptTouchEvents && mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ -// handleClick(mDown); -// mHandleSelectionOnActionUp = false; -// } - - mAllowLongPress = false; - - mDown.x = -1; - mDown.y = -1; - - if(mTouchState == TOUCH_STATE_SCROLLING){ - if(checkScrollPosition()){ - break; - } - } - - mTouchState = TOUCH_STATE_RESTING; - } - - return true; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - checkScrollFocusLeft(); - break; - case KeyEvent.KEYCODE_DPAD_RIGHT: - checkScrollFocusRight(); - break; - default: - break; - } - - return super.onKeyDown(keyCode, event); - } - - /** - * Moves with scroll window if focus hits one view before end of screen - */ - private void checkScrollFocusLeft(){ - final View focused = getFocusedChild(); - if(getChildCount() >= 2 ){ - View second = getChildAt(1); - View first = getChildAt(0); - - if(focused == second){ - scroll(-first.getWidth()); - } - } - } - - private void checkScrollFocusRight(){ - final View focused = getFocusedChild(); - if(getChildCount() >= 2 ){ - View last = getChildAt(getChildCount()-1); - View lastButOne = getChildAt(getChildCount()-2); - - if(focused == lastButOne){ - scroll(last.getWidth()); - } - } - } - - /** - * Check if list of weak references has any view still in memory to offer for recyclation - * @return cached view - */ - protected View getCachedView(){ - if (mCachedItemViews.size() != 0) { - View v; - do{ - v = mCachedItemViews.removeFirst().get(); - } - while(v == null && mCachedItemViews.size() != 0); - return v; - } - return null; - } - - protected void enableChildrenCache() { - setChildrenDrawnWithCacheEnabled(true); - setChildrenDrawingCacheEnabled(true); - } - - protected void clearChildrenCache() { - setChildrenDrawnWithCacheEnabled(false); - } - - @Override - public void setOnItemClickListener( - android.widget.AdapterView.OnItemClickListener listener) { - mOnItemClickListener = listener; - } - - @Override - public void setOnItemSelectedListener( - android.widget.AdapterView.OnItemSelectedListener listener) { - mOnItemSelectedListener = listener; - } - - @Override - @CapturedViewProperty - public int getSelectedItemPosition() { - return mSelectedPosition; - } - - /** - * Only set value for selection position field, no gui updates are done - * for setting selection with gui updates and callback calls use setSelection - * @param position - */ - public void setSeletedItemPosition(int position){ - if(mAdapter.getCount() == 0 && position == 0) position = -1; - if(position < -1 || position > mAdapter.getCount()-1) - throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect"); - - mSelectedPosition = position; - } - - @Override - @CapturedViewProperty - public long getSelectedItemId() { - return mAdapter.getItemId(mSelectedPosition); - } - - @Override - public Object getSelectedItem() { - return getSelectedView(); - } - - @Override - @CapturedViewProperty - public int getCount() { - if(mAdapter != null) return mAdapter.getCount(); - else return 0; - } - - @Override - public int getPositionForView(View view) { - final int c = getChildCount(); - View v; - for(int i = 0; i < c; i++){ - v = getChildAt(i); - if(v == view) return mFirstItemPosition + i; - } - return INVALID_POSITION; - } - - @Override - public int getFirstVisiblePosition() { - return mFirstItemPosition; - } - - @Override - public int getLastVisiblePosition() { - return mLastItemPosition; - } - - @Override - public Object getItemAtPosition(int position) { - final int index; - if(mFirstItemPosition > position){ - index = position + mAdapter.getCount() - mFirstItemPosition; - } - else{ - index = position - mFirstItemPosition; - } - if(index < 0 || index >= getChildCount()) return null; - - return getChildAt(index); - } - - @Override - public long getItemIdAtPosition(int position) { - return mAdapter.getItemId(position); - } - - @Override - public boolean performItemClick(View view, int position, long id) { - throw new UnsupportedOperationException(); - } - - - public void setViewObserver(IViewObserver viewObserver) { - this.mViewObserver = viewObserver; - } - - -} - - +package it.moondroid.coverflow.components.ui.containers; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewDebug.CapturedViewProperty; +import android.widget.Adapter; +import android.widget.AdapterView; +import android.widget.Scroller; + +import java.lang.ref.WeakReference; +import java.util.LinkedList; + +import it.moondroid.coverflow.R; +import it.moondroid.coverflow.components.general.ToolBox; +import it.moondroid.coverflow.components.general.Validate; +import it.moondroid.coverflow.components.ui.containers.interfaces.IViewObserver; + + +/** + * + * @author Martin Appl + * + * Endless loop with items filling from adapter. Currently only horizontal orientation is implemented + * View recycling in adapter is supported. You are encouraged to recycle view in adapter if possible + * + */ +public class EndlessLoopAdapterContainer extends AdapterView { + /** Children added with this layout mode will be added after the last child */ + protected static final int LAYOUT_MODE_AFTER = 0; + + /** Children added with this layout mode will be added before the first child */ + protected static final int LAYOUT_MODE_TO_BEFORE = 1; + + protected static final int SCROLLING_DURATION = 500; + + + + /** The adapter providing data for container */ + protected Adapter mAdapter; + + /** The adaptor position of the first visible item */ + protected int mFirstItemPosition; + + /** The adaptor position of the last visible item */ + protected int mLastItemPosition; + + /** The adaptor position of selected item */ + protected int mSelectedPosition = INVALID_POSITION; + + /** Left of current most left child*/ + protected int mLeftChildEdge; + + /** User is not touching the list */ + protected static final int TOUCH_STATE_RESTING = 1; + + /** User is scrolling the list */ + protected static final int TOUCH_STATE_SCROLLING = 2; + + /** Fling gesture in progress */ + protected static final int TOUCH_STATE_FLING = 3; + + /** Aligning in progress */ + protected static final int TOUCH_STATE_ALIGN = 4; + + protected static final int TOUCH_STATE_DISTANCE_SCROLL = 5; + + /** A list of cached (re-usable) item views */ + protected final LinkedList> mCachedItemViews = new LinkedList>(); + + /** 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*/ + protected boolean isSrollingDisabled = false; + + /** Whether content should be repeated when there is not enough items to fill container */ + protected boolean shouldRepeat = true; + + /** 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*/ + protected int mScrollPositionIfEndless = -1; + + private IViewObserver mViewObserver; + + + protected int mTouchState = TOUCH_STATE_RESTING; + + protected final Scroller mScroller = new Scroller(getContext()); + private VelocityTracker mVelocityTracker; + private boolean mDataChanged; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + private boolean mAllowLongPress; + private float mLastMotionX; + private float mLastMotionY; +// private long mDownTime; + + private final Point mDown = new Point(); + private boolean mHandleSelectionOnActionUp = false; + private boolean mInterceptTouchEvents; +// private boolean mCancelInIntercept; + + protected OnItemClickListener mOnItemClickListener; + protected OnItemSelectedListener mOnItemSelectedListener; + + public EndlessLoopAdapterContainer(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + + //init params from xml + if(attrs != null){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EndlessLoopAdapterContainer, defStyle, 0); + + shouldRepeat = a.getBoolean(R.styleable.EndlessLoopAdapterContainer_shouldRepeat, true); + + a.recycle(); + } + } + + public EndlessLoopAdapterContainer(Context context, AttributeSet attrs) { + this(context, attrs,0); + + } + + public EndlessLoopAdapterContainer(Context context) { + this(context,null); + } + + private final DataSetObserver fDataObserver = new DataSetObserver() { + + @Override + public void onChanged() { + synchronized(this){ + mDataChanged = true; + } + invalidate(); + } + + @Override + public void onInvalidated() { + mAdapter = null; + } + }; + + + /** + * Params describing position of child view in container + * in HORIZONTAL mode TOP,CENTER,BOTTOM are active in VERTICAL mode LEFT,CENTER,RIGHT are active + * @author Martin Appl + * + */ + public static class LoopLayoutParams extends MarginLayoutParams{ + public static final int TOP = 0; + public static final int CENTER = 1; + public static final int BOTTOM = 2; + public static final int LEFT = 3; + public static final int RIGHT = 4; + + public int position; +// public int actualWidth; +// public int actualHeight; + + public LoopLayoutParams(int w, int h) { + super(w, h); + position = CENTER; + } + + public LoopLayoutParams(int w, int h,int pos){ + super(w, h); + position = pos; + } + + public LoopLayoutParams(LayoutParams lp) { + super(lp); + + if(lp!=null && lp instanceof MarginLayoutParams){ + MarginLayoutParams mp = (MarginLayoutParams) lp; + leftMargin = mp.leftMargin; + rightMargin = mp.rightMargin; + topMargin = mp.topMargin; + bottomMargin = mp.bottomMargin; + } + + position = CENTER; + } + + + } + + protected LoopLayoutParams createLayoutParams(int w, int h){ + return new LoopLayoutParams(w, h); + } + + protected LoopLayoutParams createLayoutParams(int w, int h,int pos){ + return new LoopLayoutParams(w, h, pos); + } + + protected LoopLayoutParams createLayoutParams(LayoutParams lp){ + return new LoopLayoutParams(lp); + } + + + public boolean isRepeatable() { + return shouldRepeat; + } + + public boolean isEndlessRightNow(){ + return !isSrollingDisabled; + } + + public void setShouldRepeat(boolean shouldRepeat) { + this.shouldRepeat = shouldRepeat; + } + + /** + * Sets position in adapter of first shown item in container + * @param position + */ + public void scrollToPosition(int position){ + if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count"); + + reset(); + refillInternal(position-1, position); + invalidate(); + } + + public void scrollToPositionIfEndless(int position){ + if(position < 0 || position >= mAdapter.getCount()) throw new IndexOutOfBoundsException("Position must be in bounds of adapter values count"); + + if(isEndlessRightNow() && getChildCount() != 0){ + scrollToPosition(position); + } + else{ + mScrollPositionIfEndless = position; + } + } + + /** + * Returns position to which will container scroll on next relayout + * @return scroll position on next layout or -1 if it will scroll nowhere + */ + public int getScrollPositionIfEndless(){ + return mScrollPositionIfEndless; + } + + /** + * Get index of currently first item in adapter + * @return + */ + public int getScrollPosition(){ + return mFirstItemPosition; + } + + /** + * Return offset by which is edge off first item moved off screen. + * You can persist it and insert to setFirstItemOffset() to restore exact scroll position + * + * @return offset of first item, or 0 if there is not enough items to fill container and scrolling is disabled + */ + public int getFirstItemOffset(){ + if(isSrollingDisabled) return 0; + else return getScrollX() - mLeftChildEdge; + } + + /** + * Negative number. Offset by which is left edge of first item moved off screen. + * @param offset + */ + public void setFirstItemOffset(int offset){ + scrollTo(offset, 0); + } + + @Override + public Adapter getAdapter() { + return mAdapter; + } + + @Override + public void setAdapter(Adapter adapter) { + if(mAdapter != null) { + mAdapter.unregisterDataSetObserver(fDataObserver); + } + mAdapter = adapter; + mAdapter.registerDataSetObserver(fDataObserver); + + if(adapter instanceof IViewObserver){ + setViewObserver((IViewObserver) adapter); + } + + reset(); + refill(); + invalidate(); + } + + @Override + public View getSelectedView() { + if(mSelectedPosition == INVALID_POSITION) return null; + + final int index; + if(mFirstItemPosition > mSelectedPosition){ + index = mSelectedPosition + mAdapter.getCount() - mFirstItemPosition; + } + else{ + index = mSelectedPosition - mFirstItemPosition; + } + if(index < 0 || index >= getChildCount()) return null; + + return getChildAt(index); + } + + + /** + * Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect + */ + @Override + public void setSelection(int position) { + if(mAdapter == null) throw new IllegalStateException("You are trying to set selection on widget without adapter"); + if(mAdapter.getCount() == 0 && position == 0) position = -1; + if(position < -1 || position > mAdapter.getCount()-1) + throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect"); + + View v = getSelectedView(); + if(v != null) v.setSelected(false); + + + final int oldPos = mSelectedPosition; + mSelectedPosition = position; + + if(position == -1){ + if(mOnItemSelectedListener != null) mOnItemSelectedListener.onNothingSelected(this); + return; + } + + v = getSelectedView(); + if(v != null) v.setSelected(true); + + if(oldPos != mSelectedPosition && mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, mSelectedPosition, getSelectedItemId()); + } + + + private void reset() { + scrollTo(0, 0); + removeAllViewsInLayout(); + mFirstItemPosition = 0; + mLastItemPosition = -1; + mLeftChildEdge = 0; + } + + + @Override + public void computeScroll() { + // if we don't have an adapter, we don't need to do anything + if (mAdapter == null) { + return; + } + if(mAdapter.getCount() == 0){ + return; + } + + if (mScroller.computeScrollOffset()) { + if(mScroller.getFinalX() == mScroller.getCurrX()){ + mScroller.abortAnimation(); + mTouchState = TOUCH_STATE_RESTING; + if(!checkScrollPosition()) + clearChildrenCache(); + return; + } + + int x = mScroller.getCurrX(); + scrollTo(x, 0); + + postInvalidate(); + } + else if(mTouchState == TOUCH_STATE_FLING || mTouchState == TOUCH_STATE_DISTANCE_SCROLL){ + mTouchState = TOUCH_STATE_RESTING; + if(!checkScrollPosition()) + clearChildrenCache(); + } + + if(mDataChanged){ + removeAllViewsInLayout(); + refillOnChange(mFirstItemPosition); + return; + } + + relayout(); + removeNonVisibleViews(); + refillRight(); + refillLeft(); + + } + + /** + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + * @param velocityX The initial velocity in the X direction. Positive + * numbers mean that the finger/cursor is moving right the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY){ + mTouchState = TOUCH_STATE_FLING; + final int x = getScrollX(); + final int y = getScrollY(); + + mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, Integer.MIN_VALUE,Integer.MAX_VALUE); + + invalidate(); + } + + /** + * Scroll widget by given distance in pixels + * @param dx + */ + public void scroll(int dx){ + mScroller.startScroll(getScrollX(), 0, dx, 0, SCROLLING_DURATION); + mTouchState = TOUCH_STATE_DISTANCE_SCROLL; + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, + int bottom) { + super.onLayout(changed, left, top, right, bottom); + + // if we don't have an adapter, we don't need to do anything + if (mAdapter == null) { + return; + } + + refillInternal(mLastItemPosition,mFirstItemPosition); + } + + /** + * Method for actualizing content after data change in adapter. It is expected container was emptied before + * @param firstItemPosition + */ + protected void refillOnChange(int firstItemPosition){ + refillInternal(firstItemPosition-1, firstItemPosition); + } + + + protected void refillInternal(final int lastItemPos,final int firstItemPos){ + // if we don't have an adapter, we don't need to do anything + if (mAdapter == null) { + return; + } + if(mAdapter.getCount() == 0){ + return; + } + + if(getChildCount() == 0){ + fillFirstTime(lastItemPos, firstItemPos); + } + else{ + relayout(); + removeNonVisibleViews(); + refillRight(); + refillLeft(); + } + } + + /** + * Check if container visible area is filled and refill empty areas + */ + private void refill(){ + scrollTo(0, 0); + refillInternal(-1, 0); + } + +// protected void measureChild(View child, LoopLayoutParams params){ +// //prepare spec for measurement +// final int specW, specH; +// +// specW = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), 0, params.width); +// specH = getChildMeasureSpec(MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED), 0, params.height); +// +////// final boolean useMeasuredW, useMeasuredH; +//// if(params.height >= 0){ +//// specH = MeasureSpec.EXACTLY | params.height; +////// useMeasuredH = false; +//// } +//// else{ +//// if(params.height == LayoutParams.MATCH_PARENT){ +//// specH = MeasureSpec.EXACTLY | getHeight(); +//// params.height = getHeight(); +////// useMeasuredH = false; +//// }else{ +//// specH = MeasureSpec.AT_MOST | getHeight(); +////// useMeasuredH = true; +//// } +//// } +//// +//// if(params.width >= 0){ +//// specW = MeasureSpec.EXACTLY | params.width; +////// useMeasuredW = false; +//// } +//// else{ +//// if(params.width == LayoutParams.MATCH_PARENT){ +//// specW = MeasureSpec.EXACTLY | getWidth(); +//// params.width = getWidth(); +////// useMeasuredW = false; +//// }else{ +//// specW = MeasureSpec.UNSPECIFIED; +////// useMeasuredW = true; +//// } +//// } +// +// //measure +// child.measure(specW, specH); +// //put measured values into layout params from where they will be used in layout. +// //Use measured values only if exact values was not specified in layout params. +//// if(useMeasuredH) params.actualHeight = child.getMeasuredHeight(); +//// else params.actualHeight = params.height; +//// +//// if(useMeasuredW) params.actualWidth = child.getMeasuredWidth(); +//// else params.actualWidth = params.width; +// } + + protected void measureChild(View child){ + final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY); + final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY); + measureChild(child, pwms, phms); + } + + private void relayout(){ + final int c = getChildCount(); + int left = mLeftChildEdge; + + View child; + LoopLayoutParams lp; + for(int i = 0; i < c; i++){ + child = getChildAt(i); + lp = (LoopLayoutParams) child.getLayoutParams(); + measureChild(child); + + left = layoutChildHorizontal(child, left, lp); + } + + } + + + protected void fillFirstTime(final int lastItemPos,final int firstItemPos){ + final int leftScreenEdge = 0; + final int rightScreenEdge = leftScreenEdge + getWidth(); + + int right; + int left; + View child; + + boolean isRepeatingNow = false; + + //scrolling is enabled until we find out we don't have enough items + isSrollingDisabled = false; + + mLastItemPosition = lastItemPos; + mFirstItemPosition = firstItemPos; + mLeftChildEdge = 0; + right = mLeftChildEdge; + left = mLeftChildEdge; + + while(right < rightScreenEdge){ + mLastItemPosition++; + + if(isRepeatingNow && mLastItemPosition >= firstItemPos) return; + + if(mLastItemPosition >= mAdapter.getCount()){ + if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0; + else{ + if(firstItemPos > 0){ + mLastItemPosition = 0; + isRepeatingNow = true; + } + else if(!shouldRepeat){ + mLastItemPosition--; + isSrollingDisabled = true; + final int w = right-mLeftChildEdge; + final int dx = (getWidth() - w)/2; + scrollTo(-dx, 0); + return; + } + + } + } + + if(mLastItemPosition >= mAdapter.getCount() ){ + Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()"); + return; + } + + child = mAdapter.getView(mLastItemPosition, getCachedView(), this); + Validate.notNull(child, "Your adapter has returned null from getView."); + child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); + left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams()); + right = child.getRight(); + + //if selected view is going to screen, set selected state on him + if(mLastItemPosition == mSelectedPosition){ + child.setSelected(true); + } + + } + + if(mScrollPositionIfEndless > 0){ + final int p = mScrollPositionIfEndless; + mScrollPositionIfEndless = -1; + removeAllViewsInLayout(); + refillOnChange(p); + } + } + + + /** + * Checks and refills empty area on the right + */ + protected void refillRight(){ + if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + final int rightScreenEdge = leftScreenEdge + getWidth(); + + View child = getChildAt(getChildCount() - 1); + int right = child.getRight(); + int currLayoutLeft = right + ((LoopLayoutParams)child.getLayoutParams()).rightMargin; + while(right < rightScreenEdge){ + mLastItemPosition++; + if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0; + + child = mAdapter.getView(mLastItemPosition, getCachedView(), this); + Validate.notNull(child,"Your adapter has returned null from getView."); + child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); + currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams()); + right = child.getRight(); + + //if selected view is going to screen, set selected state on him + if(mLastItemPosition == mSelectedPosition){ + child.setSelected(true); + } + } + } + + /** + * Checks and refills empty area on the left + */ + protected void refillLeft(){ + if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override first init to scrolling disabled by falling to this branch + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + + View child = getChildAt(0); + int childLeft = child.getLeft(); + int currLayoutRight = childLeft - ((LoopLayoutParams)child.getLayoutParams()).leftMargin; + while(currLayoutRight > leftScreenEdge){ + mFirstItemPosition--; + if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; + + child = mAdapter.getView(mFirstItemPosition, getCachedView(), this); + Validate.notNull(child,"Your adapter has returned null from getView."); + child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); + currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); + childLeft = child.getLeft() - ((LoopLayoutParams)child.getLayoutParams()).leftMargin; + //update left edge of children in container + mLeftChildEdge = childLeft; + + //if selected view is going to screen, set selected state on him + if(mFirstItemPosition == mSelectedPosition){ + child.setSelected(true); + } + } + } + +// /** +// * Checks and refills empty area on the left +// */ +// protected void refillLeft(){ +// if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch +// final int leftScreenEdge = getScrollX(); +// +// View child = getChildAt(0); +// int currLayoutRight = child.getRight(); +// while(currLayoutRight > leftScreenEdge){ +// mFirstItemPosition--; +// if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; +// +// child = mAdapter.getView(mFirstItemPosition, getCachedView(mFirstItemPosition), this); +// child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); +// currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); +// +// //update left edge of children in container +// mLeftChildEdge = child.getLeft(); +// +// //if selected view is going to screen, set selected state on him +// if(mFirstItemPosition == mSelectedPosition){ +// child.setSelected(true); +// } +// } +// } + + /** + * Removes view that are outside of the visible part of the list. Will not + * remove all views. + */ + protected void removeNonVisibleViews() { + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + final int rightScreenEdge = leftScreenEdge + getWidth(); + + // check if we should remove any views in the left + View firstChild = getChildAt(0); + final int leftedge = firstChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin; + if(leftedge != mLeftChildEdge) throw new IllegalStateException("firstChild.getLeft() != mLeftChildEdge"); + while (firstChild != null && firstChild.getRight() + ((LoopLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) { + //if selected view is going off screen, remove selected state + firstChild.setSelected(false); + + // remove view + removeViewInLayout(firstChild); + + if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition); + WeakReference ref = new WeakReference(firstChild); + mCachedItemViews.addLast(ref); + + mFirstItemPosition++; + if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; + + // update left item position + mLeftChildEdge = getChildAt(0).getLeft() - ((LoopLayoutParams)getChildAt(0).getLayoutParams()).leftMargin; + + // Continue to check the next child only if we have more than + // one child left + if (getChildCount() > 1) { + firstChild = getChildAt(0); + } else { + firstChild = null; + } + } + + // check if we should remove any views in the right + View lastChild = getChildAt(getChildCount() - 1); + while (lastChild != null && firstChild!=null && lastChild.getLeft() - ((LoopLayoutParams)firstChild.getLayoutParams()).leftMargin > rightScreenEdge) { + //if selected view is going off screen, remove selected state + lastChild.setSelected(false); + + // remove the right view + removeViewInLayout(lastChild); + + if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition); + WeakReference ref = new WeakReference(lastChild); + mCachedItemViews.addLast(ref); + + mLastItemPosition--; + if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; + + // Continue to check the next child only if we have more than + // one child left + if (getChildCount() > 1) { + lastChild = getChildAt(getChildCount() - 1); + } else { + lastChild = null; + } + } + } + + + /** + * Adds a view as a child view and takes care of measuring it + * + * @param child The view to add + * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT + * @return child which was actually added to container, subclasses can override to introduce frame views + */ + protected View addAndMeasureChildHorizontal(final View child, final int layoutMode) { + LayoutParams lp = child.getLayoutParams(); + LoopLayoutParams params; + if (lp == null) { + params = createLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + } + else{ + if(lp!=null && lp instanceof LoopLayoutParams) params = (LoopLayoutParams) lp; + else params = createLayoutParams(lp); + } + final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; + addViewInLayout(child, index, params, true); + + measureChild(child); + child.setDrawingCacheEnabled(true); + + return child; + } + + + + /** + * Layouts children from left to right + * @param left positon for left edge in parent container + * @param lp layout params + * @return new left + */ + protected int layoutChildHorizontal(View v,int left, LoopLayoutParams lp){ + int l,t,r,b; + + switch(lp.position){ + case LoopLayoutParams.TOP: + l = left + lp.leftMargin; + t = lp.topMargin; + r = l + v.getMeasuredWidth(); + b = t + v.getMeasuredHeight(); + break; + case LoopLayoutParams.BOTTOM: + b = getHeight() - lp.bottomMargin; + t = b - v.getMeasuredHeight(); + l = left + lp.leftMargin; + r = l + v.getMeasuredWidth(); + break; + case LoopLayoutParams.CENTER: + l = left + lp.leftMargin; + r = l + v.getMeasuredWidth(); + final int x = (getHeight() - v.getMeasuredHeight())/2; + t = x; + b = t + v.getMeasuredHeight(); + break; + default: + throw new RuntimeException("Only TOP,BOTTOM,CENTER are alowed in horizontal orientation"); + } + + + v.layout(l, t, r, b); + return r + lp.rightMargin; + } + + /** + * Layout children from right to left + */ + protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){ + final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; + layoutChildHorizontal(v, left, lp); + return left; + } + + /** + * Allows to make scroll alignments + * @return true if invalidate() was issued, and container is going to scroll + */ + protected boolean checkScrollPosition(){ + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + final float x = ev.getX(); + final float y = ev.getY(); + switch (action) { + case MotionEvent.ACTION_MOVE: + //if we have scrolling disabled, we don't do anything + if(!shouldRepeat && isSrollingDisabled) return false; + + /* + * not dragging, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionX is set to the x value + * of the down event. + */ + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = mTouchSlop; + final boolean xMoved = xDiff > touchSlop; + final boolean yMoved = yDiff > touchSlop; + + if (xMoved) { + + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + mHandleSelectionOnActionUp = false; + enableChildrenCache(); + + // Either way, cancel any pending longpress + if (mAllowLongPress) { + mAllowLongPress = false; + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + cancelLongPress(); + } + } + if(yMoved){ + mHandleSelectionOnActionUp = false; + if (mAllowLongPress) { + mAllowLongPress = false; + cancelLongPress(); + } + } + break; + + case MotionEvent.ACTION_DOWN: + // Remember location of down touch + mLastMotionX = x; + mLastMotionY = y; + mAllowLongPress = true; +// mCancelInIntercept = false; + + mDown.x = (int) x; + mDown.y = (int) y; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; + //if he had normal click in rested state, remember for action up check + if(mTouchState == TOUCH_STATE_RESTING){ + mHandleSelectionOnActionUp = true; + } + break; + + case MotionEvent.ACTION_CANCEL: + mDown.x = -1; + mDown.y = -1; +// mCancelInIntercept = true; + break; + case MotionEvent.ACTION_UP: + //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates + if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ + final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); + if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); + } + // Release the drag + mAllowLongPress = false; + mHandleSelectionOnActionUp = false; + mDown.x = -1; + mDown.y = -1; + if(mTouchState == TOUCH_STATE_SCROLLING){ + if(checkScrollPosition()){ + break; + } + } + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + break; + } + + mInterceptTouchEvents = mTouchState == TOUCH_STATE_SCROLLING; + return mInterceptTouchEvents; + + } + +// /** +// * Allow subclasses to override this to always intercept events +// * @return +// */ +// protected boolean interceptEvents(){ +// /* +// * The only time we want to intercept motion events is if we are in the +// * drag mode. +// */ +// return mTouchState == TOUCH_STATE_SCROLLING; +// } + + protected void handleClick(Point p){ + final int c = getChildCount(); + View v; + final Rect r = new Rect(); + for(int i=0; i < c; i++){ + v = getChildAt(i); + v.getHitRect(r); + if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ + final View old = getSelectedView(); + if(old != null) old.setSelected(false); + + int position = mFirstItemPosition + i; + if(position >= mAdapter.getCount()) position = position - mAdapter.getCount(); + + + mSelectedPosition = position; + v.setSelected(true); + + if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position)); + if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position)); + + break; + } + } + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + // if we don't have an adapter, we don't need to do anything + if (mAdapter == null) { + return false; + } + + + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + + final int action = event.getAction(); + final float x = event.getX(); + final float y = event.getY(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + super.onTouchEvent(event); + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.forceFinished(true); + } + + // Remember where the motion event started + mLastMotionX = x; + mLastMotionY = y; + + break; + case MotionEvent.ACTION_MOVE: + //if we have scrolling disabled, we don't do anything + if(!shouldRepeat && isSrollingDisabled) return false; + + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int deltaX = (int) (mLastMotionX - x); + mLastMotionX = x; + mLastMotionY = y; + + int sx = getScrollX() + deltaX; + + scrollTo(sx, 0); + + } + else{ + final int xDiff = (int) Math.abs(x - mLastMotionX); + + final int touchSlop = mTouchSlop; + final boolean xMoved = xDiff > touchSlop; + + + if (xMoved) { + + // Scroll if the user moved far enough along the X axis + mTouchState = TOUCH_STATE_SCROLLING; + enableChildrenCache(); + + // Either way, cancel any pending longpress + if (mAllowLongPress) { + mAllowLongPress = false; + // Try canceling the long press. It could also have been scheduled + // by a distant descendant, so use the mAllowLongPress flag to block + // everything + cancelLongPress(); + } + } + } + break; + case MotionEvent.ACTION_UP: + + //this must be here, in case no child view returns true, + //events will propagate back here and on intercept touch event wont be called again + //in case of no parent it propagates here, in case of parent it usualy propagates to on cancel + if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ + final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); + if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); + mHandleSelectionOnActionUp = false; + } + + //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates + if (mTouchState == TOUCH_STATE_SCROLLING) { + + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int) mVelocityTracker.getXVelocity(); + int initialYVelocity = (int) mVelocityTracker.getYVelocity(); + + if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { + fling(-initialXVelocity, -initialYVelocity); + } + else{ + // Release the drag + clearChildrenCache(); + mTouchState = TOUCH_STATE_RESTING; + checkScrollPosition(); + mAllowLongPress = false; + + mDown.x = -1; + mDown.y = -1; + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + break; + } + + // Release the drag + clearChildrenCache(); + mTouchState = TOUCH_STATE_RESTING; + mAllowLongPress = false; + + mDown.x = -1; + mDown.y = -1; + + break; + case MotionEvent.ACTION_CANCEL: + + //this must be here, in case no child view returns true, + //events will propagate back here and on intercept touch event wont be called again + //instead we get cancel here, since we stated we shouldn't intercept events and propagate them to children + //but events propagated back here, because no child was interested +// if(!mInterceptTouchEvents && mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ +// handleClick(mDown); +// mHandleSelectionOnActionUp = false; +// } + + mAllowLongPress = false; + + mDown.x = -1; + mDown.y = -1; + + if(mTouchState == TOUCH_STATE_SCROLLING){ + if(checkScrollPosition()){ + break; + } + } + + mTouchState = TOUCH_STATE_RESTING; + } + + return true; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + checkScrollFocusLeft(); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + checkScrollFocusRight(); + break; + default: + break; + } + + return super.onKeyDown(keyCode, event); + } + + /** + * Moves with scroll window if focus hits one view before end of screen + */ + private void checkScrollFocusLeft(){ + final View focused = getFocusedChild(); + if(getChildCount() >= 2 ){ + View second = getChildAt(1); + View first = getChildAt(0); + + if(focused == second){ + scroll(-first.getWidth()); + } + } + } + + private void checkScrollFocusRight(){ + final View focused = getFocusedChild(); + if(getChildCount() >= 2 ){ + View last = getChildAt(getChildCount()-1); + View lastButOne = getChildAt(getChildCount()-2); + + if(focused == lastButOne){ + scroll(last.getWidth()); + } + } + } + + /** + * Check if list of weak references has any view still in memory to offer for recyclation + * @return cached view + */ + protected View getCachedView(){ + if (mCachedItemViews.size() != 0) { + View v; + do{ + v = mCachedItemViews.removeFirst().get(); + } + while(v == null && mCachedItemViews.size() != 0); + return v; + } + return null; + } + + protected void enableChildrenCache() { + setChildrenDrawnWithCacheEnabled(true); + setChildrenDrawingCacheEnabled(true); + } + + protected void clearChildrenCache() { + setChildrenDrawnWithCacheEnabled(false); + } + + @Override + public void setOnItemClickListener( + OnItemClickListener listener) { + mOnItemClickListener = listener; + } + + @Override + public void setOnItemSelectedListener( + OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + } + + @Override + @CapturedViewProperty + public int getSelectedItemPosition() { + return mSelectedPosition; + } + + /** + * Only set value for selection position field, no gui updates are done + * for setting selection with gui updates and callback calls use setSelection + * @param position + */ + public void setSeletedItemPosition(int position){ + if(mAdapter.getCount() == 0 && position == 0) position = -1; + if(position < -1 || position > mAdapter.getCount()-1) + throw new IllegalArgumentException("Position index must be in range of adapter values (0 - getCount()-1) or -1 to unselect"); + + mSelectedPosition = position; + } + + @Override + @CapturedViewProperty + public long getSelectedItemId() { + return mAdapter.getItemId(mSelectedPosition); + } + + @Override + public Object getSelectedItem() { + return getSelectedView(); + } + + @Override + @CapturedViewProperty + public int getCount() { + if(mAdapter != null) return mAdapter.getCount(); + else return 0; + } + + @Override + public int getPositionForView(View view) { + final int c = getChildCount(); + View v; + for(int i = 0; i < c; i++){ + v = getChildAt(i); + if(v == view) return mFirstItemPosition + i; + } + return INVALID_POSITION; + } + + @Override + public int getFirstVisiblePosition() { + return mFirstItemPosition; + } + + @Override + public int getLastVisiblePosition() { + return mLastItemPosition; + } + + @Override + public Object getItemAtPosition(int position) { + final int index; + if(mFirstItemPosition > position){ + index = position + mAdapter.getCount() - mFirstItemPosition; + } + else{ + index = position - mFirstItemPosition; + } + if(index < 0 || index >= getChildCount()) return null; + + return getChildAt(index); + } + + @Override + public long getItemIdAtPosition(int position) { + return mAdapter.getItemId(position); + } + + @Override + public boolean performItemClick(View view, int position, long id) { + throw new UnsupportedOperationException(); + } + + + public void setViewObserver(IViewObserver viewObserver) { + this.mViewObserver = viewObserver; + } + + +} + + diff --git a/MAComponents/src/com/martinappl/components/ui/containers/FeatureCoverFlow.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/FeatureCoverFlow.java similarity index 95% rename from MAComponents/src/com/martinappl/components/ui/containers/FeatureCoverFlow.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/FeatureCoverFlow.java index de2f2f4..aa2c3d6 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/FeatureCoverFlow.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/FeatureCoverFlow.java @@ -1,1447 +1,1486 @@ -/** - * - */ -package com.martinappl.components.ui.containers; - -import java.lang.ref.WeakReference; -import java.util.LinkedList; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Camera; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.LinearGradient; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Point; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Shader.TileMode; -import android.support.v4.util.LruCache; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Display; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.WindowManager; -import android.view.animation.DecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.Scroller; - -import com.martinappl.components.R; -import com.martinappl.components.general.Validate; - - -/** - * @author Martin Appl - * Note: Supports wrap content for height - * - */ -public class FeatureCoverFlow extends EndlessLoopAdapterContainer implements ViewTreeObserver.OnPreDrawListener { - public static final int DEFAULT_MAX_CACHE_SIZE = 32; - - /** - * Graphics Camera used for generating transformation matrices; - */ - private final Camera mCamera = new Camera(); - /** - * Relative spacing value of Views in container. If <1 Views will overlap, if >1 Views will have spaces between them - */ - private float mSpacing = 0.5f; - - /** - * Index of view in center of screen, which is most in foreground - */ - private int mReverseOrderIndex = -1; - - private int mLastCenterItemIndex = -1; - - /** - * Distance from center as fraction of half of widget size where covers start to rotate into center - * 1 means rotation starts on edge of widget, 0 means only center rotated - */ - private float mRotationThreshold = 0.3f; - - /** - * Distance from center as fraction of half of widget size where covers start to zoom in - * 1 means scaling starts on edge of widget, 0 means only center scaled - */ - private float mScalingThreshold = 0.3f; - - /** - * Distance from center as fraction of half of widget size, - * where covers start enlarge their spacing to allow for smooth passing each other without jumping over each other - * 1 means edge of widget, 0 means only center - */ - private float mAdjustPositionThreshold = 0.1f; - - /** - * By enlarging this value, you can enlarge spacing in center of widget done by position adjustment - */ - private float mAdjustPositionMultiplier = 1.0f; - - /** - * Absolute value of rotation angle of cover at edge of widget in degrees - */ - private float mMaxRotationAngle = 70.0f; - - /** - * Scale factor of item in center - */ - private float mMaxScaleFactor = 1.2f; - - /** - * Radius of circle path which covers follow. Range of screen is -1 to 1, minimal radius is therefore 1 - */ - private float mRadius = 2f; - - /** - * Radius of circle path which covers follow in coordinate space of matrix transformation. Used to scale offset - */ - private float mRadiusInMatrixSpace = 1000f; - - /** - * Size of reflection as a fraction of original image (0-1) - */ - private float mReflectionHeight = 0.5f; - - /** - * Gap between reflection and original image in pixels - */ - private int mReflectionGap = 2; - - /** - * Starting opacity of reflection. Reflection fades from this value to transparency; - */ - private int mReflectionOpacity = 0x70; - - /** - * Widget size on which was tuning of parameters done. This value is used to scale parameters on when widgets has different size - */ - private int mTuningWidgetSize = 1280; - - /** - * How long will alignment animation take - */ - private int mAlignTime = 350; - - /** - * If you don't want reflections to be transparent, you can set them background of same color as widget background - */ - private int mReflectionBackgroundColor = Color.TRANSPARENT; - - /** A list of cached (re-usable) cover frames */ - protected final LinkedList> mRecycledCoverFrames = new LinkedList>(); - - private int mPaddingTop = 0; - private int mPaddingBottom = 0; - - private int mCenterItemOffset; - private final Scroller mAlignScroller = new Scroller(getContext(), new DecelerateInterpolator()); - - private final MyCache mCachedFrames; - - private int mCoverWidth = 160; - private int mCoverHeight = 240; - - private final Matrix mMatrix = new Matrix(); - private final Matrix mTemp = new Matrix(); - private final Matrix mTempHit = new Matrix(); - private final Rect mTempRect = new Rect(); - private final RectF mTouchRect = new RectF(); - - private View mMotionTarget; - private float mTargetLeft; - private float mTargetTop; - - //reflection - private final Matrix mReflectionMatrix = new Matrix(); - private final Paint mPaint = new Paint(); - private final Paint mReflectionPaint = new Paint(); - private final PorterDuffXfermode mXfermode = new PorterDuffXfermode(Mode.DST_IN); - private final Canvas mReflectionCanvas = new Canvas(); - - private int mScrollToPositionOnNextInvalidate = -1; - - - private boolean mInvalidated = false; - - - private class MyCache extends LruCache{ - - public MyCache(int maxSize) { - super(maxSize); - } - - @Override - protected void entryRemoved(boolean evicted, Integer key, CoverFrame oldValue, CoverFrame newValue) { - if(evicted){ - if(oldValue.getChildCount() == 1){ - mCachedItemViews.addLast(new WeakReference(oldValue.getChildAt(0))); - recycleCoverFrame(oldValue); // removes children, must be after caching children - } - } - } - - } - - public FeatureCoverFlow(Context context, AttributeSet attrs, int defStyle, int cacheSize) { - super(context, attrs, defStyle); - - if(cacheSize <= 0) cacheSize = DEFAULT_MAX_CACHE_SIZE; - mCachedFrames = new MyCache(cacheSize); - - setChildrenDrawingOrderEnabled(true); - setChildrenDrawingCacheEnabled(true); - setChildrenDrawnWithCacheEnabled(true); - - mReflectionMatrix.preScale(1.0f, -1.0f); - - //init params from xml - if(attrs != null){ - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FeatureCoverFlow, defStyle, 0); - - mCoverWidth = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_coverWidth, mCoverWidth); - if(mCoverWidth % 2 == 1) mCoverWidth--; - mCoverHeight = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_coverHeight,mCoverHeight); - mSpacing = a.getFloat(R.styleable.FeatureCoverFlow_spacing, mSpacing); - mRotationThreshold = a.getFloat(R.styleable.FeatureCoverFlow_rotationThreshold, mRotationThreshold); - mScalingThreshold = a.getFloat(R.styleable.FeatureCoverFlow_scalingThreshold, mScalingThreshold); - mAdjustPositionThreshold = a.getFloat(R.styleable.FeatureCoverFlow_adjustPositionThreshold, mAdjustPositionThreshold); - mAdjustPositionMultiplier = a.getFloat(R.styleable.FeatureCoverFlow_adjustPositionMultiplier, mAdjustPositionMultiplier); - mMaxRotationAngle = a.getFloat(R.styleable.FeatureCoverFlow_maxRotationAngle, mMaxRotationAngle); - mMaxScaleFactor = a.getFloat(R.styleable.FeatureCoverFlow_maxScaleFactor, mMaxScaleFactor); - mRadius = a.getFloat(R.styleable.FeatureCoverFlow_circlePathRadius, mRadius); - mRadiusInMatrixSpace = a.getFloat(R.styleable.FeatureCoverFlow_circlePathRadiusInMatrixSpace, mRadiusInMatrixSpace); - mReflectionHeight = a.getFloat(R.styleable.FeatureCoverFlow_reflectionHeight, mReflectionHeight); - mReflectionGap = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_reflectionGap, mReflectionGap); - mReflectionOpacity = a.getInteger(R.styleable.FeatureCoverFlow_reflectionOpacity, mReflectionOpacity); - mTuningWidgetSize = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_tunningWidgetSize, mTuningWidgetSize); - mAlignTime = a.getInteger(R.styleable.FeatureCoverFlow_alignAnimationTime, mAlignTime); - mPaddingTop = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_verticalPaddingTop, mPaddingTop); - mPaddingBottom = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_verticalPaddingBottom, mPaddingBottom); - mReflectionBackgroundColor = a.getColor(R.styleable.FeatureCoverFlow_reflectionBackroundColor, Color.TRANSPARENT); - - a.recycle(); - } - } - - public FeatureCoverFlow(Context context, AttributeSet attrs) { - this(context, attrs,0); - } - - public FeatureCoverFlow(Context context) { - this(context,null); - } - - public FeatureCoverFlow(Context context, int cacheSize) { - this(context,null,0,cacheSize); - } - - public FeatureCoverFlow(Context context, AttributeSet attrs, int defStyle) { - this(context, attrs, defStyle, DEFAULT_MAX_CACHE_SIZE); - } - - - private class CoverFrame extends FrameLayout{ - private Bitmap mReflectionCache; - private boolean mReflectionCacheInvalid = true; - - - public CoverFrame(Context context, View cover) { - super(context); - setCover(cover); - } - - public void setCover(View cover){ - if(cover.getLayoutParams() != null) setLayoutParams(cover.getLayoutParams()); - - final FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - lp.leftMargin = 1; - lp.topMargin = 1; - lp.rightMargin = 1; - lp.bottomMargin = 1; - - if (cover.getParent()!=null && cover.getParent() instanceof ViewGroup) { - ViewGroup parent = (ViewGroup) cover.getParent(); - parent.removeView(cover); - } - - //register observer to catch cover redraws - cover.getViewTreeObserver().addOnPreDrawListener(FeatureCoverFlow.this); - - addView(cover,lp); - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - mReflectionCacheInvalid = true; - } - - - @Override - public Bitmap getDrawingCache(boolean autoScale) { - final Bitmap b = super.getDrawingCache(autoScale); - - if(mReflectionCacheInvalid){ - if((mTouchState != TOUCH_STATE_FLING && mTouchState != TOUCH_STATE_ALIGN) || mReflectionCache == null){ - try{ - mReflectionCache = createReflectionBitmap(b); - mReflectionCacheInvalid = false; - } - catch (NullPointerException e){ - Log.e(VIEW_LOG_TAG, "Null pointer in createReflectionBitmap. Bitmap b=" + b, e); - } - } - } - return b; - } - - public void recycle(){ - if(mReflectionCache != null){ - mReflectionCache.recycle(); - mReflectionCache = null; - } - - mReflectionCacheInvalid = true; - removeAllViewsInLayout(); - } - - } - - - private float getWidgetSizeMultiplier(){ - return ((float)mTuningWidgetSize)/((float)getWidth()); - } - - @SuppressLint("NewApi") - @Override - protected View addAndMeasureChildHorizontal(View child, int layoutMode) { - final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; - final LoopLayoutParams lp = new LoopLayoutParams(mCoverWidth, mCoverHeight); - - if(child!=null && child instanceof CoverFrame){ - addViewInLayout(child, index, lp, true); - measureChild(child); - return child; - } - - - CoverFrame frame = getRecycledCoverFrame(); - if(frame == null){ - frame = new CoverFrame(getContext(), child); - } - else{ - frame.setCover(child); - } - - //to enable drawing cache - if(android.os.Build.VERSION.SDK_INT >= 11) frame.setLayerType(LAYER_TYPE_SOFTWARE, null); - frame.setDrawingCacheEnabled(true); - - - addViewInLayout(frame, index, lp, true); - measureChild(frame); - return frame; - } - - @Override - protected int layoutChildHorizontal(View v, int left, LoopLayoutParams lp) { - int l,t,r,b; - - l = left; - r = l + v.getMeasuredWidth(); - final int x = ((getHeight() - mPaddingTop - mPaddingBottom) - v.getMeasuredHeight())/2 + mPaddingTop; // - (int)((lp.actualHeight*mReflectionHeight)/2) - t = x; - b = t + v.getMeasuredHeight(); - - v.layout(l, t, r, b); - return l + (int)(v.getMeasuredWidth() * mSpacing); - } - - /** - * Layout children from right to left - */ - protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){ - int left = right - v.getMeasuredWidth();; - left = layoutChildHorizontal(v, left, lp); - return left; - } - - private int getChildsCenter(View v){ - final int w = v.getRight() - v.getLeft(); - return v.getLeft() + w/2; - } - - private int getChildsCenter(int i){ - return getChildsCenter(getChildAt(i)); - } - - - @Override - protected int getChildDrawingOrder(int childCount, int i) { - final int screenCenter = getWidth()/2 + getScrollX(); - final int myCenter = getChildsCenter(i); - final int d = myCenter - screenCenter; - - final View v = getChildAt(i); - final int sz = (int) (mSpacing * v.getWidth()/2f); - - if(mReverseOrderIndex == -1 && (Math.abs(d) < sz || d >= 0)){ - mReverseOrderIndex = i; - mCenterItemOffset = d; - mLastCenterItemIndex = i; - return childCount-1; - } - - if(mReverseOrderIndex == -1){ - return i; - } - else{ - if(i == childCount-1) { - final int x = mReverseOrderIndex; - mReverseOrderIndex = -1; - return x; - } - return childCount - 1 - (i-mReverseOrderIndex); - } - } - - - @Override - protected void refillInternal(int lastItemPos, int firstItemPos) { - super.refillInternal(lastItemPos, firstItemPos); - - final int c = getChildCount(); - for(int i=0; i < c; i++){ - getChildDrawingOrder(c, i); //go through children to fill center item offset - } - - } - - @Override - protected void dispatchDraw(Canvas canvas) { - mInvalidated = false; //last invalidate which marked redrawInProgress, caused this dispatchDraw. Clear flag to prevent creating loop - - mReverseOrderIndex = -1; - - canvas.getClipBounds(mTempRect); - mTempRect.top = 0; - mTempRect.bottom = getHeight(); - canvas.clipRect(mTempRect); - - - super.dispatchDraw(canvas); - - if(mScrollToPositionOnNextInvalidate != -1 && mAdapter != null && mAdapter.getCount() > 0){ - final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); - final int di = lastCenterItemPosition - mScrollToPositionOnNextInvalidate; - mScrollToPositionOnNextInvalidate = -1; - if(di != 0){ - final int dst = (int) (di * mCoverWidth * mSpacing) - mCenterItemOffset; - scrollBy(-dst, 0); - shouldRepeat = true; - postInvalidate(); - return; - } - } - - //make sure we never stay unaligned after last draw in resting state - if(mTouchState == TOUCH_STATE_RESTING && mCenterItemOffset != 0){ - scrollBy(mCenterItemOffset, 0); - postInvalidate(); - } - - try { - View v = getChildAt(mLastCenterItemIndex); - if(v != null) v.requestFocus(FOCUS_FORWARD); - } - catch (Exception e) { - e.printStackTrace(); - } - } - - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - scroll((int) (-1 * mCoverWidth * mSpacing) - mCenterItemOffset); - return true; - case KeyEvent.KEYCODE_DPAD_RIGHT: - scroll((int) (mCoverWidth * mSpacing) - mCenterItemOffset); - return true; - default: - break; - } - return super.onKeyDown(keyCode, event); - } - - @Override - protected void fillFirstTime(final int lastItemPos,final int firstItemPos){ - final int leftScreenEdge = 0; - final int rightScreenEdge = leftScreenEdge + getWidth(); - - int right; - int left; - View child; - - boolean isRepeatingNow = false; - - //scrolling is enabled until we find out we don't have enough items - isSrollingDisabled = false; - - mLastItemPosition = lastItemPos; - mFirstItemPosition = firstItemPos; - mLeftChildEdge = (int) (-mCoverWidth * mSpacing); - right = 0; - left = mLeftChildEdge; - - while(right < rightScreenEdge){ - mLastItemPosition++; - - if(isRepeatingNow && mLastItemPosition >= firstItemPos) return; - - if(mLastItemPosition >= mAdapter.getCount()){ - if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0; - else{ - if(firstItemPos > 0){ - mLastItemPosition = 0; - isRepeatingNow = true; - } - else if(!shouldRepeat){ - mLastItemPosition--; - isSrollingDisabled = true; - final int w = right-mLeftChildEdge; - final int dx = (getWidth() - w)/2; - scrollTo(-dx, 0); - return; - } - - } - } - - if(mLastItemPosition >= mAdapter.getCount() ){ - Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()"); - return; - } - - child = mAdapter.getView(mLastItemPosition, getCachedView(), this); - Validate.notNull(child, "Your adapter has returned null from getView."); - child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); - left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams()); - right = child.getRight(); - - //if selected view is going to screen, set selected state on him - if(mLastItemPosition == mSelectedPosition){ - child.setSelected(true); - } - - } - - if(mScrollPositionIfEndless > 0){ - final int p = mScrollPositionIfEndless; - mScrollPositionIfEndless = -1; - removeAllViewsInLayout(); - refillOnChange(p); - } - } - - /** - * Checks and refills empty area on the right - */ - @Override - protected void refillRight(){ - if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - final int rightScreenEdge = leftScreenEdge + getWidth(); - - View child = getChildAt(getChildCount() - 1); - int currLayoutLeft = child.getLeft() + (int)(child.getWidth() * mSpacing); - while(currLayoutLeft < rightScreenEdge){ - mLastItemPosition++; - if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0; - - child = getViewAtPosition(mLastItemPosition); - child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); - currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams()); - - //if selected view is going to screen, set selected state on him - if(mLastItemPosition == mSelectedPosition){ - child.setSelected(true); - } - } - } - - - private boolean containsView(View v){ - for(int i=0; i < getChildCount(); i++){ - if(getChildAt(i) == v){ - return true; - } - } - return false; - } - - - - private View getViewAtPosition(int position){ - View v = mCachedFrames.remove(position); - if(v == null){ - v = mAdapter.getView(position, getCachedView(), this); - Validate.notNull(v,"Your adapter has returned null from getView."); - return v; - } - - if(!containsView(v)){ - return v; - } - else{ - v = mAdapter.getView(position, getCachedView(), this); - Validate.notNull(v,"Your adapter has returned null from getView."); - return v; - } - } - - /** - * Checks and refills empty area on the left - */ - @Override - protected void refillLeft(){ - if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - - View child = getChildAt(0); - int currLayoutRight = child.getRight() - (int)(child.getWidth() * mSpacing); - while(currLayoutRight > leftScreenEdge){ - mFirstItemPosition--; - if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; - - child = getViewAtPosition(mFirstItemPosition); - if(child == getChildAt(getChildCount() - 1)){ - removeViewInLayout(child); - } - child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); - currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); - - //update left edge of children in container - mLeftChildEdge = child.getLeft(); - - //if selected view is going to screen, set selected state on him - if(mFirstItemPosition == mSelectedPosition){ - child.setSelected(true); - } - } - } - - /** - * Removes view that are outside of the visible part of the list. Will not - * remove all views. - */ - protected void removeNonVisibleViews() { - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - final int rightScreenEdge = leftScreenEdge + getWidth(); - - // check if we should remove any views in the left - View firstChild = getChildAt(0); - final int leftedge = firstChild.getLeft(); - if(leftedge != mLeftChildEdge) { - Log.e("feature component", "firstChild.getLeft() != mLeftChildEdge, leftedge:" + leftedge + " ftChildEdge:"+ mLeftChildEdge); - View v = getChildAt(0); - removeAllViewsInLayout(); - addAndMeasureChildHorizontal(v,LAYOUT_MODE_TO_BEFORE); - layoutChildHorizontal(v, mLeftChildEdge, (LoopLayoutParams) v.getLayoutParams()); - return; - } - while (firstChild != null && firstChild.getRight() < leftScreenEdge) { - //if selected view is going off screen, remove selected state - firstChild.setSelected(false); - - // remove view - removeViewInLayout(firstChild); - - mCachedFrames.put(mFirstItemPosition, (CoverFrame) firstChild); - - mFirstItemPosition++; - if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; - - // update left item position - mLeftChildEdge = getChildAt(0).getLeft(); - - // Continue to check the next child only if we have more than - // one child left - if (getChildCount() > 1) { - firstChild = getChildAt(0); - } else { - firstChild = null; - } - } - - // check if we should remove any views in the right - View lastChild = getChildAt(getChildCount() - 1); - while (lastChild != null && lastChild.getLeft() > rightScreenEdge) { - //if selected view is going off screen, remove selected state - lastChild.setSelected(false); - - // remove the right view - removeViewInLayout(lastChild); - - mCachedFrames.put(mLastItemPosition, (CoverFrame) lastChild); - - mLastItemPosition--; - if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; - - // Continue to check the next child only if we have more than - // one child left - if (getChildCount() > 1) { - lastChild = getChildAt(getChildCount() - 1); - } else { - lastChild = null; - } - } - } - - - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - canvas.save(); - - //set matrix to child's transformation - setChildTransformation(child, mMatrix); - - //Generate child bitmap - Bitmap bitmap = child.getDrawingCache(); - - //initialize canvas state. Child 0,0 coordinates will match canvas 0,0 - canvas.translate(child.getLeft(), child.getTop()); - - - - //set child transformation on canvas - canvas.concat(mMatrix); - - final Bitmap rfCache = ((CoverFrame) child).mReflectionCache; - - if(mReflectionBackgroundColor != Color.TRANSPARENT){ - final int top = bitmap.getHeight() + mReflectionGap - 2; - final float frame = 1.0f; - mReflectionPaint.setColor(mReflectionBackgroundColor); - canvas.drawRect(frame, top + frame , rfCache.getWidth()-frame, top + rfCache.getHeight() - frame, mReflectionPaint); - } - - mPaint.reset(); - mPaint.setAntiAlias(true); - mPaint.setFilterBitmap(true); - - //Draw child bitmap with applied transforms - canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint); - - //Draw reflection - canvas.drawBitmap(rfCache, 0.0f, bitmap.getHeight() - 2 + mReflectionGap, mPaint); - - - canvas.restore(); - return false; - } - - private Bitmap createReflectionBitmap(Bitmap original){ - final int w = original.getWidth(); - final int h = original.getHeight(); - final int rh = (int) (h * mReflectionHeight); - final int gradientColor = Color.argb(mReflectionOpacity, 0xff, 0xff, 0xff); - - final Bitmap reflection = Bitmap.createBitmap(original, 0, rh, w, rh, mReflectionMatrix, false); - - final LinearGradient shader = new LinearGradient(0, 0, 0, reflection.getHeight(), gradientColor, 0x00ffffff,TileMode.CLAMP); - mPaint.reset(); - mPaint.setShader(shader); - mPaint.setXfermode(mXfermode); - - mReflectionCanvas.setBitmap(reflection); - mReflectionCanvas.drawRect(0, 0, reflection.getWidth(), reflection.getHeight(), mPaint); - - return reflection; - } - - /** - * Fill outRect with transformed child hit rectangle. Rectangle is not moved to its position on screen, neither getSroolX is accounted for - * @param child - * @param outRect - */ - protected void transformChildHitRectangle(View child, RectF outRect){ - outRect.left = 0; - outRect.top = 0; - outRect.right = child.getWidth(); - outRect.bottom = child.getHeight(); - - setChildTransformation(child, mTempHit); - mTempHit.mapRect(outRect); - } - - protected void transformChildHitRectangle(View child, RectF outRect, final Matrix transformation){ - outRect.left = 0; - outRect.top = 0; - outRect.right = child.getWidth(); - outRect.bottom = child.getHeight(); - - transformation.mapRect(outRect); - } - - private void setChildTransformation(View child, Matrix m){ - m.reset(); - - addChildRotation(child, m); - addChildScale(child, m); - addChildCircularPathZOffset(child, m); - addChildAdjustPosition(child,m); - - //set coordinate system origin to center of child - m.preTranslate(-child.getWidth()/2f, -child.getHeight()/2f); - //move back - m.postTranslate(child.getWidth()/2f, child.getHeight()/2f); - - } - - - private void addChildCircularPathZOffset(View child, Matrix m){ - mCamera.save(); - - final float v = getOffsetOnCircle(getChildsCenter(child)); - final float z = mRadiusInMatrixSpace * v; - - mCamera.translate(0.0f, 0.0f, z); - - mCamera.getMatrix(mTemp); - m.postConcat(mTemp); - - mCamera.restore(); - } - - - private void addChildScale(View v,Matrix m){ - final float f = getScaleFactor(getChildsCenter(v)); - m.postScale(f, f); - } - - private void addChildRotation(View v, Matrix m){ - mCamera.save(); - - final int c = getChildsCenter(v); - mCamera.rotateY(getRotationAngle(c) - getAngleOnCircle(c)); - - mCamera.getMatrix(mTemp); - m.postConcat(mTemp); - - mCamera.restore(); - } - - private void addChildAdjustPosition(View child, Matrix m) { - final int c = getChildsCenter(child); - final float crp = getClampedRelativePosition(getRelativePosition(c), mAdjustPositionThreshold * getWidgetSizeMultiplier()); - final float d = mCoverWidth * mAdjustPositionMultiplier * mSpacing * crp * getSpacingMultiplierOnCirlce(c); - - m.postTranslate(d, 0f); - } - - /** - * Calculates relative position on screen in range -1 to 1, widgets out of screen can have values ove 1 or -1 - * @param pixexPos Absolute position in pixels including scroll offset - * @return relative position - */ - private float getRelativePosition(int pixexPos){ - final int half = getWidth()/2; - final int centerPos = getScrollX() + half; - - return (pixexPos - centerPos)/((float) half); - } - - /** - * Clamps relative position by threshold, and produces values in range -1 to 1 directly usable for transformation computation - * @param position value int range -1 to 1 - * @param treshold always positive value of threshold distance from center in range 0-1 - * @return - */ - private float getClampedRelativePosition(float position, float threshold){ - if(position < 0){ - if(position < -threshold) return -1f; - else return position/threshold; - } - else{ - if(position > threshold) return 1; - else return position/threshold; - } - } - - private float getRotationAngle(int childCenter){ - return -mMaxRotationAngle * getClampedRelativePosition(getRelativePosition(childCenter), mRotationThreshold * getWidgetSizeMultiplier()); - } - - private float getScaleFactor(int childCenter){ - return 1 + (mMaxScaleFactor-1) * (1 - Math.abs(getClampedRelativePosition(getRelativePosition(childCenter), mScalingThreshold * getWidgetSizeMultiplier()))); - } - - - /** - * Compute offset following path on circle - * @param childCenter - * @return offset from position on unitary circle - */ - private float getOffsetOnCircle(int childCenter){ - float x = getRelativePosition(childCenter)/mRadius; - if(x < -1.0f) x = -1.0f; - if(x > 1.0f) x = 1.0f; - - return (float) (1 - Math.sin(Math.acos(x))); - } - - private float getAngleOnCircle(int childCenter){ - float x = getRelativePosition(childCenter)/mRadius; - if(x < -1.0f) x = -1.0f; - if(x > 1.0f) x = 1.0f; - - return (float) (Math.acos(x)/Math.PI*180.0f - 90.0f); - } - - private float getSpacingMultiplierOnCirlce(int childCenter){ - float x = getRelativePosition(childCenter)/mRadius; - return (float) Math.sin(Math.acos(x)); - } - - - - @Override - protected void handleClick(Point p) { - final int c = getChildCount(); - View v; - final RectF r = new RectF(); - final int[] childOrder = new int[c]; - - - for(int i=0; i < c; i++){ - childOrder[i] = getChildDrawingOrder(c, i); - } - - for(int i = c-1; i >= 0; i--){ - v = getChildAt(childOrder[i]); //we need reverse drawing order. Check children drawn last first - getScrolledTransformedChildRectangle(v, r); - if(r.contains(p.x,p.y)){ - final View old = getSelectedView(); - if(old != null) old.setSelected(false); - - - int position = mFirstItemPosition + childOrder[i]; - if(position >= mAdapter.getCount()) position = position - mAdapter.getCount(); - - - mSelectedPosition = position; - v.setSelected(true); - - if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position)); - if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position)); - - - break; - } - } - } - - - - @Override - public void computeScroll() { - // if we don't have an adapter, we don't need to do anything - if (mAdapter == null) { - return; - } - if(mAdapter.getCount() == 0){ - return; - } - - if(getChildCount() == 0){ //release memory resources was probably called before, and onLayout didn't get called to fill container again - requestLayout(); - } - - if (mTouchState == TOUCH_STATE_ALIGN) { - if(mAlignScroller.computeScrollOffset()) { - if(mAlignScroller.getFinalX() == mAlignScroller.getCurrX()){ - mAlignScroller.abortAnimation(); - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - return; - } - - int x = mAlignScroller.getCurrX(); - scrollTo(x, 0); - - postInvalidate(); - return; - } - else{ - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - return; - } - } - - super.computeScroll(); - } - - @Override - protected boolean checkScrollPosition() { - if(mCenterItemOffset != 0){ - mAlignScroller.startScroll(getScrollX(), 0, mCenterItemOffset, 0, mAlignTime); - mTouchState = TOUCH_STATE_ALIGN; - invalidate(); - return true; - } - return false; - } - - private void getScrolledTransformedChildRectangle(View child, RectF r){ - transformChildHitRectangle(child, r); - final int offset = child.getLeft() - getScrollX(); - r.offset(offset, child.getTop()); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - final int action = ev.getAction(); - final float xf = ev.getX(); - final float yf = ev.getY(); - final RectF frame = mTouchRect; - - if (action == MotionEvent.ACTION_DOWN) { - if (mMotionTarget != null) { - // this is weird, we got a pen down, but we thought it was - // already down! - // We should probably send an ACTION_UP to the current - // target. - mMotionTarget = null; - } - // If we're disallowing intercept or if we're allowing and we didn't - // intercept - if (!onInterceptTouchEvent(ev)) { - // reset this event's action (just to protect ourselves) - ev.setAction(MotionEvent.ACTION_DOWN); - // We know we want to dispatch the event down, find a child - // who can handle it, start with the front-most child. - - final int count = getChildCount(); - final int[] childOrder = new int[count]; - - for(int i=0; i < count; i++){ - childOrder[i] = getChildDrawingOrder(count, i); - } - - for(int i = count-1; i >= 0; i--) { - final View child = getChildAt(childOrder[i]); - if (child.getVisibility() == VISIBLE - || child.getAnimation() != null) { - - getScrolledTransformedChildRectangle(child, frame); - - if (frame.contains(xf, yf)) { - // offset the event to the view's coordinate system - final float xc = xf - frame.left; - final float yc = yf - frame.top; - ev.setLocation(xc, yc); - if (child.dispatchTouchEvent(ev)) { - // Event handled, we have a target now. - mMotionTarget = child; - mTargetTop = frame.top; - mTargetLeft = frame.left; - return true; - } - - break; - } - } - } - } - } - - boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || - (action == MotionEvent.ACTION_CANCEL); - - - // The event wasn't an ACTION_DOWN, dispatch it to our target if - // we have one. - final View target = mMotionTarget; - if (target == null) { - // We don't have a target, this means we're handling the - // event as a regular view. - ev.setLocation(xf, yf); - return onTouchEvent(ev); - } - - // if have a target, see if we're allowed to and want to intercept its - // events - if (onInterceptTouchEvent(ev)) { - final float xc = xf - mTargetLeft; - final float yc = yf - mTargetTop; - ev.setAction(MotionEvent.ACTION_CANCEL); - ev.setLocation(xc, yc); - if (!target.dispatchTouchEvent(ev)) { - // target didn't handle ACTION_CANCEL. not much we can do - // but they should have. - } - // clear the target - mMotionTarget = null; - // Don't dispatch this event to our own view, because we already - // saw it when intercepting; we just want to give the following - // event to the normal onTouchEvent(). - return true; - } - - if (isUpOrCancel) { - mMotionTarget = null; - mTargetTop = -1; - mTargetLeft = -1; - } - - // finally offset the event to the target's coordinate system and - // dispatch the event. - final float xc = xf - mTargetLeft; - final float yc = yf - mTargetTop; - ev.setLocation(xc, yc); - - return target.dispatchTouchEvent(ev); - } - - - @SuppressWarnings("deprecation") - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - final int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - - int h,w; - if(heightSpecMode == MeasureSpec.EXACTLY) h = heightSpecSize; - else{ - h = (int) ((mCoverHeight + mCoverHeight*mReflectionHeight + mReflectionGap) * mMaxScaleFactor + mPaddingTop + mPaddingBottom); - h = resolveSize(h, heightMeasureSpec); - } - - if(widthSpecMode == MeasureSpec.EXACTLY) w = widthSpecSize; - else{ - WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - w = display.getWidth(); - w = resolveSize(w, widthMeasureSpec); - } - - setMeasuredDimension(w, h); - } - - - //disable turning caches of and on, we need them always on - @Override - protected void enableChildrenCache() {} - - @Override - protected void clearChildrenCache() {} - - /** - * How many items can remain in cache. Lower in case of memory issues - * @param size number of cached covers - */ - public void trimChacheSize(int size){ - mCachedFrames.trimToSize(size); - } - - /** - * Clear internal cover cache - */ - public void clearCache(){ - mCachedFrames.evictAll(); - } - - /** - * Returns widget spacing (as fraction of widget size) - * @return Widgets spacing - */ - public float getSpacing() { - return mSpacing; - } - - /** - * Set widget spacing (float means fraction of widget size, 1 = widget size) - * @param spacing the spacing to set - */ - public void setSpacing(float spacing) { - this.mSpacing = spacing; - } - - /** - * Return width of cover in pixels - * @return the Cover Width - */ - public int getCoverWidth() { - return mCoverWidth; - } - - /** - * Set width of cover in pixels - * @param coverWidth the Cover Width to set - */ - public void setCoverWidth(int coverWidth) { - if(coverWidth % 2 == 1) coverWidth--; - this.mCoverWidth = coverWidth; - } - - /** - * Return cover height in pixels - * @return the Cover Height - */ - public int getCoverHeight() { - return mCoverHeight; - } - - /** - * Set cover height in pixels - * @param coverHeight the Cover Height to set - */ - public void setCoverHeight(int coverHeight) { - this.mCoverHeight = coverHeight; - } - - /** - * Sets distance from center as fraction of half of widget size where covers start to rotate into center - * 1 means rotation starts on edge of widget, 0 means only center rotated - * @param rotationThreshold the rotation threshold to set - */ - public void setRotationTreshold(float rotationThreshold) { - this.mRotationThreshold = rotationThreshold; - } - - /** - * Sets distance from center as fraction of half of widget size where covers start to zoom in - * 1 means scaling starts on edge of widget, 0 means only center scaled - * @param scalingThreshold the scaling threshold to set - */ - public void setScalingThreshold(float scalingThreshold) { - this.mScalingThreshold = scalingThreshold; - } - - /** - * Sets distance from center as fraction of half of widget size, - * where covers start enlarge their spacing to allow for smooth passing each other without jumping over each other - * 1 means edge of widget, 0 means only center - * @param adjustPositionThreshold the adjust position threshold to set - */ - public void setAdjustPositionThreshold(float adjustPositionThreshold) { - this.mAdjustPositionThreshold = adjustPositionThreshold; - } - - /** - * Sets adjust position multiplier. By enlarging this value, you can enlarge spacing in center of widget done by position adjustment - * @param adjustPositionMultiplier the adjust position multiplier to set - */ - public void setAdjustPositionMultiplier(float adjustPositionMultiplier) { - this.mAdjustPositionMultiplier = adjustPositionMultiplier; - } - - /** - * Sets absolute value of rotation angle of cover at edge of widget in degrees. - * Rotation made by traveling around circle path is added to this value separately. - * By enlarging this value you make covers more rotated. Max value without traveling on circle would be 90 degrees. - * With small circle radius could go even over this value sometimes. Look depends also on other parameters. - * @param maxRotationAngle the max rotation angle to set - */ - public void setMaxRotationAngle(float maxRotationAngle) { - this.mMaxRotationAngle = maxRotationAngle; - } - - /** - * Sets scale factor of item in center. Normal size is multiplied with this value - * @param maxScaleFactor the max scale factor to set - */ - public void setMaxScaleFactor(float maxScaleFactor) { - this.mMaxScaleFactor = maxScaleFactor; - } - - /** - * Sets radius of circle path which covers follow. Range of screen is -1 to 1, minimal radius is therefore 1 - * This value affect how big part of circle path you see on screen and therefore how much away are covers at edge of screen. - * And also how much they are rotated in direction of circle path. - * @param radius the radius to set - */ - public void setRadius(float radius) { - this.mRadius = radius; - } - - /** - * This value affects how far are covers at the edges of widget in Z coordinate in matrix space - * @param radiusInMatrixSpace the radius in matrix space to set - */ - public void setRadiusInMatrixSpace(float radiusInMatrixSpace) { - this.mRadiusInMatrixSpace = radiusInMatrixSpace; - } - - /** - * Reflection height as a fraction of cover height (1 means same size as original) - * @param reflectionHeight the reflection height to set - */ - public void setReflectionHeight(float reflectionHeight) { - this.mReflectionHeight = reflectionHeight; - } - - /** - * @param reflectionGap Gap between original image and reflection in pixels - */ - public void setReflectionGap(int reflectionGap) { - this.mReflectionGap = reflectionGap; - } - - /** - * @param reflectionOpacity Opacity at most opaque part of reflection fade out effect - */ - public void setReflectionOpacity(int reflectionOpacity) { - this.mReflectionOpacity = reflectionOpacity; - } - - /** - * Widget size on which was tuning of parameters done. This value is used to scale parameters when widgets has different size - * @param size returned by widgets getWidth() - */ - public void setTuningWidgetSize(int size) { - this.mTuningWidgetSize = size; - } - - /** - * @param alignTime How long takes center alignment animation in milliseconds - */ - public void setAlignTime(int alignTime) { - this.mAlignTime = alignTime; - } - - /** - * @param paddingTop - */ - public void setVerticalPaddingTop(int paddingTop) { - this.mPaddingTop = paddingTop; - } - - public void setVerticalPaddingBottom(int paddingBottom) { - this.mPaddingBottom = paddingBottom; - } - - - /** - * Set this to some color if you don't want see through reflections other reflections. Preferably set to same color as background color - * @param reflectionBackgroundColor the Reflection Background Color to set - */ - public void setReflectionBackgroundColor(int reflectionBackgroundColor) { - this.mReflectionBackgroundColor = reflectionBackgroundColor; - } - - @Override - /** - * Get position of center item in adapter. - * @return position of center item inside adapter date or -1 if there is no center item shown - */ - public int getScrollPosition() { - if(mAdapter == null || mAdapter.getCount() == 0) return -1; - - if(mLastCenterItemIndex != -1){ - return (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); - } - else return (mFirstItemPosition + (getWidth()/((int)(mCoverWidth * mSpacing)))/2) % mAdapter.getCount(); - } - - /** - * Set new center item position - */ - @Override - public void scrollToPosition(int position) { - if(mAdapter == null || mAdapter.getCount() == 0) throw new IllegalStateException("You are trying to scroll container with no adapter set. Set adapter first."); - - if(mLastCenterItemIndex != -1){ - final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); - final int di = lastCenterItemPosition - position; - final int dst = (int) (di * mCoverWidth * mSpacing); - mScrollToPositionOnNextInvalidate = -1; - scrollBy(-dst, 0); - } - else{ - mScrollToPositionOnNextInvalidate = position; - } - - invalidate(); - } - - /** - * removes children, must be after caching children - * @param cf - */ - private void recycleCoverFrame(CoverFrame cf){ - cf.recycle(); - WeakReference ref = new WeakReference(cf); - mRecycledCoverFrames.addLast(ref); - } - - protected CoverFrame getRecycledCoverFrame(){ - if (!mRecycledCoverFrames.isEmpty()) { - CoverFrame v; - do{ - v = mRecycledCoverFrames.removeFirst().get(); - } - while(v == null && !mRecycledCoverFrames.isEmpty()); - return v; - } - return null; - } - - /** - * Removes links to all pictures which are hold by coverflow to speed up rendering - * Sets environment to state from which it can be refilled on next onLayout - * Good place to release resources is in activitys onStop. - */ - public void releaseAllMemoryResources(){ - mLastItemPosition = mFirstItemPosition; - mLastItemPosition--; - - final int w = (int)(mCoverWidth*mSpacing); - int sp = getScrollX() % w; - if(sp < 0) sp = sp + w; - scrollTo(sp, 0); - - removeAllViewsInLayout(); - clearCache(); - } - - @Override - public boolean onPreDraw() { //when child view is about to be drawn we invalidate whole container - - if(!mInvalidated){ //this is hack, no idea now is possible that this works, but fixes problem where not all area was redrawn - mInvalidated = true; - invalidate(); - return false; - } - - return true; - - } - - - -} +/** + * + */ +package it.moondroid.coverflow.components.ui.containers; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Camera; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader.TileMode; +import android.support.v4.util.LruCache; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Display; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.Scroller; + +import java.lang.ref.WeakReference; +import java.util.LinkedList; + +import it.moondroid.coverflow.R; +import it.moondroid.coverflow.components.general.Validate; + + +/** + * @author Martin Appl + * Note: Supports wrap content for height + * + */ +public class FeatureCoverFlow extends EndlessLoopAdapterContainer implements ViewTreeObserver.OnPreDrawListener { + public static final int DEFAULT_MAX_CACHE_SIZE = 32; + + /** + * Graphics Camera used for generating transformation matrices; + */ + private final Camera mCamera = new Camera(); + /** + * Relative spacing value of Views in container. If <1 Views will overlap, if >1 Views will have spaces between them + */ + private float mSpacing = 0.5f; + + /** + * Index of view in center of screen, which is most in foreground + */ + private int mReverseOrderIndex = -1; + + private int mLastCenterItemIndex = -1; + + /** + * Distance from center as fraction of half of widget size where covers start to rotate into center + * 1 means rotation starts on edge of widget, 0 means only center rotated + */ + private float mRotationThreshold = 0.3f; + + /** + * Distance from center as fraction of half of widget size where covers start to zoom in + * 1 means scaling starts on edge of widget, 0 means only center scaled + */ + private float mScalingThreshold = 0.3f; + + /** + * Distance from center as fraction of half of widget size, + * where covers start enlarge their spacing to allow for smooth passing each other without jumping over each other + * 1 means edge of widget, 0 means only center + */ + private float mAdjustPositionThreshold = 0.1f; + + /** + * By enlarging this value, you can enlarge spacing in center of widget done by position adjustment + */ + private float mAdjustPositionMultiplier = 1.0f; + + /** + * Absolute value of rotation angle of cover at edge of widget in degrees + */ + private float mMaxRotationAngle = 70.0f; + + /** + * Scale factor of item in center + */ + private float mMaxScaleFactor = 1.2f; + + /** + * Radius of circle path which covers follow. Range of screen is -1 to 1, minimal radius is therefore 1 + */ + private float mRadius = 2f; + + /** + * Radius of circle path which covers follow in coordinate space of matrix transformation. Used to scale offset + */ + private float mRadiusInMatrixSpace = 1000f; + + /** + * Size of reflection as a fraction of original image (0-1) + */ + private float mReflectionHeight = 0.5f; + + /** + * Gap between reflection and original image in pixels + */ + private int mReflectionGap = 2; + + /** + * Starting opacity of reflection. Reflection fades from this value to transparency; + */ + private int mReflectionOpacity = 0x70; + + /** + * Widget size on which was tuning of parameters done. This value is used to scale parameters on when widgets has different size + */ + private int mTuningWidgetSize = 1280; + + /** + * How long will alignment animation take + */ + private int mAlignTime = 350; + + /** + * If you don't want reflections to be transparent, you can set them background of same color as widget background + */ + private int mReflectionBackgroundColor = Color.TRANSPARENT; + + /** A list of cached (re-usable) cover frames */ + protected final LinkedList> mRecycledCoverFrames = new LinkedList>(); + + /** A listener for center item position */ + private OnScrollPositionListener mOnScrollPositionListener; + + private int mLastTouchState = -1; + private int mlastCenterItemPosition = -1; + + public interface OnScrollPositionListener { + public void onScrolledToPosition(int position); + public void onScrolling(); + } + + private int mPaddingTop = 0; + private int mPaddingBottom = 0; + + private int mCenterItemOffset; + private final Scroller mAlignScroller = new Scroller(getContext(), new DecelerateInterpolator()); + + private final MyCache mCachedFrames; + + private int mCoverWidth = 160; + private int mCoverHeight = 240; + + private final Matrix mMatrix = new Matrix(); + private final Matrix mTemp = new Matrix(); + private final Matrix mTempHit = new Matrix(); + private final Rect mTempRect = new Rect(); + private final RectF mTouchRect = new RectF(); + + private View mMotionTarget; + private float mTargetLeft; + private float mTargetTop; + + //reflection + private final Matrix mReflectionMatrix = new Matrix(); + private final Paint mPaint = new Paint(); + private final Paint mReflectionPaint = new Paint(); + private final PorterDuffXfermode mXfermode = new PorterDuffXfermode(Mode.DST_IN); + private final Canvas mReflectionCanvas = new Canvas(); + + private int mScrollToPositionOnNextInvalidate = -1; + + + private boolean mInvalidated = false; + + + private class MyCache extends LruCache{ + + public MyCache(int maxSize) { + super(maxSize); + } + + @Override + protected void entryRemoved(boolean evicted, Integer key, CoverFrame oldValue, CoverFrame newValue) { + if(evicted){ + if(oldValue.getChildCount() == 1){ + mCachedItemViews.addLast(new WeakReference(oldValue.getChildAt(0))); + recycleCoverFrame(oldValue); // removes children, must be after caching children + } + } + } + + } + + public FeatureCoverFlow(Context context, AttributeSet attrs, int defStyle, int cacheSize) { + super(context, attrs, defStyle); + + if(cacheSize <= 0) cacheSize = DEFAULT_MAX_CACHE_SIZE; + mCachedFrames = new MyCache(cacheSize); + + setChildrenDrawingOrderEnabled(true); + setChildrenDrawingCacheEnabled(true); + setChildrenDrawnWithCacheEnabled(true); + + mReflectionMatrix.preScale(1.0f, -1.0f); + + //init params from xml + if(attrs != null){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FeatureCoverFlow, defStyle, 0); + + mCoverWidth = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_coverWidth, mCoverWidth); + if(mCoverWidth % 2 == 1) mCoverWidth--; + mCoverHeight = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_coverHeight,mCoverHeight); + mSpacing = a.getFloat(R.styleable.FeatureCoverFlow_spacing, mSpacing); + mRotationThreshold = a.getFloat(R.styleable.FeatureCoverFlow_rotationThreshold, mRotationThreshold); + mScalingThreshold = a.getFloat(R.styleable.FeatureCoverFlow_scalingThreshold, mScalingThreshold); + mAdjustPositionThreshold = a.getFloat(R.styleable.FeatureCoverFlow_adjustPositionThreshold, mAdjustPositionThreshold); + mAdjustPositionMultiplier = a.getFloat(R.styleable.FeatureCoverFlow_adjustPositionMultiplier, mAdjustPositionMultiplier); + mMaxRotationAngle = a.getFloat(R.styleable.FeatureCoverFlow_maxRotationAngle, mMaxRotationAngle); + mMaxScaleFactor = a.getFloat(R.styleable.FeatureCoverFlow_maxScaleFactor, mMaxScaleFactor); + mRadius = a.getFloat(R.styleable.FeatureCoverFlow_circlePathRadius, mRadius); + mRadiusInMatrixSpace = a.getFloat(R.styleable.FeatureCoverFlow_circlePathRadiusInMatrixSpace, mRadiusInMatrixSpace); + mReflectionHeight = a.getFloat(R.styleable.FeatureCoverFlow_reflectionHeight, mReflectionHeight); + mReflectionGap = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_reflectionGap, mReflectionGap); + mReflectionOpacity = a.getInteger(R.styleable.FeatureCoverFlow_reflectionOpacity, mReflectionOpacity); + mTuningWidgetSize = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_tunningWidgetSize, mTuningWidgetSize); + mAlignTime = a.getInteger(R.styleable.FeatureCoverFlow_alignAnimationTime, mAlignTime); + mPaddingTop = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_verticalPaddingTop, mPaddingTop); + mPaddingBottom = a.getDimensionPixelSize(R.styleable.FeatureCoverFlow_verticalPaddingBottom, mPaddingBottom); + mReflectionBackgroundColor = a.getColor(R.styleable.FeatureCoverFlow_reflectionBackroundColor, Color.TRANSPARENT); + + a.recycle(); + } + } + + public FeatureCoverFlow(Context context, AttributeSet attrs) { + this(context, attrs,0); + } + + public FeatureCoverFlow(Context context) { + this(context,null); + } + + public FeatureCoverFlow(Context context, int cacheSize) { + this(context,null,0,cacheSize); + } + + public FeatureCoverFlow(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, DEFAULT_MAX_CACHE_SIZE); + } + + + private class CoverFrame extends FrameLayout{ + private Bitmap mReflectionCache; + private boolean mReflectionCacheInvalid = true; + + + public CoverFrame(Context context, View cover) { + super(context); + setCover(cover); + } + + public void setCover(View cover){ + if(cover.getLayoutParams() != null) setLayoutParams(cover.getLayoutParams()); + + final LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + lp.leftMargin = 1; + lp.topMargin = 1; + lp.rightMargin = 1; + lp.bottomMargin = 1; + + if (cover.getParent()!=null && cover.getParent() instanceof ViewGroup) { + ViewGroup parent = (ViewGroup) cover.getParent(); + parent.removeView(cover); + } + + //register observer to catch cover redraws + cover.getViewTreeObserver().addOnPreDrawListener(FeatureCoverFlow.this); + + addView(cover,lp); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + mReflectionCacheInvalid = true; + } + + + @Override + public Bitmap getDrawingCache(boolean autoScale) { + final Bitmap b = super.getDrawingCache(autoScale); + + if(mReflectionCacheInvalid){ + if((mTouchState != TOUCH_STATE_FLING && mTouchState != TOUCH_STATE_ALIGN) || mReflectionCache == null){ + try{ + mReflectionCache = createReflectionBitmap(b); + mReflectionCacheInvalid = false; + } + catch (NullPointerException e){ + Log.e(VIEW_LOG_TAG, "Null pointer in createReflectionBitmap. Bitmap b=" + b, e); + } + } + } + return b; + } + + public void recycle(){ + if(mReflectionCache != null){ + mReflectionCache.recycle(); + mReflectionCache = null; + } + + mReflectionCacheInvalid = true; + removeAllViewsInLayout(); + } + + } + + + private float getWidgetSizeMultiplier(){ + return ((float)mTuningWidgetSize)/((float)getWidth()); + } + + @SuppressLint("NewApi") + @Override + protected View addAndMeasureChildHorizontal(View child, int layoutMode) { + final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; + final LoopLayoutParams lp = new LoopLayoutParams(mCoverWidth, mCoverHeight); + + if(child!=null && child instanceof CoverFrame){ + addViewInLayout(child, index, lp, true); + measureChild(child); + return child; + } + + + CoverFrame frame = getRecycledCoverFrame(); + if(frame == null){ + frame = new CoverFrame(getContext(), child); + } + else{ + frame.setCover(child); + } + + //to enable drawing cache + if(android.os.Build.VERSION.SDK_INT >= 11) frame.setLayerType(LAYER_TYPE_SOFTWARE, null); + frame.setDrawingCacheEnabled(true); + + + addViewInLayout(frame, index, lp, true); + measureChild(frame); + return frame; + } + + @Override + protected int layoutChildHorizontal(View v, int left, LoopLayoutParams lp) { + int l,t,r,b; + + l = left; + r = l + v.getMeasuredWidth(); + final int x = ((getHeight() - mPaddingTop - mPaddingBottom) - v.getMeasuredHeight())/2 + mPaddingTop; // - (int)((lp.actualHeight*mReflectionHeight)/2) + t = x; + b = t + v.getMeasuredHeight(); + + v.layout(l, t, r, b); + return l + (int)(v.getMeasuredWidth() * mSpacing); + } + + /** + * Layout children from right to left + */ + protected int layoutChildHorizontalToBefore(View v,int right , LoopLayoutParams lp){ + int left = right - v.getMeasuredWidth();; + left = layoutChildHorizontal(v, left, lp); + return left; + } + + private int getChildsCenter(View v){ + final int w = v.getRight() - v.getLeft(); + return v.getLeft() + w/2; + } + + private int getChildsCenter(int i){ + return getChildsCenter(getChildAt(i)); + } + + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int screenCenter = getWidth()/2 + getScrollX(); + final int myCenter = getChildsCenter(i); + final int d = myCenter - screenCenter; + + final View v = getChildAt(i); + final int sz = (int) (mSpacing * v.getWidth()/2f); + + if(mReverseOrderIndex == -1 && (Math.abs(d) < sz || d >= 0)){ + mReverseOrderIndex = i; + mCenterItemOffset = d; + mLastCenterItemIndex = i; + return childCount-1; + } + + if(mReverseOrderIndex == -1){ + return i; + } + else{ + if(i == childCount-1) { + final int x = mReverseOrderIndex; + mReverseOrderIndex = -1; + return x; + } + return childCount - 1 - (i-mReverseOrderIndex); + } + } + + + @Override + protected void refillInternal(int lastItemPos, int firstItemPos) { + super.refillInternal(lastItemPos, firstItemPos); + + final int c = getChildCount(); + for(int i=0; i < c; i++){ + getChildDrawingOrder(c, i); //go through children to fill center item offset + } + + } + + @Override + protected void dispatchDraw(Canvas canvas) { + mInvalidated = false; //last invalidate which marked redrawInProgress, caused this dispatchDraw. Clear flag to prevent creating loop + + mReverseOrderIndex = -1; + + canvas.getClipBounds(mTempRect); + mTempRect.top = 0; + mTempRect.bottom = getHeight(); + canvas.clipRect(mTempRect); + + + super.dispatchDraw(canvas); + + if(mScrollToPositionOnNextInvalidate != -1 && mAdapter != null && mAdapter.getCount() > 0){ + final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); + final int di = lastCenterItemPosition - mScrollToPositionOnNextInvalidate; + mScrollToPositionOnNextInvalidate = -1; + if(di != 0){ + final int dst = (int) (di * mCoverWidth * mSpacing) - mCenterItemOffset; + scrollBy(-dst, 0); + shouldRepeat = true; + postInvalidate(); + return; + } + } + + if(mTouchState == TOUCH_STATE_RESTING){ + + final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); + if (mLastTouchState != TOUCH_STATE_RESTING || mlastCenterItemPosition != lastCenterItemPosition){ + mLastTouchState = TOUCH_STATE_RESTING; + mlastCenterItemPosition = lastCenterItemPosition; + if(mOnScrollPositionListener != null) mOnScrollPositionListener.onScrolledToPosition(lastCenterItemPosition); + } + } + + if (mTouchState == TOUCH_STATE_SCROLLING && mLastTouchState != TOUCH_STATE_SCROLLING){ + mLastTouchState = TOUCH_STATE_SCROLLING; + if(mOnScrollPositionListener != null) mOnScrollPositionListener.onScrolling(); + } + if (mTouchState == TOUCH_STATE_FLING && mLastTouchState != TOUCH_STATE_FLING){ + mLastTouchState = TOUCH_STATE_FLING; + if(mOnScrollPositionListener != null) mOnScrollPositionListener.onScrolling(); + } + + + //make sure we never stay unaligned after last draw in resting state + if(mTouchState == TOUCH_STATE_RESTING && mCenterItemOffset != 0){ + scrollBy(mCenterItemOffset, 0); + postInvalidate(); + } + + try { + View v = getChildAt(mLastCenterItemIndex); + if(v != null) v.requestFocus(FOCUS_FORWARD); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + scroll((int) (-1 * mCoverWidth * mSpacing) - mCenterItemOffset); + return true; + case KeyEvent.KEYCODE_DPAD_RIGHT: + scroll((int) (mCoverWidth * mSpacing) - mCenterItemOffset); + return true; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void fillFirstTime(final int lastItemPos,final int firstItemPos){ + final int leftScreenEdge = 0; + final int rightScreenEdge = leftScreenEdge + getWidth(); + + int right; + int left; + View child; + + boolean isRepeatingNow = false; + + //scrolling is enabled until we find out we don't have enough items + isSrollingDisabled = false; + + mLastItemPosition = lastItemPos; + mFirstItemPosition = firstItemPos; + mLeftChildEdge = (int) (-mCoverWidth * mSpacing); + right = 0; + left = mLeftChildEdge; + + while(right < rightScreenEdge){ + mLastItemPosition++; + + if(isRepeatingNow && mLastItemPosition >= firstItemPos) return; + + if(mLastItemPosition >= mAdapter.getCount()){ + if(firstItemPos == 0 && shouldRepeat) mLastItemPosition = 0; + else{ + if(firstItemPos > 0){ + mLastItemPosition = 0; + isRepeatingNow = true; + } + else if(!shouldRepeat){ + mLastItemPosition--; + isSrollingDisabled = true; + final int w = right-mLeftChildEdge; + final int dx = (getWidth() - w)/2; + scrollTo(-dx, 0); + return; + } + + } + } + + if(mLastItemPosition >= mAdapter.getCount() ){ + Log.wtf("EndlessLoop", "mLastItemPosition > mAdapter.getCount()"); + return; + } + + child = mAdapter.getView(mLastItemPosition, getCachedView(), this); + Validate.notNull(child, "Your adapter has returned null from getView."); + child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); + left = layoutChildHorizontal(child, left, (LoopLayoutParams) child.getLayoutParams()); + right = child.getRight(); + + //if selected view is going to screen, set selected state on him + if(mLastItemPosition == mSelectedPosition){ + child.setSelected(true); + } + + } + + if(mScrollPositionIfEndless > 0){ + final int p = mScrollPositionIfEndless; + mScrollPositionIfEndless = -1; + removeAllViewsInLayout(); + refillOnChange(p); + } + } + + /** + * Checks and refills empty area on the right + */ + @Override + protected void refillRight(){ + if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + final int rightScreenEdge = leftScreenEdge + getWidth(); + + View child = getChildAt(getChildCount() - 1); + int currLayoutLeft = child.getLeft() + (int)(child.getWidth() * mSpacing); + while(currLayoutLeft < rightScreenEdge){ + mLastItemPosition++; + if(mLastItemPosition >= mAdapter.getCount()) mLastItemPosition = 0; + + child = getViewAtPosition(mLastItemPosition); + child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_AFTER); + currLayoutLeft = layoutChildHorizontal(child, currLayoutLeft, (LoopLayoutParams) child.getLayoutParams()); + + //if selected view is going to screen, set selected state on him + if(mLastItemPosition == mSelectedPosition){ + child.setSelected(true); + } + } + } + + + private boolean containsView(View v){ + for(int i=0; i < getChildCount(); i++){ + if(getChildAt(i) == v){ + return true; + } + } + return false; + } + + + + private View getViewAtPosition(int position){ + View v = mCachedFrames.remove(position); + if(v == null){ + v = mAdapter.getView(position, getCachedView(), this); + Validate.notNull(v,"Your adapter has returned null from getView."); + return v; + } + + if(!containsView(v)){ + return v; + } + else{ + v = mAdapter.getView(position, getCachedView(), this); + Validate.notNull(v,"Your adapter has returned null from getView."); + return v; + } + } + + /** + * Checks and refills empty area on the left + */ + @Override + protected void refillLeft(){ + if(!shouldRepeat && isSrollingDisabled) return; //prevent next layout calls to override override first init to scrolling disabled by falling to this branch + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + + View child = getChildAt(0); + int currLayoutRight = child.getRight() - (int)(child.getWidth() * mSpacing); + while(currLayoutRight > leftScreenEdge){ + mFirstItemPosition--; + if(mFirstItemPosition < 0) mFirstItemPosition = mAdapter.getCount()-1; + + child = getViewAtPosition(mFirstItemPosition); + if(child == getChildAt(getChildCount() - 1)){ + removeViewInLayout(child); + } + child = addAndMeasureChildHorizontal(child, LAYOUT_MODE_TO_BEFORE); + currLayoutRight = layoutChildHorizontalToBefore(child, currLayoutRight, (LoopLayoutParams) child.getLayoutParams()); + + //update left edge of children in container + mLeftChildEdge = child.getLeft(); + + //if selected view is going to screen, set selected state on him + if(mFirstItemPosition == mSelectedPosition){ + child.setSelected(true); + } + } + } + + /** + * Removes view that are outside of the visible part of the list. Will not + * remove all views. + */ + protected void removeNonVisibleViews() { + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + final int rightScreenEdge = leftScreenEdge + getWidth(); + + // check if we should remove any views in the left + View firstChild = getChildAt(0); + final int leftedge = firstChild.getLeft(); + if(leftedge != mLeftChildEdge) { + Log.e("feature component", "firstChild.getLeft() != mLeftChildEdge, leftedge:" + leftedge + " ftChildEdge:"+ mLeftChildEdge); + View v = getChildAt(0); + removeAllViewsInLayout(); + addAndMeasureChildHorizontal(v,LAYOUT_MODE_TO_BEFORE); + layoutChildHorizontal(v, mLeftChildEdge, (LoopLayoutParams) v.getLayoutParams()); + return; + } + while (firstChild != null && firstChild.getRight() < leftScreenEdge) { + //if selected view is going off screen, remove selected state + firstChild.setSelected(false); + + // remove view + removeViewInLayout(firstChild); + + mCachedFrames.put(mFirstItemPosition, (CoverFrame) firstChild); + + mFirstItemPosition++; + if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; + + // update left item position + mLeftChildEdge = getChildAt(0).getLeft(); + + // Continue to check the next child only if we have more than + // one child left + if (getChildCount() > 1) { + firstChild = getChildAt(0); + } else { + firstChild = null; + } + } + + // check if we should remove any views in the right + View lastChild = getChildAt(getChildCount() - 1); + while (lastChild != null && lastChild.getLeft() > rightScreenEdge) { + //if selected view is going off screen, remove selected state + lastChild.setSelected(false); + + // remove the right view + removeViewInLayout(lastChild); + + mCachedFrames.put(mLastItemPosition, (CoverFrame) lastChild); + + mLastItemPosition--; + if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; + + // Continue to check the next child only if we have more than + // one child left + if (getChildCount() > 1) { + lastChild = getChildAt(getChildCount() - 1); + } else { + lastChild = null; + } + } + } + + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + canvas.save(); + + //set matrix to child's transformation + setChildTransformation(child, mMatrix); + + //Generate child bitmap + Bitmap bitmap = child.getDrawingCache(); + + //initialize canvas state. Child 0,0 coordinates will match canvas 0,0 + canvas.translate(child.getLeft(), child.getTop()); + + + + //set child transformation on canvas + canvas.concat(mMatrix); + + final Bitmap rfCache = ((CoverFrame) child).mReflectionCache; + + if(mReflectionBackgroundColor != Color.TRANSPARENT){ + final int top = bitmap.getHeight() + mReflectionGap - 2; + final float frame = 1.0f; + mReflectionPaint.setColor(mReflectionBackgroundColor); + canvas.drawRect(frame, top + frame , rfCache.getWidth()-frame, top + rfCache.getHeight() - frame, mReflectionPaint); + } + + mPaint.reset(); + mPaint.setAntiAlias(true); + mPaint.setFilterBitmap(true); + + //Draw child bitmap with applied transforms + canvas.drawBitmap(bitmap, 0.0f, 0.0f, mPaint); + + //Draw reflection + canvas.drawBitmap(rfCache, 0.0f, bitmap.getHeight() - 2 + mReflectionGap, mPaint); + + + canvas.restore(); + return false; + } + + private Bitmap createReflectionBitmap(Bitmap original){ + final int w = original.getWidth(); + final int h = original.getHeight(); + final int rh = (int) (h * mReflectionHeight); + final int gradientColor = Color.argb(mReflectionOpacity, 0xff, 0xff, 0xff); + + final Bitmap reflection = Bitmap.createBitmap(original, 0, rh, w, rh, mReflectionMatrix, false); + + final LinearGradient shader = new LinearGradient(0, 0, 0, reflection.getHeight(), gradientColor, 0x00ffffff,TileMode.CLAMP); + mPaint.reset(); + mPaint.setShader(shader); + mPaint.setXfermode(mXfermode); + + mReflectionCanvas.setBitmap(reflection); + mReflectionCanvas.drawRect(0, 0, reflection.getWidth(), reflection.getHeight(), mPaint); + + return reflection; + } + + /** + * Fill outRect with transformed child hit rectangle. Rectangle is not moved to its position on screen, neither getSroolX is accounted for + * @param child + * @param outRect + */ + protected void transformChildHitRectangle(View child, RectF outRect){ + outRect.left = 0; + outRect.top = 0; + outRect.right = child.getWidth(); + outRect.bottom = child.getHeight(); + + setChildTransformation(child, mTempHit); + mTempHit.mapRect(outRect); + } + + protected void transformChildHitRectangle(View child, RectF outRect, final Matrix transformation){ + outRect.left = 0; + outRect.top = 0; + outRect.right = child.getWidth(); + outRect.bottom = child.getHeight(); + + transformation.mapRect(outRect); + } + + private void setChildTransformation(View child, Matrix m){ + m.reset(); + + addChildRotation(child, m); + addChildScale(child, m); + addChildCircularPathZOffset(child, m); + addChildAdjustPosition(child,m); + + //set coordinate system origin to center of child + m.preTranslate(-child.getWidth()/2f, -child.getHeight()/2f); + //move back + m.postTranslate(child.getWidth()/2f, child.getHeight()/2f); + + } + + + private void addChildCircularPathZOffset(View child, Matrix m){ + mCamera.save(); + + final float v = getOffsetOnCircle(getChildsCenter(child)); + final float z = mRadiusInMatrixSpace * v; + + mCamera.translate(0.0f, 0.0f, z); + + mCamera.getMatrix(mTemp); + m.postConcat(mTemp); + + mCamera.restore(); + } + + + private void addChildScale(View v,Matrix m){ + final float f = getScaleFactor(getChildsCenter(v)); + m.postScale(f, f); + } + + private void addChildRotation(View v, Matrix m){ + mCamera.save(); + + final int c = getChildsCenter(v); + mCamera.rotateY(getRotationAngle(c) - getAngleOnCircle(c)); + + mCamera.getMatrix(mTemp); + m.postConcat(mTemp); + + mCamera.restore(); + } + + private void addChildAdjustPosition(View child, Matrix m) { + final int c = getChildsCenter(child); + final float crp = getClampedRelativePosition(getRelativePosition(c), mAdjustPositionThreshold * getWidgetSizeMultiplier()); + final float d = mCoverWidth * mAdjustPositionMultiplier * mSpacing * crp * getSpacingMultiplierOnCirlce(c); + + m.postTranslate(d, 0f); + } + + /** + * Calculates relative position on screen in range -1 to 1, widgets out of screen can have values ove 1 or -1 + * @param pixexPos Absolute position in pixels including scroll offset + * @return relative position + */ + private float getRelativePosition(int pixexPos){ + final int half = getWidth()/2; + final int centerPos = getScrollX() + half; + + return (pixexPos - centerPos)/((float) half); + } + + /** + * Clamps relative position by threshold, and produces values in range -1 to 1 directly usable for transformation computation + * @param position value int range -1 to 1 + * @param threshold always positive value of threshold distance from center in range 0-1 + * @return + */ + private float getClampedRelativePosition(float position, float threshold){ + if(position < 0){ + if(position < -threshold) return -1f; + else return position/threshold; + } + else{ + if(position > threshold) return 1; + else return position/threshold; + } + } + + private float getRotationAngle(int childCenter){ + return -mMaxRotationAngle * getClampedRelativePosition(getRelativePosition(childCenter), mRotationThreshold * getWidgetSizeMultiplier()); + } + + private float getScaleFactor(int childCenter){ + return 1 + (mMaxScaleFactor-1) * (1 - Math.abs(getClampedRelativePosition(getRelativePosition(childCenter), mScalingThreshold * getWidgetSizeMultiplier()))); + } + + + /** + * Compute offset following path on circle + * @param childCenter + * @return offset from position on unitary circle + */ + private float getOffsetOnCircle(int childCenter){ + float x = getRelativePosition(childCenter)/mRadius; + if(x < -1.0f) x = -1.0f; + if(x > 1.0f) x = 1.0f; + + return (float) (1 - Math.sin(Math.acos(x))); + } + + private float getAngleOnCircle(int childCenter){ + float x = getRelativePosition(childCenter)/mRadius; + if(x < -1.0f) x = -1.0f; + if(x > 1.0f) x = 1.0f; + + return (float) (Math.acos(x)/Math.PI*180.0f - 90.0f); + } + + private float getSpacingMultiplierOnCirlce(int childCenter){ + float x = getRelativePosition(childCenter)/mRadius; + return (float) Math.sin(Math.acos(x)); + } + + + + @Override + protected void handleClick(Point p) { + final int c = getChildCount(); + View v; + final RectF r = new RectF(); + final int[] childOrder = new int[c]; + + + for(int i=0; i < c; i++){ + childOrder[i] = getChildDrawingOrder(c, i); + } + + for(int i = c-1; i >= 0; i--){ + v = getChildAt(childOrder[i]); //we need reverse drawing order. Check children drawn last first + getScrolledTransformedChildRectangle(v, r); + if(r.contains(p.x,p.y)){ + final View old = getSelectedView(); + if(old != null) old.setSelected(false); + + + int position = mFirstItemPosition + childOrder[i]; + if(position >= mAdapter.getCount()) position = position - mAdapter.getCount(); + + + mSelectedPosition = position; + v.setSelected(true); + + if(mOnItemClickListener != null) mOnItemClickListener.onItemClick(this, v, position , getItemIdAtPosition(position)); + if(mOnItemSelectedListener != null) mOnItemSelectedListener.onItemSelected(this, v, position, getItemIdAtPosition(position)); + + + break; + } + } + } + + + + @Override + public void computeScroll() { + // if we don't have an adapter, we don't need to do anything + if (mAdapter == null) { + return; + } + if(mAdapter.getCount() == 0){ + return; + } + + if(getChildCount() == 0){ //release memory resources was probably called before, and onLayout didn't get called to fill container again + requestLayout(); + } + + if (mTouchState == TOUCH_STATE_ALIGN) { + if(mAlignScroller.computeScrollOffset()) { + if(mAlignScroller.getFinalX() == mAlignScroller.getCurrX()){ + mAlignScroller.abortAnimation(); + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + return; + } + + int x = mAlignScroller.getCurrX(); + scrollTo(x, 0); + + postInvalidate(); + return; + } + else{ + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + return; + } + } + + super.computeScroll(); + } + + @Override + protected boolean checkScrollPosition() { + if(mCenterItemOffset != 0){ + mAlignScroller.startScroll(getScrollX(), 0, mCenterItemOffset, 0, mAlignTime); + mTouchState = TOUCH_STATE_ALIGN; + invalidate(); + return true; + } + return false; + } + + private void getScrolledTransformedChildRectangle(View child, RectF r){ + transformChildHitRectangle(child, r); + final int offset = child.getLeft() - getScrollX(); + r.offset(offset, child.getTop()); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + final float xf = ev.getX(); + final float yf = ev.getY(); + final RectF frame = mTouchRect; + + if (action == MotionEvent.ACTION_DOWN) { + if (mMotionTarget != null) { + // this is weird, we got a pen down, but we thought it was + // already down! + // We should probably send an ACTION_UP to the current + // target. + mMotionTarget = null; + } + // If we're disallowing intercept or if we're allowing and we didn't + // intercept + if (!onInterceptTouchEvent(ev)) { + // reset this event's action (just to protect ourselves) + ev.setAction(MotionEvent.ACTION_DOWN); + // We know we want to dispatch the event down, find a child + // who can handle it, start with the front-most child. + + final int count = getChildCount(); + final int[] childOrder = new int[count]; + + for(int i=0; i < count; i++){ + childOrder[i] = getChildDrawingOrder(count, i); + } + + for(int i = count-1; i >= 0; i--) { + final View child = getChildAt(childOrder[i]); + if (child.getVisibility() == VISIBLE + || child.getAnimation() != null) { + + getScrolledTransformedChildRectangle(child, frame); + + if (frame.contains(xf, yf)) { + // offset the event to the view's coordinate system + final float xc = xf - frame.left; + final float yc = yf - frame.top; + ev.setLocation(xc, yc); + if (child.dispatchTouchEvent(ev)) { + // Event handled, we have a target now. + mMotionTarget = child; + mTargetTop = frame.top; + mTargetLeft = frame.left; + return true; + } + + break; + } + } + } + } + } + + boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || + (action == MotionEvent.ACTION_CANCEL); + + + // The event wasn't an ACTION_DOWN, dispatch it to our target if + // we have one. + final View target = mMotionTarget; + if (target == null) { + // We don't have a target, this means we're handling the + // event as a regular view. + ev.setLocation(xf, yf); + return onTouchEvent(ev); + } + + // if have a target, see if we're allowed to and want to intercept its + // events + if (onInterceptTouchEvent(ev)) { + final float xc = xf - mTargetLeft; + final float yc = yf - mTargetTop; + ev.setAction(MotionEvent.ACTION_CANCEL); + ev.setLocation(xc, yc); + if (!target.dispatchTouchEvent(ev)) { + // target didn't handle ACTION_CANCEL. not much we can do + // but they should have. + } + // clear the target + mMotionTarget = null; + // Don't dispatch this event to our own view, because we already + // saw it when intercepting; we just want to give the following + // event to the normal onTouchEvent(). + return true; + } + + if (isUpOrCancel) { + mMotionTarget = null; + mTargetTop = -1; + mTargetLeft = -1; + } + + // finally offset the event to the target's coordinate system and + // dispatch the event. + final float xc = xf - mTargetLeft; + final float yc = yf - mTargetTop; + ev.setLocation(xc, yc); + + return target.dispatchTouchEvent(ev); + } + + + @SuppressWarnings("deprecation") + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + + int h,w; + if(heightSpecMode == MeasureSpec.EXACTLY) h = heightSpecSize; + else{ + h = (int) ((mCoverHeight + mCoverHeight*mReflectionHeight + mReflectionGap) * mMaxScaleFactor + mPaddingTop + mPaddingBottom); + h = resolveSize(h, heightMeasureSpec); + } + + if(widthSpecMode == MeasureSpec.EXACTLY) w = widthSpecSize; + else{ + WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + w = display.getWidth(); + w = resolveSize(w, widthMeasureSpec); + } + + setMeasuredDimension(w, h); + } + + + //disable turning caches of and on, we need them always on + @Override + protected void enableChildrenCache() {} + + @Override + protected void clearChildrenCache() {} + + /** + * How many items can remain in cache. Lower in case of memory issues + * @param size number of cached covers + */ + public void trimChacheSize(int size){ + mCachedFrames.trimToSize(size); + } + + /** + * Clear internal cover cache + */ + public void clearCache(){ + mCachedFrames.evictAll(); + } + + /** + * Returns widget spacing (as fraction of widget size) + * @return Widgets spacing + */ + public float getSpacing() { + return mSpacing; + } + + /** + * Set widget spacing (float means fraction of widget size, 1 = widget size) + * @param spacing the spacing to set + */ + public void setSpacing(float spacing) { + this.mSpacing = spacing; + } + + /** + * Return width of cover in pixels + * @return the Cover Width + */ + public int getCoverWidth() { + return mCoverWidth; + } + + /** + * Set width of cover in pixels + * @param coverWidth the Cover Width to set + */ + public void setCoverWidth(int coverWidth) { + if(coverWidth % 2 == 1) coverWidth--; + this.mCoverWidth = coverWidth; + } + + /** + * Return cover height in pixels + * @return the Cover Height + */ + public int getCoverHeight() { + return mCoverHeight; + } + + /** + * Set cover height in pixels + * @param coverHeight the Cover Height to set + */ + public void setCoverHeight(int coverHeight) { + this.mCoverHeight = coverHeight; + } + + /** + * Sets distance from center as fraction of half of widget size where covers start to rotate into center + * 1 means rotation starts on edge of widget, 0 means only center rotated + * @param rotationThreshold the rotation threshold to set + */ + public void setRotationTreshold(float rotationThreshold) { + this.mRotationThreshold = rotationThreshold; + } + + /** + * Sets distance from center as fraction of half of widget size where covers start to zoom in + * 1 means scaling starts on edge of widget, 0 means only center scaled + * @param scalingThreshold the scaling threshold to set + */ + public void setScalingThreshold(float scalingThreshold) { + this.mScalingThreshold = scalingThreshold; + } + + /** + * Sets distance from center as fraction of half of widget size, + * where covers start enlarge their spacing to allow for smooth passing each other without jumping over each other + * 1 means edge of widget, 0 means only center + * @param adjustPositionThreshold the adjust position threshold to set + */ + public void setAdjustPositionThreshold(float adjustPositionThreshold) { + this.mAdjustPositionThreshold = adjustPositionThreshold; + } + + /** + * Sets adjust position multiplier. By enlarging this value, you can enlarge spacing in center of widget done by position adjustment + * @param adjustPositionMultiplier the adjust position multiplier to set + */ + public void setAdjustPositionMultiplier(float adjustPositionMultiplier) { + this.mAdjustPositionMultiplier = adjustPositionMultiplier; + } + + /** + * Sets absolute value of rotation angle of cover at edge of widget in degrees. + * Rotation made by traveling around circle path is added to this value separately. + * By enlarging this value you make covers more rotated. Max value without traveling on circle would be 90 degrees. + * With small circle radius could go even over this value sometimes. Look depends also on other parameters. + * @param maxRotationAngle the max rotation angle to set + */ + public void setMaxRotationAngle(float maxRotationAngle) { + this.mMaxRotationAngle = maxRotationAngle; + } + + /** + * Sets scale factor of item in center. Normal size is multiplied with this value + * @param maxScaleFactor the max scale factor to set + */ + public void setMaxScaleFactor(float maxScaleFactor) { + this.mMaxScaleFactor = maxScaleFactor; + } + + /** + * Sets radius of circle path which covers follow. Range of screen is -1 to 1, minimal radius is therefore 1 + * This value affect how big part of circle path you see on screen and therefore how much away are covers at edge of screen. + * And also how much they are rotated in direction of circle path. + * @param radius the radius to set + */ + public void setRadius(float radius) { + this.mRadius = radius; + } + + /** + * This value affects how far are covers at the edges of widget in Z coordinate in matrix space + * @param radiusInMatrixSpace the radius in matrix space to set + */ + public void setRadiusInMatrixSpace(float radiusInMatrixSpace) { + this.mRadiusInMatrixSpace = radiusInMatrixSpace; + } + + /** + * Reflection height as a fraction of cover height (1 means same size as original) + * @param reflectionHeight the reflection height to set + */ + public void setReflectionHeight(float reflectionHeight) { + this.mReflectionHeight = reflectionHeight; + } + + /** + * @param reflectionGap Gap between original image and reflection in pixels + */ + public void setReflectionGap(int reflectionGap) { + this.mReflectionGap = reflectionGap; + } + + /** + * @param reflectionOpacity Opacity at most opaque part of reflection fade out effect + */ + public void setReflectionOpacity(int reflectionOpacity) { + this.mReflectionOpacity = reflectionOpacity; + } + + /** + * Widget size on which was tuning of parameters done. This value is used to scale parameters when widgets has different size + * @param size returned by widgets getWidth() + */ + public void setTuningWidgetSize(int size) { + this.mTuningWidgetSize = size; + } + + /** + * @param alignTime How long takes center alignment animation in milliseconds + */ + public void setAlignTime(int alignTime) { + this.mAlignTime = alignTime; + } + + /** + * @param paddingTop + */ + public void setVerticalPaddingTop(int paddingTop) { + this.mPaddingTop = paddingTop; + } + + public void setVerticalPaddingBottom(int paddingBottom) { + this.mPaddingBottom = paddingBottom; + } + + + /** + * Set this to some color if you don't want see through reflections other reflections. Preferably set to same color as background color + * @param reflectionBackgroundColor the Reflection Background Color to set + */ + public void setReflectionBackgroundColor(int reflectionBackgroundColor) { + this.mReflectionBackgroundColor = reflectionBackgroundColor; + } + + @Override + /** + * Get position of center item in adapter. + * @return position of center item inside adapter date or -1 if there is no center item shown + */ + public int getScrollPosition() { + if(mAdapter == null || mAdapter.getCount() == 0) return -1; + + if(mLastCenterItemIndex != -1){ + return (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); + } + else return (mFirstItemPosition + (getWidth()/((int)(mCoverWidth * mSpacing)))/2) % mAdapter.getCount(); + } + + /** + * Set new center item position + */ + @Override + public void scrollToPosition(int position) { + if(mAdapter == null || mAdapter.getCount() == 0) throw new IllegalStateException("You are trying to scroll container with no adapter set. Set adapter first."); + + if(mLastCenterItemIndex != -1){ + final int lastCenterItemPosition = (mFirstItemPosition + mLastCenterItemIndex) % mAdapter.getCount(); + final int di = lastCenterItemPosition - position; + final int dst = (int) (di * mCoverWidth * mSpacing); + mScrollToPositionOnNextInvalidate = -1; + scrollBy(-dst, 0); + } + else{ + mScrollToPositionOnNextInvalidate = position; + } + + invalidate(); + } + + /** + * sets listener for center item position + * @param onScrollPositionListener + */ + public void setOnScrollPositionListener(OnScrollPositionListener onScrollPositionListener){ + mOnScrollPositionListener = onScrollPositionListener; + } + + /** + * removes children, must be after caching children + * @param cf + */ + private void recycleCoverFrame(CoverFrame cf){ + cf.recycle(); + WeakReference ref = new WeakReference(cf); + mRecycledCoverFrames.addLast(ref); + } + + protected CoverFrame getRecycledCoverFrame(){ + if (!mRecycledCoverFrames.isEmpty()) { + CoverFrame v; + do{ + v = mRecycledCoverFrames.removeFirst().get(); + } + while(v == null && !mRecycledCoverFrames.isEmpty()); + return v; + } + return null; + } + + /** + * Removes links to all pictures which are hold by coverflow to speed up rendering + * Sets environment to state from which it can be refilled on next onLayout + * Good place to release resources is in activitys onStop. + */ + public void releaseAllMemoryResources(){ + mLastItemPosition = mFirstItemPosition; + mLastItemPosition--; + + final int w = (int)(mCoverWidth*mSpacing); + int sp = getScrollX() % w; + if(sp < 0) sp = sp + w; + scrollTo(sp, 0); + + removeAllViewsInLayout(); + clearCache(); + } + + @Override + public boolean onPreDraw() { //when child view is about to be drawn we invalidate whole container + + if(!mInvalidated){ //this is hack, no idea now is possible that this works, but fixes problem where not all area was redrawn + mInvalidated = true; + invalidate(); + return false; + } + + return true; + + } + + + +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/HorizontalList.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/HorizontalList.java similarity index 95% rename from MAComponents/src/com/martinappl/components/ui/containers/HorizontalList.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/HorizontalList.java index acecac0..19466f8 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/HorizontalList.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/HorizontalList.java @@ -1,664 +1,665 @@ -package com.martinappl.components.ui.containers; - - -import android.content.Context; -import android.database.DataSetObserver; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.Adapter; -import android.widget.Scroller; - -import com.martinappl.components.general.ToolBox; -import com.martinappl.components.ui.containers.interfaces.IViewObserver; - -public class HorizontalList extends ViewGroup { - protected final int NO_VALUE = -11; - - /** User is not touching the list */ - protected static final int TOUCH_STATE_RESTING = 0; - - /** User is scrolling the list */ - protected static final int TOUCH_STATE_SCROLLING = 1; - - /** Fling gesture in progress */ - protected static final int TOUCH_STATE_FLING = 2; - - /** Children added with this layout mode will be added after the last child */ - protected static final int LAYOUT_MODE_AFTER = 0; - - /** Children added with this layout mode will be added before the first child */ - protected static final int LAYOUT_MODE_TO_BEFORE = 1; - - protected int mFirstItemPosition; - protected int mLastItemPosition; - protected boolean isScrollingDisabled = false; - - protected Adapter mAdapter; - protected final ToolBox.ViewCache mCache = new ToolBox.ViewCache(); - private final Scroller mScroller = new Scroller(getContext()); - protected int mTouchSlop; - private int mMinimumVelocity; - private int mMaximumVelocity; - - private int mTouchState = TOUCH_STATE_RESTING; - private float mLastMotionX; - private final Point mDown = new Point(); - private VelocityTracker mVelocityTracker; - private boolean mHandleSelectionOnActionUp = false; - - protected int mRightEdge = NO_VALUE; - private int mDefaultItemWidth = 200; - - protected IViewObserver mViewObserver; - - //listeners - private OnItemClickListener mItemClickListener; - - private final DataSetObserver mDataObserver = new DataSetObserver() { - - @Override - public void onChanged() { - reset(); - invalidate(); - } - - @Override - public void onInvalidated() { - removeAllViews(); - invalidate(); - } - - }; - - /** - * Remove all data, reset to initial state and attempt to refill - * Position of first item on screen in Adapter data set is maintained - */ - private void reset() { - int scroll = getScrollX(); - - int left = 0; - if(getChildCount() != 0){ - left = getChildAt(0).getLeft() - ((MarginLayoutParams)getChildAt(0).getLayoutParams()).leftMargin; - } - - removeAllViewsInLayout(); - mLastItemPosition = mFirstItemPosition; - mRightEdge = NO_VALUE; - scrollTo(left, 0); - - final int leftScreenEdge = getScrollX(); - int rightScreenEdge = leftScreenEdge + getWidth(); - - refillLeftToRight(leftScreenEdge, rightScreenEdge); - refillRightToLeft(leftScreenEdge); - - scrollTo(scroll, 0); - } - - public HorizontalList(Context context) { - this(context, null); - } - - public HorizontalList(Context context, AttributeSet attrs) { - this(context, attrs,0); - } - - public HorizontalList(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final ViewConfiguration configuration = ViewConfiguration.get(context); - mTouchSlop = configuration.getScaledTouchSlop(); - mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); - mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); - } - - public interface OnItemClickListener{ - void onItemClick(View v); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - refill(); - } - - /** - * Checks and refills empty area on the left - * @return firstItemPosition - */ - protected void refillRightToLeft(final int leftScreenEdge){ - if(getChildCount() == 0) return; - - View child = getChildAt(0); - int childLeft = child.getLeft(); - int lastLeft = childLeft - ((MarginLayoutParams)child.getLayoutParams()).leftMargin; - - while(lastLeft > leftScreenEdge && mFirstItemPosition > 0){ - mFirstItemPosition--; - - child = mAdapter.getView(mFirstItemPosition, mCache.getCachedView(), this); - sanitizeLayoutParams(child); - - addAndMeasureChild(child, LAYOUT_MODE_TO_BEFORE); - final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - lastLeft = layoutChildToBefore(child, lastLeft, lp); - childLeft = child.getLeft() - ((MarginLayoutParams)child.getLayoutParams()).leftMargin; - - } - return; - } - - /** - * Checks and refills empty area on the right - */ - protected void refillLeftToRight(final int leftScreenEdge, final int rightScreenEdge){ - - View child; - int lastRight; - if(getChildCount() != 0){ - child = getChildAt(getChildCount() - 1); - lastRight = child.getRight() + ((MarginLayoutParams)child.getLayoutParams()).rightMargin; - } - else{ - lastRight = leftScreenEdge; - if(mLastItemPosition == mFirstItemPosition) mLastItemPosition--; - } - - while(lastRight < rightScreenEdge && mLastItemPosition < mAdapter.getCount()-1){ - mLastItemPosition++; - - child = mAdapter.getView(mLastItemPosition, mCache.getCachedView(), this); - sanitizeLayoutParams(child); - - addAndMeasureChild(child, LAYOUT_MODE_AFTER); - final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); - lastRight = layoutChild(child, lastRight, lp); - - if(mLastItemPosition >= mAdapter.getCount()-1) { - mRightEdge = lastRight; - } - } - } - - - /** - * Remove non visible views from left edge of screen - */ - protected void removeNonVisibleViewsLeftToRight(final int leftScreenEdge){ - if(getChildCount() == 0) return; - - // check if we should remove any views in the left - View firstChild = getChildAt(0); - - while (firstChild != null && firstChild.getRight() + ((MarginLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) { - - // remove view - removeViewsInLayout(0, 1); - - if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition); - mCache.cacheView(firstChild); - - mFirstItemPosition++; - if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; - - // Continue to check the next child only if we have more than - // one child left - if (getChildCount() > 1) { - firstChild = getChildAt(0); - } else { - firstChild = null; - } - } - - } - - /** - * Remove non visible views from right edge of screen - */ - protected void removeNonVisibleViewsRightToLeft(final int rightScreenEdge){ - if(getChildCount() == 0) return; - - // check if we should remove any views in the right - View lastChild = getChildAt(getChildCount() - 1); - while (lastChild != null && lastChild.getLeft() - ((MarginLayoutParams)lastChild.getLayoutParams()).leftMargin > rightScreenEdge) { - // remove the right view - removeViewsInLayout(getChildCount() - 1, 1); - - if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition); - mCache.cacheView(lastChild); - - mLastItemPosition--; - if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; - - // Continue to check the next child only if we have more than - // one child left - if (getChildCount() > 1) { - lastChild = getChildAt(getChildCount() - 1); - } else { - lastChild = null; - } - } - - } - - protected void refill(){ - if(mAdapter == null) return; - - final int leftScreenEdge = getScrollX(); - int rightScreenEdge = leftScreenEdge + getWidth(); - - removeNonVisibleViewsLeftToRight(leftScreenEdge); - removeNonVisibleViewsRightToLeft(rightScreenEdge); - - refillLeftToRight(leftScreenEdge, rightScreenEdge); - refillRightToLeft(leftScreenEdge); - } - - - - protected void sanitizeLayoutParams(View child){ - MarginLayoutParams lp; - if(child.getLayoutParams() instanceof MarginLayoutParams) lp = (MarginLayoutParams) child.getLayoutParams(); - else if(child.getLayoutParams() != null) lp = new MarginLayoutParams(child.getLayoutParams()); - else lp = new MarginLayoutParams(mDefaultItemWidth,getHeight()); - - if(lp.height == LayoutParams.MATCH_PARENT) lp.height = getHeight(); - if(lp.width == LayoutParams.MATCH_PARENT) lp.width = getWidth(); - - if(lp.height == LayoutParams.WRAP_CONTENT){ - measureUnspecified(child); - lp.height = child.getMeasuredHeight(); - } - if(lp.width == LayoutParams.WRAP_CONTENT){ - measureUnspecified(child); - lp.width = child.getMeasuredWidth(); - } - child.setLayoutParams(lp); - } - - private void measureUnspecified(View child){ - final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED); - final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED); - measureChild(child, pwms, phms); - } - - /** - * Adds a view as a child view and takes care of measuring it - * - * @param child The view to add - * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT - * @return child which was actually added to container, subclasses can override to introduce frame views - */ - protected View addAndMeasureChild(final View child, final int layoutMode) { - if(child.getLayoutParams() == null) child.setLayoutParams(new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); - - final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; - addViewInLayout(child, index, child.getLayoutParams(), true); - - final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY); - final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY); - measureChild(child, pwms, phms); - child.setDrawingCacheEnabled(false); - - return child; - } - - /** - * Layout children from right to left - */ - protected int layoutChildToBefore(View v, int right , MarginLayoutParams lp){ - final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; - layoutChild(v, left, lp); - return left; - } - - /** - * @param topline Y coordinate of topline - * @param left X coordinate where should we start layout - */ - protected int layoutChild(View v, int left, MarginLayoutParams lp){ - int l,t,r,b; - l = left + lp.leftMargin; - t = lp.topMargin; - r = l + v.getMeasuredWidth(); - b = t + v.getMeasuredHeight(); - - v.layout(l, t, r, b); - return r + lp.rightMargin; - } - - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - - /* - * This method JUST determines whether we want to intercept the motion. - * If we return true, onTouchEvent will be called and we do the actual - * scrolling there. - */ - - - /* - * Shortcut the most recurring case: the user is in the dragging - * state and he is moving his finger. We want to intercept this - * motion. - */ - final int action = ev.getAction(); - if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { - return true; - } - - final float x = ev.getX(); - final float y = ev.getY(); - switch (action) { - case MotionEvent.ACTION_MOVE: - /* - * not dragging, otherwise the shortcut would have caught it. Check - * whether the user has moved far enough from his original down touch. - */ - - /* - * Locally do absolute value. mLastMotionX is set to the x value - * of the down event. - */ - final int xDiff = (int) Math.abs(x - mLastMotionX); - - final int touchSlop = mTouchSlop; - final boolean xMoved = xDiff > touchSlop; - - if (xMoved) { - // Scroll if the user moved far enough along the axis - mTouchState = TOUCH_STATE_SCROLLING; - mHandleSelectionOnActionUp = false; - enableChildrenCache(); - cancelLongPress(); - } - - break; - - case MotionEvent.ACTION_DOWN: - // Remember location of down touch - mLastMotionX = x; - - mDown.x = (int) x; - mDown.y = (int) y; - - /* - * If being flinged and user touches the screen, initiate drag; - * otherwise don't. mScroller.isFinished should be false when - * being flinged. - */ - mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; - //if he had normal click in rested state, remember for action up check - if(mTouchState == TOUCH_STATE_RESTING){ - mHandleSelectionOnActionUp = true; - } - break; - - case MotionEvent.ACTION_CANCEL: - mDown.x = -1; - mDown.y = -1; - break; - case MotionEvent.ACTION_UP: - //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates - if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ - final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); - if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); - } - // Release the drag - mHandleSelectionOnActionUp = false; - mDown.x = -1; - mDown.y = -1; - - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - break; - } - - return mTouchState == TOUCH_STATE_SCROLLING; - - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(event); - - final int action = event.getAction(); - final float x = event.getX(); - final float y = event.getY(); - - switch (action) { - case MotionEvent.ACTION_DOWN: - /* - * If being flinged and user touches, stop the fling. isFinished - * will be false if being flinged. - */ - if (!mScroller.isFinished()) { - mScroller.forceFinished(true); - } - - // Remember where the motion event started - mLastMotionX = x; - - break; - case MotionEvent.ACTION_MOVE: - - if (mTouchState == TOUCH_STATE_SCROLLING) { - // Scroll to follow the motion event - final int deltaX = (int) (mLastMotionX - x); - mLastMotionX = x; - - scrollByDelta(deltaX); - } - else{ - final int xDiff = (int) Math.abs(x - mLastMotionX); - - final int touchSlop = mTouchSlop; - final boolean xMoved = xDiff > touchSlop; - - - if (xMoved) { - // Scroll if the user moved far enough along the axis - mTouchState = TOUCH_STATE_SCROLLING; - enableChildrenCache(); - cancelLongPress(); - } - } - break; - case MotionEvent.ACTION_UP: - - //this must be here, in case no child view returns true, - //events will propagate back here and on intercept touch event wont be called again - //in case of no parent it propagates here, in case of parent it usually propagates to on cancel - if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ - final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); - if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); - mHandleSelectionOnActionUp = false; - } - - //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates - if (mTouchState == TOUCH_STATE_SCROLLING) { - - mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int initialXVelocity = (int) mVelocityTracker.getXVelocity(); - int initialYVelocity = (int) mVelocityTracker.getYVelocity(); - - if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { - fling(-initialXVelocity, -initialYVelocity); - } - else{ - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_RESTING; - - mDown.x = -1; - mDown.y = -1; - } - - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - - break; - } - - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_RESTING; - - mDown.x = -1; - mDown.y = -1; - - break; - case MotionEvent.ACTION_CANCEL: - mTouchState = TOUCH_STATE_RESTING; - } - - return true; - } - - @Override - public void computeScroll() { - if(mRightEdge != NO_VALUE && mScroller.getFinalX() > mRightEdge - getWidth() + 1){ - mScroller.setFinalX(mRightEdge - getWidth() + 1); - } - - if(mRightEdge != NO_VALUE && getScrollX() > mRightEdge - getWidth()) { - if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0); - else scrollTo(0, 0); - return; - } - - if (mScroller.computeScrollOffset()) { - if(mScroller.getFinalX() == mScroller.getCurrX()){ - mScroller.abortAnimation(); - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - } - else{ - final int x = mScroller.getCurrX(); - scrollTo(x, 0); - - postInvalidate(); - } - } - else if(mTouchState == TOUCH_STATE_FLING){ - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - } - - refill(); - } - - public void fling(int velocityX, int velocityY){ - if(isScrollingDisabled) return; - - mTouchState = TOUCH_STATE_FLING; - final int x = getScrollX(); - final int y = getScrollY(); - - final int rightInPixels; - if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE; - else rightInPixels = mRightEdge; - - mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth() + 1,0,0); - - invalidate(); - } - - protected void scrollByDelta(int deltaX){ - if(isScrollingDisabled) return; - - final int rightInPixels; - if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE; - else { - rightInPixels = mRightEdge; - if(getScrollX() > mRightEdge - getWidth()) { - if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0); - else scrollTo(0, 0); - return; - } - } - - final int x = getScrollX() + deltaX; - - if(x < 0 ) deltaX -= x; - else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth()); - - scrollBy(deltaX, 0); - } - - protected void handleClick(Point p){ - final int c = getChildCount(); - View v; - final Rect r = new Rect(); - for(int i=0; i < c; i++){ - v = getChildAt(i); - v.getHitRect(r); - if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ - if(mItemClickListener != null) mItemClickListener.onItemClick(v); - } - } - } - - - public void setAdapter(Adapter adapter) { - if(mAdapter != null) { - mAdapter.unregisterDataSetObserver(mDataObserver); - } - mAdapter = adapter; - mAdapter.registerDataSetObserver(mDataObserver); - reset(); - } - - private void enableChildrenCache() { - setChildrenDrawnWithCacheEnabled(true); - setChildrenDrawingCacheEnabled(true); - } - - private void clearChildrenCache() { - setChildrenDrawnWithCacheEnabled(false); - } - - @Override - protected MarginLayoutParams generateDefaultLayoutParams() { - return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); - } - - @Override - protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new MarginLayoutParams(p); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof MarginLayoutParams; - } - - public void setDefaultItemWidth(int width){ - //MTODO add xml attributes - mDefaultItemWidth = width; - } - - /** - * Set listener which will fire if item in container is clicked - */ - public void setOnItemClickListener(OnItemClickListener itemClickListener) { - this.mItemClickListener = itemClickListener; - } - - public void setViewObserver(IViewObserver viewObserver) { - this.mViewObserver = viewObserver; - } - -} +package it.moondroid.coverflow.components.ui.containers; + + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.Adapter; +import android.widget.Scroller; + +import it.moondroid.coverflow.components.general.ToolBox; +import it.moondroid.coverflow.components.ui.containers.interfaces.IViewObserver; + + +public class HorizontalList extends ViewGroup { + protected final int NO_VALUE = -11; + + /** User is not touching the list */ + protected static final int TOUCH_STATE_RESTING = 0; + + /** User is scrolling the list */ + protected static final int TOUCH_STATE_SCROLLING = 1; + + /** Fling gesture in progress */ + protected static final int TOUCH_STATE_FLING = 2; + + /** Children added with this layout mode will be added after the last child */ + protected static final int LAYOUT_MODE_AFTER = 0; + + /** Children added with this layout mode will be added before the first child */ + protected static final int LAYOUT_MODE_TO_BEFORE = 1; + + protected int mFirstItemPosition; + protected int mLastItemPosition; + protected boolean isScrollingDisabled = false; + + protected Adapter mAdapter; + protected final ToolBox.ViewCache mCache = new ToolBox.ViewCache(); + private final Scroller mScroller = new Scroller(getContext()); + protected int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + private int mTouchState = TOUCH_STATE_RESTING; + private float mLastMotionX; + private final Point mDown = new Point(); + private VelocityTracker mVelocityTracker; + private boolean mHandleSelectionOnActionUp = false; + + protected int mRightEdge = NO_VALUE; + private int mDefaultItemWidth = 200; + + protected IViewObserver mViewObserver; + + //listeners + private OnItemClickListener mItemClickListener; + + private final DataSetObserver mDataObserver = new DataSetObserver() { + + @Override + public void onChanged() { + reset(); + invalidate(); + } + + @Override + public void onInvalidated() { + removeAllViews(); + invalidate(); + } + + }; + + /** + * Remove all data, reset to initial state and attempt to refill + * Position of first item on screen in Adapter data set is maintained + */ + private void reset() { + int scroll = getScrollX(); + + int left = 0; + if(getChildCount() != 0){ + left = getChildAt(0).getLeft() - ((MarginLayoutParams)getChildAt(0).getLayoutParams()).leftMargin; + } + + removeAllViewsInLayout(); + mLastItemPosition = mFirstItemPosition; + mRightEdge = NO_VALUE; + scrollTo(left, 0); + + final int leftScreenEdge = getScrollX(); + int rightScreenEdge = leftScreenEdge + getWidth(); + + refillLeftToRight(leftScreenEdge, rightScreenEdge); + refillRightToLeft(leftScreenEdge); + + scrollTo(scroll, 0); + } + + public HorizontalList(Context context) { + this(context, null); + } + + public HorizontalList(Context context, AttributeSet attrs) { + this(context, attrs,0); + } + + public HorizontalList(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + public interface OnItemClickListener{ + void onItemClick(View v); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + refill(); + } + + /** + * Checks and refills empty area on the left + * @return firstItemPosition + */ + protected void refillRightToLeft(final int leftScreenEdge){ + if(getChildCount() == 0) return; + + View child = getChildAt(0); + int childLeft = child.getLeft(); + int lastLeft = childLeft - ((MarginLayoutParams)child.getLayoutParams()).leftMargin; + + while(lastLeft > leftScreenEdge && mFirstItemPosition > 0){ + mFirstItemPosition--; + + child = mAdapter.getView(mFirstItemPosition, mCache.getCachedView(), this); + sanitizeLayoutParams(child); + + addAndMeasureChild(child, LAYOUT_MODE_TO_BEFORE); + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + lastLeft = layoutChildToBefore(child, lastLeft, lp); + childLeft = child.getLeft() - ((MarginLayoutParams)child.getLayoutParams()).leftMargin; + + } + return; + } + + /** + * Checks and refills empty area on the right + */ + protected void refillLeftToRight(final int leftScreenEdge, final int rightScreenEdge){ + + View child; + int lastRight; + if(getChildCount() != 0){ + child = getChildAt(getChildCount() - 1); + lastRight = child.getRight() + ((MarginLayoutParams)child.getLayoutParams()).rightMargin; + } + else{ + lastRight = leftScreenEdge; + if(mLastItemPosition == mFirstItemPosition) mLastItemPosition--; + } + + while(lastRight < rightScreenEdge && mLastItemPosition < mAdapter.getCount()-1){ + mLastItemPosition++; + + child = mAdapter.getView(mLastItemPosition, mCache.getCachedView(), this); + sanitizeLayoutParams(child); + + addAndMeasureChild(child, LAYOUT_MODE_AFTER); + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + lastRight = layoutChild(child, lastRight, lp); + + if(mLastItemPosition >= mAdapter.getCount()-1) { + mRightEdge = lastRight; + } + } + } + + + /** + * Remove non visible views from left edge of screen + */ + protected void removeNonVisibleViewsLeftToRight(final int leftScreenEdge){ + if(getChildCount() == 0) return; + + // check if we should remove any views in the left + View firstChild = getChildAt(0); + + while (firstChild != null && firstChild.getRight() + ((MarginLayoutParams)firstChild.getLayoutParams()).rightMargin < leftScreenEdge) { + + // remove view + removeViewsInLayout(0, 1); + + if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(firstChild, mFirstItemPosition); + mCache.cacheView(firstChild); + + mFirstItemPosition++; + if(mFirstItemPosition >= mAdapter.getCount()) mFirstItemPosition = 0; + + // Continue to check the next child only if we have more than + // one child left + if (getChildCount() > 1) { + firstChild = getChildAt(0); + } else { + firstChild = null; + } + } + + } + + /** + * Remove non visible views from right edge of screen + */ + protected void removeNonVisibleViewsRightToLeft(final int rightScreenEdge){ + if(getChildCount() == 0) return; + + // check if we should remove any views in the right + View lastChild = getChildAt(getChildCount() - 1); + while (lastChild != null && lastChild.getLeft() - ((MarginLayoutParams)lastChild.getLayoutParams()).leftMargin > rightScreenEdge) { + // remove the right view + removeViewsInLayout(getChildCount() - 1, 1); + + if(mViewObserver != null) mViewObserver.onViewRemovedFromParent(lastChild, mLastItemPosition); + mCache.cacheView(lastChild); + + mLastItemPosition--; + if(mLastItemPosition < 0) mLastItemPosition = mAdapter.getCount()-1; + + // Continue to check the next child only if we have more than + // one child left + if (getChildCount() > 1) { + lastChild = getChildAt(getChildCount() - 1); + } else { + lastChild = null; + } + } + + } + + protected void refill(){ + if(mAdapter == null) return; + + final int leftScreenEdge = getScrollX(); + int rightScreenEdge = leftScreenEdge + getWidth(); + + removeNonVisibleViewsLeftToRight(leftScreenEdge); + removeNonVisibleViewsRightToLeft(rightScreenEdge); + + refillLeftToRight(leftScreenEdge, rightScreenEdge); + refillRightToLeft(leftScreenEdge); + } + + + + protected void sanitizeLayoutParams(View child){ + MarginLayoutParams lp; + if(child.getLayoutParams() instanceof MarginLayoutParams) lp = (MarginLayoutParams) child.getLayoutParams(); + else if(child.getLayoutParams() != null) lp = new MarginLayoutParams(child.getLayoutParams()); + else lp = new MarginLayoutParams(mDefaultItemWidth,getHeight()); + + if(lp.height == LayoutParams.MATCH_PARENT) lp.height = getHeight(); + if(lp.width == LayoutParams.MATCH_PARENT) lp.width = getWidth(); + + if(lp.height == LayoutParams.WRAP_CONTENT){ + measureUnspecified(child); + lp.height = child.getMeasuredHeight(); + } + if(lp.width == LayoutParams.WRAP_CONTENT){ + measureUnspecified(child); + lp.width = child.getMeasuredWidth(); + } + child.setLayoutParams(lp); + } + + private void measureUnspecified(View child){ + final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED); + final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED); + measureChild(child, pwms, phms); + } + + /** + * Adds a view as a child view and takes care of measuring it + * + * @param child The view to add + * @param layoutMode Either LAYOUT_MODE_LEFT or LAYOUT_MODE_RIGHT + * @return child which was actually added to container, subclasses can override to introduce frame views + */ + protected View addAndMeasureChild(final View child, final int layoutMode) { + if(child.getLayoutParams() == null) child.setLayoutParams(new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + + final int index = layoutMode == LAYOUT_MODE_TO_BEFORE ? 0 : -1; + addViewInLayout(child, index, child.getLayoutParams(), true); + + final int pwms = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY); + final int phms = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY); + measureChild(child, pwms, phms); + child.setDrawingCacheEnabled(false); + + return child; + } + + /** + * Layout children from right to left + */ + protected int layoutChildToBefore(View v, int right , MarginLayoutParams lp){ + final int left = right - v.getMeasuredWidth() - lp.leftMargin - lp.rightMargin; + layoutChild(v, left, lp); + return left; + } + + /** + * @param topline Y coordinate of topline + * @param left X coordinate where should we start layout + */ + protected int layoutChild(View v, int left, MarginLayoutParams lp){ + int l,t,r,b; + l = left + lp.leftMargin; + t = lp.topMargin; + r = l + v.getMeasuredWidth(); + b = t + v.getMeasuredHeight(); + + v.layout(l, t, r, b); + return r + lp.rightMargin; + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + final float x = ev.getX(); + final float y = ev.getY(); + switch (action) { + case MotionEvent.ACTION_MOVE: + /* + * not dragging, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionX is set to the x value + * of the down event. + */ + final int xDiff = (int) Math.abs(x - mLastMotionX); + + final int touchSlop = mTouchSlop; + final boolean xMoved = xDiff > touchSlop; + + if (xMoved) { + // Scroll if the user moved far enough along the axis + mTouchState = TOUCH_STATE_SCROLLING; + mHandleSelectionOnActionUp = false; + enableChildrenCache(); + cancelLongPress(); + } + + break; + + case MotionEvent.ACTION_DOWN: + // Remember location of down touch + mLastMotionX = x; + + mDown.x = (int) x; + mDown.y = (int) y; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; + //if he had normal click in rested state, remember for action up check + if(mTouchState == TOUCH_STATE_RESTING){ + mHandleSelectionOnActionUp = true; + } + break; + + case MotionEvent.ACTION_CANCEL: + mDown.x = -1; + mDown.y = -1; + break; + case MotionEvent.ACTION_UP: + //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates + if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ + final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); + if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); + } + // Release the drag + mHandleSelectionOnActionUp = false; + mDown.x = -1; + mDown.y = -1; + + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + break; + } + + return mTouchState == TOUCH_STATE_SCROLLING; + + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + + final int action = event.getAction(); + final float x = event.getX(); + final float y = event.getY(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.forceFinished(true); + } + + // Remember where the motion event started + mLastMotionX = x; + + break; + case MotionEvent.ACTION_MOVE: + + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int deltaX = (int) (mLastMotionX - x); + mLastMotionX = x; + + scrollByDelta(deltaX); + } + else{ + final int xDiff = (int) Math.abs(x - mLastMotionX); + + final int touchSlop = mTouchSlop; + final boolean xMoved = xDiff > touchSlop; + + + if (xMoved) { + // Scroll if the user moved far enough along the axis + mTouchState = TOUCH_STATE_SCROLLING; + enableChildrenCache(); + cancelLongPress(); + } + } + break; + case MotionEvent.ACTION_UP: + + //this must be here, in case no child view returns true, + //events will propagate back here and on intercept touch event wont be called again + //in case of no parent it propagates here, in case of parent it usually propagates to on cancel + if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ + final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); + if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); + mHandleSelectionOnActionUp = false; + } + + //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates + if (mTouchState == TOUCH_STATE_SCROLLING) { + + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int) mVelocityTracker.getXVelocity(); + int initialYVelocity = (int) mVelocityTracker.getYVelocity(); + + if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { + fling(-initialXVelocity, -initialYVelocity); + } + else{ + // Release the drag + clearChildrenCache(); + mTouchState = TOUCH_STATE_RESTING; + + mDown.x = -1; + mDown.y = -1; + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + break; + } + + // Release the drag + clearChildrenCache(); + mTouchState = TOUCH_STATE_RESTING; + + mDown.x = -1; + mDown.y = -1; + + break; + case MotionEvent.ACTION_CANCEL: + mTouchState = TOUCH_STATE_RESTING; + } + + return true; + } + + @Override + public void computeScroll() { + if(mRightEdge != NO_VALUE && mScroller.getFinalX() > mRightEdge - getWidth() + 1){ + mScroller.setFinalX(mRightEdge - getWidth() + 1); + } + + if(mRightEdge != NO_VALUE && getScrollX() > mRightEdge - getWidth()) { + if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0); + else scrollTo(0, 0); + return; + } + + if (mScroller.computeScrollOffset()) { + if(mScroller.getFinalX() == mScroller.getCurrX()){ + mScroller.abortAnimation(); + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + } + else{ + final int x = mScroller.getCurrX(); + scrollTo(x, 0); + + postInvalidate(); + } + } + else if(mTouchState == TOUCH_STATE_FLING){ + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + } + + refill(); + } + + public void fling(int velocityX, int velocityY){ + if(isScrollingDisabled) return; + + mTouchState = TOUCH_STATE_FLING; + final int x = getScrollX(); + final int y = getScrollY(); + + final int rightInPixels; + if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE; + else rightInPixels = mRightEdge; + + mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth() + 1,0,0); + + invalidate(); + } + + protected void scrollByDelta(int deltaX){ + if(isScrollingDisabled) return; + + final int rightInPixels; + if(mRightEdge == NO_VALUE) rightInPixels = Integer.MAX_VALUE; + else { + rightInPixels = mRightEdge; + if(getScrollX() > mRightEdge - getWidth()) { + if(mRightEdge - getWidth() > 0) scrollTo(mRightEdge - getWidth(), 0); + else scrollTo(0, 0); + return; + } + } + + final int x = getScrollX() + deltaX; + + if(x < 0 ) deltaX -= x; + else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth()); + + scrollBy(deltaX, 0); + } + + protected void handleClick(Point p){ + final int c = getChildCount(); + View v; + final Rect r = new Rect(); + for(int i=0; i < c; i++){ + v = getChildAt(i); + v.getHitRect(r); + if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ + if(mItemClickListener != null) mItemClickListener.onItemClick(v); + } + } + } + + + public void setAdapter(Adapter adapter) { + if(mAdapter != null) { + mAdapter.unregisterDataSetObserver(mDataObserver); + } + mAdapter = adapter; + mAdapter.registerDataSetObserver(mDataObserver); + reset(); + } + + private void enableChildrenCache() { + setChildrenDrawnWithCacheEnabled(true); + setChildrenDrawingCacheEnabled(true); + } + + private void clearChildrenCache() { + setChildrenDrawnWithCacheEnabled(false); + } + + @Override + protected MarginLayoutParams generateDefaultLayoutParams() { + return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected MarginLayoutParams generateLayoutParams(LayoutParams p) { + return new MarginLayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return p instanceof MarginLayoutParams; + } + + public void setDefaultItemWidth(int width){ + //MTODO add xml attributes + mDefaultItemWidth = width; + } + + /** + * Set listener which will fire if item in container is clicked + */ + public void setOnItemClickListener(OnItemClickListener itemClickListener) { + this.mItemClickListener = itemClickListener; + } + + public void setViewObserver(IViewObserver viewObserver) { + this.mViewObserver = viewObserver; + } + +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/HorizontalListWithRemovableItems.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/HorizontalListWithRemovableItems.java similarity index 93% rename from MAComponents/src/com/martinappl/components/ui/containers/HorizontalListWithRemovableItems.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/HorizontalListWithRemovableItems.java index e1cf388..c3306e9 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/HorizontalListWithRemovableItems.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/HorizontalListWithRemovableItems.java @@ -1,322 +1,322 @@ -package com.martinappl.components.ui.containers; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.animation.ValueAnimator.AnimatorUpdateListener; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import com.martinappl.components.R; -import com.martinappl.components.general.ToolBox; -import com.martinappl.components.ui.containers.interfaces.IRemovableItemsAdapterComponent; -import com.martinappl.components.ui.containers.interfaces.IRemoveFromAdapter; - - -public class HorizontalListWithRemovableItems extends HorizontalList { - private static final int FADE_TIME = 250; - private static final int SLIDE_TIME = 350; - - private Drawable mRemoveItemIconDrawable = getResources().getDrawable(R.drawable.ico_delete_asset); - private Drawable mIconForAnimation; - - private IRemovableItemsAdapterComponent mRemoveListener; - - private int mIconMarginTop = (int) ToolBox.dpToPixels(10, getContext()); - private int mIconMarginRight = (int) ToolBox.dpToPixels(10, getContext()); - private int mIconClickableMarginExtend = (int) ToolBox.dpToPixels(10, getContext()); - - private int mDownX; - private int mDownY; - private boolean isPointerDown; - - private View mContainingView; - private int mContainingViewPosition; - private int mContainingViewIndex; - private Object mData; - - private final Rect mTempRect = new Rect(); - private int mAnimationLastValue; - - private int mAlphaAnimationRunningOnIndex = -1; - - private boolean mEditable; - - public HorizontalListWithRemovableItems(Context context, - AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public HorizontalListWithRemovableItems(Context context, AttributeSet attrs) { - this(context, attrs,0); - } - - public HorizontalListWithRemovableItems(Context context) { - this(context,null); - } - - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if(!mEditable) return; - - final int c = getChildCount(); - final int iw = mRemoveItemIconDrawable.getIntrinsicWidth(); - final int ih = mRemoveItemIconDrawable.getIntrinsicHeight(); - - View v; - int r,t; - Drawable d; - for(int i = 0; i < c; i++){ - if(i != mAlphaAnimationRunningOnIndex) d = mRemoveItemIconDrawable; - else d = mIconForAnimation; - - v = getChildAt(i); - r = v.getRight(); - t = v.getTop(); - mTempRect.left = r-iw-mIconMarginRight; - mTempRect.top = t+mIconMarginTop; - mTempRect.right = r-mIconMarginRight; - mTempRect.bottom = t+mIconMarginTop+ih; - d.setBounds(mTempRect); - d.draw(canvas); - } - - } - - - - - @Override - protected View addAndMeasureChild(View child, int layoutMode) { - if(layoutMode == LAYOUT_MODE_TO_BEFORE && mAlphaAnimationRunningOnIndex != -1){ - mAlphaAnimationRunningOnIndex++; - } - - return super.addAndMeasureChild(child, layoutMode); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - if(!mEditable || ev.getActionMasked() != MotionEvent.ACTION_DOWN) return super.onInterceptTouchEvent(ev); - //only down event will get through initial condition - - final int x = (int) ev.getX(); - final int y = (int) ev.getY(); - - final int iw = mRemoveItemIconDrawable.getIntrinsicWidth(); - final int ih = mRemoveItemIconDrawable.getIntrinsicHeight(); - - View v; - int r,t; - final int c = getChildCount(); - for(int i = 0; i < c; i++){ - v = getChildAt(i); - r = v.getRight(); - t = v.getTop(); - mTempRect.left = r-iw-mIconMarginRight - mIconClickableMarginExtend; - mTempRect.top = t+mIconMarginTop - mIconClickableMarginExtend; - mTempRect.right = r-mIconMarginRight + mIconClickableMarginExtend; - mTempRect.bottom = t+mIconMarginTop+ih + mIconClickableMarginExtend; - - if(mTempRect.contains(getScrollX() + x, y)){ - mDownX = x; - mDownY = y; - isPointerDown = true; - - mContainingView = v; - mContainingViewPosition = mFirstItemPosition + i; - mData = mAdapter.getItem(mContainingViewPosition); - mContainingViewIndex = i; - - return true; - } - } - - isPointerDown = false; - - return super.onInterceptTouchEvent(ev); - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if(isPointerDown){ - if(ev.getActionMasked() == MotionEvent.ACTION_UP){ - if(ToolBox.getLineLength(ev.getX(), ev.getY(), mDownX, mDownY) < mTouchSlop && mAlphaAnimationRunningOnIndex == -1){ - createRemoveAnimations(mContainingViewIndex).start(); - } - - isPointerDown = false; - } - - return true; - } - else{ - return super.onTouchEvent(ev); - } - } - -// protected void refill(){ -// if(mAdapter == null) return; -// -// final int leftScreenEdge = getScrollX(); -// int rightScreenEdge = leftScreenEdge + getWidth(); -// -// if(mAlphaAnimationRunningOnIndex != -1) rightScreenEdge += mContainingView.getWidth(); -// -// removeNonVisibleViewsLeftToRight(leftScreenEdge); -// removeNonVisibleViewsRightToLeft(rightScreenEdge); -// -// refillLeftToRight(leftScreenEdge, rightScreenEdge); -// refillRightToLeft(leftScreenEdge); -// } - - private void onRemoveAnimationFinished(int position, View view, Object item){ - if(mRemoveListener == null && mAdapter instanceof IRemoveFromAdapter){ - ((IRemoveFromAdapter) mAdapter).removeItemFromAdapter(position); - } - else if(!mRemoveListener.onItemRemove(position,view,item) && mAdapter instanceof IRemoveFromAdapter){ - ((IRemoveFromAdapter) mAdapter).removeItemFromAdapter(position); - } - - - mContainingView = null; - mContainingViewIndex = -1; - mContainingViewPosition = -1; - mData = null; - } - - private Animator createRemoveAnimations(final int removedViewIndex){ - if(mIconForAnimation == null) mIconForAnimation = mRemoveItemIconDrawable.getConstantState().newDrawable(getResources()).mutate(); - mAlphaAnimationRunningOnIndex = removedViewIndex; - isScrollingDisabled = true; - View removed = getChildAt(removedViewIndex); - - ObjectAnimator fader = ObjectAnimator.ofFloat(removed, "alpha", 1f, 0f); - fader.setDuration(FADE_TIME); - fader.addUpdateListener(new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator anim) { - mIconForAnimation.setAlpha((int) (255*((Float)anim.getAnimatedValue()))); - invalidate(mIconForAnimation.getBounds()); - } - }); - - - mAnimationLastValue = 0; - final int distance = removed.getWidth(); - final boolean scrollDuringSlide; - if(mRightEdge != NO_VALUE && getScrollX() + distance > mRightEdge - getWidth()) scrollDuringSlide = true; - else scrollDuringSlide = false; - - ValueAnimator slider = ValueAnimator.ofInt(0,-distance); - slider.addUpdateListener(new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator anim) { - final int val = (Integer) anim.getAnimatedValue(); - int dx = val - mAnimationLastValue; - mAnimationLastValue = val; - - final int c = getChildCount(); - View v; - for(int i=removedViewIndex+1; i < c; i++){ - v = getChildAt(i); - v.layout(v.getLeft()+dx, v.getTop(), v.getRight()+dx, v.getBottom()); - } - - if(scrollDuringSlide){ - if(getScrollX() + dx < 0) dx = -getScrollX(); - scrollBy(dx, 0); - } - } - }); - slider.setDuration(SLIDE_TIME); - -// View v; -// -// final float distance = -removed.getWidth(); -// final ArrayList anims = new ArrayList(); -// ObjectAnimator slider = null; -// for(int i=removedViewIndex+1; i < getChildCount(); i++){ -// v = getChildAt(i); -// slider = ObjectAnimator.ofFloat(v, "translationX", 0f, distance); -// anims.add(slider); -// } -// if(slider != null) slider.addUpdateListener(new AnimatorUpdateListener() { -// @Override -// public void onAnimationUpdate(ValueAnimator anim) { -// invalidate(); -// } -// }); -// -// AnimatorSet sliderSet = new AnimatorSet(); -// sliderSet.playTogether(anims); -// sliderSet.setDuration(SLIDE_TIME); - - final AnimatorListener listener = new AnimatorListener() { - public void onAnimationStart(Animator arg0) {} - public void onAnimationRepeat(Animator arg0) {} - public void onAnimationCancel(Animator arg0) {} - - public void onAnimationEnd(Animator arg0) { - mAlphaAnimationRunningOnIndex = -1; - isScrollingDisabled = false; - - onRemoveAnimationFinished(mContainingViewPosition, mContainingView, mData); - } - }; - - - AnimatorSet resultSet = new AnimatorSet(); - resultSet.playSequentially(fader,slider); - - resultSet.addListener(listener); - - return resultSet; - } - - - - - /** - * Sets icon for overlay which removes item on click - */ - public void setRemoveItemIcon(int resId){ - mRemoveItemIconDrawable = getResources().getDrawable(resId); - mIconForAnimation = null; - } - - public void setRemoveItemIconMarginTop(int px){ - mIconMarginTop = px; - } - - public void setRemoveItemIconMarginRight(int px){ - mIconMarginRight = px; - } - - /** - * - * @param px - */ - public void setClickableMarginOfIcon(int px){ - mIconClickableMarginExtend = px; - } - - public void setRemoveItemListener(IRemovableItemsAdapterComponent listener){ - mRemoveListener = listener; - } - - public void setEditable(boolean isEditable){ - mEditable = isEditable; - } - -} +package it.moondroid.coverflow.components.ui.containers; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import it.moondroid.coverflow.R; +import it.moondroid.coverflow.components.general.ToolBox; +import it.moondroid.coverflow.components.ui.containers.interfaces.IRemovableItemsAdapterComponent; +import it.moondroid.coverflow.components.ui.containers.interfaces.IRemoveFromAdapter; + + +public class HorizontalListWithRemovableItems extends HorizontalList { + private static final int FADE_TIME = 250; + private static final int SLIDE_TIME = 350; + + private Drawable mRemoveItemIconDrawable = getResources().getDrawable(R.drawable.ico_delete_asset); + private Drawable mIconForAnimation; + + private IRemovableItemsAdapterComponent mRemoveListener; + + private int mIconMarginTop = (int) ToolBox.dpToPixels(10, getContext()); + private int mIconMarginRight = (int) ToolBox.dpToPixels(10, getContext()); + private int mIconClickableMarginExtend = (int) ToolBox.dpToPixels(10, getContext()); + + private int mDownX; + private int mDownY; + private boolean isPointerDown; + + private View mContainingView; + private int mContainingViewPosition; + private int mContainingViewIndex; + private Object mData; + + private final Rect mTempRect = new Rect(); + private int mAnimationLastValue; + + private int mAlphaAnimationRunningOnIndex = -1; + + private boolean mEditable; + + public HorizontalListWithRemovableItems(Context context, + AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public HorizontalListWithRemovableItems(Context context, AttributeSet attrs) { + this(context, attrs,0); + } + + public HorizontalListWithRemovableItems(Context context) { + this(context,null); + } + + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if(!mEditable) return; + + final int c = getChildCount(); + final int iw = mRemoveItemIconDrawable.getIntrinsicWidth(); + final int ih = mRemoveItemIconDrawable.getIntrinsicHeight(); + + View v; + int r,t; + Drawable d; + for(int i = 0; i < c; i++){ + if(i != mAlphaAnimationRunningOnIndex) d = mRemoveItemIconDrawable; + else d = mIconForAnimation; + + v = getChildAt(i); + r = v.getRight(); + t = v.getTop(); + mTempRect.left = r-iw-mIconMarginRight; + mTempRect.top = t+mIconMarginTop; + mTempRect.right = r-mIconMarginRight; + mTempRect.bottom = t+mIconMarginTop+ih; + d.setBounds(mTempRect); + d.draw(canvas); + } + + } + + + + + @Override + protected View addAndMeasureChild(View child, int layoutMode) { + if(layoutMode == LAYOUT_MODE_TO_BEFORE && mAlphaAnimationRunningOnIndex != -1){ + mAlphaAnimationRunningOnIndex++; + } + + return super.addAndMeasureChild(child, layoutMode); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if(!mEditable || ev.getActionMasked() != MotionEvent.ACTION_DOWN) return super.onInterceptTouchEvent(ev); + //only down event will get through initial condition + + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + + final int iw = mRemoveItemIconDrawable.getIntrinsicWidth(); + final int ih = mRemoveItemIconDrawable.getIntrinsicHeight(); + + View v; + int r,t; + final int c = getChildCount(); + for(int i = 0; i < c; i++){ + v = getChildAt(i); + r = v.getRight(); + t = v.getTop(); + mTempRect.left = r-iw-mIconMarginRight - mIconClickableMarginExtend; + mTempRect.top = t+mIconMarginTop - mIconClickableMarginExtend; + mTempRect.right = r-mIconMarginRight + mIconClickableMarginExtend; + mTempRect.bottom = t+mIconMarginTop+ih + mIconClickableMarginExtend; + + if(mTempRect.contains(getScrollX() + x, y)){ + mDownX = x; + mDownY = y; + isPointerDown = true; + + mContainingView = v; + mContainingViewPosition = mFirstItemPosition + i; + mData = mAdapter.getItem(mContainingViewPosition); + mContainingViewIndex = i; + + return true; + } + } + + isPointerDown = false; + + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if(isPointerDown){ + if(ev.getActionMasked() == MotionEvent.ACTION_UP){ + if(ToolBox.getLineLength(ev.getX(), ev.getY(), mDownX, mDownY) < mTouchSlop && mAlphaAnimationRunningOnIndex == -1){ + createRemoveAnimations(mContainingViewIndex).start(); + } + + isPointerDown = false; + } + + return true; + } + else{ + return super.onTouchEvent(ev); + } + } + +// protected void refill(){ +// if(mAdapter == null) return; +// +// final int leftScreenEdge = getScrollX(); +// int rightScreenEdge = leftScreenEdge + getWidth(); +// +// if(mAlphaAnimationRunningOnIndex != -1) rightScreenEdge += mContainingView.getWidth(); +// +// removeNonVisibleViewsLeftToRight(leftScreenEdge); +// removeNonVisibleViewsRightToLeft(rightScreenEdge); +// +// refillLeftToRight(leftScreenEdge, rightScreenEdge); +// refillRightToLeft(leftScreenEdge); +// } + + private void onRemoveAnimationFinished(int position, View view, Object item){ + if(mRemoveListener == null && mAdapter instanceof IRemoveFromAdapter){ + ((IRemoveFromAdapter) mAdapter).removeItemFromAdapter(position); + } + else if(!mRemoveListener.onItemRemove(position,view,item) && mAdapter instanceof IRemoveFromAdapter){ + ((IRemoveFromAdapter) mAdapter).removeItemFromAdapter(position); + } + + + mContainingView = null; + mContainingViewIndex = -1; + mContainingViewPosition = -1; + mData = null; + } + + private Animator createRemoveAnimations(final int removedViewIndex){ + if(mIconForAnimation == null) mIconForAnimation = mRemoveItemIconDrawable.getConstantState().newDrawable(getResources()).mutate(); + mAlphaAnimationRunningOnIndex = removedViewIndex; + isScrollingDisabled = true; + View removed = getChildAt(removedViewIndex); + + ObjectAnimator fader = ObjectAnimator.ofFloat(removed, "alpha", 1f, 0f); + fader.setDuration(FADE_TIME); + fader.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator anim) { + mIconForAnimation.setAlpha((int) (255*((Float)anim.getAnimatedValue()))); + invalidate(mIconForAnimation.getBounds()); + } + }); + + + mAnimationLastValue = 0; + final int distance = removed.getWidth(); + final boolean scrollDuringSlide; + if(mRightEdge != NO_VALUE && getScrollX() + distance > mRightEdge - getWidth()) scrollDuringSlide = true; + else scrollDuringSlide = false; + + ValueAnimator slider = ValueAnimator.ofInt(0,-distance); + slider.addUpdateListener(new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator anim) { + final int val = (Integer) anim.getAnimatedValue(); + int dx = val - mAnimationLastValue; + mAnimationLastValue = val; + + final int c = getChildCount(); + View v; + for(int i=removedViewIndex+1; i < c; i++){ + v = getChildAt(i); + v.layout(v.getLeft()+dx, v.getTop(), v.getRight()+dx, v.getBottom()); + } + + if(scrollDuringSlide){ + if(getScrollX() + dx < 0) dx = -getScrollX(); + scrollBy(dx, 0); + } + } + }); + slider.setDuration(SLIDE_TIME); + +// View v; +// +// final float distance = -removed.getWidth(); +// final ArrayList anims = new ArrayList(); +// ObjectAnimator slider = null; +// for(int i=removedViewIndex+1; i < getChildCount(); i++){ +// v = getChildAt(i); +// slider = ObjectAnimator.ofFloat(v, "translationX", 0f, distance); +// anims.add(slider); +// } +// if(slider != null) slider.addUpdateListener(new AnimatorUpdateListener() { +// @Override +// public void onAnimationUpdate(ValueAnimator anim) { +// invalidate(); +// } +// }); +// +// AnimatorSet sliderSet = new AnimatorSet(); +// sliderSet.playTogether(anims); +// sliderSet.setDuration(SLIDE_TIME); + + final AnimatorListener listener = new AnimatorListener() { + public void onAnimationStart(Animator arg0) {} + public void onAnimationRepeat(Animator arg0) {} + public void onAnimationCancel(Animator arg0) {} + + public void onAnimationEnd(Animator arg0) { + mAlphaAnimationRunningOnIndex = -1; + isScrollingDisabled = false; + + onRemoveAnimationFinished(mContainingViewPosition, mContainingView, mData); + } + }; + + + AnimatorSet resultSet = new AnimatorSet(); + resultSet.playSequentially(fader,slider); + + resultSet.addListener(listener); + + return resultSet; + } + + + + + /** + * Sets icon for overlay which removes item on click + */ + public void setRemoveItemIcon(int resId){ + mRemoveItemIconDrawable = getResources().getDrawable(resId); + mIconForAnimation = null; + } + + public void setRemoveItemIconMarginTop(int px){ + mIconMarginTop = px; + } + + public void setRemoveItemIconMarginRight(int px){ + mIconMarginRight = px; + } + + /** + * + * @param px + */ + public void setClickableMarginOfIcon(int px){ + mIconClickableMarginExtend = px; + } + + public void setRemoveItemListener(IRemovableItemsAdapterComponent listener){ + mRemoveListener = listener; + } + + public void setEditable(boolean isEditable){ + mEditable = isEditable; + } + +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/contentbands/BasicContentBand.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/BasicContentBand.java similarity index 96% rename from MAComponents/src/com/martinappl/components/ui/containers/contentbands/BasicContentBand.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/BasicContentBand.java index a0a75b9..1503758 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/contentbands/BasicContentBand.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/BasicContentBand.java @@ -1,1168 +1,1168 @@ -package com.martinappl.components.ui.containers.contentbands; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedList; -import java.util.List; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Point; -import android.graphics.Rect; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.ViewGroup; -import android.widget.Scroller; - -import com.martinappl.components.R; -import com.martinappl.components.general.ToolBox; -import com.martinappl.components.general.Validate; - - -/** - * @author Martin Appl - * - * Horizontally scrollable container with boundaries on the ends, which places Views on coordinates specified - * by tile objects. Data binding is specified by adapter interface. Use abstract adapter which has already implemented - * algorithms for searching views in requested ranges. You only need to implement getViewForTile method where you map - * Tile objects from dataset to corresponding View objects, which get displayed. Position on screen is described by LayoutParams object. - * Method getLayoutParamsForTile helps generate layout params from data objects. If you don't set Layout params in getViewForTile, this - * methods is called automatically afterwards. - * - * DSP = device specific pixel - */ -public class BasicContentBand extends ViewGroup { - //CONSTANTS -// private static final String LOG_TAG = "Basic_ContentBand_Component"; - private static final int NO_VALUE = -11; - private static final int DSP_DEFAULT = 10; - - /** User is not touching the list */ - protected static final int TOUCH_STATE_RESTING = 0; - - /** User is scrolling the list */ - protected static final int TOUCH_STATE_SCROLLING = 1; - - /** Fling gesture in progress */ - protected static final int TOUCH_STATE_FLING = 2; - - /** - * In this mode we have pixel size of DSP specified, if dspHeight is bigger than window, content band can be scrolled vertically. - */ - public static final int GRID_MODE_FIXED_SIZE = 0; - /** - * In this mode is pixel size of DSP calculated dynamically, based on widget height in pixels and value of dspHeight which is fixed - * and taken from adapters getBottom method - */ - public static final int GRID_MODE_DYNAMIC_SIZE = 1; - - //to which direction on X axis are window coordinates sliding - protected static final int DIRECTION_RIGHT = 0; - protected static final int DIRECTION_LEFT = 1; - - - //VARIABLES - protected Adapter mAdapter; - private int mGridMode = GRID_MODE_DYNAMIC_SIZE; - /**How many normal pixels corresponds to one DSP pixel*/ - private int mDspPixelRatio = DSP_DEFAULT; - private int mDspHeight = NO_VALUE; - protected int mDspHeightModulo; - //refilling - protected int mCurrentlyLayoutedViewsLeftEdgeDsp; - protected int mCurrentlyLayoutedViewsRightEdgeDsp; - private final ArrayList mTempViewArray = new ArrayList(); - //touch, scrolling - protected int mTouchState = TOUCH_STATE_RESTING; - private float mLastMotionX; - private float mLastMotionY; - private final Point mDown = new Point(); - private VelocityTracker mVelocityTracker; - protected final Scroller mScroller; - private boolean mHandleSelectionOnActionUp = false; - protected int mScrollDirection = NO_VALUE; - //constant values - private final int mTouchSlop; - private final int mMinimumVelocity; - private final int mMaximumVelocity; -// private final Rect mTempRect = new Rect(); - - private boolean mIsZOrderEnabled; - private int[] mDrawingOrderArray; - - //listeners - private OnItemClickListener mItemClickListener; - - public BasicContentBand(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - final ViewConfiguration configuration = ViewConfiguration.get(context); - mTouchSlop = configuration.getScaledTouchSlop(); - mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); - mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); - mScroller = new Scroller(context); - - if(attrs != null){ - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BasicContentBand, defStyle, 0); - - mDspPixelRatio = a.getInteger(R.styleable.BasicContentBand_deviceSpecificPixelSize, mDspPixelRatio); - mGridMode = a.getInteger(R.styleable.BasicContentBand_gridMode, mGridMode); - - a.recycle(); - } - - - } - - public BasicContentBand(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public BasicContentBand(Context context) { - this(context,null); - } - - protected int dspToPx(int dsp){ - return dsp * mDspPixelRatio; - } - - protected int pxToDsp(int px){ - return px / mDspPixelRatio; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - - if(mAdapter != null){ - mDspHeight = mAdapter.getBottom(); - Validate.isTrue(mDspHeight > 0, "Adapter getBottom must return value greater than zero"); - } - else{ - setMeasuredDimension(widthSpecSize, heightSpecSize); - return; - } - - int measuredWidth, measuredHeight; - if(mGridMode == GRID_MODE_FIXED_SIZE){ - /*HEIGHT*/ - measuredHeight = mDspPixelRatio * mDspHeight; - - if(heightSpecMode == MeasureSpec.AT_MOST){ - if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize; - } - else if(heightSpecMode == MeasureSpec.EXACTLY){ - measuredHeight = heightSpecSize; - } - - /*WIDTH*/ - measuredWidth = widthSpecSize; - if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio; - - if(widthSpecMode == MeasureSpec.AT_MOST){ - if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize; - } - else if(widthSpecMode == MeasureSpec.EXACTLY){ - measuredWidth = widthSpecSize; - } - } - else{ - if (heightSpecMode == MeasureSpec.UNSPECIFIED) { - throw new RuntimeException("Can not have unspecified hight dimension in dynamic grid mode"); - } - /*HEIGHT*/ - measuredHeight = heightSpecSize; - - mDspPixelRatio = measuredHeight / mDspHeight; - mDspHeightModulo = measuredHeight % mDspHeight; - - measuredHeight = mDspPixelRatio * mDspHeight; - - if(heightSpecMode == MeasureSpec.AT_MOST){ - if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize; - else mDspHeightModulo = 0; - } - else if(heightSpecMode == MeasureSpec.EXACTLY){ - measuredHeight = heightSpecSize; - } - - /*WIDTH*/ - measuredWidth = widthSpecSize; - if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio; - - if(widthSpecMode == MeasureSpec.AT_MOST){ - if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize; - } - else if(widthSpecMode == MeasureSpec.EXACTLY){ - measuredWidth = widthSpecSize; - } - - } - - setMeasuredDimension(measuredWidth, measuredHeight); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int c = getChildCount(); - - if(c == 0) { - fillEmptyContainer(); - c = getChildCount(); - } - - 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 -// if(((LayoutParams)getChildAt(j).getLayoutParams()).tileNumber == lp.tileNumber) { -// arr[i] = null; -// nullCounter++; -// break; -// } -// } -// } -// -// final View[] res = new View[arr.length - nullCounter]; -// for(int i=0,j=0; i comparator = new Comparator() { - @Override - public int compare(View lhs, View rhs) { - final LayoutParams l = (LayoutParams) lhs.getLayoutParams(); - final LayoutParams r = (LayoutParams) rhs.getLayoutParams(); - - if(l.z == r.z) return 0; - else if(l.z < r.z) return -1; - else return 1; - } - }; - - Arrays.sort(tempArr, comparator); - mDrawingOrderArray = new int[tempArr.length]; - for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight(); - if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; - addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); - } - - if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder(); - - mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; - mCurrentlyLayoutedViewsRightEdgeDsp= dspMostRight; - } - - /** - * Checks and refills empty area on the left edge of screen - */ - protected void refillLeftSide(){ - if(mAdapter == null) return; - - final int leftScreenEdge = getScrollX(); - final int dspLeftScreenEdge = pxToDsp(leftScreenEdge); - final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp; - - if(dspLeftScreenEdge >= dspNextViewsRight) return; -// Logger.d(LOG_TAG, "from " + dspLeftScreenEdge + ", to " + dspNextViewsRight); - - View[] list = mAdapter.getViewsByRightSideRange(dspLeftScreenEdge, dspNextViewsRight); -// list = filterAlreadyPresentViews(list); - - int dspMostLeft = dspNextViewsRight; - LayoutParams lp; - for(int i=0; i < list.length; i++){ - lp = (LayoutParams) list[i].getLayoutParams(); - if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; - addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); - } - - if(list.length > 0){ - layoutNewChildren(list); - } - - mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; - } - - /** - * Checks and refills empty area on the right - */ - protected void refillRightSide(){ - if(mAdapter == null) return; - - final int rightScreenEdge = getScrollX() + getWidth(); - final int dspNextAddedViewsLeft = mCurrentlyLayoutedViewsRightEdgeDsp; - - int dspRightScreenEdge = pxToDsp(rightScreenEdge) + 1; - if(dspRightScreenEdge > mAdapter.getEnd()) dspRightScreenEdge = mAdapter.getEnd(); - - if(dspNextAddedViewsLeft >= dspRightScreenEdge) return; - - View[] list = mAdapter.getViewsByLeftSideRange(dspNextAddedViewsLeft, dspRightScreenEdge); -// list = filterAlreadyPresentViews(list); - - int dspMostRight = 0; - LayoutParams lp; - for(int i=0; i < list.length; i++){ - lp = (LayoutParams) list[i].getLayoutParams(); - if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight(); - addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); - } - - if(list.length > 0){ - layoutNewChildren(list); - } - - mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; - } - - /** - * Remove non visible views laid out of the screen - */ - private void removeNonVisibleViews(){ - if(getChildCount() == 0) return; - - final int leftScreenEdge = getScrollX(); - final int rightScreenEdge = leftScreenEdge + getWidth(); - - int dspRightScreenEdge = pxToDsp(rightScreenEdge); - if(dspRightScreenEdge >= 0) dspRightScreenEdge++; //to avoid problem with rounding of values - - int dspLeftScreenEdge = pxToDsp(leftScreenEdge); - if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger - - mTempViewArray.clear(); - View v; - for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight(); - if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; - } - - mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; - mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; - - } - - //check if View with specified LayoutParams is currently on screen - private boolean isOnScreen(LayoutParams lp, int dspLeftScreenEdge, int dspRightScreenEdge){ - final int left = lp.dspLeft; - final int right = left + lp.dspWidth; - - if(right > dspLeftScreenEdge && left < dspRightScreenEdge) return true; - else return false; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - - /* - * This method JUST determines whether we want to intercept the motion. - * If we return true, onTouchEvent will be called and we do the actual - * scrolling there. - */ - - - /* - * Shortcut the most recurring case: the user is in the dragging - * state and he is moving his finger. We want to intercept this - * motion. - */ - final int action = ev.getAction(); - if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { - return true; - } - - final float x = ev.getX(); - final float y = ev.getY(); - switch (action) { - case MotionEvent.ACTION_MOVE: - /* - * not dragging, otherwise the shortcut would have caught it. Check - * whether the user has moved far enough from his original down touch. - */ - - /* - * Locally do absolute value. mLastMotionX is set to the x value - * of the down event. - */ - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - final boolean xMoved = xDiff > touchSlop; - final boolean yMoved = yDiff > touchSlop; - - - if (xMoved || yMoved) { - // Scroll if the user moved far enough along the axis - mTouchState = TOUCH_STATE_SCROLLING; - mHandleSelectionOnActionUp = false; - enableChildrenCache(); - cancelLongPress(); - } - - break; - - case MotionEvent.ACTION_DOWN: - // Remember location of down touch - mLastMotionX = x; - - mDown.x = (int) x; - mDown.y = (int) y; - - /* - * If being flinged and user touches the screen, initiate drag; - * otherwise don't. mScroller.isFinished should be false when - * being flinged. - */ - mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; - //if he had normal click in rested state, remember for action up check - if(mTouchState == TOUCH_STATE_RESTING){ - mHandleSelectionOnActionUp = true; - } - break; - - case MotionEvent.ACTION_CANCEL: - mDown.x = -1; - mDown.y = -1; - break; - case MotionEvent.ACTION_UP: - //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates - if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ - final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); - if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); - } - // Release the drag - mHandleSelectionOnActionUp = false; - mDown.x = -1; - mDown.y = -1; - - mTouchState = TOUCH_STATE_RESTING; - clearChildrenCache(); - break; - } - - return mTouchState == TOUCH_STATE_SCROLLING; - - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - if (mVelocityTracker == null) { - mVelocityTracker = VelocityTracker.obtain(); - } - mVelocityTracker.addMovement(event); - - final int action = event.getAction(); - final float x = event.getX(); - final float y = event.getY(); - - switch (action) { - case MotionEvent.ACTION_DOWN: - /* - * If being flinged and user touches, stop the fling. isFinished - * will be false if being flinged. - */ - if (!mScroller.isFinished()) { - mScroller.forceFinished(true); - } - - // Remember where the motion event started - mLastMotionX = x; - mLastMotionY = y; - - break; - case MotionEvent.ACTION_MOVE: - - if (mTouchState == TOUCH_STATE_SCROLLING) { - // Scroll to follow the motion event - final int deltaX = (int) (mLastMotionX - x); - final int deltaY = (int) (mLastMotionY - y); - mLastMotionX = x; - mLastMotionY = y; - - scrollByDelta(deltaX, deltaY); - } - else{ - final int xDiff = (int) Math.abs(x - mLastMotionX); - final int yDiff = (int) Math.abs(y - mLastMotionY); - - final int touchSlop = mTouchSlop; - final boolean xMoved = xDiff > touchSlop; - final boolean yMoved = yDiff > touchSlop; - - - if (xMoved || yMoved) { - // Scroll if the user moved far enough along the axis - mTouchState = TOUCH_STATE_SCROLLING; - enableChildrenCache(); - cancelLongPress(); - } - } - break; - case MotionEvent.ACTION_UP: - - //this must be here, in case no child view returns true, - //events will propagate back here and on intercept touch event wont be called again - //in case of no parent it propagates here, in case of parent it usually propagates to on cancel - if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ - final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); - if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); - mHandleSelectionOnActionUp = false; - } - - //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates - if (mTouchState == TOUCH_STATE_SCROLLING) { - - mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int initialXVelocity = (int) mVelocityTracker.getXVelocity(); - int initialYVelocity = (int) mVelocityTracker.getYVelocity(); - - if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { - fling(-initialXVelocity, -initialYVelocity); - } - else{ - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_RESTING; - - mDown.x = -1; - mDown.y = -1; - } - - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - - break; - } - - // Release the drag - clearChildrenCache(); - mTouchState = TOUCH_STATE_RESTING; - - mDown.x = -1; - mDown.y = -1; - - break; - case MotionEvent.ACTION_CANCEL: - mTouchState = TOUCH_STATE_RESTING; - } - - return true; - } - - - @Override - public void computeScroll() { - if (mScroller.computeScrollOffset()) { - if(mScroller.getFinalX() == mScroller.getCurrX()){ - mScroller.abortAnimation(); - mTouchState = TOUCH_STATE_RESTING; - mScrollDirection = NO_VALUE; - clearChildrenCache(); - } - else{ - final int x = mScroller.getCurrX(); - final int y = mScroller.getCurrY(); - scrollTo(x, y); - - postInvalidate(); - } - } - else if(mTouchState == TOUCH_STATE_FLING){ - mTouchState = TOUCH_STATE_RESTING; - mScrollDirection = NO_VALUE; - clearChildrenCache(); - } - - removeNonVisibleViews(); - if(mScrollDirection == DIRECTION_LEFT) refillLeftSide(); - if(mScrollDirection == DIRECTION_RIGHT) refillRightSide(); - - if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder(); - } - - public void fling(int velocityX, int velocityY){ - mTouchState = TOUCH_STATE_FLING; - final int x = getScrollX(); - final int y = getScrollY(); - final int rightInPixels = dspToPx(mAdapter.getEnd()); - final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; - - mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth(),0,bottomInPixels - getHeight()); - - if(velocityX < 0) { - mScrollDirection = DIRECTION_LEFT; - } - else if(velocityX > 0) { - mScrollDirection = DIRECTION_RIGHT; - } - - - invalidate(); - } - - protected void scrollByDelta(int deltaX, int deltaY){ - final int rightInPixels = dspToPx(mAdapter.getEnd()); - final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; - final int x = getScrollX() + deltaX; - final int y = getScrollY() + deltaY; - - if(x < 0 ) deltaX -= x; - else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth()); - - if(y < 0 ) deltaY -= y; - else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight()); - - if(deltaX < 0) { - mScrollDirection = DIRECTION_LEFT; - } - else { - mScrollDirection = DIRECTION_RIGHT; - } - - scrollBy(deltaX, deltaY); - } - - protected void handleClick(Point p){ - final int c = getChildCount(); - View v; - final Rect r = new Rect(); - for(int i=0; i < c; i++){ - v = getChildAt(i); - v.getHitRect(r); - if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ - if(mItemClickListener != null) mItemClickListener.onItemClick(v); - } - } - } - - /** - * Returns current Adapter with backing data - */ - public Adapter getAdapter() { - return mAdapter; - } - - /** - * Set Adapter with backing data - */ - public void setAdapter(Adapter adapter) { - this.mAdapter = adapter; - requestLayout(); - } - - /** - * Set listener which will fire if item in container is clicked - */ - public void setOnItemClickListener(OnItemClickListener itemClickListener) { - this.mItemClickListener = itemClickListener; - } - - private void enableChildrenCache() { - setChildrenDrawingCacheEnabled(true); - setChildrenDrawnWithCacheEnabled(true); - } - - private void clearChildrenCache() { - setChildrenDrawnWithCacheEnabled(false); - } - - /** - * In GRID_MODE_FIXED_SIZE mode has one dsp dimension set by setDspSize(), If band height is after transformation to normal pixels bigger than - * available space, content becomes scrollable also vertically. - * - * In GRID_MODE_DYNAMIC_SIZE is dsp dimension computed from measured height and band height to always - */ - public void setGridMode(int mode){ - mGridMode = mode; - } - - /** - * Specifies how many normal pixels is in length of one device specific pixel - * This method is significant only in GRID_MODE_FIXED_SIZE mode (use setGridMode) - */ - public void setDspSize(int pixels){ - mDspPixelRatio = pixels; - } - - /** - * Set to true if you want component to work with tile z parameter; - * If you don't have any overlapping view, leave it on default false, because computing - * with z order makes rendering slower. - */ - public void setZOrderEnabled(boolean enable){ - mIsZOrderEnabled = enable; - setChildrenDrawingOrderEnabled(enable); - } - - @Override - protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(); - } - - @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { - return new LayoutParams(); - } - - @Override - protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { - return p instanceof LayoutParams; - } - -//----------------CONTENT BAND END-------------------------------------------------------------------- - - public interface OnItemClickListener{ - void onItemClick(View v); - } - - public interface Adapter { - - /** - * Return Views which have left edge in device specific coordinates in range from-to, - * @param from inclusive - * @param to exclusive - */ - public abstract View[] getViewsByLeftSideRange(int from, int to); - - /** - * Return Views which have right edge in device specific coordinates in range from-to, - * @param from exclusive - * @param to inclusive - */ - public abstract View[] getViewsByRightSideRange(int from, int to); - - /** - * @return Right Coordinate of last tile in DSP - */ - int getEnd(); - - /** - * @return Bottom Coordinate of tiles on bottom edge in DSP, Must be > 0 - * - */ - int getBottom(); - - /** - * @return total number of tiles - */ - public int getCount(); - - /** - * Makes union between View returned by left side and right side ranges - * Needed for initialization of component - */ - public abstract View[] getViewsVisibleInRange(int from, int to); - - /** - * Puts View, which is not needed anymore back to Adapter. View will be used later instead of creating or inflating same view. - */ - public void offerViewForRecycling(View view); - } - - - public static class LayoutParams extends ViewGroup.LayoutParams{ - public int tileId; - public int dspLeft; - public int dspTop; - public int dspWidth; - public int dspHeight; - public int z; - - private int viewgroupIndex; - - public LayoutParams() { - super(NO_VALUE, NO_VALUE); - } - - public int getDspRight(){ - return dspLeft + dspWidth; - } - } - - - - public static abstract class AbstractAdapter implements Adapter{ - private final ViewCache mViewCache = new ViewCache(); - - protected ArrayList mTilesByBegining; - protected ArrayList mTilesByEnd; -// protected SparseArray mTilesByNumber; - protected IDataListener mChangeListener; - - public AbstractAdapter(){} - public AbstractAdapter(ArrayList tiles){ - initWithNewData(tiles); - } - - private final Comparator beginingComparator = new Comparator() { - @Override - public int compare(Tile o1, Tile o2) { - if(o1.getX() == o2.getX()) return 0; - else if(o1.getX() < o2.getX()) return -1; - else return 1; - } - }; - - private final Comparator endComparator = new Comparator() { - @Override - public int compare(Tile o1, Tile o2) { - if(o1.getXRight() == o2.getXRight()) return 0; - else if(o1.getXRight() < o2.getXRight()) return -1; - else return 1; - } - }; - - @SuppressWarnings("unchecked") - @Override - public void offerViewForRecycling(View view){ - mViewCache.cacheView((V) view); - } - - - /** - * Use getLayoutParamsForTile to get correct layout params for Tile data and set them with setLayoutParams before returning View - * @param t Tile data from datamodel - * @param recycled View no more used and returned for recycling. Use together with ViewHolder pattern to avoid performance loss - * in inflating and searching by ids in more complex xml layouts. - * @return View which will be displayed in component using layout data from Tile - * - *
-		 * 	public ImageView getViewForTile(Tile t, ImageView recycled) { 
-		 * 		ImageView iw;
-		 *		if(recycled != null) iw = recycled;
-		 * 		else iw = new ImageView(MainActivity.this);
-		 *
-		 *		iw.setLayoutParams(getLayoutParamsForTile(t));
-		 *		return iw;
-		 *	}
-		 * 
- */ - public abstract V getViewForTile(Tile t, V recycled); - - /** - * @return total number of tiles - */ - public int getCount(){ - return mTilesByBegining.size(); - } - - public int getEnd(){ - if(mTilesByEnd.size() > 0)return mTilesByEnd.get(mTilesByEnd.size()-1).getXRight(); - else return 0; - } - - private void checkAndFixLayoutParams(View v, Tile t){ - if(!(v.getLayoutParams() instanceof LayoutParams)) v.setLayoutParams(getLayoutParamsForTile(t)); - } - - @Override - public View[] getViewsByLeftSideRange(int from, int to) { - if(from == to) return new View[0]; - final List list = getTilesWithLeftRange(from, to); - - final View[] arr = new View[list.size()]; - for(int i=0; i < arr.length; i++){ - Tile t = list.get(i); - arr[i] = getViewForTile(t, mViewCache.getCachedView()); - checkAndFixLayoutParams(arr[i], t); - } - - return arr; - } - - @Override - public View[] getViewsByRightSideRange(int from, int to) { - if(from == to) return new View[0]; - final List list = getTilesWithRightRange(from, to); - - final View[] arr = new View[list.size()]; - for(int i=0; i < arr.length; i++){ - Tile t = list.get(i); - arr[i] = getViewForTile(t, mViewCache.getCachedView()); - checkAndFixLayoutParams(arr[i], t); - } - - return arr; - } - - public View[] getViewsVisibleInRange(int from, int to){ - final List listLeft = getTilesWithLeftRange(from, to); - final List listRight = getTilesWithRightRange(from, to); - - ArrayList union = ToolBox.union(listLeft, listRight); - - final View[] arr = new View[union.size()]; - for(int i=0; i < arr.length; i++){ - Tile t = union.get(i); - arr[i] = getViewForTile(t, mViewCache.getCachedView()); - checkAndFixLayoutParams(arr[i], t); - } - - return arr; - } - - public void setTiles(ArrayList tiles) { - initWithNewData(tiles); - if(mChangeListener != null) mChangeListener.onDataSetChanged(); - } - - public void setDataChangeListener(IDataListener listener){ - mChangeListener = listener; - } - - @SuppressWarnings("unchecked") - protected void initWithNewData(ArrayList tiles){ - mTilesByBegining = (ArrayList) tiles.clone(); - - Collections.sort(mTilesByBegining, beginingComparator); - - mTilesByEnd = (ArrayList) mTilesByBegining.clone(); - Collections.sort(mTilesByEnd, endComparator); - } - - /** - * @param from inclusive - * @param to exclusive - */ - public List getTilesWithLeftRange(int from, int to){ - if(mTilesByBegining.size() == 0) return Collections.emptyList(); - final int fromIndex = binarySearchLeftEdges(from); - if(mTilesByBegining.get(fromIndex).getX() > to) return Collections.emptyList(); - - int i = fromIndex; - Tile t = mTilesByBegining.get(i); - while(t.getX() < to){ - i++; - if(i < mTilesByBegining.size())t = mTilesByBegining.get(i); - else break; - } - - return mTilesByBegining.subList(fromIndex, i); - } - - /** - * - * @param from exclusive - * @param to inclusive - */ - public List getTilesWithRightRange(int from, int to){ - if(mTilesByEnd.size() == 0) return Collections.emptyList(); - - final int fromIndex = binarySearchRightEdges(from + 1); //from is exclusive - final int fromRight = mTilesByEnd.get(fromIndex).getXRight(); - - if(fromRight > to) return Collections.emptyList(); - - int i = fromIndex; - Tile t = mTilesByEnd.get(i); - while(t.getXRight() <= to){ - i++; - if(i < mTilesByEnd.size()) t = mTilesByEnd.get(i); - else break; - } - - return mTilesByEnd.subList(fromIndex, i); - } - - /** Continues to split same values until it rests on first of them - * returns first tile with left equal than value or greater - */ - private int binarySearchLeftEdges(int value){ - int lo = 0; - int hi = mTilesByBegining.size() - 1; - int mid = 0; - Tile t = null; - while (lo <= hi) { - // Key is in a[lo..hi] or not present. - mid = lo + (hi - lo) / 2; - t = mTilesByBegining.get(mid); - - if (value > t.getX()) lo = mid + 1; - else hi = mid - 1; - - } - - while(t != null && t.getX() < value && mid < mTilesByBegining.size()-1){ - mid++; - t = mTilesByBegining.get(mid); - } - - return mid; - } - - /** Continues to split same values until it rests on first of them - * returns first tile with right equal than value or greater - */ - private int binarySearchRightEdges(int value){ - int lo = 0; - int hi = mTilesByEnd.size() - 1; - int mid = 0; - Tile t = null; - while (lo <= hi) { - // Key is in a[lo..hi] or not present. - mid = lo + (hi - lo) / 2; - t = mTilesByEnd.get(mid); - - final int r = t.getXRight(); - if (value > r) lo = mid + 1; - else hi = mid - 1; - } - - while(t != null && t.getXRight() < value && mid < mTilesByEnd.size()-1){ - mid++; - t = mTilesByEnd.get(mid); - } - - return mid; - } - - - - /** - * Use this in getViewForTile implementation to provide correctly initialized layout params for component - * @param t Tile data from datamodel - * @return ContendBand layout params - */ - public LayoutParams getLayoutParamsForTile(Tile t){ - LayoutParams lp = new LayoutParams(); - lp.tileId = t.getId(); - lp.dspLeft = t.getX(); - lp.dspTop = t.getY(); - lp.dspWidth = t.getWidth(); - lp.dspHeight = t.getHeight(); - lp.z = t.getZ(); - return lp; - } - - - interface IDataListener { - void onDataSetChanged(); - } - - } - - private static class ViewCache { - final LinkedList> mCachedItemViews = new LinkedList>(); - - /** - * Check if list of weak references has any view still in memory to offer for recycling - * @return cached view - */ - T getCachedView(){ - if (mCachedItemViews.size() != 0) { - T v; - do{ - v = mCachedItemViews.removeFirst().get(); - } - while(v == null && mCachedItemViews.size() != 0); - return v; - } - return null; - } - - void cacheView(T v){ - WeakReference ref = new WeakReference(v); - mCachedItemViews.addLast(ref); - } - } - -} +package it.moondroid.coverflow.components.ui.containers.contentbands; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.widget.Scroller; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +import it.moondroid.coverflow.R; +import it.moondroid.coverflow.components.general.ToolBox; +import it.moondroid.coverflow.components.general.Validate; + + +/** + * @author Martin Appl + * + * Horizontally scrollable container with boundaries on the ends, which places Views on coordinates specified + * by tile objects. Data binding is specified by adapter interface. Use abstract adapter which has already implemented + * algorithms for searching views in requested ranges. You only need to implement getViewForTile method where you map + * Tile objects from dataset to corresponding View objects, which get displayed. Position on screen is described by LayoutParams object. + * Method getLayoutParamsForTile helps generate layout params from data objects. If you don't set Layout params in getViewForTile, this + * methods is called automatically afterwards. + * + * DSP = device specific pixel + */ +public class BasicContentBand extends ViewGroup { + //CONSTANTS +// private static final String LOG_TAG = "Basic_ContentBand_Component"; + private static final int NO_VALUE = -11; + private static final int DSP_DEFAULT = 10; + + /** User is not touching the list */ + protected static final int TOUCH_STATE_RESTING = 0; + + /** User is scrolling the list */ + protected static final int TOUCH_STATE_SCROLLING = 1; + + /** Fling gesture in progress */ + protected static final int TOUCH_STATE_FLING = 2; + + /** + * In this mode we have pixel size of DSP specified, if dspHeight is bigger than window, content band can be scrolled vertically. + */ + public static final int GRID_MODE_FIXED_SIZE = 0; + /** + * In this mode is pixel size of DSP calculated dynamically, based on widget height in pixels and value of dspHeight which is fixed + * and taken from adapters getBottom method + */ + public static final int GRID_MODE_DYNAMIC_SIZE = 1; + + //to which direction on X axis are window coordinates sliding + protected static final int DIRECTION_RIGHT = 0; + protected static final int DIRECTION_LEFT = 1; + + + //VARIABLES + protected Adapter mAdapter; + private int mGridMode = GRID_MODE_DYNAMIC_SIZE; + /**How many normal pixels corresponds to one DSP pixel*/ + private int mDspPixelRatio = DSP_DEFAULT; + private int mDspHeight = NO_VALUE; + protected int mDspHeightModulo; + //refilling + protected int mCurrentlyLayoutedViewsLeftEdgeDsp; + protected int mCurrentlyLayoutedViewsRightEdgeDsp; + private final ArrayList mTempViewArray = new ArrayList(); + //touch, scrolling + protected int mTouchState = TOUCH_STATE_RESTING; + private float mLastMotionX; + private float mLastMotionY; + private final Point mDown = new Point(); + private VelocityTracker mVelocityTracker; + protected final Scroller mScroller; + private boolean mHandleSelectionOnActionUp = false; + protected int mScrollDirection = NO_VALUE; + //constant values + private final int mTouchSlop; + private final int mMinimumVelocity; + private final int mMaximumVelocity; +// private final Rect mTempRect = new Rect(); + + private boolean mIsZOrderEnabled; + private int[] mDrawingOrderArray; + + //listeners + private OnItemClickListener mItemClickListener; + + public BasicContentBand(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mScroller = new Scroller(context); + + if(attrs != null){ + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BasicContentBand, defStyle, 0); + + mDspPixelRatio = a.getInteger(R.styleable.BasicContentBand_deviceSpecificPixelSize, mDspPixelRatio); + mGridMode = a.getInteger(R.styleable.BasicContentBand_gridMode, mGridMode); + + a.recycle(); + } + + + } + + public BasicContentBand(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BasicContentBand(Context context) { + this(context,null); + } + + protected int dspToPx(int dsp){ + return dsp * mDspPixelRatio; + } + + protected int pxToDsp(int px){ + return px / mDspPixelRatio; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); + + if(mAdapter != null){ + mDspHeight = mAdapter.getBottom(); + Validate.isTrue(mDspHeight > 0, "Adapter getBottom must return value greater than zero"); + } + else{ + setMeasuredDimension(widthSpecSize, heightSpecSize); + return; + } + + int measuredWidth, measuredHeight; + if(mGridMode == GRID_MODE_FIXED_SIZE){ + /*HEIGHT*/ + measuredHeight = mDspPixelRatio * mDspHeight; + + if(heightSpecMode == MeasureSpec.AT_MOST){ + if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize; + } + else if(heightSpecMode == MeasureSpec.EXACTLY){ + measuredHeight = heightSpecSize; + } + + /*WIDTH*/ + measuredWidth = widthSpecSize; + if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio; + + if(widthSpecMode == MeasureSpec.AT_MOST){ + if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize; + } + else if(widthSpecMode == MeasureSpec.EXACTLY){ + measuredWidth = widthSpecSize; + } + } + else{ + if (heightSpecMode == MeasureSpec.UNSPECIFIED) { + throw new RuntimeException("Can not have unspecified hight dimension in dynamic grid mode"); + } + /*HEIGHT*/ + measuredHeight = heightSpecSize; + + mDspPixelRatio = measuredHeight / mDspHeight; + mDspHeightModulo = measuredHeight % mDspHeight; + + measuredHeight = mDspPixelRatio * mDspHeight; + + if(heightSpecMode == MeasureSpec.AT_MOST){ + if(measuredHeight > heightSpecSize) measuredHeight = heightSpecSize; + else mDspHeightModulo = 0; + } + else if(heightSpecMode == MeasureSpec.EXACTLY){ + measuredHeight = heightSpecSize; + } + + /*WIDTH*/ + measuredWidth = widthSpecSize; + if(mAdapter != null) measuredWidth = mAdapter.getEnd() * mDspPixelRatio; + + if(widthSpecMode == MeasureSpec.AT_MOST){ + if(measuredWidth > widthSpecSize) measuredWidth = widthSpecSize; + } + else if(widthSpecMode == MeasureSpec.EXACTLY){ + measuredWidth = widthSpecSize; + } + + } + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int c = getChildCount(); + + if(c == 0) { + fillEmptyContainer(); + c = getChildCount(); + } + + 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 +// if(((LayoutParams)getChildAt(j).getLayoutParams()).tileNumber == lp.tileNumber) { +// arr[i] = null; +// nullCounter++; +// break; +// } +// } +// } +// +// final View[] res = new View[arr.length - nullCounter]; +// for(int i=0,j=0; i comparator = new Comparator() { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams l = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams r = (LayoutParams) rhs.getLayoutParams(); + + if(l.z == r.z) return 0; + else if(l.z < r.z) return -1; + else return 1; + } + }; + + Arrays.sort(tempArr, comparator); + mDrawingOrderArray = new int[tempArr.length]; + for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight(); + if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; + addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); + } + + if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder(); + + mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; + mCurrentlyLayoutedViewsRightEdgeDsp= dspMostRight; + } + + /** + * Checks and refills empty area on the left edge of screen + */ + protected void refillLeftSide(){ + if(mAdapter == null) return; + + final int leftScreenEdge = getScrollX(); + final int dspLeftScreenEdge = pxToDsp(leftScreenEdge); + final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp; + + if(dspLeftScreenEdge >= dspNextViewsRight) return; +// Logger.d(LOG_TAG, "from " + dspLeftScreenEdge + ", to " + dspNextViewsRight); + + View[] list = mAdapter.getViewsByRightSideRange(dspLeftScreenEdge, dspNextViewsRight); +// list = filterAlreadyPresentViews(list); + + int dspMostLeft = dspNextViewsRight; + LayoutParams lp; + for(int i=0; i < list.length; i++){ + lp = (LayoutParams) list[i].getLayoutParams(); + if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; + addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); + } + + if(list.length > 0){ + layoutNewChildren(list); + } + + mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; + } + + /** + * Checks and refills empty area on the right + */ + protected void refillRightSide(){ + if(mAdapter == null) return; + + final int rightScreenEdge = getScrollX() + getWidth(); + final int dspNextAddedViewsLeft = mCurrentlyLayoutedViewsRightEdgeDsp; + + int dspRightScreenEdge = pxToDsp(rightScreenEdge) + 1; + if(dspRightScreenEdge > mAdapter.getEnd()) dspRightScreenEdge = mAdapter.getEnd(); + + if(dspNextAddedViewsLeft >= dspRightScreenEdge) return; + + View[] list = mAdapter.getViewsByLeftSideRange(dspNextAddedViewsLeft, dspRightScreenEdge); +// list = filterAlreadyPresentViews(list); + + int dspMostRight = 0; + LayoutParams lp; + for(int i=0; i < list.length; i++){ + lp = (LayoutParams) list[i].getLayoutParams(); + if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight(); + addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); + } + + if(list.length > 0){ + layoutNewChildren(list); + } + + mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; + } + + /** + * Remove non visible views laid out of the screen + */ + private void removeNonVisibleViews(){ + if(getChildCount() == 0) return; + + final int leftScreenEdge = getScrollX(); + final int rightScreenEdge = leftScreenEdge + getWidth(); + + int dspRightScreenEdge = pxToDsp(rightScreenEdge); + if(dspRightScreenEdge >= 0) dspRightScreenEdge++; //to avoid problem with rounding of values + + int dspLeftScreenEdge = pxToDsp(leftScreenEdge); + if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger + + mTempViewArray.clear(); + View v; + for(int i=0; i dspMostRight) dspMostRight = lp.getDspRight(); + if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; + } + + mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; + mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; + + } + + //check if View with specified LayoutParams is currently on screen + private boolean isOnScreen(LayoutParams lp, int dspLeftScreenEdge, int dspRightScreenEdge){ + final int left = lp.dspLeft; + final int right = left + lp.dspWidth; + + if(right > dspLeftScreenEdge && left < dspRightScreenEdge) return true; + else return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onTouchEvent will be called and we do the actual + * scrolling there. + */ + + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { + return true; + } + + final float x = ev.getX(); + final float y = ev.getY(); + switch (action) { + case MotionEvent.ACTION_MOVE: + /* + * not dragging, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionX is set to the x value + * of the down event. + */ + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = mTouchSlop; + final boolean xMoved = xDiff > touchSlop; + final boolean yMoved = yDiff > touchSlop; + + + if (xMoved || yMoved) { + // Scroll if the user moved far enough along the axis + mTouchState = TOUCH_STATE_SCROLLING; + mHandleSelectionOnActionUp = false; + enableChildrenCache(); + cancelLongPress(); + } + + break; + + case MotionEvent.ACTION_DOWN: + // Remember location of down touch + mLastMotionX = x; + + mDown.x = (int) x; + mDown.y = (int) y; + + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mTouchState = mScroller.isFinished() ? TOUCH_STATE_RESTING : TOUCH_STATE_SCROLLING; + //if he had normal click in rested state, remember for action up check + if(mTouchState == TOUCH_STATE_RESTING){ + mHandleSelectionOnActionUp = true; + } + break; + + case MotionEvent.ACTION_CANCEL: + mDown.x = -1; + mDown.y = -1; + break; + case MotionEvent.ACTION_UP: + //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates + if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ + final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); + if((ev.getEventTime() - ev.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); + } + // Release the drag + mHandleSelectionOnActionUp = false; + mDown.x = -1; + mDown.y = -1; + + mTouchState = TOUCH_STATE_RESTING; + clearChildrenCache(); + break; + } + + return mTouchState == TOUCH_STATE_SCROLLING; + + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(event); + + final int action = event.getAction(); + final float x = event.getX(); + final float y = event.getY(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.forceFinished(true); + } + + // Remember where the motion event started + mLastMotionX = x; + mLastMotionY = y; + + break; + case MotionEvent.ACTION_MOVE: + + if (mTouchState == TOUCH_STATE_SCROLLING) { + // Scroll to follow the motion event + final int deltaX = (int) (mLastMotionX - x); + final int deltaY = (int) (mLastMotionY - y); + mLastMotionX = x; + mLastMotionY = y; + + scrollByDelta(deltaX, deltaY); + } + else{ + final int xDiff = (int) Math.abs(x - mLastMotionX); + final int yDiff = (int) Math.abs(y - mLastMotionY); + + final int touchSlop = mTouchSlop; + final boolean xMoved = xDiff > touchSlop; + final boolean yMoved = yDiff > touchSlop; + + + if (xMoved || yMoved) { + // Scroll if the user moved far enough along the axis + mTouchState = TOUCH_STATE_SCROLLING; + enableChildrenCache(); + cancelLongPress(); + } + } + break; + case MotionEvent.ACTION_UP: + + //this must be here, in case no child view returns true, + //events will propagate back here and on intercept touch event wont be called again + //in case of no parent it propagates here, in case of parent it usually propagates to on cancel + if(mHandleSelectionOnActionUp && mTouchState == TOUCH_STATE_RESTING){ + final float d = ToolBox.getLineLength(mDown.x, mDown.y, x, y); + if((event.getEventTime() - event.getDownTime()) < ViewConfiguration.getLongPressTimeout() && d < mTouchSlop) handleClick(mDown); + mHandleSelectionOnActionUp = false; + } + + //if we had normal down click and we haven't moved enough to initiate drag, take action as a click on down coordinates + if (mTouchState == TOUCH_STATE_SCROLLING) { + + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialXVelocity = (int) mVelocityTracker.getXVelocity(); + int initialYVelocity = (int) mVelocityTracker.getYVelocity(); + + if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) { + fling(-initialXVelocity, -initialYVelocity); + } + else{ + // Release the drag + clearChildrenCache(); + mTouchState = TOUCH_STATE_RESTING; + + mDown.x = -1; + mDown.y = -1; + } + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + + break; + } + + // Release the drag + clearChildrenCache(); + mTouchState = TOUCH_STATE_RESTING; + + mDown.x = -1; + mDown.y = -1; + + break; + case MotionEvent.ACTION_CANCEL: + mTouchState = TOUCH_STATE_RESTING; + } + + return true; + } + + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + if(mScroller.getFinalX() == mScroller.getCurrX()){ + mScroller.abortAnimation(); + mTouchState = TOUCH_STATE_RESTING; + mScrollDirection = NO_VALUE; + clearChildrenCache(); + } + else{ + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + scrollTo(x, y); + + postInvalidate(); + } + } + else if(mTouchState == TOUCH_STATE_FLING){ + mTouchState = TOUCH_STATE_RESTING; + mScrollDirection = NO_VALUE; + clearChildrenCache(); + } + + removeNonVisibleViews(); + if(mScrollDirection == DIRECTION_LEFT) refillLeftSide(); + if(mScrollDirection == DIRECTION_RIGHT) refillRightSide(); + + if(mIsZOrderEnabled) rearrangeViewsAccordingZOrder(); + } + + public void fling(int velocityX, int velocityY){ + mTouchState = TOUCH_STATE_FLING; + final int x = getScrollX(); + final int y = getScrollY(); + final int rightInPixels = dspToPx(mAdapter.getEnd()); + final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; + + mScroller.fling(x, y, velocityX, velocityY, 0,rightInPixels - getWidth(),0,bottomInPixels - getHeight()); + + if(velocityX < 0) { + mScrollDirection = DIRECTION_LEFT; + } + else if(velocityX > 0) { + mScrollDirection = DIRECTION_RIGHT; + } + + + invalidate(); + } + + protected void scrollByDelta(int deltaX, int deltaY){ + final int rightInPixels = dspToPx(mAdapter.getEnd()); + final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; + final int x = getScrollX() + deltaX; + final int y = getScrollY() + deltaY; + + if(x < 0 ) deltaX -= x; + else if(x > rightInPixels - getWidth()) deltaX -= x - (rightInPixels - getWidth()); + + if(y < 0 ) deltaY -= y; + else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight()); + + if(deltaX < 0) { + mScrollDirection = DIRECTION_LEFT; + } + else { + mScrollDirection = DIRECTION_RIGHT; + } + + scrollBy(deltaX, deltaY); + } + + protected void handleClick(Point p){ + final int c = getChildCount(); + View v; + final Rect r = new Rect(); + for(int i=0; i < c; i++){ + v = getChildAt(i); + v.getHitRect(r); + if(r.contains(getScrollX() + p.x, getScrollY() + p.y)){ + if(mItemClickListener != null) mItemClickListener.onItemClick(v); + } + } + } + + /** + * Returns current Adapter with backing data + */ + public Adapter getAdapter() { + return mAdapter; + } + + /** + * Set Adapter with backing data + */ + public void setAdapter(Adapter adapter) { + this.mAdapter = adapter; + requestLayout(); + } + + /** + * Set listener which will fire if item in container is clicked + */ + public void setOnItemClickListener(OnItemClickListener itemClickListener) { + this.mItemClickListener = itemClickListener; + } + + private void enableChildrenCache() { + setChildrenDrawingCacheEnabled(true); + setChildrenDrawnWithCacheEnabled(true); + } + + private void clearChildrenCache() { + setChildrenDrawnWithCacheEnabled(false); + } + + /** + * In GRID_MODE_FIXED_SIZE mode has one dsp dimension set by setDspSize(), If band height is after transformation to normal pixels bigger than + * available space, content becomes scrollable also vertically. + * + * In GRID_MODE_DYNAMIC_SIZE is dsp dimension computed from measured height and band height to always + */ + public void setGridMode(int mode){ + mGridMode = mode; + } + + /** + * Specifies how many normal pixels is in length of one device specific pixel + * This method is significant only in GRID_MODE_FIXED_SIZE mode (use setGridMode) + */ + public void setDspSize(int pixels){ + mDspPixelRatio = pixels; + } + + /** + * Set to true if you want component to work with tile z parameter; + * If you don't have any overlapping view, leave it on default false, because computing + * with z order makes rendering slower. + */ + public void setZOrderEnabled(boolean enable){ + mIsZOrderEnabled = enable; + setChildrenDrawingOrderEnabled(enable); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + +//----------------CONTENT BAND END-------------------------------------------------------------------- + + public interface OnItemClickListener{ + void onItemClick(View v); + } + + public interface Adapter { + + /** + * Return Views which have left edge in device specific coordinates in range from-to, + * @param from inclusive + * @param to exclusive + */ + public abstract View[] getViewsByLeftSideRange(int from, int to); + + /** + * Return Views which have right edge in device specific coordinates in range from-to, + * @param from exclusive + * @param to inclusive + */ + public abstract View[] getViewsByRightSideRange(int from, int to); + + /** + * @return Right Coordinate of last tile in DSP + */ + int getEnd(); + + /** + * @return Bottom Coordinate of tiles on bottom edge in DSP, Must be > 0 + * + */ + int getBottom(); + + /** + * @return total number of tiles + */ + public int getCount(); + + /** + * Makes union between View returned by left side and right side ranges + * Needed for initialization of component + */ + public abstract View[] getViewsVisibleInRange(int from, int to); + + /** + * Puts View, which is not needed anymore back to Adapter. View will be used later instead of creating or inflating same view. + */ + public void offerViewForRecycling(View view); + } + + + public static class LayoutParams extends ViewGroup.LayoutParams{ + public int tileId; + public int dspLeft; + public int dspTop; + public int dspWidth; + public int dspHeight; + public int z; + + private int viewgroupIndex; + + public LayoutParams() { + super(NO_VALUE, NO_VALUE); + } + + public int getDspRight(){ + return dspLeft + dspWidth; + } + } + + + + public static abstract class AbstractAdapter implements Adapter{ + private final ViewCache mViewCache = new ViewCache(); + + protected ArrayList mTilesByBegining; + protected ArrayList mTilesByEnd; +// protected SparseArray mTilesByNumber; + protected IDataListener mChangeListener; + + public AbstractAdapter(){} + public AbstractAdapter(ArrayList tiles){ + initWithNewData(tiles); + } + + private final Comparator beginingComparator = new Comparator() { + @Override + public int compare(Tile o1, Tile o2) { + if(o1.getX() == o2.getX()) return 0; + else if(o1.getX() < o2.getX()) return -1; + else return 1; + } + }; + + private final Comparator endComparator = new Comparator() { + @Override + public int compare(Tile o1, Tile o2) { + if(o1.getXRight() == o2.getXRight()) return 0; + else if(o1.getXRight() < o2.getXRight()) return -1; + else return 1; + } + }; + + @SuppressWarnings("unchecked") + @Override + public void offerViewForRecycling(View view){ + mViewCache.cacheView((V) view); + } + + + /** + * Use getLayoutParamsForTile to get correct layout params for Tile data and set them with setLayoutParams before returning View + * @param t Tile data from datamodel + * @param recycled View no more used and returned for recycling. Use together with ViewHolder pattern to avoid performance loss + * in inflating and searching by ids in more complex xml layouts. + * @return View which will be displayed in component using layout data from Tile + * + *
+		 * 	public ImageView getViewForTile(Tile t, ImageView recycled) { 
+		 * 		ImageView iw;
+		 *		if(recycled != null) iw = recycled;
+		 * 		else iw = new ImageView(MainActivity.this);
+		 *
+		 *		iw.setLayoutParams(getLayoutParamsForTile(t));
+		 *		return iw;
+		 *	}
+		 * 
+ */ + public abstract V getViewForTile(Tile t, V recycled); + + /** + * @return total number of tiles + */ + public int getCount(){ + return mTilesByBegining.size(); + } + + public int getEnd(){ + if(mTilesByEnd.size() > 0)return mTilesByEnd.get(mTilesByEnd.size()-1).getXRight(); + else return 0; + } + + private void checkAndFixLayoutParams(View v, Tile t){ + if(!(v.getLayoutParams() instanceof LayoutParams)) v.setLayoutParams(getLayoutParamsForTile(t)); + } + + @Override + public View[] getViewsByLeftSideRange(int from, int to) { + if(from == to) return new View[0]; + final List list = getTilesWithLeftRange(from, to); + + final View[] arr = new View[list.size()]; + for(int i=0; i < arr.length; i++){ + Tile t = list.get(i); + arr[i] = getViewForTile(t, mViewCache.getCachedView()); + checkAndFixLayoutParams(arr[i], t); + } + + return arr; + } + + @Override + public View[] getViewsByRightSideRange(int from, int to) { + if(from == to) return new View[0]; + final List list = getTilesWithRightRange(from, to); + + final View[] arr = new View[list.size()]; + for(int i=0; i < arr.length; i++){ + Tile t = list.get(i); + arr[i] = getViewForTile(t, mViewCache.getCachedView()); + checkAndFixLayoutParams(arr[i], t); + } + + return arr; + } + + public View[] getViewsVisibleInRange(int from, int to){ + final List listLeft = getTilesWithLeftRange(from, to); + final List listRight = getTilesWithRightRange(from, to); + + ArrayList union = ToolBox.union(listLeft, listRight); + + final View[] arr = new View[union.size()]; + for(int i=0; i < arr.length; i++){ + Tile t = union.get(i); + arr[i] = getViewForTile(t, mViewCache.getCachedView()); + checkAndFixLayoutParams(arr[i], t); + } + + return arr; + } + + public void setTiles(ArrayList tiles) { + initWithNewData(tiles); + if(mChangeListener != null) mChangeListener.onDataSetChanged(); + } + + public void setDataChangeListener(IDataListener listener){ + mChangeListener = listener; + } + + @SuppressWarnings("unchecked") + protected void initWithNewData(ArrayList tiles){ + mTilesByBegining = (ArrayList) tiles.clone(); + + Collections.sort(mTilesByBegining, beginingComparator); + + mTilesByEnd = (ArrayList) mTilesByBegining.clone(); + Collections.sort(mTilesByEnd, endComparator); + } + + /** + * @param from inclusive + * @param to exclusive + */ + public List getTilesWithLeftRange(int from, int to){ + if(mTilesByBegining.size() == 0) return Collections.emptyList(); + final int fromIndex = binarySearchLeftEdges(from); + if(mTilesByBegining.get(fromIndex).getX() > to) return Collections.emptyList(); + + int i = fromIndex; + Tile t = mTilesByBegining.get(i); + while(t.getX() < to){ + i++; + if(i < mTilesByBegining.size())t = mTilesByBegining.get(i); + else break; + } + + return mTilesByBegining.subList(fromIndex, i); + } + + /** + * + * @param from exclusive + * @param to inclusive + */ + public List getTilesWithRightRange(int from, int to){ + if(mTilesByEnd.size() == 0) return Collections.emptyList(); + + final int fromIndex = binarySearchRightEdges(from + 1); //from is exclusive + final int fromRight = mTilesByEnd.get(fromIndex).getXRight(); + + if(fromRight > to) return Collections.emptyList(); + + int i = fromIndex; + Tile t = mTilesByEnd.get(i); + while(t.getXRight() <= to){ + i++; + if(i < mTilesByEnd.size()) t = mTilesByEnd.get(i); + else break; + } + + return mTilesByEnd.subList(fromIndex, i); + } + + /** Continues to split same values until it rests on first of them + * returns first tile with left equal than value or greater + */ + private int binarySearchLeftEdges(int value){ + int lo = 0; + int hi = mTilesByBegining.size() - 1; + int mid = 0; + Tile t = null; + while (lo <= hi) { + // Key is in a[lo..hi] or not present. + mid = lo + (hi - lo) / 2; + t = mTilesByBegining.get(mid); + + if (value > t.getX()) lo = mid + 1; + else hi = mid - 1; + + } + + while(t != null && t.getX() < value && mid < mTilesByBegining.size()-1){ + mid++; + t = mTilesByBegining.get(mid); + } + + return mid; + } + + /** Continues to split same values until it rests on first of them + * returns first tile with right equal than value or greater + */ + private int binarySearchRightEdges(int value){ + int lo = 0; + int hi = mTilesByEnd.size() - 1; + int mid = 0; + Tile t = null; + while (lo <= hi) { + // Key is in a[lo..hi] or not present. + mid = lo + (hi - lo) / 2; + t = mTilesByEnd.get(mid); + + final int r = t.getXRight(); + if (value > r) lo = mid + 1; + else hi = mid - 1; + } + + while(t != null && t.getXRight() < value && mid < mTilesByEnd.size()-1){ + mid++; + t = mTilesByEnd.get(mid); + } + + return mid; + } + + + + /** + * Use this in getViewForTile implementation to provide correctly initialized layout params for component + * @param t Tile data from datamodel + * @return ContendBand layout params + */ + public LayoutParams getLayoutParamsForTile(Tile t){ + LayoutParams lp = new LayoutParams(); + lp.tileId = t.getId(); + lp.dspLeft = t.getX(); + lp.dspTop = t.getY(); + lp.dspWidth = t.getWidth(); + lp.dspHeight = t.getHeight(); + lp.z = t.getZ(); + return lp; + } + + + interface IDataListener { + void onDataSetChanged(); + } + + } + + private static class ViewCache { + final LinkedList> mCachedItemViews = new LinkedList>(); + + /** + * Check if list of weak references has any view still in memory to offer for recycling + * @return cached view + */ + T getCachedView(){ + if (mCachedItemViews.size() != 0) { + T v; + do{ + v = mCachedItemViews.removeFirst().get(); + } + while(v == null && mCachedItemViews.size() != 0); + return v; + } + return null; + } + + void cacheView(T v){ + WeakReference ref = new WeakReference(v); + mCachedItemViews.addLast(ref); + } + } + +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/contentbands/EndlessContentBand.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/EndlessContentBand.java similarity index 94% rename from MAComponents/src/com/martinappl/components/ui/containers/contentbands/EndlessContentBand.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/EndlessContentBand.java index 524f840..9f76244 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/contentbands/EndlessContentBand.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/EndlessContentBand.java @@ -1,195 +1,196 @@ -package com.martinappl.components.ui.containers.contentbands; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; - -import com.martinappl.components.general.ToolBox; - -/** - * @author Martin Appl - * DSP = device specific pixel - * TODO last poster is disappearing prematurely and reappearing late. Time to time container isn't drawn after Activity initialization. - */ -public class EndlessContentBand extends BasicContentBand { - - public EndlessContentBand(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public EndlessContentBand(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public EndlessContentBand(Context context) { - super(context); - } - - - - /** - * Checks and refills empty area on the left edge of screen - */ - @Override - protected void refillLeftSide(){ - final int leftScreenEdge = getScrollX(); - final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp; - - int dspLeftScreenEdge = pxToDsp(leftScreenEdge); - if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger - - int end = mAdapter.getEnd(); - - if(dspLeftScreenEdge >= dspNextViewsRight || end == 0) return; - - int dspModuloLeftScreenEdge = dspLeftScreenEdge % end; - int dspModuloNextViewsRight = dspNextViewsRight % end; - int dspOffsetLeftScreenEdge = dspLeftScreenEdge / end; - int dspOffsetNextViewsRight = dspNextViewsRight / end; - - if(dspModuloLeftScreenEdge < 0) { - dspModuloLeftScreenEdge += end; - dspOffsetLeftScreenEdge -= 1; - } - if(dspModuloNextViewsRight < 0){ - dspModuloNextViewsRight += end; - dspOffsetNextViewsRight -= 1; - } - - View[] list; - if(dspModuloLeftScreenEdge > dspModuloNextViewsRight){ - View[] list1,list2; - list1 = mAdapter.getViewsByRightSideRange(dspModuloLeftScreenEdge, end); - list2 = mAdapter.getViewsByRightSideRange(0, dspModuloNextViewsRight); - translateLayoutParams(list1, dspOffsetLeftScreenEdge); - translateLayoutParams(list2, dspOffsetNextViewsRight); - - list = ToolBox.concatenateArray(list1,list2); - } - else{ - list = mAdapter.getViewsByRightSideRange(dspModuloLeftScreenEdge, dspModuloNextViewsRight); - translateLayoutParams(list, dspOffsetLeftScreenEdge); - } - - int dspMostLeft = dspNextViewsRight; - LayoutParams lp; - for(int i=0; i < list.length; i++){ - lp = (LayoutParams) list[i].getLayoutParams(); - if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; - addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); - } - - if(list.length > 0){ - layoutNewChildren(list); - } - - mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; - } - - private void translateLayoutParams(View[] list,int offset){ - if(offset == 0 || list.length == 0) return; - - final int end = mAdapter.getEnd(); - LayoutParams lp; - - for(int i=0; i= 0) dspRightScreenEdge++; //to avoid problem with rounding of values - - if(dspNextAddedViewsLeft >= dspRightScreenEdge || end == 0) return; - - int dspModuloRightScreenEdge = dspRightScreenEdge % end; - int dspModuloNextAddedViewsLeft = dspNextAddedViewsLeft % end; - int dspOffsetRightScreenEdge = dspRightScreenEdge / end; - int dspOffsetNextAddedViewsLeft = dspNextAddedViewsLeft / end; - - if(dspModuloRightScreenEdge < 0) { - dspModuloRightScreenEdge += end; - dspOffsetRightScreenEdge -= 1; - } - if(dspModuloNextAddedViewsLeft < 0) { - dspModuloNextAddedViewsLeft += end; - dspOffsetNextAddedViewsLeft -= 1; - } - - View[] list; - if(dspModuloNextAddedViewsLeft > dspModuloRightScreenEdge){ - View[] list1,list2; - list1 = mAdapter.getViewsByLeftSideRange(dspModuloNextAddedViewsLeft, end); - list2 = mAdapter.getViewsByLeftSideRange(0, dspModuloRightScreenEdge); - translateLayoutParams(list1, dspOffsetNextAddedViewsLeft); - translateLayoutParams(list2, dspOffsetRightScreenEdge); - - list = ToolBox.concatenateArray(list1,list2); - } - else{ - list = mAdapter.getViewsByLeftSideRange(dspModuloNextAddedViewsLeft, dspModuloRightScreenEdge); - translateLayoutParams(list, dspOffsetNextAddedViewsLeft); - } - - int dspMostRight = 0; - LayoutParams lp; - for(int i=0; i < list.length; i++){ - lp = (LayoutParams) list[i].getLayoutParams(); - if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight(); - addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); - } - - if(list.length > 0){ - layoutNewChildren(list); - } - - mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; - } - - public void fling(int velocityX, int velocityY){ - mTouchState = TOUCH_STATE_FLING; - final int x = getScrollX(); - final int y = getScrollY(); - final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; - - mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, 0, bottomInPixels - getHeight()); - - if(velocityX < 0) { - mScrollDirection = DIRECTION_LEFT; - } - else if(velocityX > 0) { - mScrollDirection = DIRECTION_RIGHT; - } - - invalidate(); - } - - @Override - protected void scrollByDelta(int deltaX, int deltaY){ - final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; - final int y = getScrollY() + deltaY; - - if(y < 0 ) deltaY -= y; - else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight()); - - if(deltaX < 0) { - mScrollDirection = DIRECTION_LEFT; - } - else { - mScrollDirection = DIRECTION_RIGHT; - } - - scrollBy(deltaX, deltaY); - } - -} +package it.moondroid.coverflow.components.ui.containers.contentbands; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import it.moondroid.coverflow.components.general.ToolBox; + + +/** + * @author Martin Appl + * DSP = device specific pixel + * TODO last poster is disappearing prematurely and reappearing late. Time to time container isn't drawn after Activity initialization. + */ +public class EndlessContentBand extends BasicContentBand { + + public EndlessContentBand(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public EndlessContentBand(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EndlessContentBand(Context context) { + super(context); + } + + + + /** + * Checks and refills empty area on the left edge of screen + */ + @Override + protected void refillLeftSide(){ + final int leftScreenEdge = getScrollX(); + final int dspNextViewsRight = mCurrentlyLayoutedViewsLeftEdgeDsp; + + int dspLeftScreenEdge = pxToDsp(leftScreenEdge); + if(dspLeftScreenEdge <= 0) dspLeftScreenEdge--; //when values are <0 they get floored to value which is larger + + int end = mAdapter.getEnd(); + + if(dspLeftScreenEdge >= dspNextViewsRight || end == 0) return; + + int dspModuloLeftScreenEdge = dspLeftScreenEdge % end; + int dspModuloNextViewsRight = dspNextViewsRight % end; + int dspOffsetLeftScreenEdge = dspLeftScreenEdge / end; + int dspOffsetNextViewsRight = dspNextViewsRight / end; + + if(dspModuloLeftScreenEdge < 0) { + dspModuloLeftScreenEdge += end; + dspOffsetLeftScreenEdge -= 1; + } + if(dspModuloNextViewsRight < 0){ + dspModuloNextViewsRight += end; + dspOffsetNextViewsRight -= 1; + } + + View[] list; + if(dspModuloLeftScreenEdge > dspModuloNextViewsRight){ + View[] list1,list2; + list1 = mAdapter.getViewsByRightSideRange(dspModuloLeftScreenEdge, end); + list2 = mAdapter.getViewsByRightSideRange(0, dspModuloNextViewsRight); + translateLayoutParams(list1, dspOffsetLeftScreenEdge); + translateLayoutParams(list2, dspOffsetNextViewsRight); + + list = ToolBox.concatenateArray(list1, list2); + } + else{ + list = mAdapter.getViewsByRightSideRange(dspModuloLeftScreenEdge, dspModuloNextViewsRight); + translateLayoutParams(list, dspOffsetLeftScreenEdge); + } + + int dspMostLeft = dspNextViewsRight; + LayoutParams lp; + for(int i=0; i < list.length; i++){ + lp = (LayoutParams) list[i].getLayoutParams(); + if(lp.dspLeft < dspMostLeft) dspMostLeft = lp.dspLeft; + addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); + } + + if(list.length > 0){ + layoutNewChildren(list); + } + + mCurrentlyLayoutedViewsLeftEdgeDsp = dspMostLeft; + } + + private void translateLayoutParams(View[] list,int offset){ + if(offset == 0 || list.length == 0) return; + + final int end = mAdapter.getEnd(); + LayoutParams lp; + + for(int i=0; i= 0) dspRightScreenEdge++; //to avoid problem with rounding of values + + if(dspNextAddedViewsLeft >= dspRightScreenEdge || end == 0) return; + + int dspModuloRightScreenEdge = dspRightScreenEdge % end; + int dspModuloNextAddedViewsLeft = dspNextAddedViewsLeft % end; + int dspOffsetRightScreenEdge = dspRightScreenEdge / end; + int dspOffsetNextAddedViewsLeft = dspNextAddedViewsLeft / end; + + if(dspModuloRightScreenEdge < 0) { + dspModuloRightScreenEdge += end; + dspOffsetRightScreenEdge -= 1; + } + if(dspModuloNextAddedViewsLeft < 0) { + dspModuloNextAddedViewsLeft += end; + dspOffsetNextAddedViewsLeft -= 1; + } + + View[] list; + if(dspModuloNextAddedViewsLeft > dspModuloRightScreenEdge){ + View[] list1,list2; + list1 = mAdapter.getViewsByLeftSideRange(dspModuloNextAddedViewsLeft, end); + list2 = mAdapter.getViewsByLeftSideRange(0, dspModuloRightScreenEdge); + translateLayoutParams(list1, dspOffsetNextAddedViewsLeft); + translateLayoutParams(list2, dspOffsetRightScreenEdge); + + list = ToolBox.concatenateArray(list1,list2); + } + else{ + list = mAdapter.getViewsByLeftSideRange(dspModuloNextAddedViewsLeft, dspModuloRightScreenEdge); + translateLayoutParams(list, dspOffsetNextAddedViewsLeft); + } + + int dspMostRight = 0; + LayoutParams lp; + for(int i=0; i < list.length; i++){ + lp = (LayoutParams) list[i].getLayoutParams(); + if(lp.getDspRight() > dspMostRight) dspMostRight = lp.getDspRight(); + addViewInLayout(list[i], -1, list[i].getLayoutParams(), true); + } + + if(list.length > 0){ + layoutNewChildren(list); + } + + mCurrentlyLayoutedViewsRightEdgeDsp = dspMostRight; + } + + public void fling(int velocityX, int velocityY){ + mTouchState = TOUCH_STATE_FLING; + final int x = getScrollX(); + final int y = getScrollY(); + final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; + + mScroller.fling(x, y, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, 0, bottomInPixels - getHeight()); + + if(velocityX < 0) { + mScrollDirection = DIRECTION_LEFT; + } + else if(velocityX > 0) { + mScrollDirection = DIRECTION_RIGHT; + } + + invalidate(); + } + + @Override + protected void scrollByDelta(int deltaX, int deltaY){ + final int bottomInPixels = dspToPx(mAdapter.getBottom()) + mDspHeightModulo; + final int y = getScrollY() + deltaY; + + if(y < 0 ) deltaY -= y; + else if(y > bottomInPixels - getHeight()) deltaY -= y - (bottomInPixels - getHeight()); + + if(deltaX < 0) { + mScrollDirection = DIRECTION_LEFT; + } + else { + mScrollDirection = DIRECTION_RIGHT; + } + + scrollBy(deltaX, deltaY); + } + +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/contentbands/TileBase.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/TileBase.java similarity index 89% rename from MAComponents/src/com/martinappl/components/ui/containers/contentbands/TileBase.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/TileBase.java index fa85c0b..2e2c2bf 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/contentbands/TileBase.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/contentbands/TileBase.java @@ -1,98 +1,98 @@ -package com.martinappl.components.ui.containers.contentbands; - -/** - * @author Martin Appl - * - * Base class for Content band datamodel. Extend to add data specific for your tiles and their behavior. - * This class includes data needed for positioning of tiles inside container. - */ -public class TileBase { - - private int id; - - private int x; - private int y; - private int z; - private int width; - private int height; - - - public int getX(){ - return x; - } - - public int getXRight(){ - return getX() + getWidth(); - } - - public int getY(){ - return y; - } - - public int getZ(){ - return z; - } - - public int getWidth(){ - return width; - } - - public int getHeight(){ - return height; - } - - public int getId(){ - return id; - } - - public void setId(String id) { - this.id = Integer.parseInt(id); - } - - public void setId(int id) { - this.id = id; - } - - public void setX(String x) { - this.x = Integer.parseInt(x); - } - - public void setX(int x) { - this.x = x; - } - - public void setY(String y) { - this.y = Integer.parseInt(y); - } - - public void setY(int y) { - this.y = y; - } - - public void setZ(String z) { - this.z = Integer.parseInt(z); - } - - public void setZ(int z) { - this.z = z; - } - - public void setWidth(String width) { - this.width = Integer.parseInt(width); - } - - public void setWidth(int width) { - this.width = width; - } - - public void setHeight(String height) { - this.height = Integer.parseInt(height); - } - - public void setHeight(int height) { - this.height = height; - } - - - -} +package it.moondroid.coverflow.components.ui.containers.contentbands; + +/** + * @author Martin Appl + * + * Base class for Content band datamodel. Extend to add data specific for your tiles and their behavior. + * This class includes data needed for positioning of tiles inside container. + */ +public class TileBase { + + private int id; + + private int x; + private int y; + private int z; + private int width; + private int height; + + + public int getX(){ + return x; + } + + public int getXRight(){ + return getX() + getWidth(); + } + + public int getY(){ + return y; + } + + public int getZ(){ + return z; + } + + public int getWidth(){ + return width; + } + + public int getHeight(){ + return height; + } + + public int getId(){ + return id; + } + + public void setId(String id) { + this.id = Integer.parseInt(id); + } + + public void setId(int id) { + this.id = id; + } + + public void setX(String x) { + this.x = Integer.parseInt(x); + } + + public void setX(int x) { + this.x = x; + } + + public void setY(String y) { + this.y = Integer.parseInt(y); + } + + public void setY(int y) { + this.y = y; + } + + public void setZ(String z) { + this.z = Integer.parseInt(z); + } + + public void setZ(int z) { + this.z = z; + } + + public void setWidth(String width) { + this.width = Integer.parseInt(width); + } + + public void setWidth(int width) { + this.width = width; + } + + public void setHeight(String height) { + this.height = Integer.parseInt(height); + } + + public void setHeight(int height) { + this.height = height; + } + + + +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java similarity index 81% rename from MAComponents/src/com/martinappl/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java index ad13499..df29a8a 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IRemovableItemsAdapterComponent.java @@ -1,11 +1,11 @@ -package com.martinappl.components.ui.containers.interfaces; - -import android.view.View; - -public interface IRemovableItemsAdapterComponent { - /** - * Called when item is removed from component by user clicking on remove button - * @return true, if you removed item from adapter manually in this step - */ - boolean onItemRemove(int position, View view, Object item); -} +package it.moondroid.coverflow.components.ui.containers.interfaces; + +import android.view.View; + +public interface IRemovableItemsAdapterComponent { + /** + * Called when item is removed from component by user clicking on remove button + * @return true, if you removed item from adapter manually in this step + */ + boolean onItemRemove(int position, View view, Object item); +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/interfaces/IRemoveFromAdapter.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IRemoveFromAdapter.java similarity index 55% rename from MAComponents/src/com/martinappl/components/ui/containers/interfaces/IRemoveFromAdapter.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IRemoveFromAdapter.java index 2530942..b16cc0b 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/interfaces/IRemoveFromAdapter.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IRemoveFromAdapter.java @@ -1,6 +1,6 @@ -package com.martinappl.components.ui.containers.interfaces; - - -public interface IRemoveFromAdapter{ - void removeItemFromAdapter(int position); -} +package it.moondroid.coverflow.components.ui.containers.interfaces; + + +public interface IRemoveFromAdapter{ + void removeItemFromAdapter(int position); +} diff --git a/MAComponents/src/com/martinappl/components/ui/containers/interfaces/IViewObserver.java b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IViewObserver.java similarity index 75% rename from MAComponents/src/com/martinappl/components/ui/containers/interfaces/IViewObserver.java rename to lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IViewObserver.java index 9328fce..eaf6224 100644 --- a/MAComponents/src/com/martinappl/components/ui/containers/interfaces/IViewObserver.java +++ b/lib/src/main/java/it/moondroid/coverflow/components/ui/containers/interfaces/IViewObserver.java @@ -1,11 +1,11 @@ -package com.martinappl.components.ui.containers.interfaces; - -import android.view.View; - -public interface IViewObserver { - /** - * @param v View which is getting removed - * @param position View position in adapter - */ - void onViewRemovedFromParent(View v, int position); -} +package it.moondroid.coverflow.components.ui.containers.interfaces; + +import android.view.View; + +public interface IViewObserver { + /** + * @param v View which is getting removed + * @param position View position in adapter + */ + void onViewRemovedFromParent(View v, int position); +} diff --git a/MAComponents/res/drawable-mdpi/ico_delete_asset.png b/lib/src/main/res/drawable-mdpi/ico_delete_asset.png similarity index 100% rename from MAComponents/res/drawable-mdpi/ico_delete_asset.png rename to lib/src/main/res/drawable-mdpi/ico_delete_asset.png diff --git a/MAComponents/res/drawable-xhdpi/ico_delete_asset.png b/lib/src/main/res/drawable-xhdpi/ico_delete_asset.png similarity index 100% rename from MAComponents/res/drawable-xhdpi/ico_delete_asset.png rename to lib/src/main/res/drawable-xhdpi/ico_delete_asset.png diff --git a/MAComponents/res/values/attrs.xml b/lib/src/main/res/values/attrs.xml similarity index 97% rename from MAComponents/res/values/attrs.xml rename to lib/src/main/res/values/attrs.xml index 0431cb6..1c350f9 100644 --- a/MAComponents/res/values/attrs.xml +++ b/lib/src/main/res/values/attrs.xml @@ -1,40 +1,40 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/src/main/res/values/strings.xml b/lib/src/main/res/values/strings.xml new file mode 100644 index 0000000..5232723 --- /dev/null +++ b/lib/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + CoverFlow + diff --git a/maven_push.gradle b/maven_push.gradle new file mode 100644 index 0000000..b741e61 --- /dev/null +++ b/maven_push.gradle @@ -0,0 +1,92 @@ +apply plugin: 'maven' +apply plugin: 'signing' + +def sonatypeRepositoryUrl +if (isReleaseBuild()) { + println 'RELEASE BUILD' + sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL + : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +} else { + println 'DEBUG BUILD' + sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL + : "https://oss.sonatype.org/content/repositories/snapshots/" +} + +def getRepositoryUsername() { + return hasProperty('nexusUsername') ? nexusUsername : "" +} + +def getRepositoryPassword() { + return hasProperty('nexusPassword') ? nexusPassword : "" +} + +afterEvaluate { project -> + uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + pom.artifactId = POM_ARTIFACT_ID + + repository(url: sonatypeRepositoryUrl) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + } + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives + } + + task androidJavadocs(type: Javadoc) { + source = android.sourceSets.main.java.sourceFiles + } + + task androidJavadocsJar(type: Jar) { + classifier = 'javadoc' + //basename = artifact_id + from androidJavadocs.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + //basename = artifact_id + from android.sourceSets.main.java.sourceFiles + } + + artifacts { + //archives packageReleaseJar + archives androidSourcesJar + archives androidJavadocsJar + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3a5a919 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':lib', ':app'