├── .gitignore ├── LICENSE ├── LICENSE.txt ├── README.md ├── android ├── LICENSE ├── assets │ └── README ├── build.properties ├── build.xml ├── dist │ ├── androidflip.jar │ ├── de.manumaticx.androidflip-android-2.0.0.zip │ └── de.manumaticx.androidflip-android-4.0.0.zip ├── documentation │ ├── demo.gif │ └── index.md ├── example │ └── app.js ├── lib │ └── README ├── libs │ ├── armeabi-v7a │ │ └── libde.manumaticx.androidflip.so │ └── x86 │ │ └── libde.manumaticx.androidflip.so ├── manifest ├── platform │ └── README ├── src │ ├── de │ │ └── manumaticx │ │ │ └── androidflip │ │ │ ├── AndroidflipModule.java │ │ │ ├── FlipViewAdapter.java │ │ │ ├── FlipViewProxy.java │ │ │ └── TiFlipView.java │ └── se │ │ └── emilsjolander │ │ └── flipview │ │ ├── FlipView.java │ │ ├── GlowOverFlipper.java │ │ ├── OverFlipMode.java │ │ ├── OverFlipper.java │ │ ├── OverFlipperFactory.java │ │ ├── Recycler.java │ │ └── RubberBandOverFlipper.java └── timodule.xml ├── assets └── README ├── documentation ├── demo.gif └── index.md ├── example └── app.js └── manifest /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | bin 3 | build 4 | node_modules 5 | package.json 6 | .DS_Store 7 | .classpath 8 | .project 9 | .settings 10 | .apt_generated 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Lehner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Lehner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TiAndroidFlip 2 | 3 | This is a Titanium module built upon [FlipView](https://github.com/emilsjolander/android-FlipView) for Android. You can consider it as a replacement for Ti.UI.ScrollableView as it behaves similar. Both flipping directions, vertical and horizontal, are supported. 4 | 5 | ![](documentation/demo.gif) 6 | 7 | ## Get it [![gitTio](http://gitt.io/badge.png)](http://gitt.io/component/de.manumaticx.androidflip) 8 | Download the latest distribution ZIP-file and consult the [Titanium Documentation](http://docs.appcelerator.com/titanium/latest/#!/guide/Using_a_Module) on how install it, or simply use the [gitTio CLI](http://gitt.io/cli): 9 | 10 | `$ gittio install de.manumaticx.androidflip` 11 | 12 | ## Using it 13 | 14 | Full example is [here](example/) 15 | 16 | ```javascript 17 | // require the module 18 | var Flip = require('de.manumaticx.androidflip'); 19 | 20 | // create the flipView 21 | var flipView = Flip.createFlipView({ 22 | orientation: Flip.ORIENTATION_HORIZONTAL, 23 | overFlipMode: Flip.OVERFLIPMODE_RUBBER_BAND, 24 | views: views 25 | }); 26 | 27 | // add flip listener 28 | flipView.addEventListener('flipped', function(e){ 29 | Ti.API.info("flipped to page " + e.index); 30 | }); 31 | 32 | ``` 33 | 34 | _Tip:_ Use `flipView.peakNext();` to teach the user how to interact with the flipView. It will indicate that there is more content. 35 | 36 | __RTFM?__ - [Documentation](documentation/index.md) 37 | 38 | ## License 39 | The MIT License (MIT) 40 | 41 | Copyright (c) 2014 Manuel Lehner 42 | 43 | Permission is hereby granted, free of charge, to any person obtaining a copy 44 | of this software and associated documentation files (the "Software"), to deal 45 | in the Software without restriction, including without limitation the rights 46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 47 | copies of the Software, and to permit persons to whom the Software is 48 | furnished to do so, subject to the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be included in 51 | all copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 56 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 59 | THE SOFTWARE. 60 | -------------------------------------------------------------------------------- /android/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel Lehner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /android/assets/README: -------------------------------------------------------------------------------- 1 | Place your assets like PNG files in this directory and they will be packaged 2 | with your module. 3 | 4 | All JavaScript files in the assets directory are IGNORED except if you create a 5 | file named "com.manumaticx.androidflip.js" in this directory in which case it will be 6 | wrapped by native code, compiled, and used as your module. This allows you to 7 | run pure JavaScript modules that are pre-compiled. 8 | 9 | Note: Mobile Web does not support this assets directory. 10 | -------------------------------------------------------------------------------- /android/build.properties: -------------------------------------------------------------------------------- 1 | titanium.platform=/Users/manuel/Library/Application Support/Titanium/mobilesdk/osx/6.0.0.GA/android 2 | android.platform=/Users/manuel/android-sdk-macosx/platforms/android-24 3 | android.sdk=/Users/manuel/android-sdk-macosx/ 4 | -------------------------------------------------------------------------------- /android/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ant build script for Titanium Android module androidflip 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /android/dist/androidflip.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/android/dist/androidflip.jar -------------------------------------------------------------------------------- /android/dist/de.manumaticx.androidflip-android-2.0.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/android/dist/de.manumaticx.androidflip-android-2.0.0.zip -------------------------------------------------------------------------------- /android/dist/de.manumaticx.androidflip-android-4.0.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/android/dist/de.manumaticx.androidflip-android-4.0.0.zip -------------------------------------------------------------------------------- /android/documentation/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/android/documentation/demo.gif -------------------------------------------------------------------------------- /android/documentation/index.md: -------------------------------------------------------------------------------- 1 | # TiAndroidFlip API 2 | 3 | ## Properties 4 | 5 | * __currentPage__ `Number` - Index of the active page 6 | * __views__ `Ti.UI.View[]` - The pages within the flipView 7 | * __orientation__ `String` - The flipping orientation (either _ORIENTATION_VERTICAL_ or _ORIENTATION_HORIZONTAL_) 8 | * __overFlipMode__ `Number` - Same as OverScrollMode on ScrollableView (use _OVERFLIPMODE_GLOW_ to get the default android overscroll indicator or use _OVERFLIPMODE_RUBBER_BAND_ to use a more iOS-like indication ) 9 | 10 | ## Methods 11 | 12 | * __getViews( )__ - Gets the value of the __views__ property. 13 | * __setViews( `views` )__ - Sets the value of the __views__ property. 14 | - `views`: `Ti.UI.View[]` - The pages within the flipView 15 | * __addView( )__ - Adds a new page to the flipView 16 | * __removeView( `view` )__ - Removes an existing page from the flipView 17 | - `view`: `Number/Ti.UI.View` - index or view of the page 18 | * __flipToView( `view` )__ - flips to a specific page 19 | - `view`: `Number/Ti.UI.View` - index or view of the page 20 | * __movePrevious( )__ - Sets the current page to the previous consecutive page in __views__. 21 | * __moveNext( )__ - Sets the current page to the next consecutive page in __views__. 22 | * __getCurrentPage( )__ - Gets the value of the __currentPage__ property. 23 | * __getCurrentPage( `currentPage` )__ - Sets the value of the __currentPage__ property. 24 | 25 | ## Events 26 | 27 | * __flipped__ - fired when page was flipped 28 | * `index` - index of the new page 29 | -------------------------------------------------------------------------------- /android/example/app.js: -------------------------------------------------------------------------------- 1 | var win = Ti.UI.createWindow(); 2 | 3 | // create some views 4 | var views = []; 5 | for (var i=0; i <= 5; i++){ 6 | views.push(Ti.UI.createView({ backgroundColor: '#'+Math.floor(Math.random()*16777215).toString(16)})); 7 | } 8 | 9 | // require the module 10 | var Flip = require('de.manumaticx.androidflip'); 11 | 12 | // create the flipView 13 | var flipView = Flip.createFlipView({ 14 | orientation: Flip.ORIENTATION_HORIZONTAL, 15 | overFlipMode: Flip.OVERFLIPMODE_RUBBER_BAND, 16 | views: views 17 | }); 18 | 19 | // add flip listener 20 | flipView.addEventListener('flipped', function(e){ 21 | Ti.API.info("flipped to page " + e.index); 22 | }); 23 | 24 | // add it to a parent view 25 | win.add(flipView); 26 | 27 | win.open(); -------------------------------------------------------------------------------- /android/lib/README: -------------------------------------------------------------------------------- 1 | You can place any .jar dependencies in this directory and they will be included 2 | when your module is being compiled. -------------------------------------------------------------------------------- /android/libs/armeabi-v7a/libde.manumaticx.androidflip.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/android/libs/armeabi-v7a/libde.manumaticx.androidflip.so -------------------------------------------------------------------------------- /android/libs/x86/libde.manumaticx.androidflip.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/android/libs/x86/libde.manumaticx.androidflip.so -------------------------------------------------------------------------------- /android/manifest: -------------------------------------------------------------------------------- 1 | version: 4.0.0 2 | apiversion: 4 3 | description: Android FlipView for Titanium 4 | author: Manuel Lehner 5 | license: MIT 6 | copyright: Copyright (c) 2014 by Manuel Lehner 7 | architectures: arm64-v8a armeabi-v7a x86 8 | 9 | # these should not be edited 10 | name: AndroidFlip 11 | moduleid: de.manumaticx.androidflip 12 | guid: c748346d-7c10-48aa-9821-c071859d616f 13 | platform: android 14 | minsdk: 7.0.0.GA 15 | -------------------------------------------------------------------------------- /android/platform/README: -------------------------------------------------------------------------------- 1 | You can place platform-specific files here in sub-folders named "android" and/or "iphone", just as you can with normal Titanium Mobile SDK projects. Any folders and files you place here will be merged with the platform-specific files in a Titanium Mobile project that uses this module. 2 | 3 | When a Titanium Mobile project that uses this module is built, the files from this platform/ folder will be treated the same as files (if any) from the Titanium Mobile project's platform/ folder. 4 | -------------------------------------------------------------------------------- /android/src/de/manumaticx/androidflip/AndroidflipModule.java: -------------------------------------------------------------------------------- 1 | package de.manumaticx.androidflip; 2 | 3 | import org.appcelerator.kroll.KrollModule; 4 | import org.appcelerator.kroll.annotations.Kroll; 5 | import org.appcelerator.titanium.TiApplication; 6 | 7 | 8 | @Kroll.module(name="Androidflip", id="de.manumaticx.androidflip") 9 | public class AndroidflipModule extends KrollModule 10 | { 11 | @Kroll.constant public static final String ORIENTATION_VERTICAL = "vertical"; 12 | @Kroll.constant public static final String ORIENTATION_HORIZONTAL = "horizontal"; 13 | @Kroll.constant public static final int OVERFLIPMODE_GLOW = 1; 14 | @Kroll.constant public static final int OVERFLIPMODE_RUBBER_BAND = 2; 15 | 16 | public AndroidflipModule() { 17 | super(); 18 | } 19 | 20 | @Kroll.onAppCreate 21 | public static void onAppCreate(TiApplication app) { 22 | 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /android/src/de/manumaticx/androidflip/FlipViewAdapter.java: -------------------------------------------------------------------------------- 1 | package de.manumaticx.androidflip; 2 | 3 | import java.util.ArrayList; 4 | 5 | import android.app.Activity; 6 | import android.content.Context; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.BaseAdapter; 10 | import android.widget.FrameLayout; 11 | 12 | import org.appcelerator.titanium.proxy.TiViewProxy; 13 | import org.appcelerator.titanium.view.TiUIView; 14 | 15 | public class FlipViewAdapter extends BaseAdapter { 16 | 17 | private final ArrayList mViewProxies; 18 | protected final Context context; 19 | 20 | public FlipViewAdapter (Activity activity, ArrayList viewProxies) { 21 | this.context = activity.getBaseContext(); 22 | mViewProxies = viewProxies; 23 | } 24 | 25 | public int getCount() { 26 | return mViewProxies.size(); 27 | } 28 | 29 | public Object getItem(int position) { 30 | if (position >= getCount()) return null; 31 | return mViewProxies.get(position); 32 | } 33 | 34 | public long getItemId(int position) { 35 | return position; 36 | } 37 | 38 | public View getView(int position, View convertView, ViewGroup parent) { 39 | 40 | TiViewProxy tiProxy = mViewProxies.get(position); 41 | TiUIView tiView = tiProxy.getOrCreateView(); 42 | View view = tiView.getOuterView(); 43 | 44 | return view; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /android/src/de/manumaticx/androidflip/FlipViewProxy.java: -------------------------------------------------------------------------------- 1 | package de.manumaticx.androidflip; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.appcelerator.kroll.annotations.Kroll; 7 | import org.appcelerator.kroll.common.AsyncResult; 8 | import org.appcelerator.kroll.common.Log; 9 | import org.appcelerator.kroll.common.TiMessenger; 10 | import org.appcelerator.titanium.proxy.TiViewProxy; 11 | import org.appcelerator.titanium.util.TiConvert; 12 | import org.appcelerator.titanium.view.TiUIView; 13 | 14 | import android.app.Activity; 15 | import android.os.Message; 16 | 17 | 18 | @Kroll.proxy(creatableInModule=AndroidflipModule.class) 19 | public class FlipViewProxy extends TiViewProxy { 20 | 21 | private static final String TAG = "de.manumaticx.androidflip"; 22 | 23 | private static final int MSG_FIRST_ID = TiViewProxy.MSG_LAST_ID + 1; 24 | public static final int MSG_MOVE_PREV = MSG_FIRST_ID + 101; 25 | public static final int MSG_MOVE_NEXT = MSG_FIRST_ID + 102; 26 | public static final int MSG_FLIP_TO = MSG_FIRST_ID + 103; 27 | public static final int MSG_SET_VIEWS = MSG_FIRST_ID + 104; 28 | public static final int MSG_ADD_VIEW = MSG_FIRST_ID + 105; 29 | public static final int MSG_SET_CURRENT = MSG_FIRST_ID + 106; 30 | public static final int MSG_REMOVE_VIEW = MSG_FIRST_ID + 107; 31 | public static final int MSG_PEAK_PREV = MSG_FIRST_ID + 108; 32 | public static final int MSG_PEAK_NEXT = MSG_FIRST_ID + 109; 33 | public static final int MSG_LAST_ID = MSG_FIRST_ID + 999; 34 | 35 | public FlipViewProxy() { 36 | super(); 37 | } 38 | 39 | @Override 40 | public TiUIView createView(Activity activity) { 41 | 42 | TiUIView flipview = new TiFlipView(this); 43 | flipview.getLayoutParams().autoFillsHeight = true; 44 | flipview.getLayoutParams().autoFillsWidth = true; 45 | return flipview; 46 | } 47 | 48 | protected TiFlipView getView() { 49 | return (TiFlipView) getOrCreateView(); 50 | } 51 | 52 | public boolean handleMessage(Message msg) 53 | { 54 | boolean handled = false; 55 | 56 | switch(msg.what) { 57 | case MSG_MOVE_PREV: 58 | getView().movePrevious(); 59 | handled = true; 60 | break; 61 | case MSG_MOVE_NEXT: 62 | getView().moveNext(); 63 | handled = true; 64 | break; 65 | case MSG_FLIP_TO: 66 | getView().flipTo(msg.obj); 67 | handled = true; 68 | break; 69 | case MSG_SET_CURRENT: 70 | getView().setCurrentPage(msg.obj); 71 | handled = true; 72 | break; 73 | case MSG_SET_VIEWS: { 74 | AsyncResult holder = (AsyncResult) msg.obj; 75 | Object views = holder.getArg(); 76 | getView().setViews(views); 77 | holder.setResult(null); 78 | handled = true; 79 | break; 80 | } 81 | case MSG_ADD_VIEW: { 82 | AsyncResult holder = (AsyncResult) msg.obj; 83 | Object view = holder.getArg(); 84 | if (view instanceof TiViewProxy) { 85 | getView().addView((TiViewProxy) view); 86 | handled = true; 87 | } else if (view != null) { 88 | Log.w(TAG, "addView() ignored. Expected a Titanium view object, got " + view.getClass().getSimpleName()); 89 | } 90 | holder.setResult(null); 91 | break; 92 | } 93 | case MSG_REMOVE_VIEW: { 94 | AsyncResult holder = (AsyncResult) msg.obj; 95 | Object view = holder.getArg(); 96 | if (view instanceof TiViewProxy) { 97 | getView().removeView((TiViewProxy) view); 98 | handled = true; 99 | } else if (view != null) { 100 | Log.w(TAG, "removeView() ignored. Expected a Titanium view object, got " + view.getClass().getSimpleName()); 101 | } 102 | holder.setResult(null); 103 | break; 104 | } 105 | case MSG_PEAK_PREV: 106 | getView().peakPrevious((Boolean) msg.obj); 107 | handled = true; 108 | break; 109 | case MSG_PEAK_NEXT: 110 | getView().peakNext((Boolean) msg.obj); 111 | handled = true; 112 | break; 113 | default: 114 | handled = super.handleMessage(msg); 115 | } 116 | 117 | return handled; 118 | } 119 | 120 | @Kroll.getProperty @Kroll.method 121 | public Object getViews() 122 | { 123 | List list = new ArrayList(); 124 | return getView().getViews().toArray(new TiViewProxy[list.size()]); 125 | } 126 | 127 | @Kroll.setProperty @Kroll.method 128 | public void setViews(Object viewsObject) 129 | { 130 | TiMessenger.sendBlockingMainMessage(getMainHandler().obtainMessage(MSG_SET_VIEWS), viewsObject); 131 | } 132 | 133 | @Kroll.method 134 | public void addView(Object viewObject) 135 | { 136 | TiMessenger.sendBlockingMainMessage(getMainHandler().obtainMessage(MSG_ADD_VIEW), viewObject); 137 | } 138 | 139 | @Kroll.method 140 | public void removeView(Object viewObject) 141 | { 142 | TiMessenger.sendBlockingMainMessage(getMainHandler().obtainMessage(MSG_REMOVE_VIEW), viewObject); 143 | } 144 | 145 | @Kroll.method 146 | public void flipToView(Object view) 147 | { 148 | getMainHandler().obtainMessage(MSG_FLIP_TO, view).sendToTarget(); 149 | } 150 | 151 | @Kroll.method 152 | public void movePrevious() 153 | { 154 | getMainHandler().removeMessages(MSG_MOVE_PREV); 155 | getMainHandler().sendEmptyMessage(MSG_MOVE_PREV); 156 | } 157 | 158 | @Kroll.method 159 | public void moveNext() 160 | { 161 | getMainHandler().removeMessages(MSG_MOVE_NEXT); 162 | getMainHandler().sendEmptyMessage(MSG_MOVE_NEXT); 163 | } 164 | 165 | @Kroll.getProperty @Kroll.method 166 | public int getCurrentPage() 167 | { 168 | return getView().getCurrentPage(); 169 | } 170 | 171 | @Kroll.setProperty @Kroll.method 172 | public void setCurrentPage(Object page) 173 | { 174 | getMainHandler().obtainMessage(MSG_SET_CURRENT, page).sendToTarget(); 175 | } 176 | 177 | @Kroll.method 178 | public void peakPrevious(@Kroll.argument(optional = true) Boolean arg) 179 | { 180 | Boolean once = false; 181 | 182 | if (arg != null) { 183 | once = TiConvert.toBoolean(arg); 184 | } 185 | 186 | getMainHandler().obtainMessage(MSG_PEAK_PREV, once).sendToTarget(); 187 | } 188 | 189 | @Kroll.method 190 | public void peakNext(@Kroll.argument(optional = true) Boolean arg) 191 | { 192 | Boolean once = false; 193 | 194 | if (arg != null) { 195 | once = TiConvert.toBoolean(arg); 196 | } 197 | 198 | getMainHandler().obtainMessage(MSG_PEAK_NEXT, once).sendToTarget(); 199 | } 200 | } -------------------------------------------------------------------------------- /android/src/de/manumaticx/androidflip/TiFlipView.java: -------------------------------------------------------------------------------- 1 | package de.manumaticx.androidflip; 2 | 3 | import java.util.ArrayList; 4 | 5 | import org.appcelerator.kroll.KrollDict; 6 | import org.appcelerator.kroll.KrollProxy; 7 | import org.appcelerator.kroll.common.Log; 8 | import org.appcelerator.titanium.TiC; 9 | import org.appcelerator.titanium.proxy.TiViewProxy; 10 | import org.appcelerator.titanium.util.TiConvert; 11 | import org.appcelerator.titanium.view.TiUIView; 12 | 13 | import se.emilsjolander.flipview.FlipView; 14 | import se.emilsjolander.flipview.OverFlipMode; 15 | import android.app.Activity; 16 | 17 | public class TiFlipView extends TiUIView implements FlipView.OnFlipListener { 18 | 19 | private static final String TAG = "de.manumaticx.androidflip"; 20 | 21 | public static final String PROPERTY_ORIENTATION = "orientation"; 22 | public static final String PROPERTY_OVERFLIPMODE = "overFlipMode"; 23 | 24 | private FlipView mFlipView; 25 | private final ArrayList mViews; 26 | private final FlipViewAdapter mAdapter; 27 | private int mCurIndex = 0; 28 | 29 | public TiFlipView(TiViewProxy proxy) { 30 | super(proxy); 31 | Activity activity = proxy.getActivity(); 32 | mViews = new ArrayList(); 33 | 34 | mAdapter = new FlipViewAdapter(activity, mViews); 35 | mFlipView = new FlipView(activity); 36 | mFlipView.setAdapter(mAdapter); 37 | mFlipView.setOnFlipListener(this); 38 | } 39 | 40 | public void onFlippedToPage(FlipView v, int position, long id) { 41 | 42 | mCurIndex = position; 43 | 44 | if (proxy.hasListeners("flipped")) { 45 | KrollDict options = new KrollDict(); 46 | options.put("index", position); 47 | proxy.fireEvent("flipped", options); 48 | } 49 | } 50 | 51 | @Override 52 | public void processProperties(KrollDict d) 53 | { 54 | if (d.containsKey(TiC.PROPERTY_VIEWS)) { 55 | setViews(d.get(TiC.PROPERTY_VIEWS)); 56 | } 57 | 58 | if (d.containsKey(TiC.PROPERTY_CURRENT_PAGE)) { 59 | int page = TiConvert.toInt(d, TiC.PROPERTY_CURRENT_PAGE); 60 | if (page > 0) { 61 | setCurrentPage(page); 62 | } 63 | } 64 | 65 | if (d.containsKey(PROPERTY_ORIENTATION)) { 66 | mFlipView.setOrientation((String) d.get(PROPERTY_ORIENTATION)); 67 | } 68 | 69 | if (d.containsKey(PROPERTY_OVERFLIPMODE)) { 70 | int mode = TiConvert.toInt(d, PROPERTY_OVERFLIPMODE); 71 | 72 | if (mode == AndroidflipModule.OVERFLIPMODE_GLOW) { 73 | mFlipView.setOverFlipMode(OverFlipMode.GLOW); 74 | } 75 | 76 | if (mode == AndroidflipModule.OVERFLIPMODE_RUBBER_BAND) { 77 | mFlipView.setOverFlipMode(OverFlipMode.RUBBER_BAND); 78 | } 79 | } 80 | 81 | setNativeView(mFlipView); 82 | 83 | super.processProperties(d); 84 | } 85 | 86 | @Override 87 | public void propertyChanged(String key, Object oldValue, Object newValue, KrollProxy proxy) { 88 | 89 | if (TiC.PROPERTY_CURRENT_PAGE.equals(key)) { 90 | setCurrentPage(TiConvert.toInt(newValue)); 91 | } else if (key.equals(TiC.PROPERTY_VIEWS)) { 92 | setViews(newValue); 93 | } else if (key.equals(PROPERTY_ORIENTATION)) { 94 | mFlipView.setOrientation((String) newValue); 95 | } else if (key.equals(PROPERTY_OVERFLIPMODE)) { 96 | mFlipView.setOverFlipMode((OverFlipMode) newValue); 97 | } else { 98 | super.propertyChanged(key, oldValue, newValue, proxy); 99 | } 100 | } 101 | 102 | private void clearViewsList() 103 | { 104 | if (mViews == null || mViews.size() == 0) { 105 | return; 106 | } 107 | for (TiViewProxy viewProxy : mViews) { 108 | viewProxy.releaseViews(); 109 | viewProxy.setParent(null); 110 | } 111 | mViews.clear(); 112 | } 113 | 114 | public void setViews(Object viewsObject) 115 | { 116 | boolean changed = false; 117 | clearViewsList(); 118 | 119 | if (viewsObject instanceof Object[]) { 120 | Object[] views = (Object[])viewsObject; 121 | Activity activity = this.proxy.getActivity(); 122 | for (int i = 0; i < views.length; i++) { 123 | if (views[i] instanceof TiViewProxy) { 124 | TiViewProxy tv = (TiViewProxy)views[i]; 125 | tv.setActivity(activity); 126 | tv.setParent(this.proxy); 127 | mViews.add(tv); 128 | changed = true; 129 | } 130 | } 131 | } 132 | if (changed) { 133 | mAdapter.notifyDataSetChanged(); 134 | } 135 | } 136 | 137 | public ArrayList getViews() 138 | { 139 | return mViews; 140 | } 141 | 142 | public void addView(TiViewProxy proxy) 143 | { 144 | if (!mViews.contains(proxy)) { 145 | proxy.setActivity(this.proxy.getActivity()); 146 | proxy.setParent(this.proxy); 147 | mViews.add(proxy); 148 | getProxy().setProperty(TiC.PROPERTY_VIEWS, mViews.toArray()); 149 | mAdapter.notifyDataSetChanged(); 150 | } 151 | } 152 | 153 | public void removeView(TiViewProxy proxy) 154 | { 155 | if (mViews.contains(proxy)) { 156 | mViews.remove(proxy); 157 | proxy.setParent(null); 158 | getProxy().setProperty(TiC.PROPERTY_VIEWS, mViews.toArray()); 159 | mAdapter.notifyDataSetChanged(); 160 | } 161 | } 162 | 163 | public void moveNext() 164 | { 165 | move(mCurIndex + 1, true); 166 | } 167 | 168 | public void movePrevious() 169 | { 170 | move(mCurIndex - 1, true); 171 | } 172 | 173 | private void move(int index, boolean smoothFlip) 174 | { 175 | if (index < 0 || index >= mViews.size()) { 176 | if (Log.isDebugModeEnabled()) { 177 | Log.w(TAG, "Request to move to index " + index+ " ignored, as it is out-of-bounds.", Log.DEBUG_MODE); 178 | } 179 | return; 180 | } 181 | mCurIndex = index; 182 | 183 | if (smoothFlip) { 184 | mFlipView.smoothFlipTo(index); 185 | } else { 186 | mFlipView.flipTo(index); 187 | } 188 | } 189 | 190 | public void flipTo(Object view) 191 | { 192 | if (view instanceof Number) { 193 | move(((Number) view).intValue(), true); 194 | } else if (view instanceof TiViewProxy) { 195 | move(mViews.indexOf(view), true); 196 | } 197 | } 198 | 199 | public int getCurrentPage() 200 | { 201 | return mCurIndex; 202 | } 203 | 204 | public void setCurrentPage(Object view) 205 | { 206 | if (view instanceof Number) { 207 | move(((Number) view).intValue(), false); 208 | } else if (Log.isDebugModeEnabled()) { 209 | Log.w(TAG, "Request to set current page is ignored, as it is not a number.", Log.DEBUG_MODE); 210 | } 211 | } 212 | 213 | public void peakNext(boolean once) 214 | { 215 | mFlipView.peakNext(once); 216 | } 217 | 218 | public void peakPrevious(boolean once) 219 | { 220 | mFlipView.peakPrevious(once); 221 | } 222 | } -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/FlipView.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | import de.manumaticx.androidflip.AndroidflipModule; 4 | import se.emilsjolander.flipview.Recycler.Scrap; 5 | import android.animation.Animator; 6 | import android.animation.AnimatorListenerAdapter; 7 | import android.animation.TimeInterpolator; 8 | import android.animation.ValueAnimator; 9 | import android.animation.ValueAnimator.AnimatorUpdateListener; 10 | import android.content.Context; 11 | import android.content.res.TypedArray; 12 | import android.database.DataSetObserver; 13 | import android.graphics.Camera; 14 | import android.graphics.Canvas; 15 | import android.graphics.Color; 16 | import android.graphics.Matrix; 17 | import android.graphics.Paint; 18 | import android.graphics.Paint.Style; 19 | import android.graphics.Rect; 20 | import android.support.v4.view.MotionEventCompat; 21 | import android.support.v4.view.VelocityTrackerCompat; 22 | import android.util.AttributeSet; 23 | import android.view.MotionEvent; 24 | import android.view.VelocityTracker; 25 | import android.view.View; 26 | import android.view.ViewConfiguration; 27 | import android.view.animation.AccelerateDecelerateInterpolator; 28 | import android.view.animation.DecelerateInterpolator; 29 | import android.view.animation.Interpolator; 30 | import android.widget.FrameLayout; 31 | import android.widget.ListAdapter; 32 | import android.widget.Scroller; 33 | 34 | public class FlipView extends FrameLayout { 35 | 36 | public interface OnFlipListener { 37 | public void onFlippedToPage(FlipView v, int position, long id); 38 | } 39 | 40 | public interface OnOverFlipListener { 41 | public void onOverFlip(FlipView v, OverFlipMode mode, 42 | boolean overFlippingPrevious, float overFlipDistance, 43 | float flipDistancePerPage); 44 | } 45 | 46 | /** 47 | * 48 | * @author emilsjolander 49 | * 50 | * Class to hold a view and its corresponding info 51 | */ 52 | static class Page { 53 | View v; 54 | int position; 55 | int viewType; 56 | boolean valid; 57 | } 58 | 59 | // this will be the postion when there is not data 60 | private static final int INVALID_PAGE_POSITION = -1; 61 | // "null" flip distance 62 | private static final int INVALID_FLIP_DISTANCE = -1; 63 | 64 | private static final int PEAK_ANIM_DURATION = 600;// in ms 65 | private static final int MAX_SINGLE_PAGE_FLIP_ANIM_DURATION = 300;// in ms 66 | 67 | // for normalizing width/height 68 | private static final int FLIP_DISTANCE_PER_PAGE = 180; 69 | private static final int MAX_SHADOW_ALPHA = 180;// out of 255 70 | private static final int MAX_SHADE_ALPHA = 130;// out of 255 71 | private static final int MAX_SHINE_ALPHA = 100;// out of 255 72 | 73 | // value for no pointer 74 | private static final int INVALID_POINTER = -1; 75 | 76 | // constant used by the attributes 77 | private static final int VERTICAL_FLIP = 0; 78 | 79 | // constant used by the attributes 80 | @SuppressWarnings("unused") 81 | private static final int HORIZONTAL_FLIP = 1; 82 | 83 | private DataSetObserver dataSetObserver = new DataSetObserver() { 84 | 85 | @Override 86 | public void onChanged() { 87 | dataSetChanged(); 88 | } 89 | 90 | @Override 91 | public void onInvalidated() { 92 | dataSetInvalidated(); 93 | } 94 | 95 | }; 96 | 97 | private Scroller mScroller; 98 | private final Interpolator flipInterpolator = new DecelerateInterpolator(); 99 | private ValueAnimator mPeakAnim; 100 | private TimeInterpolator mPeakInterpolator = new AccelerateDecelerateInterpolator(); 101 | 102 | private boolean mIsFlippingVertically = true; 103 | private boolean mIsFlipping; 104 | private boolean mIsUnableToFlip; 105 | private boolean mIsFlippingEnabled = true; 106 | private boolean mLastTouchAllowed = true; 107 | private int mTouchSlop; 108 | private boolean mIsOverFlipping; 109 | 110 | // keep track of pointer 111 | private float mLastX = -1; 112 | private float mLastY = -1; 113 | private int mActivePointerId = INVALID_POINTER; 114 | 115 | // velocity stuff 116 | private VelocityTracker mVelocityTracker; 117 | private int mMinimumVelocity; 118 | private int mMaximumVelocity; 119 | 120 | // views get recycled after they have been pushed out of the active queue 121 | private Recycler mRecycler = new Recycler(); 122 | 123 | private ListAdapter mAdapter; 124 | private int mPageCount = 0; 125 | private Page mPreviousPage = new Page(); 126 | private Page mCurrentPage = new Page(); 127 | private Page mNextPage = new Page(); 128 | private View mEmptyView; 129 | 130 | private OnFlipListener mOnFlipListener; 131 | private OnOverFlipListener mOnOverFlipListener; 132 | 133 | private float mFlipDistance = INVALID_FLIP_DISTANCE; 134 | private int mCurrentPageIndex = INVALID_PAGE_POSITION; 135 | private int mLastDispatchedPageEventIndex = 0; 136 | private long mCurrentPageId = 0; 137 | 138 | private OverFlipMode mOverFlipMode; 139 | private OverFlipper mOverFlipper; 140 | 141 | // clipping rects 142 | private Rect mTopRect = new Rect(); 143 | private Rect mBottomRect = new Rect(); 144 | private Rect mRightRect = new Rect(); 145 | private Rect mLeftRect = new Rect(); 146 | 147 | // used for transforming the canvas 148 | private Camera mCamera = new Camera(); 149 | private Matrix mMatrix = new Matrix(); 150 | 151 | // paints drawn above views when flipping 152 | private Paint mShadowPaint = new Paint(); 153 | private Paint mShadePaint = new Paint(); 154 | private Paint mShinePaint = new Paint(); 155 | 156 | public FlipView(Context context) { 157 | this(context, null); 158 | } 159 | 160 | public FlipView(Context context, AttributeSet attrs) { 161 | this(context, attrs, 0); 162 | } 163 | 164 | public FlipView(Context context, AttributeSet attrs, int defStyle) { 165 | super(context, attrs, defStyle); 166 | 167 | mIsFlippingVertically = false; 168 | setOverFlipMode(OverFlipMode.GLOW); 169 | 170 | init(); 171 | } 172 | 173 | private void init() { 174 | final Context context = getContext(); 175 | final ViewConfiguration configuration = ViewConfiguration.get(context); 176 | 177 | mScroller = new Scroller(context, flipInterpolator); 178 | mTouchSlop = configuration.getScaledPagingTouchSlop(); 179 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 180 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 181 | 182 | mShadowPaint.setColor(Color.BLACK); 183 | mShadowPaint.setStyle(Style.FILL); 184 | mShadePaint.setColor(Color.BLACK); 185 | mShadePaint.setStyle(Style.FILL); 186 | mShinePaint.setColor(Color.WHITE); 187 | mShinePaint.setStyle(Style.FILL); 188 | } 189 | 190 | private void dataSetChanged() { 191 | final int currentPage = mCurrentPageIndex; 192 | int newPosition = currentPage; 193 | 194 | // if the adapter has stable ids, try to keep the page currently on 195 | // stable. 196 | if (mAdapter.hasStableIds() && currentPage != INVALID_PAGE_POSITION) { 197 | newPosition = getNewPositionOfCurrentPage(); 198 | } else if (currentPage == INVALID_PAGE_POSITION) { 199 | newPosition = 0; 200 | } 201 | 202 | // remove all the current views 203 | recycleActiveViews(); 204 | mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 205 | mRecycler.invalidateScraps(); 206 | 207 | mPageCount = mAdapter.getCount(); 208 | 209 | // put the current page within the new adapter range 210 | newPosition = Math.min(mPageCount - 1, 211 | newPosition == INVALID_PAGE_POSITION ? 0 : newPosition); 212 | 213 | if (newPosition != INVALID_PAGE_POSITION) { 214 | // TODO pretty confusing 215 | // this will be correctly set in setFlipDistance method 216 | mCurrentPageIndex = INVALID_PAGE_POSITION; 217 | mFlipDistance = INVALID_FLIP_DISTANCE; 218 | flipTo(newPosition); 219 | } else { 220 | mFlipDistance = INVALID_FLIP_DISTANCE; 221 | mPageCount = 0; 222 | setFlipDistance(0); 223 | } 224 | 225 | updateEmptyStatus(); 226 | } 227 | 228 | private int getNewPositionOfCurrentPage() { 229 | // check if id is on same position, this is because it will 230 | // often be that and this way you do not need to iterate the whole 231 | // dataset. If it is the same position, you are done. 232 | if (mCurrentPageId == mAdapter.getItemId(mCurrentPageIndex)) { 233 | return mCurrentPageIndex; 234 | } 235 | 236 | // iterate the dataset and look for the correct id. If it 237 | // exists, set that position as the current position. 238 | for (int i = 0; i < mAdapter.getCount(); i++) { 239 | if (mCurrentPageId == mAdapter.getItemId(i)) { 240 | return i; 241 | } 242 | } 243 | 244 | // Id no longer is dataset, keep current page 245 | return mCurrentPageIndex; 246 | } 247 | 248 | private void dataSetInvalidated() { 249 | if (mAdapter != null) { 250 | mAdapter.unregisterDataSetObserver(dataSetObserver); 251 | mAdapter = null; 252 | } 253 | mRecycler = new Recycler(); 254 | removeAllViews(); 255 | } 256 | 257 | @Override 258 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 259 | int width = getDefaultSize(0, widthMeasureSpec); 260 | int height = getDefaultSize(0, heightMeasureSpec); 261 | 262 | measureChildren(widthMeasureSpec, heightMeasureSpec); 263 | 264 | setMeasuredDimension(width, height); 265 | } 266 | 267 | @Override 268 | protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { 269 | int width = getDefaultSize(0, widthMeasureSpec); 270 | int height = getDefaultSize(0, heightMeasureSpec); 271 | 272 | int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, 273 | MeasureSpec.EXACTLY); 274 | int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, 275 | MeasureSpec.EXACTLY); 276 | final int childCount = getChildCount(); 277 | for (int i = 0; i < childCount; i++) { 278 | final View child = getChildAt(i); 279 | measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); 280 | } 281 | } 282 | 283 | @Override 284 | protected void measureChild(View child, int parentWidthMeasureSpec, 285 | int parentHeightMeasureSpec) { 286 | child.measure(parentWidthMeasureSpec, parentHeightMeasureSpec); 287 | } 288 | 289 | @Override 290 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 291 | layoutChildren(); 292 | 293 | mTopRect.top = 0; 294 | mTopRect.left = 0; 295 | mTopRect.right = getWidth(); 296 | mTopRect.bottom = getHeight() / 2; 297 | 298 | mBottomRect.top = getHeight() / 2; 299 | mBottomRect.left = 0; 300 | mBottomRect.right = getWidth(); 301 | mBottomRect.bottom = getHeight(); 302 | 303 | mLeftRect.top = 0; 304 | mLeftRect.left = 0; 305 | mLeftRect.right = getWidth() / 2; 306 | mLeftRect.bottom = getHeight(); 307 | 308 | mRightRect.top = 0; 309 | mRightRect.left = getWidth() / 2; 310 | mRightRect.right = getWidth(); 311 | mRightRect.bottom = getHeight(); 312 | } 313 | 314 | private void layoutChildren() { 315 | final int childCount = getChildCount(); 316 | for (int i = 0; i < childCount; i++) { 317 | final View child = getChildAt(i); 318 | layoutChild(child); 319 | } 320 | } 321 | 322 | private void layoutChild(View child) { 323 | child.layout(0, 0, getWidth(), getHeight()); 324 | } 325 | 326 | private void setFlipDistance(float flipDistance) { 327 | 328 | if (mPageCount < 1) { 329 | mFlipDistance = 0; 330 | mCurrentPageIndex = INVALID_PAGE_POSITION; 331 | mCurrentPageId = -1; 332 | recycleActiveViews(); 333 | return; 334 | } 335 | 336 | if (flipDistance == mFlipDistance) { 337 | return; 338 | } 339 | 340 | mFlipDistance = flipDistance; 341 | 342 | final int currentPageIndex = (int) Math.round(mFlipDistance 343 | / FLIP_DISTANCE_PER_PAGE); 344 | 345 | if (mCurrentPageIndex != currentPageIndex) { 346 | mCurrentPageIndex = currentPageIndex; 347 | mCurrentPageId = mAdapter.getItemId(mCurrentPageIndex); 348 | 349 | // TODO be smarter about this. Dont remove a view that will be added 350 | // again on the next line. 351 | recycleActiveViews(); 352 | 353 | // add the new active views 354 | if (mCurrentPageIndex > 0) { 355 | fillPageForIndex(mPreviousPage, mCurrentPageIndex - 1); 356 | addView(mPreviousPage.v); 357 | } 358 | if (mCurrentPageIndex >= 0 && mCurrentPageIndex < mPageCount) { 359 | fillPageForIndex(mCurrentPage, mCurrentPageIndex); 360 | addView(mCurrentPage.v); 361 | } 362 | if (mCurrentPageIndex < mPageCount - 1) { 363 | fillPageForIndex(mNextPage, mCurrentPageIndex + 1); 364 | addView(mNextPage.v); 365 | } 366 | } 367 | 368 | invalidate(); 369 | } 370 | 371 | private void fillPageForIndex(Page p, int i) { 372 | p.position = i; 373 | p.viewType = mAdapter.getItemViewType(p.position); 374 | p.v = getView(p.position, p.viewType); 375 | p.valid = true; 376 | } 377 | 378 | private void recycleActiveViews() { 379 | // remove and recycle the currently active views 380 | if (mPreviousPage.valid) { 381 | removeView(mPreviousPage.v); 382 | mRecycler.addScrapView(mPreviousPage.v, mPreviousPage.position, 383 | mPreviousPage.viewType); 384 | mPreviousPage.valid = false; 385 | } 386 | if (mCurrentPage.valid) { 387 | removeView(mCurrentPage.v); 388 | mRecycler.addScrapView(mCurrentPage.v, mCurrentPage.position, 389 | mCurrentPage.viewType); 390 | mCurrentPage.valid = false; 391 | } 392 | if (mNextPage.valid) { 393 | removeView(mNextPage.v); 394 | mRecycler.addScrapView(mNextPage.v, mNextPage.position, 395 | mNextPage.viewType); 396 | mNextPage.valid = false; 397 | } 398 | } 399 | 400 | private View getView(int index, int viewType) { 401 | // get the scrap from the recycler corresponding to the correct view 402 | // type 403 | Scrap scrap = mRecycler.getScrapView(index, viewType); 404 | 405 | // get a view from the adapter if a scrap was not found or it is 406 | // invalid. 407 | View v = null; 408 | if (scrap == null || !scrap.valid) { 409 | v = mAdapter.getView(index, scrap == null ? null : scrap.v, this); 410 | } else { 411 | v = scrap.v; 412 | } 413 | 414 | // return view 415 | return v; 416 | } 417 | 418 | @Override 419 | public boolean onInterceptTouchEvent(MotionEvent ev) { 420 | 421 | if (!mIsFlippingEnabled) { 422 | return false; 423 | } 424 | 425 | if (mPageCount < 1) { 426 | return false; 427 | } 428 | 429 | final int action = ev.getAction() & MotionEvent.ACTION_MASK; 430 | 431 | if (action == MotionEvent.ACTION_CANCEL 432 | || action == MotionEvent.ACTION_UP) { 433 | mIsFlipping = false; 434 | mIsUnableToFlip = false; 435 | mActivePointerId = INVALID_POINTER; 436 | if (mVelocityTracker != null) { 437 | mVelocityTracker.recycle(); 438 | mVelocityTracker = null; 439 | } 440 | return false; 441 | } 442 | 443 | if (action != MotionEvent.ACTION_DOWN) { 444 | if (mIsFlipping) { 445 | return true; 446 | } else if (mIsUnableToFlip) { 447 | return false; 448 | } 449 | } 450 | 451 | switch (action) { 452 | case MotionEvent.ACTION_MOVE: 453 | final int activePointerId = mActivePointerId; 454 | if (activePointerId == INVALID_POINTER) { 455 | break; 456 | } 457 | 458 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, 459 | activePointerId); 460 | if (pointerIndex == -1) { 461 | mActivePointerId = INVALID_POINTER; 462 | break; 463 | } 464 | 465 | final float x = MotionEventCompat.getX(ev, pointerIndex); 466 | final float dx = x - mLastX; 467 | final float xDiff = Math.abs(dx); 468 | final float y = MotionEventCompat.getY(ev, pointerIndex); 469 | final float dy = y - mLastY; 470 | final float yDiff = Math.abs(dy); 471 | 472 | if ((mIsFlippingVertically && yDiff > mTouchSlop && yDiff > xDiff) 473 | || (!mIsFlippingVertically && xDiff > mTouchSlop && xDiff > yDiff)) { 474 | mIsFlipping = true; 475 | mLastX = x; 476 | mLastY = y; 477 | } else if ((mIsFlippingVertically && xDiff > mTouchSlop) 478 | || (!mIsFlippingVertically && yDiff > mTouchSlop)) { 479 | mIsUnableToFlip = true; 480 | } 481 | break; 482 | 483 | case MotionEvent.ACTION_DOWN: 484 | mActivePointerId = ev.getAction() 485 | & MotionEvent.ACTION_POINTER_INDEX_MASK; 486 | mLastX = MotionEventCompat.getX(ev, mActivePointerId); 487 | mLastY = MotionEventCompat.getY(ev, mActivePointerId); 488 | 489 | mIsFlipping = !mScroller.isFinished() | mPeakAnim != null; 490 | mIsUnableToFlip = false; 491 | mLastTouchAllowed = true; 492 | 493 | break; 494 | case MotionEventCompat.ACTION_POINTER_UP: 495 | onSecondaryPointerUp(ev); 496 | break; 497 | } 498 | 499 | if (!mIsFlipping) { 500 | trackVelocity(ev); 501 | } 502 | 503 | return mIsFlipping; 504 | } 505 | 506 | @Override 507 | public boolean onTouchEvent(MotionEvent ev) { 508 | 509 | if (!mIsFlippingEnabled) { 510 | return false; 511 | } 512 | 513 | if (mPageCount < 1) { 514 | return false; 515 | } 516 | 517 | if (!mIsFlipping && !mLastTouchAllowed) { 518 | return false; 519 | } 520 | 521 | final int action = ev.getAction(); 522 | 523 | if (action == MotionEvent.ACTION_UP 524 | || action == MotionEvent.ACTION_CANCEL 525 | || action == MotionEvent.ACTION_OUTSIDE) { 526 | mLastTouchAllowed = false; 527 | } else { 528 | mLastTouchAllowed = true; 529 | } 530 | 531 | trackVelocity(ev); 532 | 533 | switch (action & MotionEvent.ACTION_MASK) { 534 | case MotionEvent.ACTION_DOWN: 535 | 536 | // start flipping immediately if interrupting some sort of animation 537 | if (endScroll() || endPeak()) { 538 | mIsFlipping = true; 539 | } 540 | 541 | // Remember where the motion event started 542 | mLastX = ev.getX(); 543 | mLastY = ev.getY(); 544 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 545 | break; 546 | case MotionEvent.ACTION_MOVE: 547 | if (!mIsFlipping) { 548 | final int pointerIndex = MotionEventCompat.findPointerIndex(ev, 549 | mActivePointerId); 550 | if (pointerIndex == -1) { 551 | mActivePointerId = INVALID_POINTER; 552 | break; 553 | } 554 | final float x = MotionEventCompat.getX(ev, pointerIndex); 555 | final float xDiff = Math.abs(x - mLastX); 556 | final float y = MotionEventCompat.getY(ev, pointerIndex); 557 | final float yDiff = Math.abs(y - mLastY); 558 | if ((mIsFlippingVertically && yDiff > mTouchSlop && yDiff > xDiff) 559 | || (!mIsFlippingVertically && xDiff > mTouchSlop && xDiff > yDiff)) { 560 | mIsFlipping = true; 561 | mLastX = x; 562 | mLastY = y; 563 | } 564 | } 565 | if (mIsFlipping) { 566 | // Scroll to follow the motion event 567 | final int activePointerIndex = MotionEventCompat 568 | .findPointerIndex(ev, mActivePointerId); 569 | if (activePointerIndex == -1) { 570 | mActivePointerId = INVALID_POINTER; 571 | break; 572 | } 573 | final float x = MotionEventCompat.getX(ev, activePointerIndex); 574 | final float deltaX = mLastX - x; 575 | final float y = MotionEventCompat.getY(ev, activePointerIndex); 576 | final float deltaY = mLastY - y; 577 | mLastX = x; 578 | mLastY = y; 579 | 580 | float deltaFlipDistance = 0; 581 | if (mIsFlippingVertically) { 582 | deltaFlipDistance = deltaY; 583 | } else { 584 | deltaFlipDistance = deltaX; 585 | } 586 | 587 | deltaFlipDistance /= ((isFlippingVertically() ? getHeight() 588 | : getWidth()) / FLIP_DISTANCE_PER_PAGE); 589 | setFlipDistance(mFlipDistance + deltaFlipDistance); 590 | 591 | final int minFlipDistance = 0; 592 | final int maxFlipDistance = (mPageCount - 1) 593 | * FLIP_DISTANCE_PER_PAGE; 594 | final boolean isOverFlipping = mFlipDistance < minFlipDistance 595 | || mFlipDistance > maxFlipDistance; 596 | if (isOverFlipping) { 597 | mIsOverFlipping = true; 598 | setFlipDistance(mOverFlipper.calculate(mFlipDistance, 599 | minFlipDistance, maxFlipDistance)); 600 | if (mOnOverFlipListener != null) { 601 | float overFlip = mOverFlipper.getTotalOverFlip(); 602 | mOnOverFlipListener.onOverFlip(this, mOverFlipMode, 603 | overFlip < 0, Math.abs(overFlip), 604 | FLIP_DISTANCE_PER_PAGE); 605 | } 606 | } else if (mIsOverFlipping) { 607 | mIsOverFlipping = false; 608 | if (mOnOverFlipListener != null) { 609 | // TODO in the future should only notify flip distance 0 610 | // on the correct edge (previous/next) 611 | mOnOverFlipListener.onOverFlip(this, mOverFlipMode, 612 | false, 0, FLIP_DISTANCE_PER_PAGE); 613 | mOnOverFlipListener.onOverFlip(this, mOverFlipMode, 614 | true, 0, FLIP_DISTANCE_PER_PAGE); 615 | } 616 | } 617 | } 618 | break; 619 | case MotionEvent.ACTION_UP: 620 | case MotionEvent.ACTION_CANCEL: 621 | if (mIsFlipping) { 622 | final VelocityTracker velocityTracker = mVelocityTracker; 623 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 624 | 625 | int velocity = 0; 626 | if (isFlippingVertically()) { 627 | velocity = (int) VelocityTrackerCompat.getYVelocity( 628 | velocityTracker, mActivePointerId); 629 | } else { 630 | velocity = (int) VelocityTrackerCompat.getXVelocity( 631 | velocityTracker, mActivePointerId); 632 | } 633 | smoothFlipTo(getNextPage(velocity)); 634 | 635 | mActivePointerId = INVALID_POINTER; 636 | endFlip(); 637 | 638 | mOverFlipper.overFlipEnded(); 639 | } 640 | break; 641 | case MotionEventCompat.ACTION_POINTER_DOWN: { 642 | final int index = MotionEventCompat.getActionIndex(ev); 643 | final float x = MotionEventCompat.getX(ev, index); 644 | final float y = MotionEventCompat.getY(ev, index); 645 | mLastX = x; 646 | mLastY = y; 647 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 648 | break; 649 | } 650 | case MotionEventCompat.ACTION_POINTER_UP: 651 | onSecondaryPointerUp(ev); 652 | final int index = MotionEventCompat.findPointerIndex(ev, 653 | mActivePointerId); 654 | final float x = MotionEventCompat.getX(ev, index); 655 | final float y = MotionEventCompat.getY(ev, index); 656 | mLastX = x; 657 | mLastY = y; 658 | break; 659 | } 660 | if (mActivePointerId == INVALID_POINTER) { 661 | mLastTouchAllowed = false; 662 | } 663 | return true; 664 | } 665 | 666 | @Override 667 | protected void dispatchDraw(Canvas canvas) { 668 | 669 | if (mPageCount < 1) { 670 | return; 671 | } 672 | 673 | if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 674 | setFlipDistance(mScroller.getCurrY()); 675 | } 676 | 677 | if (mIsFlipping || !mScroller.isFinished() || mPeakAnim != null) { 678 | showAllPages(); 679 | drawPreviousHalf(canvas); 680 | drawNextHalf(canvas); 681 | drawFlippingHalf(canvas); 682 | } else { 683 | endScroll(); 684 | setDrawWithLayer(mCurrentPage.v, false); 685 | hideOtherPages(mCurrentPage); 686 | drawChild(canvas, mCurrentPage.v, 0); 687 | 688 | // dispatch listener event now that we have "landed" on a page. 689 | // TODO not the prettiest to have this with the drawing logic, 690 | // should change. 691 | if (mLastDispatchedPageEventIndex != mCurrentPageIndex) { 692 | mLastDispatchedPageEventIndex = mCurrentPageIndex; 693 | postFlippedToPage(mCurrentPageIndex); 694 | } 695 | } 696 | 697 | // if overflip is GLOW mode and the edge effects needed drawing, make 698 | // sure to invalidate 699 | if (mOverFlipper.draw(canvas)) { 700 | // always invalidate whole screen as it is needed 99% of the time. 701 | // This is because of the shadows and shines put on the non-flipping 702 | // pages 703 | invalidate(); 704 | } 705 | } 706 | 707 | private void hideOtherPages(Page p) { 708 | if (mPreviousPage != p && mPreviousPage.valid && mPreviousPage.v.getVisibility() != GONE) { 709 | mPreviousPage.v.setVisibility(GONE); 710 | } 711 | if (mCurrentPage != p && mCurrentPage.valid && mCurrentPage.v.getVisibility() != GONE) { 712 | mCurrentPage.v.setVisibility(GONE); 713 | } 714 | if (mNextPage != p && mNextPage.valid && mNextPage.v.getVisibility() != GONE) { 715 | mNextPage.v.setVisibility(GONE); 716 | } 717 | p.v.setVisibility(VISIBLE); 718 | } 719 | 720 | private void showAllPages() { 721 | if (mPreviousPage.valid && mPreviousPage.v.getVisibility() != VISIBLE) { 722 | mPreviousPage.v.setVisibility(VISIBLE); 723 | } 724 | if (mCurrentPage.valid && mCurrentPage.v.getVisibility() != VISIBLE) { 725 | mCurrentPage.v.setVisibility(VISIBLE); 726 | } 727 | if (mNextPage.valid && mNextPage.v.getVisibility() != VISIBLE) { 728 | mNextPage.v.setVisibility(VISIBLE); 729 | } 730 | } 731 | 732 | /** 733 | * draw top/left half 734 | * 735 | * @param canvas 736 | */ 737 | private void drawPreviousHalf(Canvas canvas) { 738 | canvas.save(); 739 | canvas.clipRect(isFlippingVertically() ? mTopRect : mLeftRect); 740 | 741 | final float degreesFlipped = getDegreesFlipped(); 742 | final Page p = degreesFlipped > 90 ? mPreviousPage : mCurrentPage; 743 | 744 | // if the view does not exist, skip drawing it 745 | if (p.valid) { 746 | setDrawWithLayer(p.v, true); 747 | drawChild(canvas, p.v, 0); 748 | } 749 | 750 | drawPreviousShadow(canvas); 751 | canvas.restore(); 752 | } 753 | 754 | /** 755 | * draw top/left half shadow 756 | * 757 | * @param canvas 758 | */ 759 | private void drawPreviousShadow(Canvas canvas) { 760 | final float degreesFlipped = getDegreesFlipped(); 761 | if (degreesFlipped > 90) { 762 | final int alpha = (int) (((degreesFlipped - 90) / 90f) * MAX_SHADOW_ALPHA); 763 | mShadowPaint.setAlpha(alpha); 764 | canvas.drawPaint(mShadowPaint); 765 | } 766 | } 767 | 768 | /** 769 | * draw bottom/right half 770 | * 771 | * @param canvas 772 | */ 773 | private void drawNextHalf(Canvas canvas) { 774 | canvas.save(); 775 | canvas.clipRect(isFlippingVertically() ? mBottomRect : mRightRect); 776 | 777 | final float degreesFlipped = getDegreesFlipped(); 778 | final Page p = degreesFlipped > 90 ? mCurrentPage : mNextPage; 779 | 780 | // if the view does not exist, skip drawing it 781 | if (p.valid) { 782 | setDrawWithLayer(p.v, true); 783 | drawChild(canvas, p.v, 0); 784 | } 785 | 786 | drawNextShadow(canvas); 787 | canvas.restore(); 788 | } 789 | 790 | /** 791 | * draw bottom/right half shadow 792 | * 793 | * @param canvas 794 | */ 795 | private void drawNextShadow(Canvas canvas) { 796 | final float degreesFlipped = getDegreesFlipped(); 797 | if (degreesFlipped < 90) { 798 | final int alpha = (int) ((Math.abs(degreesFlipped - 90) / 90f) * MAX_SHADOW_ALPHA); 799 | mShadowPaint.setAlpha(alpha); 800 | canvas.drawPaint(mShadowPaint); 801 | } 802 | } 803 | 804 | private void drawFlippingHalf(Canvas canvas) { 805 | canvas.save(); 806 | mCamera.save(); 807 | 808 | final float degreesFlipped = getDegreesFlipped(); 809 | 810 | if (degreesFlipped > 90) { 811 | canvas.clipRect(isFlippingVertically() ? mTopRect : mLeftRect); 812 | if (mIsFlippingVertically) { 813 | mCamera.rotateX(degreesFlipped - 180); 814 | } else { 815 | mCamera.rotateY(180 - degreesFlipped); 816 | } 817 | } else { 818 | canvas.clipRect(isFlippingVertically() ? mBottomRect : mRightRect); 819 | if (mIsFlippingVertically) { 820 | mCamera.rotateX(degreesFlipped); 821 | } else { 822 | mCamera.rotateY(-degreesFlipped); 823 | } 824 | } 825 | 826 | mCamera.getMatrix(mMatrix); 827 | 828 | positionMatrix(); 829 | canvas.concat(mMatrix); 830 | 831 | setDrawWithLayer(mCurrentPage.v, true); 832 | drawChild(canvas, mCurrentPage.v, 0); 833 | 834 | drawFlippingShadeShine(canvas); 835 | 836 | mCamera.restore(); 837 | canvas.restore(); 838 | } 839 | 840 | /** 841 | * will draw a shade if flipping on the previous(top/left) half and a shine 842 | * if flipping on the next(bottom/right) half 843 | * 844 | * @param canvas 845 | */ 846 | private void drawFlippingShadeShine(Canvas canvas) { 847 | final float degreesFlipped = getDegreesFlipped(); 848 | if (degreesFlipped < 90) { 849 | final int alpha = (int) ((degreesFlipped / 90f) * MAX_SHINE_ALPHA); 850 | mShinePaint.setAlpha(alpha); 851 | canvas.drawRect(isFlippingVertically() ? mBottomRect : mRightRect, 852 | mShinePaint); 853 | } else { 854 | final int alpha = (int) ((Math.abs(degreesFlipped - 180) / 90f) * MAX_SHADE_ALPHA); 855 | mShadePaint.setAlpha(alpha); 856 | canvas.drawRect(isFlippingVertically() ? mTopRect : mLeftRect, 857 | mShadePaint); 858 | } 859 | } 860 | 861 | /** 862 | * Enable a hardware layer for the view. 863 | * 864 | * @param v 865 | * @param drawWithLayer 866 | */ 867 | private void setDrawWithLayer(View v, boolean drawWithLayer) { 868 | if (isHardwareAccelerated()) { 869 | if (v.getLayerType() != LAYER_TYPE_HARDWARE && drawWithLayer) { 870 | v.setLayerType(LAYER_TYPE_HARDWARE, null); 871 | } else if (v.getLayerType() != LAYER_TYPE_NONE && !drawWithLayer) { 872 | v.setLayerType(LAYER_TYPE_NONE, null); 873 | } 874 | } 875 | } 876 | 877 | private void positionMatrix() { 878 | mMatrix.preScale(0.25f, 0.25f); 879 | mMatrix.postScale(4.0f, 4.0f); 880 | mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2); 881 | mMatrix.postTranslate(getWidth() / 2, getHeight() / 2); 882 | } 883 | 884 | private float getDegreesFlipped() { 885 | float localFlipDistance = mFlipDistance % FLIP_DISTANCE_PER_PAGE; 886 | 887 | // fix for negative modulo. always want a positive flip degree 888 | if (localFlipDistance < 0) { 889 | localFlipDistance += FLIP_DISTANCE_PER_PAGE; 890 | } 891 | 892 | return (localFlipDistance / FLIP_DISTANCE_PER_PAGE) * 180; 893 | } 894 | 895 | private void postFlippedToPage(final int page) { 896 | post(new Runnable() { 897 | 898 | public void run() { 899 | if (mOnFlipListener != null) { 900 | mOnFlipListener.onFlippedToPage(FlipView.this, page, 901 | mAdapter.getItemId(page)); 902 | } 903 | } 904 | }); 905 | } 906 | 907 | private void onSecondaryPointerUp(MotionEvent ev) { 908 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 909 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 910 | if (pointerId == mActivePointerId) { 911 | // This was our active pointer going up. Choose a new 912 | // active pointer and adjust accordingly. 913 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 914 | mLastX = MotionEventCompat.getX(ev, newPointerIndex); 915 | mActivePointerId = MotionEventCompat.getPointerId(ev, 916 | newPointerIndex); 917 | if (mVelocityTracker != null) { 918 | mVelocityTracker.clear(); 919 | } 920 | } 921 | } 922 | 923 | /** 924 | * 925 | * @param deltaFlipDistance 926 | * The distance to flip. 927 | * @return The duration for a flip, bigger deltaFlipDistance = longer 928 | * duration. The increase if duration gets smaller for bigger values 929 | * of deltaFlipDistance. 930 | */ 931 | private int getFlipDuration(int deltaFlipDistance) { 932 | float distance = Math.abs(deltaFlipDistance); 933 | return (int) (MAX_SINGLE_PAGE_FLIP_ANIM_DURATION * Math.sqrt(distance 934 | / FLIP_DISTANCE_PER_PAGE)); 935 | } 936 | 937 | /** 938 | * 939 | * @param velocity 940 | * @return the page you should "land" on 941 | */ 942 | private int getNextPage(int velocity) { 943 | int nextPage; 944 | if (velocity > mMinimumVelocity) { 945 | nextPage = getCurrentPageFloor(); 946 | } else if (velocity < -mMinimumVelocity) { 947 | nextPage = getCurrentPageCeil(); 948 | } else { 949 | nextPage = getCurrentPageRound(); 950 | } 951 | return Math.min(Math.max(nextPage, 0), mPageCount - 1); 952 | } 953 | 954 | private int getCurrentPageRound() { 955 | return Math.round(mFlipDistance / FLIP_DISTANCE_PER_PAGE); 956 | } 957 | 958 | private int getCurrentPageFloor() { 959 | return (int) Math.floor(mFlipDistance / FLIP_DISTANCE_PER_PAGE); 960 | } 961 | 962 | private int getCurrentPageCeil() { 963 | return (int) Math.ceil(mFlipDistance / FLIP_DISTANCE_PER_PAGE); 964 | } 965 | 966 | /** 967 | * 968 | * @return true if ended a flip 969 | */ 970 | private boolean endFlip() { 971 | final boolean wasflipping = mIsFlipping; 972 | mIsFlipping = false; 973 | mIsUnableToFlip = false; 974 | mLastTouchAllowed = false; 975 | 976 | if (mVelocityTracker != null) { 977 | mVelocityTracker.recycle(); 978 | mVelocityTracker = null; 979 | } 980 | return wasflipping; 981 | } 982 | 983 | /** 984 | * 985 | * @return true if ended a scroll 986 | */ 987 | private boolean endScroll() { 988 | final boolean wasScrolling = !mScroller.isFinished(); 989 | mScroller.abortAnimation(); 990 | return wasScrolling; 991 | } 992 | 993 | /** 994 | * 995 | * @return true if ended a peak 996 | */ 997 | private boolean endPeak() { 998 | final boolean wasPeaking = mPeakAnim != null; 999 | if (mPeakAnim != null) { 1000 | mPeakAnim.cancel(); 1001 | mPeakAnim = null; 1002 | } 1003 | return wasPeaking; 1004 | } 1005 | 1006 | private void peak(boolean next, boolean once) { 1007 | final float baseFlipDistance = mCurrentPageIndex 1008 | * FLIP_DISTANCE_PER_PAGE; 1009 | if (next) { 1010 | mPeakAnim = ValueAnimator.ofFloat(baseFlipDistance, 1011 | baseFlipDistance + FLIP_DISTANCE_PER_PAGE / 4); 1012 | } else { 1013 | mPeakAnim = ValueAnimator.ofFloat(baseFlipDistance, 1014 | baseFlipDistance - FLIP_DISTANCE_PER_PAGE / 4); 1015 | } 1016 | mPeakAnim.setInterpolator(mPeakInterpolator); 1017 | mPeakAnim.addUpdateListener(new AnimatorUpdateListener() { 1018 | 1019 | public void onAnimationUpdate(ValueAnimator animation) { 1020 | setFlipDistance((Float) animation.getAnimatedValue()); 1021 | } 1022 | }); 1023 | mPeakAnim.addListener(new AnimatorListenerAdapter() { 1024 | @Override 1025 | public void onAnimationEnd(Animator animation) { 1026 | endPeak(); 1027 | } 1028 | }); 1029 | mPeakAnim.setDuration(PEAK_ANIM_DURATION); 1030 | mPeakAnim.setRepeatMode(ValueAnimator.REVERSE); 1031 | mPeakAnim.setRepeatCount(once ? 1 : ValueAnimator.INFINITE); 1032 | mPeakAnim.start(); 1033 | } 1034 | 1035 | private void trackVelocity(MotionEvent ev) { 1036 | if (mVelocityTracker == null) { 1037 | mVelocityTracker = VelocityTracker.obtain(); 1038 | } 1039 | mVelocityTracker.addMovement(ev); 1040 | } 1041 | 1042 | private void updateEmptyStatus() { 1043 | boolean empty = mAdapter == null || mPageCount == 0; 1044 | 1045 | if (empty) { 1046 | if (mEmptyView != null) { 1047 | mEmptyView.setVisibility(View.VISIBLE); 1048 | setVisibility(View.GONE); 1049 | } else { 1050 | setVisibility(View.VISIBLE); 1051 | } 1052 | 1053 | } else { 1054 | if (mEmptyView != null) { 1055 | mEmptyView.setVisibility(View.GONE); 1056 | } 1057 | setVisibility(View.VISIBLE); 1058 | } 1059 | } 1060 | 1061 | /* ---------- API ---------- */ 1062 | 1063 | /** 1064 | * 1065 | * @param adapter 1066 | * a regular ListAdapter, not all methods if the list adapter are 1067 | * used by the flipview 1068 | * 1069 | */ 1070 | public void setAdapter(ListAdapter adapter) { 1071 | if (mAdapter != null) { 1072 | mAdapter.unregisterDataSetObserver(dataSetObserver); 1073 | } 1074 | 1075 | // remove all the current views 1076 | removeAllViews(); 1077 | 1078 | mAdapter = adapter; 1079 | mPageCount = adapter == null ? 0 : mAdapter.getCount(); 1080 | 1081 | if (adapter != null) { 1082 | mAdapter.registerDataSetObserver(dataSetObserver); 1083 | 1084 | mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); 1085 | mRecycler.invalidateScraps(); 1086 | } 1087 | 1088 | // TODO pretty confusing 1089 | // this will be correctly set in setFlipDistance method 1090 | mCurrentPageIndex = INVALID_PAGE_POSITION; 1091 | mFlipDistance = INVALID_FLIP_DISTANCE; 1092 | setFlipDistance(0); 1093 | 1094 | updateEmptyStatus(); 1095 | } 1096 | 1097 | public ListAdapter getAdapter() { 1098 | return mAdapter; 1099 | } 1100 | 1101 | public int getPageCount() { 1102 | return mPageCount; 1103 | } 1104 | 1105 | public int getCurrentPage() { 1106 | return mCurrentPageIndex; 1107 | } 1108 | 1109 | public void flipTo(int page) { 1110 | if (page < 0 || page > mPageCount - 1) { 1111 | throw new IllegalArgumentException("That page does not exist"); 1112 | } 1113 | endFlip(); 1114 | setFlipDistance(page * FLIP_DISTANCE_PER_PAGE); 1115 | } 1116 | 1117 | public void flipBy(int delta) { 1118 | flipTo(mCurrentPageIndex + delta); 1119 | } 1120 | 1121 | public void smoothFlipTo(int page) { 1122 | if (page < 0 || page > mPageCount - 1) { 1123 | throw new IllegalArgumentException("That page does not exist"); 1124 | } 1125 | final int start = (int) mFlipDistance; 1126 | final int delta = page * FLIP_DISTANCE_PER_PAGE - start; 1127 | 1128 | endFlip(); 1129 | mScroller.startScroll(0, start, 0, delta, getFlipDuration(delta)); 1130 | invalidate(); 1131 | } 1132 | 1133 | public void smoothFlipBy(int delta) { 1134 | smoothFlipTo(mCurrentPageIndex + delta); 1135 | } 1136 | 1137 | /** 1138 | * Hint that there is a next page will do nothing if there is no next page 1139 | * 1140 | * @param once 1141 | * if true, only peak once. else peak until user interacts with 1142 | * view 1143 | */ 1144 | public void peakNext(boolean once) { 1145 | if (mCurrentPageIndex < mPageCount - 1) { 1146 | peak(true, once); 1147 | } 1148 | } 1149 | 1150 | /** 1151 | * Hint that there is a previous page will do nothing if there is no 1152 | * previous page 1153 | * 1154 | * @param once 1155 | * if true, only peak once. else peak until user interacts with 1156 | * view 1157 | */ 1158 | public void peakPrevious(boolean once) { 1159 | if (mCurrentPageIndex > 0) { 1160 | peak(false, once); 1161 | } 1162 | } 1163 | 1164 | /** 1165 | * 1166 | * @return true if the view is flipping vertically, can only be set via xml 1167 | * attribute "orientation" 1168 | */ 1169 | public boolean isFlippingVertically() { 1170 | return mIsFlippingVertically; 1171 | } 1172 | 1173 | /** 1174 | * The OnFlipListener will notify you when a page has been fully turned. 1175 | * 1176 | * @param onFlipListener 1177 | */ 1178 | public void setOnFlipListener(OnFlipListener onFlipListener) { 1179 | mOnFlipListener = onFlipListener; 1180 | } 1181 | 1182 | /** 1183 | * The OnOverFlipListener will notify of over flipping. This is a great 1184 | * listener to have when implementing pull-to-refresh 1185 | * 1186 | * @param onOverFlipListener 1187 | */ 1188 | public void setOnOverFlipListener(OnOverFlipListener onOverFlipListener) { 1189 | this.mOnOverFlipListener = onOverFlipListener; 1190 | } 1191 | 1192 | /** 1193 | * 1194 | * @return the overflip mode of this flipview. Default is GLOW 1195 | */ 1196 | public OverFlipMode getOverFlipMode() { 1197 | return mOverFlipMode; 1198 | } 1199 | 1200 | /** 1201 | * Set the overflip mode of the flipview. GLOW is the standard seen in all 1202 | * andriod lists. RUBBER_BAND is more like iOS lists which list you flip 1203 | * past the first/last page but adding friction, like a rubber band. 1204 | * 1205 | * @param overFlipMode 1206 | */ 1207 | public void setOverFlipMode(OverFlipMode overFlipMode) { 1208 | this.mOverFlipMode = overFlipMode; 1209 | mOverFlipper = OverFlipperFactory.create(this, mOverFlipMode); 1210 | } 1211 | 1212 | /** 1213 | * @param emptyView 1214 | * The view to show when either no adapter is set or the adapter 1215 | * has no items. This should be a view already in the view 1216 | * hierarchy which the FlipView will set the visibility of. 1217 | */ 1218 | public void setEmptyView(View emptyView) { 1219 | mEmptyView = emptyView; 1220 | updateEmptyStatus(); 1221 | } 1222 | 1223 | /** 1224 | * Set the flipping orientation to either "vertical" or "horizontal" 1225 | * 1226 | * @param orientation 1227 | */ 1228 | public void setOrientation(String orientation) { 1229 | 1230 | if (orientation.equals(AndroidflipModule.ORIENTATION_VERTICAL)){ 1231 | if (!mIsFlippingVertically){ 1232 | mIsFlippingVertically = true; 1233 | init(); 1234 | } 1235 | } 1236 | 1237 | if (orientation.equals(AndroidflipModule.ORIENTATION_HORIZONTAL)){ 1238 | if (mIsFlippingVertically){ 1239 | mIsFlippingVertically = false; 1240 | init(); 1241 | } 1242 | } 1243 | } 1244 | 1245 | } 1246 | -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/GlowOverFlipper.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | import android.graphics.Canvas; 4 | import android.support.v4.widget.EdgeEffectCompat; 5 | 6 | public class GlowOverFlipper implements OverFlipper { 7 | 8 | private EdgeEffectCompat mTopEdgeEffect; 9 | private EdgeEffectCompat mBottomEdgeEffect; 10 | private FlipView mFlipView; 11 | private float mTotalOverFlip; 12 | 13 | public GlowOverFlipper(FlipView v) { 14 | mFlipView = v; 15 | mTopEdgeEffect = new EdgeEffectCompat(v.getContext()); 16 | mBottomEdgeEffect = new EdgeEffectCompat(v.getContext()); 17 | } 18 | 19 | @Override 20 | public float calculate(float flipDistance, float minFlipDistance, 21 | float maxFlipDistance) { 22 | float deltaOverFlip = flipDistance - (flipDistance < 0 ? minFlipDistance : maxFlipDistance); 23 | 24 | mTotalOverFlip += deltaOverFlip; 25 | 26 | if (deltaOverFlip > 0) { 27 | mBottomEdgeEffect.onPull(deltaOverFlip 28 | / (mFlipView.isFlippingVertically() ? mFlipView.getHeight() : mFlipView.getWidth())); 29 | } else if (deltaOverFlip < 0) { 30 | mTopEdgeEffect.onPull(-deltaOverFlip 31 | / (mFlipView.isFlippingVertically() ? mFlipView.getHeight() : mFlipView.getWidth())); 32 | } 33 | return flipDistance < 0 ? minFlipDistance : maxFlipDistance; 34 | } 35 | 36 | @Override 37 | public boolean draw(Canvas c) { 38 | return drawTopEdgeEffect(c) | drawBottomEdgeEffect(c); 39 | } 40 | 41 | private boolean drawTopEdgeEffect(Canvas canvas) { 42 | boolean needsMoreDrawing = false; 43 | if (!mTopEdgeEffect.isFinished()) { 44 | canvas.save(); 45 | if (mFlipView.isFlippingVertically()) { 46 | mTopEdgeEffect.setSize(mFlipView.getWidth(), mFlipView.getHeight()); 47 | canvas.rotate(0); 48 | } else { 49 | mTopEdgeEffect.setSize(mFlipView.getHeight(), mFlipView.getWidth()); 50 | canvas.rotate(270); 51 | canvas.translate(-mFlipView.getHeight(), 0); 52 | } 53 | needsMoreDrawing = mTopEdgeEffect.draw(canvas); 54 | canvas.restore(); 55 | } 56 | return needsMoreDrawing; 57 | } 58 | 59 | private boolean drawBottomEdgeEffect(Canvas canvas) { 60 | boolean needsMoreDrawing = false; 61 | if (!mBottomEdgeEffect.isFinished()) { 62 | canvas.save(); 63 | if (mFlipView.isFlippingVertically()) { 64 | mBottomEdgeEffect.setSize(mFlipView.getWidth(), mFlipView.getHeight()); 65 | canvas.rotate(180); 66 | canvas.translate(-mFlipView.getWidth(), -mFlipView.getHeight()); 67 | } else { 68 | mBottomEdgeEffect.setSize(mFlipView.getHeight(), mFlipView.getWidth()); 69 | canvas.rotate(90); 70 | canvas.translate(0, -mFlipView.getWidth()); 71 | } 72 | needsMoreDrawing = mBottomEdgeEffect.draw(canvas); 73 | canvas.restore(); 74 | } 75 | return needsMoreDrawing; 76 | } 77 | 78 | @Override 79 | public void overFlipEnded() { 80 | mTopEdgeEffect.onRelease(); 81 | mBottomEdgeEffect.onRelease(); 82 | mTotalOverFlip = 0; 83 | } 84 | 85 | @Override 86 | public float getTotalOverFlip() { 87 | return mTotalOverFlip; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/OverFlipMode.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | public enum OverFlipMode { 4 | GLOW, RUBBER_BAND 5 | } 6 | -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/OverFlipper.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | import android.graphics.Canvas; 4 | 5 | public interface OverFlipper { 6 | 7 | /** 8 | * 9 | * @param flipDistance 10 | * the current flip distance 11 | * 12 | * @param minFlipDistance 13 | * the minimum flip distance, usually 0 14 | * 15 | * @param maxFlipDistance 16 | * the maximum flip distance 17 | * 18 | * @return the flip distance after calculations 19 | * 20 | */ 21 | float calculate(float flipDistance, float minFlipDistance, 22 | float maxFlipDistance); 23 | 24 | /** 25 | * 26 | * @param v 27 | * the view to apply any drawing onto 28 | * 29 | * @return a boolean flag indicating if the view needs to be invalidated 30 | * 31 | */ 32 | boolean draw(Canvas c); 33 | 34 | /** 35 | * Triggered from a touch up or cancel event. reset and release state 36 | * variables here. 37 | */ 38 | void overFlipEnded(); 39 | 40 | /** 41 | * 42 | * @return the total flip distance the has been over flipped. This is used 43 | * by the onOverFlipListener so make sure to return the correct 44 | * value. 45 | */ 46 | float getTotalOverFlip(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/OverFlipperFactory.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | 4 | public class OverFlipperFactory { 5 | 6 | static OverFlipper create(FlipView v, OverFlipMode mode) { 7 | switch(mode) { 8 | case GLOW: 9 | return new GlowOverFlipper(v); 10 | case RUBBER_BAND: 11 | return new RubberBandOverFlipper(); 12 | } 13 | return null; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/Recycler.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | import android.util.SparseArray; 6 | import android.view.View; 7 | 8 | public class Recycler { 9 | 10 | static class Scrap { 11 | View v; 12 | boolean valid; 13 | 14 | public Scrap(View scrap, boolean valid) { 15 | this.v = scrap; 16 | this.valid = valid; 17 | } 18 | } 19 | 20 | /** Unsorted views that can be used by the adapter as a convert view. */ 21 | private SparseArray[] scraps; 22 | private SparseArray currentScraps; 23 | 24 | private int viewTypeCount; 25 | 26 | void setViewTypeCount(int viewTypeCount) { 27 | if (viewTypeCount < 1) { 28 | throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); 29 | } 30 | // do nothing if the view type count has not changed. 31 | if (currentScraps != null && viewTypeCount == scraps.length) { 32 | return; 33 | } 34 | // noinspection unchecked 35 | @SuppressWarnings("unchecked") 36 | SparseArray[] scrapViews = new SparseArray[viewTypeCount]; 37 | for (int i = 0; i < viewTypeCount; i++) { 38 | scrapViews[i] = new SparseArray(); 39 | } 40 | this.viewTypeCount = viewTypeCount; 41 | currentScraps = scrapViews[0]; 42 | this.scraps = scrapViews; 43 | } 44 | 45 | /** @return A view from the ScrapViews collection. These are unordered. */ 46 | Scrap getScrapView(int position, int viewType) { 47 | if (viewTypeCount == 1) { 48 | return retrieveFromScrap(currentScraps, position); 49 | } else if (viewType >= 0 && viewType < scraps.length) { 50 | return retrieveFromScrap(scraps[viewType], position); 51 | } 52 | return null; 53 | } 54 | 55 | /** 56 | * Put a view into the ScrapViews list. These views are unordered. 57 | * 58 | * @param scrap 59 | * The view to add 60 | */ 61 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 62 | void addScrapView(View scrap, int position, int viewType) { 63 | // create a new Scrap 64 | Scrap item = new Scrap(scrap, true); 65 | 66 | if (viewTypeCount == 1) { 67 | currentScraps.put(position, item); 68 | } else { 69 | scraps[viewType].put(position, item); 70 | } 71 | if (Build.VERSION.SDK_INT >= 14) { 72 | scrap.setAccessibilityDelegate(null); 73 | } 74 | } 75 | 76 | static Scrap retrieveFromScrap(SparseArray scrapViews, int position) { 77 | int size = scrapViews.size(); 78 | if (size > 0) { 79 | // See if we still have a view for this position. 80 | Scrap result = scrapViews.get(position, null); 81 | if (result != null) { 82 | scrapViews.remove(position); 83 | return result; 84 | } 85 | int index = size - 1; 86 | result = scrapViews.valueAt(index); 87 | scrapViews.removeAt(index); 88 | result.valid = false; 89 | return result; 90 | } 91 | return null; 92 | } 93 | 94 | void invalidateScraps() { 95 | for (SparseArray array : scraps) { 96 | for (int i = 0; i < array.size(); i++) { 97 | array.valueAt(i).valid = false; 98 | } 99 | } 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /android/src/se/emilsjolander/flipview/RubberBandOverFlipper.java: -------------------------------------------------------------------------------- 1 | package se.emilsjolander.flipview; 2 | 3 | import android.graphics.Canvas; 4 | 5 | public class RubberBandOverFlipper implements OverFlipper { 6 | 7 | private static final float MAX_OVER_FLIP_DISTANCE = 70; 8 | private static final float EXPONENTIAL_DECREES = 0.85f; 9 | 10 | private float mTotalOverFlip; 11 | private float mCurrentOverFlip; 12 | 13 | @Override 14 | public float calculate(float flipDistance, float minFlipDistance, 15 | float maxFlipDistance) { 16 | 17 | float deltaOverFlip; 18 | if(flipDistance 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /assets/README: -------------------------------------------------------------------------------- 1 | Place your assets like PNG files in this directory and they will be packaged 2 | with your module. 3 | 4 | All JavaScript files in the assets directory are IGNORED except if you create a 5 | file named "com.manumaticx.androidflip.js" in this directory in which case it will be 6 | wrapped by native code, compiled, and used as your module. This allows you to 7 | run pure JavaScript modules that are pre-compiled. 8 | 9 | Note: Mobile Web does not support this assets directory. 10 | -------------------------------------------------------------------------------- /documentation/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manumaticx/TiAndroidFlip/998d7dbdb23b2a39f413b907fa9f4b1769ad9ace/documentation/demo.gif -------------------------------------------------------------------------------- /documentation/index.md: -------------------------------------------------------------------------------- 1 | # TiAndroidFlip API 2 | 3 | ## Properties 4 | 5 | * __currentPage__ `Number` - Index of the active page 6 | * __views__ `Ti.UI.View[]` - The pages within the flipView 7 | * __orientation__ `String` - The flipping orientation (either _ORIENTATION_VERTICAL_ or _ORIENTATION_HORIZONTAL_) 8 | * __overFlipMode__ `Number` - Same as OverScrollMode on ScrollableView (use _OVERFLIPMODE_GLOW_ to get the default android overscroll indicator or use _OVERFLIPMODE_RUBBER_BAND_ to use a more iOS-like indication ) 9 | 10 | ## Methods 11 | 12 | * __getViews( )__ - Gets the value of the __views__ property. 13 | * __setViews( `views` )__ - Sets the value of the __views__ property. 14 | - `views`: `Ti.UI.View[]` - The pages within the flipView 15 | * __addView( )__ - Adds a new page to the flipView 16 | * __removeView( `view` )__ - Removes an existing page from the flipView 17 | - `view`: `Number/Ti.UI.View` - index or view of the page 18 | * __flipToView( `view` )__ - flips to a specific page 19 | - `view`: `Number/Ti.UI.View` - index or view of the page 20 | * __movePrevious( )__ - Sets the current page to the previous consecutive page in __views__. 21 | * __moveNext( )__ - Sets the current page to the next consecutive page in __views__. 22 | * __getCurrentPage( )__ - Gets the value of the __currentPage__ property. 23 | * __setCurrentPage( `currentPage` )__ - Sets the value of the __currentPage__ property. 24 | * __peakPrevious( `once` )__ - Indicates that the previous Page can be flipped. 25 | - `once`: `boolean` - (optional) if only peak once or continue peaking until the user inderacts with the view 26 | * __peakNext( `once` )__ - Indicates that the next Page can be flipped. 27 | - `once`: `boolean` - (optional) if only peak once or continue peaking until the user inderacts with the view 28 | 29 | ## Events 30 | 31 | * __flipped__ - fired when page was flipped 32 | * `index` - index of the new page 33 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var win = Ti.UI.createWindow(); 2 | 3 | // create some views 4 | var views = []; 5 | for (var i=0; i <= 5; i++){ 6 | views.push(Ti.UI.createView({ backgroundColor: '#'+Math.floor(Math.random()*16777215).toString(16)})); 7 | } 8 | 9 | // require the module 10 | var Flip = require('de.manumaticx.androidflip'); 11 | 12 | // create the flipView 13 | var flipView = Flip.createFlipView({ 14 | orientation: Flip.ORIENTATION_HORIZONTAL, 15 | overFlipMode: Flip.OVERFLIPMODE_RUBBER_BAND, 16 | views: views 17 | }); 18 | 19 | // add flip listener 20 | flipView.addEventListener('flipped', function(e){ 21 | Ti.API.info("flipped to page " + e.index); 22 | }); 23 | 24 | // add it to a parent view 25 | win.add(flipView); 26 | 27 | win.open(); -------------------------------------------------------------------------------- /manifest: -------------------------------------------------------------------------------- 1 | #appname: AndroidFlip 2 | #publisher: Manuel Lehner 3 | #url: 4 | #image: appicon.png 5 | #appid: de.manumaticx.androidflip 6 | #desc: Android FlipView for Titanium 7 | #type: module 8 | #guid: 6310e1cf-f5d9-4de0-9f96-e976685aeb9a 9 | --------------------------------------------------------------------------------