├── .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 | 
6 |
7 | ## Get it [](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 |
--------------------------------------------------------------------------------