├── .classpath
├── .gitignore
├── .project
├── AndroidManifest.xml
├── LICENSE
├── NOTICE
├── README.md
├── paper1.jpg
├── paper2.jpg
├── paper3.jpg
├── project.properties
├── res
├── drawable
│ ├── icon.png
│ ├── obama.jpg
│ ├── road_rage.jpg
│ ├── taipei_101.jpg
│ └── world.gif
├── layout
│ └── main.xml
└── values
│ └── strings.xml
└── src
└── fi
└── harism
└── curl
├── CurlActivity.java
├── CurlMesh.java
├── CurlPage.java
├── CurlRenderer.java
└── CurlView.java
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | bin/
3 | gen/
4 | doc/
5 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | Page Curl Example
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2011 Harri Smått
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 | Exceptions to Apache Licence:
16 | * Images used in the demo as they are randomly selected from Google Images. They may or may not be copyrighted.
17 | * Application icon is borrowed from deviantART. http://browse.deviantart.com/customization/icons/dock/#/dz0w8n
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | NOTE
2 | ====
3 |
4 | As of this commit all "pages" will be 2-sided. There's a new class CurlPage, using which
5 | you can assign a separate texture on both sides, one for front side only, or same
6 | texture can be shared on both sides. CurlPage contains also color values for blending
7 | which allows you to e.g share texture on both sides but do alpha blending
8 | only on back side of the page, leaving you with exactly same effect what earlier
9 | version of this example application did. This time with the difference there's
10 | some more freedom included.
11 |
12 | Here's an example video from [cags12](https://github.com/cags12) showing 2-sided page support in landscape mode;
13 |
14 | [http://www.youtube.com/watch?v=iwu7P5PCpsw]
15 |
16 | Introduction
17 | ============
18 | Project for implementing 'page curl' effect on Android + OpenGL ES 1.0 (possibly 1.1/2.0 too if there's clear advantage in using them).
19 |
20 | The source code is released under Apache 2.0 and can be used in commercial or personal projects. See LICENSE for more information. See NOTICE for any exceptions, these include namely the application icon and images used in the demo. Besides these exceptions, let it be as-is implementation or - maybe more preferably - as an example for implementing your own effect.
21 |
22 | For the ones without Android development environment, and/or people willing to take only
23 | a brief look on what's happening here, there are a few video captures. While person responsible for capturing
24 | them is not a happy owner of Android device yet It's truly hoped performance is not that poor on actual device compared
25 | to what you see here;
26 |
27 | * [With textures](http://www.youtube.com/watch?v=WbNyapB9jvI)
28 | * [Without textures](http://www.youtube.com/watch?v=AFmJ-ON-ulI)
29 |
30 | So what you saw there;
31 |
32 | * There are approximately 26 + 26 + 4 + 4 = 60 vertices at most.
33 | * 8 vertices for underlying pages, 4 for each.
34 | * ~26 vertices for curled page + ~26 vertices for fake soft shadow. These numbers are maximum
35 | values and vary depending on curl position and angle.
36 | * Rendering them as triangle strips end up producing approximately 50 polygons at most. To give
37 | some perspective rendering a cube without back face culling requires 8 vertices and 12 polygons.
38 |
39 | What you didn't see;
40 |
41 | * There is an experimental flag unable to show on emulator. With the kind help of
42 | [Andrew Winter](https://github.com/drewjw81) it was possible to do some experimenting
43 | on using touch pressure information. If you go and call CurlView.setEnableTouchPressure(true),
44 | curl radius will be adjusted based on the touch pressure. The more you press, the smaller it gets.
45 | Currently it's a vast one line 'hack', simply to show off it's there, mostly for experimenting,
46 | but who knows. If someone takes the work and calibrates it properly it might turn out to something.
47 | * Z-buffering, really, we don't need to do depth check giving us a minor performance boost.
48 | * Lightning, it's a more of a trick we use here, leaving us with something very close to flat shading actually.
49 | * Perspective projection. Since it's very much easier to map render target to screen size using
50 | orthogonal projection it's used by default. For quick prototyping you can enable it though,
51 | check USE_PERSPECTIVE_PROJECTION flag in CurlRenderer.
52 |
53 | ToDo
54 | ====
55 | * Adjust fake soft shadow calculation. Current 'drop shadow', cast behind curl, implementation
56 | is not exactly what one wants necessarily. It might be better strategy to leave some 'softness'
57 | for situations in which radius is close to zero. Currently it means shadow is decreased to
58 | non-existent. Well, that's how shadows work, but considering from usability side..
59 | * Add some details to make it a bit more book alike.
60 |
61 | Some details
62 | ============
63 | Implementation can be divided roughly in two parts.
64 |
65 | * CurlMesh - which handles actual curl calculations and everything related to its rendering.
66 | * CurlView - which is responsible for providing rendering environment - but more importantly -
67 | handles curl manipulation. Meaning it receives touch events and repositions curl based on them.
68 | While this sounds something utterly trivial, it really isn't, and becomes more complex if curl didn't happen
69 | around a cylinder. Depending on what you're trying to achieve of course. For me it was
70 | most important from the beginning that 'paper edge' follows pointer at all times.
71 |
72 | Anyway, here are a few links describing this page curl implementation somewhat well.
73 | Only difference is that instead of using a static grid an algorithm which 'splits'
74 | rectangle dynamically regarding curl position and direction was implemented.
75 | This is done in order to get better render quality and to reduce polygon count.
76 | It's an absolute win-win situation if these things can be combined with limited amount
77 | of extra calculation to ease the work of renderer. In this particular case, we really
78 | do not want to draw polygons separately if they lie next to each other on same plane.
79 | It's more appropriate to have more vertices used for drawing rotating part instead.
80 | On negative side lots of code complexity comes from the need for creating a triangle strip for rendering.
81 | Using a solid grid such problems do not occur at all.
82 |
83 | * Page Flip 2D [http://nomtek.com/tips-for-developers/page-flip-2d/]
84 | * Page Flip 3D [http://nomtek.com/tips-for-developers/page-flip-3d/]
85 |
86 | It isn't very difficult to see what happens here once you take a paper and simply
87 | curl it to some direction. If you fold paper completely, cylinder, curl happens around,
88 | radius becomes zero, making it more of a 2D effect. And likewise folding the paper so
89 | that curl radius is constant most of the characteristics remain - most importantly there
90 | is a line - at the center of this 'cylinder' - which has constant slope not dependent on radius.
91 | Its distance from the point you're holding the paper varies only. Keeping this in mind makes
92 | curl position handling based on touch events a lot easier compared to using a cone
93 | as solid curling is done around. For information on using a cone, it's highly recommended to take a look on W. Dana Nuon's [blog
94 | post](http://wdnuon.blogspot.com/2010/05/implementing-ibooks-page-curling-using.html)
95 | on the topic. Chris Luke's [article](http://blog.flirble.org/2010/10/08/the-anatomy-of-a-page-curl/)
96 | is a good read too, even though he came to pretty much the same conclusion as me,
97 | better to go with cylinder instead.
98 |
99 | Curl/cylinder is defined with three parameters, position, which is any point on a line collinear to
100 | curl. Direction vector which tells direction curl 'opens to'. And curl/cylinder
101 | radius. 'Paper' is first translated and rotated; curl position translates
102 | to origin and then rotated so that curl opens to right (1, 0). This transformation makes
103 | it a bit easier to calculate rotating vertices as all vertices which have x -coordinate
104 | at least 0 are not affected. Vertices which have x -coordinate between (-PI*radius, 0)
105 | are within 'curl', and if x -coordinate is less than equal to -PI*radius they are completely rotated.
106 | And scan line algorithm for splitting lines within rotating area is more simple as
107 | scan lines are always vertical. Not to forget rotating happens around y -axis as
108 | cylinder center is positioned at x = 0. And after we translate these vertices back to
109 | original position we have a curl which direction heads to direction vector and it's center
110 | is located at given curl position.
111 |
112 | 1. At first there is a piece of paper represented by 4 vertices at its corners.
113 | 
114 | 2. And let's say we want to curl it approximately like this.
115 | 
116 | 3. Which could results in something like these vertices. Adding more scan lines within the rotating
117 | area increases its quality. But the idea remains, bounding lines of original rectangle are split on
118 | more/less dense basis.
119 | 
120 |
121 | But, once again, using a piece of paper and doing some experiments by yourself works
122 | much more as a proper explanation than words, mine at least, can tell.
123 | And maybe gives some idea how to make better/more realistic implementation.
124 | Happy page flipping :)
125 |
126 | Sources of inspiration
127 | ======================
128 | Some YouTube links to page curl/flip implementations which were found interesting for a reason or another.
129 |
130 | * Tiffany [http://www.youtube.com/watch?v=Yg04wfnDpiQ]
131 | * Huone Inc [http://www.youtube.com/watch?v=EVHksX0GdIQ]
132 | * CodeFlakes [http://www.youtube.com/watch?v=ynu4Ov-29Po]
133 |
134 | Not to forget many of the true real-time rendering heros. One day We're about to beat them all. One day. Right?
135 |
136 | * Rgba&Tbc - Elevated [http://www.youtube.com/watch?v=_YWMGuh15nE]
137 | * Andromeda&Orb - Stargazer [http://www.youtube.com/watch?v=5u1cqYLNbJI]
138 | * Cncd&Flt - Numb Res [http://www.youtube.com/watch?v=LTOC_ajkRkU]
139 |
--------------------------------------------------------------------------------
/paper1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/paper1.jpg
--------------------------------------------------------------------------------
/paper2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/paper2.jpg
--------------------------------------------------------------------------------
/paper3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/paper3.jpg
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system use,
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 |
10 | # Project target.
11 | target=android-8
12 |
--------------------------------------------------------------------------------
/res/drawable/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/res/drawable/icon.png
--------------------------------------------------------------------------------
/res/drawable/obama.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/res/drawable/obama.jpg
--------------------------------------------------------------------------------
/res/drawable/road_rage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/res/drawable/road_rage.jpg
--------------------------------------------------------------------------------
/res/drawable/taipei_101.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/res/drawable/taipei_101.jpg
--------------------------------------------------------------------------------
/res/drawable/world.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harism/android-pagecurl/7a2c8f152bb4f1b0de3b1aa72b3cb79e1fe8e3bd/res/drawable/world.gif
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Page Curl
4 |
5 |
--------------------------------------------------------------------------------
/src/fi/harism/curl/CurlActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2012 Harri Smatt
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package fi.harism.curl;
18 |
19 | import android.app.Activity;
20 | import android.graphics.Bitmap;
21 | import android.graphics.Canvas;
22 | import android.graphics.Color;
23 | import android.graphics.Paint;
24 | import android.graphics.Rect;
25 | import android.graphics.drawable.Drawable;
26 | import android.os.Bundle;
27 |
28 | /**
29 | * Simple Activity for curl testing.
30 | *
31 | * @author harism
32 | */
33 | public class CurlActivity extends Activity {
34 |
35 | private CurlView mCurlView;
36 |
37 | @Override
38 | public void onCreate(Bundle savedInstanceState) {
39 | super.onCreate(savedInstanceState);
40 | setContentView(R.layout.main);
41 |
42 | int index = 0;
43 | if (getLastNonConfigurationInstance() != null) {
44 | index = (Integer) getLastNonConfigurationInstance();
45 | }
46 | mCurlView = (CurlView) findViewById(R.id.curl);
47 | mCurlView.setPageProvider(new PageProvider());
48 | mCurlView.setSizeChangedObserver(new SizeChangedObserver());
49 | mCurlView.setCurrentIndex(index);
50 | mCurlView.setBackgroundColor(0xFF202830);
51 |
52 | // This is something somewhat experimental. Before uncommenting next
53 | // line, please see method comments in CurlView.
54 | // mCurlView.setEnableTouchPressure(true);
55 | }
56 |
57 | @Override
58 | public void onPause() {
59 | super.onPause();
60 | mCurlView.onPause();
61 | }
62 |
63 | @Override
64 | public void onResume() {
65 | super.onResume();
66 | mCurlView.onResume();
67 | }
68 |
69 | @Override
70 | public Object onRetainNonConfigurationInstance() {
71 | return mCurlView.getCurrentIndex();
72 | }
73 |
74 | /**
75 | * Bitmap provider.
76 | */
77 | private class PageProvider implements CurlView.PageProvider {
78 |
79 | // Bitmap resources.
80 | private int[] mBitmapIds = { R.drawable.obama, R.drawable.road_rage,
81 | R.drawable.taipei_101, R.drawable.world };
82 |
83 | @Override
84 | public int getPageCount() {
85 | return 5;
86 | }
87 |
88 | private Bitmap loadBitmap(int width, int height, int index) {
89 | Bitmap b = Bitmap.createBitmap(width, height,
90 | Bitmap.Config.ARGB_8888);
91 | b.eraseColor(0xFFFFFFFF);
92 | Canvas c = new Canvas(b);
93 | Drawable d = getResources().getDrawable(mBitmapIds[index]);
94 |
95 | int margin = 7;
96 | int border = 3;
97 | Rect r = new Rect(margin, margin, width - margin, height - margin);
98 |
99 | int imageWidth = r.width() - (border * 2);
100 | int imageHeight = imageWidth * d.getIntrinsicHeight()
101 | / d.getIntrinsicWidth();
102 | if (imageHeight > r.height() - (border * 2)) {
103 | imageHeight = r.height() - (border * 2);
104 | imageWidth = imageHeight * d.getIntrinsicWidth()
105 | / d.getIntrinsicHeight();
106 | }
107 |
108 | r.left += ((r.width() - imageWidth) / 2) - border;
109 | r.right = r.left + imageWidth + border + border;
110 | r.top += ((r.height() - imageHeight) / 2) - border;
111 | r.bottom = r.top + imageHeight + border + border;
112 |
113 | Paint p = new Paint();
114 | p.setColor(0xFFC0C0C0);
115 | c.drawRect(r, p);
116 | r.left += border;
117 | r.right -= border;
118 | r.top += border;
119 | r.bottom -= border;
120 |
121 | d.setBounds(r);
122 | d.draw(c);
123 |
124 | return b;
125 | }
126 |
127 | @Override
128 | public void updatePage(CurlPage page, int width, int height, int index) {
129 |
130 | switch (index) {
131 | // First case is image on front side, solid colored back.
132 | case 0: {
133 | Bitmap front = loadBitmap(width, height, 0);
134 | page.setTexture(front, CurlPage.SIDE_FRONT);
135 | page.setColor(Color.rgb(180, 180, 180), CurlPage.SIDE_BACK);
136 | break;
137 | }
138 | // Second case is image on back side, solid colored front.
139 | case 1: {
140 | Bitmap back = loadBitmap(width, height, 2);
141 | page.setTexture(back, CurlPage.SIDE_BACK);
142 | page.setColor(Color.rgb(127, 140, 180), CurlPage.SIDE_FRONT);
143 | break;
144 | }
145 | // Third case is images on both sides.
146 | case 2: {
147 | Bitmap front = loadBitmap(width, height, 1);
148 | Bitmap back = loadBitmap(width, height, 3);
149 | page.setTexture(front, CurlPage.SIDE_FRONT);
150 | page.setTexture(back, CurlPage.SIDE_BACK);
151 | break;
152 | }
153 | // Fourth case is images on both sides - plus they are blend against
154 | // separate colors.
155 | case 3: {
156 | Bitmap front = loadBitmap(width, height, 2);
157 | Bitmap back = loadBitmap(width, height, 1);
158 | page.setTexture(front, CurlPage.SIDE_FRONT);
159 | page.setTexture(back, CurlPage.SIDE_BACK);
160 | page.setColor(Color.argb(127, 170, 130, 255),
161 | CurlPage.SIDE_FRONT);
162 | page.setColor(Color.rgb(255, 190, 150), CurlPage.SIDE_BACK);
163 | break;
164 | }
165 | // Fifth case is same image is assigned to front and back. In this
166 | // scenario only one texture is used and shared for both sides.
167 | case 4:
168 | Bitmap front = loadBitmap(width, height, 0);
169 | page.setTexture(front, CurlPage.SIDE_BOTH);
170 | page.setColor(Color.argb(127, 255, 255, 255),
171 | CurlPage.SIDE_BACK);
172 | break;
173 | }
174 | }
175 |
176 | }
177 |
178 | /**
179 | * CurlView size changed observer.
180 | */
181 | private class SizeChangedObserver implements CurlView.SizeChangedObserver {
182 | @Override
183 | public void onSizeChanged(int w, int h) {
184 | if (w > h) {
185 | mCurlView.setViewMode(CurlView.SHOW_TWO_PAGES);
186 | mCurlView.setMargins(.1f, .05f, .1f, .05f);
187 | } else {
188 | mCurlView.setViewMode(CurlView.SHOW_ONE_PAGE);
189 | mCurlView.setMargins(.1f, .1f, .1f, .1f);
190 | }
191 | }
192 | }
193 |
194 | }
--------------------------------------------------------------------------------
/src/fi/harism/curl/CurlMesh.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2012 Harri Smatt
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package fi.harism.curl;
18 |
19 | import java.nio.ByteBuffer;
20 | import java.nio.ByteOrder;
21 | import java.nio.FloatBuffer;
22 |
23 | import javax.microedition.khronos.opengles.GL10;
24 |
25 | import android.graphics.Bitmap;
26 | import android.graphics.Color;
27 | import android.graphics.PointF;
28 | import android.graphics.RectF;
29 | import android.opengl.GLUtils;
30 |
31 | /**
32 | * Class implementing actual curl/page rendering.
33 | *
34 | * @author harism
35 | */
36 | public class CurlMesh {
37 |
38 | // Flag for rendering some lines used for developing. Shows
39 | // curl position and one for the direction from the
40 | // position given. Comes handy once playing around with different
41 | // ways for following pointer.
42 | private static final boolean DRAW_CURL_POSITION = false;
43 | // Flag for drawing polygon outlines. Using this flag crashes on emulator
44 | // due to reason unknown to me. Leaving it here anyway as seeing polygon
45 | // outlines gives good insight how original rectangle is divided.
46 | private static final boolean DRAW_POLYGON_OUTLINES = false;
47 | // Flag for enabling shadow rendering.
48 | private static final boolean DRAW_SHADOW = true;
49 | // Flag for texture rendering. While this is likely something you
50 | // don't want to do it's been used for development purposes as texture
51 | // rendering is rather slow on emulator.
52 | private static final boolean DRAW_TEXTURE = true;
53 |
54 | // Colors for shadow. Inner one is the color drawn next to surface where
55 | // shadowed area starts and outer one is color shadow ends to.
56 | private static final float[] SHADOW_INNER_COLOR = { 0f, 0f, 0f, .5f };
57 | private static final float[] SHADOW_OUTER_COLOR = { 0f, 0f, 0f, .0f };
58 |
59 | // Let's avoid using 'new' as much as possible. Meaning we introduce arrays
60 | // once here and reuse them on runtime. Doesn't really have very much effect
61 | // but avoids some garbage collections from happening.
62 | private Array mArrDropShadowVertices;
63 | private Array mArrIntersections;
64 | private Array mArrOutputVertices;
65 | private Array mArrRotatedVertices;
66 | private Array mArrScanLines;
67 | private Array mArrSelfShadowVertices;
68 | private Array mArrTempShadowVertices;
69 | private Array mArrTempVertices;
70 |
71 | // Buffers for feeding rasterizer.
72 | private FloatBuffer mBufColors;
73 | private FloatBuffer mBufCurlPositionLines;
74 | private FloatBuffer mBufShadowColors;
75 | private FloatBuffer mBufShadowVertices;
76 | private FloatBuffer mBufTexCoords;
77 | private FloatBuffer mBufVertices;
78 |
79 | private int mCurlPositionLinesCount;
80 | private int mDropShadowCount;
81 |
82 | // Boolean for 'flipping' texture sideways.
83 | private boolean mFlipTexture = false;
84 | // Maximum number of split lines used for creating a curl.
85 | private int mMaxCurlSplits;
86 |
87 | // Bounding rectangle for this mesh. mRectagle[0] = top-left corner,
88 | // mRectangle[1] = bottom-left, mRectangle[2] = top-right and mRectangle[3]
89 | // bottom-right.
90 | private final Vertex[] mRectangle = new Vertex[4];
91 | private int mSelfShadowCount;
92 |
93 | private boolean mTextureBack = false;
94 | // Texture ids and other variables.
95 | private int[] mTextureIds = null;
96 | private final CurlPage mTexturePage = new CurlPage();
97 | private final RectF mTextureRectBack = new RectF();
98 | private final RectF mTextureRectFront = new RectF();
99 |
100 | private int mVerticesCountBack;
101 | private int mVerticesCountFront;
102 |
103 | /**
104 | * Constructor for mesh object.
105 | *
106 | * @param maxCurlSplits
107 | * Maximum number curl can be divided into. The bigger the value
108 | * the smoother curl will be. With the cost of having more
109 | * polygons for drawing.
110 | */
111 | public CurlMesh(int maxCurlSplits) {
112 | // There really is no use for 0 splits.
113 | mMaxCurlSplits = maxCurlSplits < 1 ? 1 : maxCurlSplits;
114 |
115 | mArrScanLines = new Array(maxCurlSplits + 2);
116 | mArrOutputVertices = new Array(7);
117 | mArrRotatedVertices = new Array(4);
118 | mArrIntersections = new Array(2);
119 | mArrTempVertices = new Array(7 + 4);
120 | for (int i = 0; i < 7 + 4; ++i) {
121 | mArrTempVertices.add(new Vertex());
122 | }
123 |
124 | if (DRAW_SHADOW) {
125 | mArrSelfShadowVertices = new Array(
126 | (mMaxCurlSplits + 2) * 2);
127 | mArrDropShadowVertices = new Array(
128 | (mMaxCurlSplits + 2) * 2);
129 | mArrTempShadowVertices = new Array(
130 | (mMaxCurlSplits + 2) * 2);
131 | for (int i = 0; i < (mMaxCurlSplits + 2) * 2; ++i) {
132 | mArrTempShadowVertices.add(new ShadowVertex());
133 | }
134 | }
135 |
136 | // Rectangle consists of 4 vertices. Index 0 = top-left, index 1 =
137 | // bottom-left, index 2 = top-right and index 3 = bottom-right.
138 | for (int i = 0; i < 4; ++i) {
139 | mRectangle[i] = new Vertex();
140 | }
141 | // Set up shadow penumbra direction to each vertex. We do fake 'self
142 | // shadow' calculations based on this information.
143 | mRectangle[0].mPenumbraX = mRectangle[1].mPenumbraX = mRectangle[1].mPenumbraY = mRectangle[3].mPenumbraY = -1;
144 | mRectangle[0].mPenumbraY = mRectangle[2].mPenumbraX = mRectangle[2].mPenumbraY = mRectangle[3].mPenumbraX = 1;
145 |
146 | if (DRAW_CURL_POSITION) {
147 | mCurlPositionLinesCount = 3;
148 | ByteBuffer hvbb = ByteBuffer
149 | .allocateDirect(mCurlPositionLinesCount * 2 * 2 * 4);
150 | hvbb.order(ByteOrder.nativeOrder());
151 | mBufCurlPositionLines = hvbb.asFloatBuffer();
152 | mBufCurlPositionLines.position(0);
153 | }
154 |
155 | // There are 4 vertices from bounding rect, max 2 from adding split line
156 | // to two corners and curl consists of max mMaxCurlSplits lines each
157 | // outputting 2 vertices.
158 | int maxVerticesCount = 4 + 2 + (2 * mMaxCurlSplits);
159 | ByteBuffer vbb = ByteBuffer.allocateDirect(maxVerticesCount * 3 * 4);
160 | vbb.order(ByteOrder.nativeOrder());
161 | mBufVertices = vbb.asFloatBuffer();
162 | mBufVertices.position(0);
163 |
164 | if (DRAW_TEXTURE) {
165 | ByteBuffer tbb = ByteBuffer
166 | .allocateDirect(maxVerticesCount * 2 * 4);
167 | tbb.order(ByteOrder.nativeOrder());
168 | mBufTexCoords = tbb.asFloatBuffer();
169 | mBufTexCoords.position(0);
170 | }
171 |
172 | ByteBuffer cbb = ByteBuffer.allocateDirect(maxVerticesCount * 4 * 4);
173 | cbb.order(ByteOrder.nativeOrder());
174 | mBufColors = cbb.asFloatBuffer();
175 | mBufColors.position(0);
176 |
177 | if (DRAW_SHADOW) {
178 | int maxShadowVerticesCount = (mMaxCurlSplits + 2) * 2 * 2;
179 | ByteBuffer scbb = ByteBuffer
180 | .allocateDirect(maxShadowVerticesCount * 4 * 4);
181 | scbb.order(ByteOrder.nativeOrder());
182 | mBufShadowColors = scbb.asFloatBuffer();
183 | mBufShadowColors.position(0);
184 |
185 | ByteBuffer sibb = ByteBuffer
186 | .allocateDirect(maxShadowVerticesCount * 3 * 4);
187 | sibb.order(ByteOrder.nativeOrder());
188 | mBufShadowVertices = sibb.asFloatBuffer();
189 | mBufShadowVertices.position(0);
190 |
191 | mDropShadowCount = mSelfShadowCount = 0;
192 | }
193 | }
194 |
195 | /**
196 | * Adds vertex to buffers.
197 | */
198 | private void addVertex(Vertex vertex) {
199 | mBufVertices.put((float) vertex.mPosX);
200 | mBufVertices.put((float) vertex.mPosY);
201 | mBufVertices.put((float) vertex.mPosZ);
202 | mBufColors.put(vertex.mColorFactor * Color.red(vertex.mColor) / 255f);
203 | mBufColors.put(vertex.mColorFactor * Color.green(vertex.mColor) / 255f);
204 | mBufColors.put(vertex.mColorFactor * Color.blue(vertex.mColor) / 255f);
205 | mBufColors.put(Color.alpha(vertex.mColor) / 255f);
206 | if (DRAW_TEXTURE) {
207 | mBufTexCoords.put((float) vertex.mTexX);
208 | mBufTexCoords.put((float) vertex.mTexY);
209 | }
210 | }
211 |
212 | /**
213 | * Sets curl for this mesh.
214 | *
215 | * @param curlPos
216 | * Position for curl 'center'. Can be any point on line collinear
217 | * to curl.
218 | * @param curlDir
219 | * Curl direction, should be normalized.
220 | * @param radius
221 | * Radius of curl.
222 | */
223 | public synchronized void curl(PointF curlPos, PointF curlDir, double radius) {
224 |
225 | // First add some 'helper' lines used for development.
226 | if (DRAW_CURL_POSITION) {
227 | mBufCurlPositionLines.position(0);
228 |
229 | mBufCurlPositionLines.put(curlPos.x);
230 | mBufCurlPositionLines.put(curlPos.y - 1.0f);
231 | mBufCurlPositionLines.put(curlPos.x);
232 | mBufCurlPositionLines.put(curlPos.y + 1.0f);
233 | mBufCurlPositionLines.put(curlPos.x - 1.0f);
234 | mBufCurlPositionLines.put(curlPos.y);
235 | mBufCurlPositionLines.put(curlPos.x + 1.0f);
236 | mBufCurlPositionLines.put(curlPos.y);
237 |
238 | mBufCurlPositionLines.put(curlPos.x);
239 | mBufCurlPositionLines.put(curlPos.y);
240 | mBufCurlPositionLines.put(curlPos.x + curlDir.x * 2);
241 | mBufCurlPositionLines.put(curlPos.y + curlDir.y * 2);
242 |
243 | mBufCurlPositionLines.position(0);
244 | }
245 |
246 | // Actual 'curl' implementation starts here.
247 | mBufVertices.position(0);
248 | mBufColors.position(0);
249 | if (DRAW_TEXTURE) {
250 | mBufTexCoords.position(0);
251 | }
252 |
253 | // Calculate curl angle from direction.
254 | double curlAngle = Math.acos(curlDir.x);
255 | curlAngle = curlDir.y > 0 ? -curlAngle : curlAngle;
256 |
257 | // Initiate rotated rectangle which's is translated to curlPos and
258 | // rotated so that curl direction heads to right (1,0). Vertices are
259 | // ordered in ascending order based on x -coordinate at the same time.
260 | // And using y -coordinate in very rare case in which two vertices have
261 | // same x -coordinate.
262 | mArrTempVertices.addAll(mArrRotatedVertices);
263 | mArrRotatedVertices.clear();
264 | for (int i = 0; i < 4; ++i) {
265 | Vertex v = mArrTempVertices.remove(0);
266 | v.set(mRectangle[i]);
267 | v.translate(-curlPos.x, -curlPos.y);
268 | v.rotateZ(-curlAngle);
269 | int j = 0;
270 | for (; j < mArrRotatedVertices.size(); ++j) {
271 | Vertex v2 = mArrRotatedVertices.get(j);
272 | if (v.mPosX > v2.mPosX) {
273 | break;
274 | }
275 | if (v.mPosX == v2.mPosX && v.mPosY > v2.mPosY) {
276 | break;
277 | }
278 | }
279 | mArrRotatedVertices.add(j, v);
280 | }
281 |
282 | // Rotated rectangle lines/vertex indices. We need to find bounding
283 | // lines for rotated rectangle. After sorting vertices according to
284 | // their x -coordinate we don't have to worry about vertices at indices
285 | // 0 and 1. But due to inaccuracy it's possible vertex 3 is not the
286 | // opposing corner from vertex 0. So we are calculating distance from
287 | // vertex 0 to vertices 2 and 3 - and altering line indices if needed.
288 | // Also vertices/lines are given in an order first one has x -coordinate
289 | // at least the latter one. This property is used in getIntersections to
290 | // see if there is an intersection.
291 | int lines[][] = { { 0, 1 }, { 0, 2 }, { 1, 3 }, { 2, 3 } };
292 | {
293 | // TODO: There really has to be more 'easier' way of doing this -
294 | // not including extensive use of sqrt.
295 | Vertex v0 = mArrRotatedVertices.get(0);
296 | Vertex v2 = mArrRotatedVertices.get(2);
297 | Vertex v3 = mArrRotatedVertices.get(3);
298 | double dist2 = Math.sqrt((v0.mPosX - v2.mPosX)
299 | * (v0.mPosX - v2.mPosX) + (v0.mPosY - v2.mPosY)
300 | * (v0.mPosY - v2.mPosY));
301 | double dist3 = Math.sqrt((v0.mPosX - v3.mPosX)
302 | * (v0.mPosX - v3.mPosX) + (v0.mPosY - v3.mPosY)
303 | * (v0.mPosY - v3.mPosY));
304 | if (dist2 > dist3) {
305 | lines[1][1] = 3;
306 | lines[2][1] = 2;
307 | }
308 | }
309 |
310 | mVerticesCountFront = mVerticesCountBack = 0;
311 |
312 | if (DRAW_SHADOW) {
313 | mArrTempShadowVertices.addAll(mArrDropShadowVertices);
314 | mArrTempShadowVertices.addAll(mArrSelfShadowVertices);
315 | mArrDropShadowVertices.clear();
316 | mArrSelfShadowVertices.clear();
317 | }
318 |
319 | // Length of 'curl' curve.
320 | double curlLength = Math.PI * radius;
321 | // Calculate scan lines.
322 | // TODO: Revisit this code one day. There is room for optimization here.
323 | mArrScanLines.clear();
324 | if (mMaxCurlSplits > 0) {
325 | mArrScanLines.add((double) 0);
326 | }
327 | for (int i = 1; i < mMaxCurlSplits; ++i) {
328 | mArrScanLines.add((-curlLength * i) / (mMaxCurlSplits - 1));
329 | }
330 | // As mRotatedVertices is ordered regarding x -coordinate, adding
331 | // this scan line produces scan area picking up vertices which are
332 | // rotated completely. One could say 'until infinity'.
333 | mArrScanLines.add(mArrRotatedVertices.get(3).mPosX - 1);
334 |
335 | // Start from right most vertex. Pretty much the same as first scan area
336 | // is starting from 'infinity'.
337 | double scanXmax = mArrRotatedVertices.get(0).mPosX + 1;
338 |
339 | for (int i = 0; i < mArrScanLines.size(); ++i) {
340 | // Once we have scanXmin and scanXmax we have a scan area to start
341 | // working with.
342 | double scanXmin = mArrScanLines.get(i);
343 | // First iterate 'original' rectangle vertices within scan area.
344 | for (int j = 0; j < mArrRotatedVertices.size(); ++j) {
345 | Vertex v = mArrRotatedVertices.get(j);
346 | // Test if vertex lies within this scan area.
347 | // TODO: Frankly speaking, can't remember why equality check was
348 | // added to both ends. Guessing it was somehow related to case
349 | // where radius=0f, which, given current implementation, could
350 | // be handled much more effectively anyway.
351 | if (v.mPosX >= scanXmin && v.mPosX <= scanXmax) {
352 | // Pop out a vertex from temp vertices.
353 | Vertex n = mArrTempVertices.remove(0);
354 | n.set(v);
355 | // This is done solely for triangulation reasons. Given a
356 | // rotated rectangle it has max 2 vertices having
357 | // intersection.
358 | Array intersections = getIntersections(
359 | mArrRotatedVertices, lines, n.mPosX);
360 | // In a sense one could say we're adding vertices always in
361 | // two, positioned at the ends of intersecting line. And for
362 | // triangulation to work properly they are added based on y
363 | // -coordinate. And this if-else is doing it for us.
364 | if (intersections.size() == 1
365 | && intersections.get(0).mPosY > v.mPosY) {
366 | // In case intersecting vertex is higher add it first.
367 | mArrOutputVertices.addAll(intersections);
368 | mArrOutputVertices.add(n);
369 | } else if (intersections.size() <= 1) {
370 | // Otherwise add original vertex first.
371 | mArrOutputVertices.add(n);
372 | mArrOutputVertices.addAll(intersections);
373 | } else {
374 | // There should never be more than 1 intersecting
375 | // vertex. But if it happens as a fallback simply skip
376 | // everything.
377 | mArrTempVertices.add(n);
378 | mArrTempVertices.addAll(intersections);
379 | }
380 | }
381 | }
382 |
383 | // Search for scan line intersections.
384 | Array intersections = getIntersections(mArrRotatedVertices,
385 | lines, scanXmin);
386 |
387 | // We expect to get 0 or 2 vertices. In rare cases there's only one
388 | // but in general given a scan line intersecting rectangle there
389 | // should be 2 intersecting vertices.
390 | if (intersections.size() == 2) {
391 | // There were two intersections, add them based on y
392 | // -coordinate, higher first, lower last.
393 | Vertex v1 = intersections.get(0);
394 | Vertex v2 = intersections.get(1);
395 | if (v1.mPosY < v2.mPosY) {
396 | mArrOutputVertices.add(v2);
397 | mArrOutputVertices.add(v1);
398 | } else {
399 | mArrOutputVertices.addAll(intersections);
400 | }
401 | } else if (intersections.size() != 0) {
402 | // This happens in a case in which there is a original vertex
403 | // exactly at scan line or something went very much wrong if
404 | // there are 3+ vertices. What ever the reason just return the
405 | // vertices to temp vertices for later use. In former case it
406 | // was handled already earlier once iterating through
407 | // mRotatedVertices, in latter case it's better to avoid doing
408 | // anything with them.
409 | mArrTempVertices.addAll(intersections);
410 | }
411 |
412 | // Add vertices found during this iteration to vertex etc buffers.
413 | while (mArrOutputVertices.size() > 0) {
414 | Vertex v = mArrOutputVertices.remove(0);
415 | mArrTempVertices.add(v);
416 |
417 | // Local texture front-facing flag.
418 | boolean textureFront;
419 |
420 | // Untouched vertices.
421 | if (i == 0) {
422 | textureFront = true;
423 | mVerticesCountFront++;
424 | }
425 | // 'Completely' rotated vertices.
426 | else if (i == mArrScanLines.size() - 1 || curlLength == 0) {
427 | v.mPosX = -(curlLength + v.mPosX);
428 | v.mPosZ = 2 * radius;
429 | v.mPenumbraX = -v.mPenumbraX;
430 |
431 | textureFront = false;
432 | mVerticesCountBack++;
433 | }
434 | // Vertex lies within 'curl'.
435 | else {
436 | // Even though it's not obvious from the if-else clause,
437 | // here v.mPosX is between [-curlLength, 0]. And we can do
438 | // calculations around a half cylinder.
439 | double rotY = Math.PI * (v.mPosX / curlLength);
440 | v.mPosX = radius * Math.sin(rotY);
441 | v.mPosZ = radius - (radius * Math.cos(rotY));
442 | v.mPenumbraX *= Math.cos(rotY);
443 | // Map color multiplier to [.1f, 1f] range.
444 | v.mColorFactor = (float) (.1f + .9f * Math.sqrt(Math
445 | .sin(rotY) + 1));
446 |
447 | if (v.mPosZ >= radius) {
448 | textureFront = false;
449 | mVerticesCountBack++;
450 | } else {
451 | textureFront = true;
452 | mVerticesCountFront++;
453 | }
454 | }
455 |
456 | // We use local textureFront for flipping backside texture
457 | // locally. Plus additionally if mesh is in flip texture mode,
458 | // we'll make the procedure "backwards". Also, until this point,
459 | // texture coordinates are within [0, 1] range so we'll adjust
460 | // them to final texture coordinates too.
461 | if (textureFront != mFlipTexture) {
462 | v.mTexX *= mTextureRectFront.right;
463 | v.mTexY *= mTextureRectFront.bottom;
464 | v.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
465 | } else {
466 | v.mTexX *= mTextureRectBack.right;
467 | v.mTexY *= mTextureRectBack.bottom;
468 | v.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
469 | }
470 |
471 | // Move vertex back to 'world' coordinates.
472 | v.rotateZ(curlAngle);
473 | v.translate(curlPos.x, curlPos.y);
474 | addVertex(v);
475 |
476 | // Drop shadow is cast 'behind' the curl.
477 | if (DRAW_SHADOW && v.mPosZ > 0 && v.mPosZ <= radius) {
478 | ShadowVertex sv = mArrTempShadowVertices.remove(0);
479 | sv.mPosX = v.mPosX;
480 | sv.mPosY = v.mPosY;
481 | sv.mPosZ = v.mPosZ;
482 | sv.mPenumbraX = (v.mPosZ / 2) * -curlDir.x;
483 | sv.mPenumbraY = (v.mPosZ / 2) * -curlDir.y;
484 | sv.mPenumbraColor = v.mPosZ / radius;
485 | int idx = (mArrDropShadowVertices.size() + 1) / 2;
486 | mArrDropShadowVertices.add(idx, sv);
487 | }
488 | // Self shadow is cast partly over mesh.
489 | if (DRAW_SHADOW && v.mPosZ > radius) {
490 | ShadowVertex sv = mArrTempShadowVertices.remove(0);
491 | sv.mPosX = v.mPosX;
492 | sv.mPosY = v.mPosY;
493 | sv.mPosZ = v.mPosZ;
494 | sv.mPenumbraX = ((v.mPosZ - radius) / 3) * v.mPenumbraX;
495 | sv.mPenumbraY = ((v.mPosZ - radius) / 3) * v.mPenumbraY;
496 | sv.mPenumbraColor = (v.mPosZ - radius) / (2 * radius);
497 | int idx = (mArrSelfShadowVertices.size() + 1) / 2;
498 | mArrSelfShadowVertices.add(idx, sv);
499 | }
500 | }
501 |
502 | // Switch scanXmin as scanXmax for next iteration.
503 | scanXmax = scanXmin;
504 | }
505 |
506 | mBufVertices.position(0);
507 | mBufColors.position(0);
508 | if (DRAW_TEXTURE) {
509 | mBufTexCoords.position(0);
510 | }
511 |
512 | // Add shadow Vertices.
513 | if (DRAW_SHADOW) {
514 | mBufShadowColors.position(0);
515 | mBufShadowVertices.position(0);
516 | mDropShadowCount = 0;
517 |
518 | for (int i = 0; i < mArrDropShadowVertices.size(); ++i) {
519 | ShadowVertex sv = mArrDropShadowVertices.get(i);
520 | mBufShadowVertices.put((float) sv.mPosX);
521 | mBufShadowVertices.put((float) sv.mPosY);
522 | mBufShadowVertices.put((float) sv.mPosZ);
523 | mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
524 | mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
525 | mBufShadowVertices.put((float) sv.mPosZ);
526 | for (int j = 0; j < 4; ++j) {
527 | double color = SHADOW_OUTER_COLOR[j]
528 | + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
529 | * sv.mPenumbraColor;
530 | mBufShadowColors.put((float) color);
531 | }
532 | mBufShadowColors.put(SHADOW_OUTER_COLOR);
533 | mDropShadowCount += 2;
534 | }
535 | mSelfShadowCount = 0;
536 | for (int i = 0; i < mArrSelfShadowVertices.size(); ++i) {
537 | ShadowVertex sv = mArrSelfShadowVertices.get(i);
538 | mBufShadowVertices.put((float) sv.mPosX);
539 | mBufShadowVertices.put((float) sv.mPosY);
540 | mBufShadowVertices.put((float) sv.mPosZ);
541 | mBufShadowVertices.put((float) (sv.mPosX + sv.mPenumbraX));
542 | mBufShadowVertices.put((float) (sv.mPosY + sv.mPenumbraY));
543 | mBufShadowVertices.put((float) sv.mPosZ);
544 | for (int j = 0; j < 4; ++j) {
545 | double color = SHADOW_OUTER_COLOR[j]
546 | + (SHADOW_INNER_COLOR[j] - SHADOW_OUTER_COLOR[j])
547 | * sv.mPenumbraColor;
548 | mBufShadowColors.put((float) color);
549 | }
550 | mBufShadowColors.put(SHADOW_OUTER_COLOR);
551 | mSelfShadowCount += 2;
552 | }
553 | mBufShadowColors.position(0);
554 | mBufShadowVertices.position(0);
555 | }
556 | }
557 |
558 | /**
559 | * Calculates intersections for given scan line.
560 | */
561 | private Array getIntersections(Array vertices,
562 | int[][] lineIndices, double scanX) {
563 | mArrIntersections.clear();
564 | // Iterate through rectangle lines each re-presented as a pair of
565 | // vertices.
566 | for (int j = 0; j < lineIndices.length; j++) {
567 | Vertex v1 = vertices.get(lineIndices[j][0]);
568 | Vertex v2 = vertices.get(lineIndices[j][1]);
569 | // Here we expect that v1.mPosX >= v2.mPosX and wont do intersection
570 | // test the opposite way.
571 | if (v1.mPosX > scanX && v2.mPosX < scanX) {
572 | // There is an intersection, calculate coefficient telling 'how
573 | // far' scanX is from v2.
574 | double c = (scanX - v2.mPosX) / (v1.mPosX - v2.mPosX);
575 | Vertex n = mArrTempVertices.remove(0);
576 | n.set(v2);
577 | n.mPosX = scanX;
578 | n.mPosY += (v1.mPosY - v2.mPosY) * c;
579 | if (DRAW_TEXTURE) {
580 | n.mTexX += (v1.mTexX - v2.mTexX) * c;
581 | n.mTexY += (v1.mTexY - v2.mTexY) * c;
582 | }
583 | if (DRAW_SHADOW) {
584 | n.mPenumbraX += (v1.mPenumbraX - v2.mPenumbraX) * c;
585 | n.mPenumbraY += (v1.mPenumbraY - v2.mPenumbraY) * c;
586 | }
587 | mArrIntersections.add(n);
588 | }
589 | }
590 | return mArrIntersections;
591 | }
592 |
593 | /**
594 | * Getter for textures page for this mesh.
595 | */
596 | public synchronized CurlPage getTexturePage() {
597 | return mTexturePage;
598 | }
599 |
600 | /**
601 | * Renders our page curl mesh.
602 | */
603 | public synchronized void onDrawFrame(GL10 gl) {
604 | // First allocate texture if there is not one yet.
605 | if (DRAW_TEXTURE && mTextureIds == null) {
606 | // Generate texture.
607 | mTextureIds = new int[2];
608 | gl.glGenTextures(2, mTextureIds, 0);
609 | for (int textureId : mTextureIds) {
610 | // Set texture attributes.
611 | gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);
612 | gl.glTexParameterf(GL10.GL_TEXTURE_2D,
613 | GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
614 | gl.glTexParameterf(GL10.GL_TEXTURE_2D,
615 | GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);
616 | gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
617 | GL10.GL_CLAMP_TO_EDGE);
618 | gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
619 | GL10.GL_CLAMP_TO_EDGE);
620 | }
621 | }
622 |
623 | if (DRAW_TEXTURE && mTexturePage.getTexturesChanged()) {
624 | gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
625 | Bitmap texture = mTexturePage.getTexture(mTextureRectFront,
626 | CurlPage.SIDE_FRONT);
627 | GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
628 | texture.recycle();
629 |
630 | mTextureBack = mTexturePage.hasBackTexture();
631 | if (mTextureBack) {
632 | gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
633 | texture = mTexturePage.getTexture(mTextureRectBack,
634 | CurlPage.SIDE_BACK);
635 | GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, texture, 0);
636 | texture.recycle();
637 | } else {
638 | mTextureRectBack.set(mTextureRectFront);
639 | }
640 |
641 | mTexturePage.recycle();
642 | reset();
643 | }
644 |
645 | // Some 'global' settings.
646 | gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
647 |
648 | // TODO: Drop shadow drawing is done temporarily here to hide some
649 | // problems with its calculation.
650 | if (DRAW_SHADOW) {
651 | gl.glDisable(GL10.GL_TEXTURE_2D);
652 | gl.glEnable(GL10.GL_BLEND);
653 | gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
654 | gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
655 | gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
656 | gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
657 | gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mDropShadowCount);
658 | gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
659 | gl.glDisable(GL10.GL_BLEND);
660 | }
661 |
662 | if (DRAW_TEXTURE) {
663 | gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
664 | gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mBufTexCoords);
665 | }
666 | gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
667 | // Enable color array.
668 | gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
669 | gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufColors);
670 |
671 | // Draw front facing blank vertices.
672 | gl.glDisable(GL10.GL_TEXTURE_2D);
673 | gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
674 |
675 | // Draw front facing texture.
676 | if (DRAW_TEXTURE) {
677 | gl.glEnable(GL10.GL_BLEND);
678 | gl.glEnable(GL10.GL_TEXTURE_2D);
679 |
680 | if (!mFlipTexture || !mTextureBack) {
681 | gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
682 | } else {
683 | gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
684 | }
685 |
686 | gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
687 | gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mVerticesCountFront);
688 |
689 | gl.glDisable(GL10.GL_BLEND);
690 | gl.glDisable(GL10.GL_TEXTURE_2D);
691 | }
692 |
693 | int backStartIdx = Math.max(0, mVerticesCountFront - 2);
694 | int backCount = mVerticesCountFront + mVerticesCountBack - backStartIdx;
695 |
696 | // Draw back facing blank vertices.
697 | gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
698 |
699 | // Draw back facing texture.
700 | if (DRAW_TEXTURE) {
701 | gl.glEnable(GL10.GL_BLEND);
702 | gl.glEnable(GL10.GL_TEXTURE_2D);
703 |
704 | if (mFlipTexture || !mTextureBack) {
705 | gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[0]);
706 | } else {
707 | gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIds[1]);
708 | }
709 |
710 | gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
711 | gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, backStartIdx, backCount);
712 |
713 | gl.glDisable(GL10.GL_BLEND);
714 | gl.glDisable(GL10.GL_TEXTURE_2D);
715 | }
716 |
717 | // Disable textures and color array.
718 | gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
719 | gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
720 |
721 | if (DRAW_POLYGON_OUTLINES) {
722 | gl.glEnable(GL10.GL_BLEND);
723 | gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
724 | gl.glLineWidth(1.0f);
725 | gl.glColor4f(0.5f, 0.5f, 1.0f, 1.0f);
726 | gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufVertices);
727 | gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mVerticesCountFront);
728 | gl.glDisable(GL10.GL_BLEND);
729 | }
730 |
731 | if (DRAW_CURL_POSITION) {
732 | gl.glEnable(GL10.GL_BLEND);
733 | gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
734 | gl.glLineWidth(1.0f);
735 | gl.glColor4f(1.0f, 0.5f, 0.5f, 1.0f);
736 | gl.glVertexPointer(2, GL10.GL_FLOAT, 0, mBufCurlPositionLines);
737 | gl.glDrawArrays(GL10.GL_LINES, 0, mCurlPositionLinesCount * 2);
738 | gl.glDisable(GL10.GL_BLEND);
739 | }
740 |
741 | if (DRAW_SHADOW) {
742 | gl.glEnable(GL10.GL_BLEND);
743 | gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
744 | gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
745 | gl.glColorPointer(4, GL10.GL_FLOAT, 0, mBufShadowColors);
746 | gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mBufShadowVertices);
747 | gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, mDropShadowCount,
748 | mSelfShadowCount);
749 | gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
750 | gl.glDisable(GL10.GL_BLEND);
751 | }
752 |
753 | gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
754 | }
755 |
756 | /**
757 | * Resets mesh to 'initial' state. Meaning this mesh will draw a plain
758 | * textured rectangle after call to this method.
759 | */
760 | public synchronized void reset() {
761 | mBufVertices.position(0);
762 | mBufColors.position(0);
763 | if (DRAW_TEXTURE) {
764 | mBufTexCoords.position(0);
765 | }
766 | for (int i = 0; i < 4; ++i) {
767 | Vertex tmp = mArrTempVertices.get(0);
768 | tmp.set(mRectangle[i]);
769 |
770 | if (mFlipTexture) {
771 | tmp.mTexX *= mTextureRectBack.right;
772 | tmp.mTexY *= mTextureRectBack.bottom;
773 | tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_BACK);
774 | } else {
775 | tmp.mTexX *= mTextureRectFront.right;
776 | tmp.mTexY *= mTextureRectFront.bottom;
777 | tmp.mColor = mTexturePage.getColor(CurlPage.SIDE_FRONT);
778 | }
779 |
780 | addVertex(tmp);
781 | }
782 | mVerticesCountFront = 4;
783 | mVerticesCountBack = 0;
784 | mBufVertices.position(0);
785 | mBufColors.position(0);
786 | if (DRAW_TEXTURE) {
787 | mBufTexCoords.position(0);
788 | }
789 |
790 | mDropShadowCount = mSelfShadowCount = 0;
791 | }
792 |
793 | /**
794 | * Resets allocated texture id forcing creation of new one. After calling
795 | * this method you most likely want to set bitmap too as it's lost. This
796 | * method should be called only once e.g GL context is re-created as this
797 | * method does not release previous texture id, only makes sure new one is
798 | * requested on next render.
799 | */
800 | public synchronized void resetTexture() {
801 | mTextureIds = null;
802 | }
803 |
804 | /**
805 | * If true, flips texture sideways.
806 | */
807 | public synchronized void setFlipTexture(boolean flipTexture) {
808 | mFlipTexture = flipTexture;
809 | if (flipTexture) {
810 | setTexCoords(1f, 0f, 0f, 1f);
811 | } else {
812 | setTexCoords(0f, 0f, 1f, 1f);
813 | }
814 | }
815 |
816 | /**
817 | * Update mesh bounds.
818 | */
819 | public void setRect(RectF r) {
820 | mRectangle[0].mPosX = r.left;
821 | mRectangle[0].mPosY = r.top;
822 | mRectangle[1].mPosX = r.left;
823 | mRectangle[1].mPosY = r.bottom;
824 | mRectangle[2].mPosX = r.right;
825 | mRectangle[2].mPosY = r.top;
826 | mRectangle[3].mPosX = r.right;
827 | mRectangle[3].mPosY = r.bottom;
828 | }
829 |
830 | /**
831 | * Sets texture coordinates to mRectangle vertices.
832 | */
833 | private synchronized void setTexCoords(float left, float top, float right,
834 | float bottom) {
835 | mRectangle[0].mTexX = left;
836 | mRectangle[0].mTexY = top;
837 | mRectangle[1].mTexX = left;
838 | mRectangle[1].mTexY = bottom;
839 | mRectangle[2].mTexX = right;
840 | mRectangle[2].mTexY = top;
841 | mRectangle[3].mTexX = right;
842 | mRectangle[3].mTexY = bottom;
843 | }
844 |
845 | /**
846 | * Simple fixed size array implementation.
847 | */
848 | private class Array {
849 | private Object[] mArray;
850 | private int mCapacity;
851 | private int mSize;
852 |
853 | public Array(int capacity) {
854 | mCapacity = capacity;
855 | mArray = new Object[capacity];
856 | }
857 |
858 | public void add(int index, T item) {
859 | if (index < 0 || index > mSize || mSize >= mCapacity) {
860 | throw new IndexOutOfBoundsException();
861 | }
862 | for (int i = mSize; i > index; --i) {
863 | mArray[i] = mArray[i - 1];
864 | }
865 | mArray[index] = item;
866 | ++mSize;
867 | }
868 |
869 | public void add(T item) {
870 | if (mSize >= mCapacity) {
871 | throw new IndexOutOfBoundsException();
872 | }
873 | mArray[mSize++] = item;
874 | }
875 |
876 | public void addAll(Array array) {
877 | if (mSize + array.size() > mCapacity) {
878 | throw new IndexOutOfBoundsException();
879 | }
880 | for (int i = 0; i < array.size(); ++i) {
881 | mArray[mSize++] = array.get(i);
882 | }
883 | }
884 |
885 | public void clear() {
886 | mSize = 0;
887 | }
888 |
889 | @SuppressWarnings("unchecked")
890 | public T get(int index) {
891 | if (index < 0 || index >= mSize) {
892 | throw new IndexOutOfBoundsException();
893 | }
894 | return (T) mArray[index];
895 | }
896 |
897 | @SuppressWarnings("unchecked")
898 | public T remove(int index) {
899 | if (index < 0 || index >= mSize) {
900 | throw new IndexOutOfBoundsException();
901 | }
902 | T item = (T) mArray[index];
903 | for (int i = index; i < mSize - 1; ++i) {
904 | mArray[i] = mArray[i + 1];
905 | }
906 | --mSize;
907 | return item;
908 | }
909 |
910 | public int size() {
911 | return mSize;
912 | }
913 |
914 | }
915 |
916 | /**
917 | * Holder for shadow vertex information.
918 | */
919 | private class ShadowVertex {
920 | public double mPenumbraColor;
921 | public double mPenumbraX;
922 | public double mPenumbraY;
923 | public double mPosX;
924 | public double mPosY;
925 | public double mPosZ;
926 | }
927 |
928 | /**
929 | * Holder for vertex information.
930 | */
931 | private class Vertex {
932 | public int mColor;
933 | public float mColorFactor;
934 | public double mPenumbraX;
935 | public double mPenumbraY;
936 | public double mPosX;
937 | public double mPosY;
938 | public double mPosZ;
939 | public double mTexX;
940 | public double mTexY;
941 |
942 | public Vertex() {
943 | mPosX = mPosY = mPosZ = mTexX = mTexY = 0;
944 | mColorFactor = 1.0f;
945 | }
946 |
947 | public void rotateZ(double theta) {
948 | double cos = Math.cos(theta);
949 | double sin = Math.sin(theta);
950 | double x = mPosX * cos + mPosY * sin;
951 | double y = mPosX * -sin + mPosY * cos;
952 | mPosX = x;
953 | mPosY = y;
954 | double px = mPenumbraX * cos + mPenumbraY * sin;
955 | double py = mPenumbraX * -sin + mPenumbraY * cos;
956 | mPenumbraX = px;
957 | mPenumbraY = py;
958 | }
959 |
960 | public void set(Vertex vertex) {
961 | mPosX = vertex.mPosX;
962 | mPosY = vertex.mPosY;
963 | mPosZ = vertex.mPosZ;
964 | mTexX = vertex.mTexX;
965 | mTexY = vertex.mTexY;
966 | mPenumbraX = vertex.mPenumbraX;
967 | mPenumbraY = vertex.mPenumbraY;
968 | mColor = vertex.mColor;
969 | mColorFactor = vertex.mColorFactor;
970 | }
971 |
972 | public void translate(double dx, double dy) {
973 | mPosX += dx;
974 | mPosY += dy;
975 | }
976 | }
977 | }
978 |
--------------------------------------------------------------------------------
/src/fi/harism/curl/CurlPage.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2012 Harri Smatt
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package fi.harism.curl;
18 |
19 | import android.graphics.Bitmap;
20 | import android.graphics.Canvas;
21 | import android.graphics.Color;
22 | import android.graphics.RectF;
23 |
24 | /**
25 | * Storage class for page textures, blend colors and possibly some other values
26 | * in the future.
27 | *
28 | * @author harism
29 | */
30 | public class CurlPage {
31 |
32 | public static final int SIDE_BACK = 2;
33 | public static final int SIDE_BOTH = 3;
34 | public static final int SIDE_FRONT = 1;
35 |
36 | private int mColorBack;
37 | private int mColorFront;
38 | private Bitmap mTextureBack;
39 | private Bitmap mTextureFront;
40 | private boolean mTexturesChanged;
41 |
42 | /**
43 | * Default constructor.
44 | */
45 | public CurlPage() {
46 | reset();
47 | }
48 |
49 | /**
50 | * Getter for color.
51 | */
52 | public int getColor(int side) {
53 | switch (side) {
54 | case SIDE_FRONT:
55 | return mColorFront;
56 | default:
57 | return mColorBack;
58 | }
59 | }
60 |
61 | /**
62 | * Calculates the next highest power of two for a given integer.
63 | */
64 | private int getNextHighestPO2(int n) {
65 | n -= 1;
66 | n = n | (n >> 1);
67 | n = n | (n >> 2);
68 | n = n | (n >> 4);
69 | n = n | (n >> 8);
70 | n = n | (n >> 16);
71 | n = n | (n >> 32);
72 | return n + 1;
73 | }
74 |
75 | /**
76 | * Generates nearest power of two sized Bitmap for give Bitmap. Returns this
77 | * new Bitmap using default return statement + original texture coordinates
78 | * are stored into RectF.
79 | */
80 | private Bitmap getTexture(Bitmap bitmap, RectF textureRect) {
81 | // Bitmap original size.
82 | int w = bitmap.getWidth();
83 | int h = bitmap.getHeight();
84 | // Bitmap size expanded to next power of two. This is done due to
85 | // the requirement on many devices, texture width and height should
86 | // be power of two.
87 | int newW = getNextHighestPO2(w);
88 | int newH = getNextHighestPO2(h);
89 |
90 | // TODO: Is there another way to create a bigger Bitmap and copy
91 | // original Bitmap to it more efficiently? Immutable bitmap anyone?
92 | Bitmap bitmapTex = Bitmap.createBitmap(newW, newH, bitmap.getConfig());
93 | Canvas c = new Canvas(bitmapTex);
94 | c.drawBitmap(bitmap, 0, 0, null);
95 |
96 | // Calculate final texture coordinates.
97 | float texX = (float) w / newW;
98 | float texY = (float) h / newH;
99 | textureRect.set(0f, 0f, texX, texY);
100 |
101 | return bitmapTex;
102 | }
103 |
104 | /**
105 | * Getter for textures. Creates Bitmap sized to nearest power of two, copies
106 | * original Bitmap into it and returns it. RectF given as parameter is
107 | * filled with actual texture coordinates in this new upscaled texture
108 | * Bitmap.
109 | */
110 | public Bitmap getTexture(RectF textureRect, int side) {
111 | switch (side) {
112 | case SIDE_FRONT:
113 | return getTexture(mTextureFront, textureRect);
114 | default:
115 | return getTexture(mTextureBack, textureRect);
116 | }
117 | }
118 |
119 | /**
120 | * Returns true if textures have changed.
121 | */
122 | public boolean getTexturesChanged() {
123 | return mTexturesChanged;
124 | }
125 |
126 | /**
127 | * Returns true if back siding texture exists and it differs from front
128 | * facing one.
129 | */
130 | public boolean hasBackTexture() {
131 | return !mTextureFront.equals(mTextureBack);
132 | }
133 |
134 | /**
135 | * Recycles and frees underlying Bitmaps.
136 | */
137 | public void recycle() {
138 | if (mTextureFront != null) {
139 | mTextureFront.recycle();
140 | }
141 | mTextureFront = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
142 | mTextureFront.eraseColor(mColorFront);
143 | if (mTextureBack != null) {
144 | mTextureBack.recycle();
145 | }
146 | mTextureBack = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
147 | mTextureBack.eraseColor(mColorBack);
148 | mTexturesChanged = false;
149 | }
150 |
151 | /**
152 | * Resets this CurlPage into its initial state.
153 | */
154 | public void reset() {
155 | mColorBack = Color.WHITE;
156 | mColorFront = Color.WHITE;
157 | recycle();
158 | }
159 |
160 | /**
161 | * Setter blend color.
162 | */
163 | public void setColor(int color, int side) {
164 | switch (side) {
165 | case SIDE_FRONT:
166 | mColorFront = color;
167 | break;
168 | case SIDE_BACK:
169 | mColorBack = color;
170 | break;
171 | default:
172 | mColorFront = mColorBack = color;
173 | break;
174 | }
175 | }
176 |
177 | /**
178 | * Setter for textures.
179 | */
180 | public void setTexture(Bitmap texture, int side) {
181 | if (texture == null) {
182 | texture = Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565);
183 | if (side == SIDE_BACK) {
184 | texture.eraseColor(mColorBack);
185 | } else {
186 | texture.eraseColor(mColorFront);
187 | }
188 | }
189 | switch (side) {
190 | case SIDE_FRONT:
191 | if (mTextureFront != null)
192 | mTextureFront.recycle();
193 | mTextureFront = texture;
194 | break;
195 | case SIDE_BACK:
196 | if (mTextureBack != null)
197 | mTextureBack.recycle();
198 | mTextureBack = texture;
199 | break;
200 | case SIDE_BOTH:
201 | if (mTextureFront != null)
202 | mTextureFront.recycle();
203 | if (mTextureBack != null)
204 | mTextureBack.recycle();
205 | mTextureFront = mTextureBack = texture;
206 | break;
207 | }
208 | mTexturesChanged = true;
209 | }
210 |
211 | }
212 |
--------------------------------------------------------------------------------
/src/fi/harism/curl/CurlRenderer.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2012 Harri Smatt
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package fi.harism.curl;
18 |
19 | import java.util.Vector;
20 |
21 | import javax.microedition.khronos.egl.EGLConfig;
22 | import javax.microedition.khronos.opengles.GL10;
23 |
24 | import android.graphics.Color;
25 | import android.graphics.PointF;
26 | import android.graphics.RectF;
27 | import android.opengl.GLSurfaceView;
28 | import android.opengl.GLU;
29 |
30 | /**
31 | * Actual renderer class.
32 | *
33 | * @author harism
34 | */
35 | public class CurlRenderer implements GLSurfaceView.Renderer {
36 |
37 | // Constant for requesting left page rect.
38 | public static final int PAGE_LEFT = 1;
39 | // Constant for requesting right page rect.
40 | public static final int PAGE_RIGHT = 2;
41 | // Constants for changing view mode.
42 | public static final int SHOW_ONE_PAGE = 1;
43 | public static final int SHOW_TWO_PAGES = 2;
44 | // Set to true for checking quickly how perspective projection looks.
45 | private static final boolean USE_PERSPECTIVE_PROJECTION = false;
46 | // Background fill color.
47 | private int mBackgroundColor;
48 | // Curl meshes used for static and dynamic rendering.
49 | private Vector mCurlMeshes;
50 | private RectF mMargins = new RectF();
51 | private CurlRenderer.Observer mObserver;
52 | // Page rectangles.
53 | private RectF mPageRectLeft;
54 | private RectF mPageRectRight;
55 | // View mode.
56 | private int mViewMode = SHOW_ONE_PAGE;
57 | // Screen size.
58 | private int mViewportWidth, mViewportHeight;
59 | // Rect for render area.
60 | private RectF mViewRect = new RectF();
61 |
62 | /**
63 | * Basic constructor.
64 | */
65 | public CurlRenderer(CurlRenderer.Observer observer) {
66 | mObserver = observer;
67 | mCurlMeshes = new Vector();
68 | mPageRectLeft = new RectF();
69 | mPageRectRight = new RectF();
70 | }
71 |
72 | /**
73 | * Adds CurlMesh to this renderer.
74 | */
75 | public synchronized void addCurlMesh(CurlMesh mesh) {
76 | removeCurlMesh(mesh);
77 | mCurlMeshes.add(mesh);
78 | }
79 |
80 | /**
81 | * Returns rect reserved for left or right page. Value page should be
82 | * PAGE_LEFT or PAGE_RIGHT.
83 | */
84 | public RectF getPageRect(int page) {
85 | if (page == PAGE_LEFT) {
86 | return mPageRectLeft;
87 | } else if (page == PAGE_RIGHT) {
88 | return mPageRectRight;
89 | }
90 | return null;
91 | }
92 |
93 | @Override
94 | public synchronized void onDrawFrame(GL10 gl) {
95 |
96 | mObserver.onDrawFrame();
97 |
98 | gl.glClearColor(Color.red(mBackgroundColor) / 255f,
99 | Color.green(mBackgroundColor) / 255f,
100 | Color.blue(mBackgroundColor) / 255f,
101 | Color.alpha(mBackgroundColor) / 255f);
102 | gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
103 | gl.glLoadIdentity();
104 |
105 | if (USE_PERSPECTIVE_PROJECTION) {
106 | gl.glTranslatef(0, 0, -6f);
107 | }
108 |
109 | for (int i = 0; i < mCurlMeshes.size(); ++i) {
110 | mCurlMeshes.get(i).onDrawFrame(gl);
111 | }
112 | }
113 |
114 | @Override
115 | public void onSurfaceChanged(GL10 gl, int width, int height) {
116 | gl.glViewport(0, 0, width, height);
117 | mViewportWidth = width;
118 | mViewportHeight = height;
119 |
120 | float ratio = (float) width / height;
121 | mViewRect.top = 1.0f;
122 | mViewRect.bottom = -1.0f;
123 | mViewRect.left = -ratio;
124 | mViewRect.right = ratio;
125 | updatePageRects();
126 |
127 | gl.glMatrixMode(GL10.GL_PROJECTION);
128 | gl.glLoadIdentity();
129 | if (USE_PERSPECTIVE_PROJECTION) {
130 | GLU.gluPerspective(gl, 20f, (float) width / height, .1f, 100f);
131 | } else {
132 | GLU.gluOrtho2D(gl, mViewRect.left, mViewRect.right,
133 | mViewRect.bottom, mViewRect.top);
134 | }
135 |
136 | gl.glMatrixMode(GL10.GL_MODELVIEW);
137 | gl.glLoadIdentity();
138 | }
139 |
140 | @Override
141 | public void onSurfaceCreated(GL10 gl, EGLConfig config) {
142 | gl.glClearColor(0f, 0f, 0f, 1f);
143 | gl.glShadeModel(GL10.GL_SMOOTH);
144 | gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
145 | gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST);
146 | gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST);
147 | gl.glEnable(GL10.GL_LINE_SMOOTH);
148 | gl.glDisable(GL10.GL_DEPTH_TEST);
149 | gl.glDisable(GL10.GL_CULL_FACE);
150 |
151 | mObserver.onSurfaceCreated();
152 | }
153 |
154 | /**
155 | * Removes CurlMesh from this renderer.
156 | */
157 | public synchronized void removeCurlMesh(CurlMesh mesh) {
158 | while (mCurlMeshes.remove(mesh))
159 | ;
160 | }
161 |
162 | /**
163 | * Change background/clear color.
164 | */
165 | public void setBackgroundColor(int color) {
166 | mBackgroundColor = color;
167 | }
168 |
169 | /**
170 | * Set margins or padding. Note: margins are proportional. Meaning a value
171 | * of .1f will produce a 10% margin.
172 | */
173 | public synchronized void setMargins(float left, float top, float right,
174 | float bottom) {
175 | mMargins.left = left;
176 | mMargins.top = top;
177 | mMargins.right = right;
178 | mMargins.bottom = bottom;
179 | updatePageRects();
180 | }
181 |
182 | /**
183 | * Sets visible page count to one or two. Should be either SHOW_ONE_PAGE or
184 | * SHOW_TWO_PAGES.
185 | */
186 | public synchronized void setViewMode(int viewmode) {
187 | if (viewmode == SHOW_ONE_PAGE) {
188 | mViewMode = viewmode;
189 | updatePageRects();
190 | } else if (viewmode == SHOW_TWO_PAGES) {
191 | mViewMode = viewmode;
192 | updatePageRects();
193 | }
194 | }
195 |
196 | /**
197 | * Translates screen coordinates into view coordinates.
198 | */
199 | public void translate(PointF pt) {
200 | pt.x = mViewRect.left + (mViewRect.width() * pt.x / mViewportWidth);
201 | pt.y = mViewRect.top - (-mViewRect.height() * pt.y / mViewportHeight);
202 | }
203 |
204 | /**
205 | * Recalculates page rectangles.
206 | */
207 | private void updatePageRects() {
208 | if (mViewRect.width() == 0 || mViewRect.height() == 0) {
209 | return;
210 | } else if (mViewMode == SHOW_ONE_PAGE) {
211 | mPageRectRight.set(mViewRect);
212 | mPageRectRight.left += mViewRect.width() * mMargins.left;
213 | mPageRectRight.right -= mViewRect.width() * mMargins.right;
214 | mPageRectRight.top += mViewRect.height() * mMargins.top;
215 | mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom;
216 |
217 | mPageRectLeft.set(mPageRectRight);
218 | mPageRectLeft.offset(-mPageRectRight.width(), 0);
219 |
220 | int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect
221 | .width());
222 | int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect
223 | .height());
224 | mObserver.onPageSizeChanged(bitmapW, bitmapH);
225 | } else if (mViewMode == SHOW_TWO_PAGES) {
226 | mPageRectRight.set(mViewRect);
227 | mPageRectRight.left += mViewRect.width() * mMargins.left;
228 | mPageRectRight.right -= mViewRect.width() * mMargins.right;
229 | mPageRectRight.top += mViewRect.height() * mMargins.top;
230 | mPageRectRight.bottom -= mViewRect.height() * mMargins.bottom;
231 |
232 | mPageRectLeft.set(mPageRectRight);
233 | mPageRectLeft.right = (mPageRectLeft.right + mPageRectLeft.left) / 2;
234 | mPageRectRight.left = mPageRectLeft.right;
235 |
236 | int bitmapW = (int) ((mPageRectRight.width() * mViewportWidth) / mViewRect
237 | .width());
238 | int bitmapH = (int) ((mPageRectRight.height() * mViewportHeight) / mViewRect
239 | .height());
240 | mObserver.onPageSizeChanged(bitmapW, bitmapH);
241 | }
242 | }
243 |
244 | /**
245 | * Observer for waiting render engine/state updates.
246 | */
247 | public interface Observer {
248 | /**
249 | * Called from onDrawFrame called before rendering is started. This is
250 | * intended to be used for animation purposes.
251 | */
252 | public void onDrawFrame();
253 |
254 | /**
255 | * Called once page size is changed. Width and height tell the page size
256 | * in pixels making it possible to update textures accordingly.
257 | */
258 | public void onPageSizeChanged(int width, int height);
259 |
260 | /**
261 | * Called from onSurfaceCreated to enable texture re-initialization etc
262 | * what needs to be done when this happens.
263 | */
264 | public void onSurfaceCreated();
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/src/fi/harism/curl/CurlView.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2012 Harri Smatt
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package fi.harism.curl;
18 |
19 | import android.content.Context;
20 | import android.graphics.PointF;
21 | import android.graphics.RectF;
22 | import android.opengl.GLSurfaceView;
23 | import android.util.AttributeSet;
24 | import android.view.MotionEvent;
25 | import android.view.View;
26 |
27 | /**
28 | * OpenGL ES View.
29 | *
30 | * @author harism
31 | */
32 | public class CurlView extends GLSurfaceView implements View.OnTouchListener,
33 | CurlRenderer.Observer {
34 |
35 | // Curl state. We are flipping none, left or right page.
36 | private static final int CURL_LEFT = 1;
37 | private static final int CURL_NONE = 0;
38 | private static final int CURL_RIGHT = 2;
39 |
40 | // Constants for mAnimationTargetEvent.
41 | private static final int SET_CURL_TO_LEFT = 1;
42 | private static final int SET_CURL_TO_RIGHT = 2;
43 |
44 | // Shows one page at the center of view.
45 | public static final int SHOW_ONE_PAGE = 1;
46 | // Shows two pages side by side.
47 | public static final int SHOW_TWO_PAGES = 2;
48 |
49 | private boolean mAllowLastPageCurl = true;
50 |
51 | private boolean mAnimate = false;
52 | private long mAnimationDurationTime = 300;
53 | private PointF mAnimationSource = new PointF();
54 | private long mAnimationStartTime;
55 | private PointF mAnimationTarget = new PointF();
56 | private int mAnimationTargetEvent;
57 |
58 | private PointF mCurlDir = new PointF();
59 |
60 | private PointF mCurlPos = new PointF();
61 | private int mCurlState = CURL_NONE;
62 | // Current bitmap index. This is always showed as front of right page.
63 | private int mCurrentIndex = 0;
64 |
65 | // Start position for dragging.
66 | private PointF mDragStartPos = new PointF();
67 |
68 | private boolean mEnableTouchPressure = false;
69 | // Bitmap size. These are updated from renderer once it's initialized.
70 | private int mPageBitmapHeight = -1;
71 |
72 | private int mPageBitmapWidth = -1;
73 | // Page meshes. Left and right meshes are 'static' while curl is used to
74 | // show page flipping.
75 | private CurlMesh mPageCurl;
76 |
77 | private CurlMesh mPageLeft;
78 | private PageProvider mPageProvider;
79 | private CurlMesh mPageRight;
80 |
81 | private PointerPosition mPointerPos = new PointerPosition();
82 |
83 | private CurlRenderer mRenderer;
84 | private boolean mRenderLeftPage = true;
85 | private SizeChangedObserver mSizeChangedObserver;
86 |
87 | // One page is the default.
88 | private int mViewMode = SHOW_ONE_PAGE;
89 |
90 | /**
91 | * Default constructor.
92 | */
93 | public CurlView(Context ctx) {
94 | super(ctx);
95 | init(ctx);
96 | }
97 |
98 | /**
99 | * Default constructor.
100 | */
101 | public CurlView(Context ctx, AttributeSet attrs) {
102 | super(ctx, attrs);
103 | init(ctx);
104 | }
105 |
106 | /**
107 | * Default constructor.
108 | */
109 | public CurlView(Context ctx, AttributeSet attrs, int defStyle) {
110 | this(ctx, attrs);
111 | }
112 |
113 | /**
114 | * Get current page index. Page indices are zero based values presenting
115 | * page being shown on right side of the book.
116 | */
117 | public int getCurrentIndex() {
118 | return mCurrentIndex;
119 | }
120 |
121 | /**
122 | * Initialize method.
123 | */
124 | private void init(Context ctx) {
125 | mRenderer = new CurlRenderer(this);
126 | setRenderer(mRenderer);
127 | setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
128 | setOnTouchListener(this);
129 |
130 | // Even though left and right pages are static we have to allocate room
131 | // for curl on them too as we are switching meshes. Another way would be
132 | // to swap texture ids only.
133 | mPageLeft = new CurlMesh(10);
134 | mPageRight = new CurlMesh(10);
135 | mPageCurl = new CurlMesh(10);
136 | mPageLeft.setFlipTexture(true);
137 | mPageRight.setFlipTexture(false);
138 | }
139 |
140 | @Override
141 | public void onDrawFrame() {
142 | // We are not animating.
143 | if (mAnimate == false) {
144 | return;
145 | }
146 |
147 | long currentTime = System.currentTimeMillis();
148 | // If animation is done.
149 | if (currentTime >= mAnimationStartTime + mAnimationDurationTime) {
150 | if (mAnimationTargetEvent == SET_CURL_TO_RIGHT) {
151 | // Switch curled page to right.
152 | CurlMesh right = mPageCurl;
153 | CurlMesh curl = mPageRight;
154 | right.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
155 | right.setFlipTexture(false);
156 | right.reset();
157 | mRenderer.removeCurlMesh(curl);
158 | mPageCurl = curl;
159 | mPageRight = right;
160 | // If we were curling left page update current index.
161 | if (mCurlState == CURL_LEFT) {
162 | --mCurrentIndex;
163 | }
164 | } else if (mAnimationTargetEvent == SET_CURL_TO_LEFT) {
165 | // Switch curled page to left.
166 | CurlMesh left = mPageCurl;
167 | CurlMesh curl = mPageLeft;
168 | left.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
169 | left.setFlipTexture(true);
170 | left.reset();
171 | mRenderer.removeCurlMesh(curl);
172 | if (!mRenderLeftPage) {
173 | mRenderer.removeCurlMesh(left);
174 | }
175 | mPageCurl = curl;
176 | mPageLeft = left;
177 | // If we were curling right page update current index.
178 | if (mCurlState == CURL_RIGHT) {
179 | ++mCurrentIndex;
180 | }
181 | }
182 | mCurlState = CURL_NONE;
183 | mAnimate = false;
184 | requestRender();
185 | } else {
186 | mPointerPos.mPos.set(mAnimationSource);
187 | float t = 1f - ((float) (currentTime - mAnimationStartTime) / mAnimationDurationTime);
188 | t = 1f - (t * t * t * (3 - 2 * t));
189 | mPointerPos.mPos.x += (mAnimationTarget.x - mAnimationSource.x) * t;
190 | mPointerPos.mPos.y += (mAnimationTarget.y - mAnimationSource.y) * t;
191 | updateCurlPos(mPointerPos);
192 | }
193 | }
194 |
195 | @Override
196 | public void onPageSizeChanged(int width, int height) {
197 | mPageBitmapWidth = width;
198 | mPageBitmapHeight = height;
199 | updatePages();
200 | requestRender();
201 | }
202 |
203 | @Override
204 | public void onSizeChanged(int w, int h, int ow, int oh) {
205 | super.onSizeChanged(w, h, ow, oh);
206 | requestRender();
207 | if (mSizeChangedObserver != null) {
208 | mSizeChangedObserver.onSizeChanged(w, h);
209 | }
210 | }
211 |
212 | @Override
213 | public void onSurfaceCreated() {
214 | // In case surface is recreated, let page meshes drop allocated texture
215 | // ids and ask for new ones. There's no need to set textures here as
216 | // onPageSizeChanged should be called later on.
217 | mPageLeft.resetTexture();
218 | mPageRight.resetTexture();
219 | mPageCurl.resetTexture();
220 | }
221 |
222 | @Override
223 | public boolean onTouch(View view, MotionEvent me) {
224 | // No dragging during animation at the moment.
225 | // TODO: Stop animation on touch event and return to drag mode.
226 | if (mAnimate || mPageProvider == null) {
227 | return false;
228 | }
229 |
230 | // We need page rects quite extensively so get them for later use.
231 | RectF rightRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT);
232 | RectF leftRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT);
233 |
234 | // Store pointer position.
235 | mPointerPos.mPos.set(me.getX(), me.getY());
236 | mRenderer.translate(mPointerPos.mPos);
237 | if (mEnableTouchPressure) {
238 | mPointerPos.mPressure = me.getPressure();
239 | } else {
240 | mPointerPos.mPressure = 0.8f;
241 | }
242 |
243 | switch (me.getAction()) {
244 | case MotionEvent.ACTION_DOWN: {
245 |
246 | // Once we receive pointer down event its position is mapped to
247 | // right or left edge of page and that'll be the position from where
248 | // user is holding the paper to make curl happen.
249 | mDragStartPos.set(mPointerPos.mPos);
250 |
251 | // First we make sure it's not over or below page. Pages are
252 | // supposed to be same height so it really doesn't matter do we use
253 | // left or right one.
254 | if (mDragStartPos.y > rightRect.top) {
255 | mDragStartPos.y = rightRect.top;
256 | } else if (mDragStartPos.y < rightRect.bottom) {
257 | mDragStartPos.y = rightRect.bottom;
258 | }
259 |
260 | // Then we have to make decisions for the user whether curl is going
261 | // to happen from left or right, and on which page.
262 | if (mViewMode == SHOW_TWO_PAGES) {
263 | // If we have an open book and pointer is on the left from right
264 | // page we'll mark drag position to left edge of left page.
265 | // Additionally checking mCurrentIndex is higher than zero tells
266 | // us there is a visible page at all.
267 | if (mDragStartPos.x < rightRect.left && mCurrentIndex > 0) {
268 | mDragStartPos.x = leftRect.left;
269 | startCurl(CURL_LEFT);
270 | }
271 | // Otherwise check pointer is on right page's side.
272 | else if (mDragStartPos.x >= rightRect.left
273 | && mCurrentIndex < mPageProvider.getPageCount()) {
274 | mDragStartPos.x = rightRect.right;
275 | if (!mAllowLastPageCurl
276 | && mCurrentIndex >= mPageProvider.getPageCount() - 1) {
277 | return false;
278 | }
279 | startCurl(CURL_RIGHT);
280 | }
281 | } else if (mViewMode == SHOW_ONE_PAGE) {
282 | float halfX = (rightRect.right + rightRect.left) / 2;
283 | if (mDragStartPos.x < halfX && mCurrentIndex > 0) {
284 | mDragStartPos.x = rightRect.left;
285 | startCurl(CURL_LEFT);
286 | } else if (mDragStartPos.x >= halfX
287 | && mCurrentIndex < mPageProvider.getPageCount()) {
288 | mDragStartPos.x = rightRect.right;
289 | if (!mAllowLastPageCurl
290 | && mCurrentIndex >= mPageProvider.getPageCount() - 1) {
291 | return false;
292 | }
293 | startCurl(CURL_RIGHT);
294 | }
295 | }
296 | // If we have are in curl state, let this case clause flow through
297 | // to next one. We have pointer position and drag position defined
298 | // and this will create first render request given these points.
299 | if (mCurlState == CURL_NONE) {
300 | return false;
301 | }
302 | }
303 | case MotionEvent.ACTION_MOVE: {
304 | updateCurlPos(mPointerPos);
305 | break;
306 | }
307 | case MotionEvent.ACTION_CANCEL:
308 | case MotionEvent.ACTION_UP: {
309 | if (mCurlState == CURL_LEFT || mCurlState == CURL_RIGHT) {
310 | // Animation source is the point from where animation starts.
311 | // Also it's handled in a way we actually simulate touch events
312 | // meaning the output is exactly the same as if user drags the
313 | // page to other side. While not producing the best looking
314 | // result (which is easier done by altering curl position and/or
315 | // direction directly), this is done in a hope it made code a
316 | // bit more readable and easier to maintain.
317 | mAnimationSource.set(mPointerPos.mPos);
318 | mAnimationStartTime = System.currentTimeMillis();
319 |
320 | // Given the explanation, here we decide whether to simulate
321 | // drag to left or right end.
322 | if ((mViewMode == SHOW_ONE_PAGE && mPointerPos.mPos.x > (rightRect.left + rightRect.right) / 2)
323 | || mViewMode == SHOW_TWO_PAGES
324 | && mPointerPos.mPos.x > rightRect.left) {
325 | // On right side target is always right page's right border.
326 | mAnimationTarget.set(mDragStartPos);
327 | mAnimationTarget.x = mRenderer
328 | .getPageRect(CurlRenderer.PAGE_RIGHT).right;
329 | mAnimationTargetEvent = SET_CURL_TO_RIGHT;
330 | } else {
331 | // On left side target depends on visible pages.
332 | mAnimationTarget.set(mDragStartPos);
333 | if (mCurlState == CURL_RIGHT || mViewMode == SHOW_TWO_PAGES) {
334 | mAnimationTarget.x = leftRect.left;
335 | } else {
336 | mAnimationTarget.x = rightRect.left;
337 | }
338 | mAnimationTargetEvent = SET_CURL_TO_LEFT;
339 | }
340 | mAnimate = true;
341 | requestRender();
342 | }
343 | break;
344 | }
345 | }
346 |
347 | return true;
348 | }
349 |
350 | /**
351 | * Allow the last page to curl.
352 | */
353 | public void setAllowLastPageCurl(boolean allowLastPageCurl) {
354 | mAllowLastPageCurl = allowLastPageCurl;
355 | }
356 |
357 | /**
358 | * Sets background color - or OpenGL clear color to be more precise. Color
359 | * is a 32bit value consisting of 0xAARRGGBB and is extracted using
360 | * android.graphics.Color eventually.
361 | */
362 | @Override
363 | public void setBackgroundColor(int color) {
364 | mRenderer.setBackgroundColor(color);
365 | requestRender();
366 | }
367 |
368 | /**
369 | * Sets mPageCurl curl position.
370 | */
371 | private void setCurlPos(PointF curlPos, PointF curlDir, double radius) {
372 |
373 | // First reposition curl so that page doesn't 'rip off' from book.
374 | if (mCurlState == CURL_RIGHT
375 | || (mCurlState == CURL_LEFT && mViewMode == SHOW_ONE_PAGE)) {
376 | RectF pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT);
377 | if (curlPos.x >= pageRect.right) {
378 | mPageCurl.reset();
379 | requestRender();
380 | return;
381 | }
382 | if (curlPos.x < pageRect.left) {
383 | curlPos.x = pageRect.left;
384 | }
385 | if (curlDir.y != 0) {
386 | float diffX = curlPos.x - pageRect.left;
387 | float leftY = curlPos.y + (diffX * curlDir.x / curlDir.y);
388 | if (curlDir.y < 0 && leftY < pageRect.top) {
389 | curlDir.x = curlPos.y - pageRect.top;
390 | curlDir.y = pageRect.left - curlPos.x;
391 | } else if (curlDir.y > 0 && leftY > pageRect.bottom) {
392 | curlDir.x = pageRect.bottom - curlPos.y;
393 | curlDir.y = curlPos.x - pageRect.left;
394 | }
395 | }
396 | } else if (mCurlState == CURL_LEFT) {
397 | RectF pageRect = mRenderer.getPageRect(CurlRenderer.PAGE_LEFT);
398 | if (curlPos.x <= pageRect.left) {
399 | mPageCurl.reset();
400 | requestRender();
401 | return;
402 | }
403 | if (curlPos.x > pageRect.right) {
404 | curlPos.x = pageRect.right;
405 | }
406 | if (curlDir.y != 0) {
407 | float diffX = curlPos.x - pageRect.right;
408 | float rightY = curlPos.y + (diffX * curlDir.x / curlDir.y);
409 | if (curlDir.y < 0 && rightY < pageRect.top) {
410 | curlDir.x = pageRect.top - curlPos.y;
411 | curlDir.y = curlPos.x - pageRect.right;
412 | } else if (curlDir.y > 0 && rightY > pageRect.bottom) {
413 | curlDir.x = curlPos.y - pageRect.bottom;
414 | curlDir.y = pageRect.right - curlPos.x;
415 | }
416 | }
417 | }
418 |
419 | // Finally normalize direction vector and do rendering.
420 | double dist = Math.sqrt(curlDir.x * curlDir.x + curlDir.y * curlDir.y);
421 | if (dist != 0) {
422 | curlDir.x /= dist;
423 | curlDir.y /= dist;
424 | mPageCurl.curl(curlPos, curlDir, radius);
425 | } else {
426 | mPageCurl.reset();
427 | }
428 |
429 | requestRender();
430 | }
431 |
432 | /**
433 | * Set current page index. Page indices are zero based values presenting
434 | * page being shown on right side of the book. E.g if you set value to 4;
435 | * right side front facing bitmap will be with index 4, back facing 5 and
436 | * for left side page index 3 is front facing, and index 2 back facing (once
437 | * page is on left side it's flipped over).
438 | *
439 | * Current index is rounded to closest value divisible with 2.
440 | */
441 | public void setCurrentIndex(int index) {
442 | if (mPageProvider == null || index < 0) {
443 | mCurrentIndex = 0;
444 | } else {
445 | if (mAllowLastPageCurl) {
446 | mCurrentIndex = Math.min(index, mPageProvider.getPageCount());
447 | } else {
448 | mCurrentIndex = Math.min(index,
449 | mPageProvider.getPageCount() - 1);
450 | }
451 | }
452 | updatePages();
453 | requestRender();
454 | }
455 |
456 | /**
457 | * If set to true, touch event pressure information is used to adjust curl
458 | * radius. The more you press, the flatter the curl becomes. This is
459 | * somewhat experimental and results may vary significantly between devices.
460 | * On emulator pressure information seems to be flat 1.0f which is maximum
461 | * value and therefore not very much of use.
462 | */
463 | public void setEnableTouchPressure(boolean enableTouchPressure) {
464 | mEnableTouchPressure = enableTouchPressure;
465 | }
466 |
467 | /**
468 | * Set margins (or padding). Note: margins are proportional. Meaning a value
469 | * of .1f will produce a 10% margin.
470 | */
471 | public void setMargins(float left, float top, float right, float bottom) {
472 | mRenderer.setMargins(left, top, right, bottom);
473 | }
474 |
475 | /**
476 | * Update/set page provider.
477 | */
478 | public void setPageProvider(PageProvider pageProvider) {
479 | mPageProvider = pageProvider;
480 | mCurrentIndex = 0;
481 | updatePages();
482 | requestRender();
483 | }
484 |
485 | /**
486 | * Setter for whether left side page is rendered. This is useful mostly for
487 | * situations where right (main) page is aligned to left side of screen and
488 | * left page is not visible anyway.
489 | */
490 | public void setRenderLeftPage(boolean renderLeftPage) {
491 | mRenderLeftPage = renderLeftPage;
492 | }
493 |
494 | /**
495 | * Sets SizeChangedObserver for this View. Call back method is called from
496 | * this View's onSizeChanged method.
497 | */
498 | public void setSizeChangedObserver(SizeChangedObserver observer) {
499 | mSizeChangedObserver = observer;
500 | }
501 |
502 | /**
503 | * Sets view mode. Value can be either SHOW_ONE_PAGE or SHOW_TWO_PAGES. In
504 | * former case right page is made size of display, and in latter case two
505 | * pages are laid on visible area.
506 | */
507 | public void setViewMode(int viewMode) {
508 | switch (viewMode) {
509 | case SHOW_ONE_PAGE:
510 | mViewMode = viewMode;
511 | mPageLeft.setFlipTexture(true);
512 | mRenderer.setViewMode(CurlRenderer.SHOW_ONE_PAGE);
513 | break;
514 | case SHOW_TWO_PAGES:
515 | mViewMode = viewMode;
516 | mPageLeft.setFlipTexture(false);
517 | mRenderer.setViewMode(CurlRenderer.SHOW_TWO_PAGES);
518 | break;
519 | }
520 | }
521 |
522 | /**
523 | * Switches meshes and loads new bitmaps if available. Updated to support 2
524 | * pages in landscape
525 | */
526 | private void startCurl(int page) {
527 | switch (page) {
528 |
529 | // Once right side page is curled, first right page is assigned into
530 | // curled page. And if there are more bitmaps available new bitmap is
531 | // loaded into right side mesh.
532 | case CURL_RIGHT: {
533 | // Remove meshes from renderer.
534 | mRenderer.removeCurlMesh(mPageLeft);
535 | mRenderer.removeCurlMesh(mPageRight);
536 | mRenderer.removeCurlMesh(mPageCurl);
537 |
538 | // We are curling right page.
539 | CurlMesh curl = mPageRight;
540 | mPageRight = mPageCurl;
541 | mPageCurl = curl;
542 |
543 | if (mCurrentIndex > 0) {
544 | mPageLeft.setFlipTexture(true);
545 | mPageLeft
546 | .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
547 | mPageLeft.reset();
548 | if (mRenderLeftPage) {
549 | mRenderer.addCurlMesh(mPageLeft);
550 | }
551 | }
552 | if (mCurrentIndex < mPageProvider.getPageCount() - 1) {
553 | updatePage(mPageRight.getTexturePage(), mCurrentIndex + 1);
554 | mPageRight.setRect(mRenderer
555 | .getPageRect(CurlRenderer.PAGE_RIGHT));
556 | mPageRight.setFlipTexture(false);
557 | mPageRight.reset();
558 | mRenderer.addCurlMesh(mPageRight);
559 | }
560 |
561 | // Add curled page to renderer.
562 | mPageCurl.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
563 | mPageCurl.setFlipTexture(false);
564 | mPageCurl.reset();
565 | mRenderer.addCurlMesh(mPageCurl);
566 |
567 | mCurlState = CURL_RIGHT;
568 | break;
569 | }
570 |
571 | // On left side curl, left page is assigned to curled page. And if
572 | // there are more bitmaps available before currentIndex, new bitmap
573 | // is loaded into left page.
574 | case CURL_LEFT: {
575 | // Remove meshes from renderer.
576 | mRenderer.removeCurlMesh(mPageLeft);
577 | mRenderer.removeCurlMesh(mPageRight);
578 | mRenderer.removeCurlMesh(mPageCurl);
579 |
580 | // We are curling left page.
581 | CurlMesh curl = mPageLeft;
582 | mPageLeft = mPageCurl;
583 | mPageCurl = curl;
584 |
585 | if (mCurrentIndex > 1) {
586 | updatePage(mPageLeft.getTexturePage(), mCurrentIndex - 2);
587 | mPageLeft.setFlipTexture(true);
588 | mPageLeft
589 | .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
590 | mPageLeft.reset();
591 | if (mRenderLeftPage) {
592 | mRenderer.addCurlMesh(mPageLeft);
593 | }
594 | }
595 |
596 | // If there is something to show on right page add it to renderer.
597 | if (mCurrentIndex < mPageProvider.getPageCount()) {
598 | mPageRight.setFlipTexture(false);
599 | mPageRight.setRect(mRenderer
600 | .getPageRect(CurlRenderer.PAGE_RIGHT));
601 | mPageRight.reset();
602 | mRenderer.addCurlMesh(mPageRight);
603 | }
604 |
605 | // How dragging previous page happens depends on view mode.
606 | if (mViewMode == SHOW_ONE_PAGE
607 | || (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) {
608 | mPageCurl.setRect(mRenderer
609 | .getPageRect(CurlRenderer.PAGE_RIGHT));
610 | mPageCurl.setFlipTexture(false);
611 | } else {
612 | mPageCurl
613 | .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
614 | mPageCurl.setFlipTexture(true);
615 | }
616 | mPageCurl.reset();
617 | mRenderer.addCurlMesh(mPageCurl);
618 |
619 | mCurlState = CURL_LEFT;
620 | break;
621 | }
622 |
623 | }
624 | }
625 |
626 | /**
627 | * Updates curl position.
628 | */
629 | private void updateCurlPos(PointerPosition pointerPos) {
630 |
631 | // Default curl radius.
632 | double radius = mRenderer.getPageRect(CURL_RIGHT).width() / 3;
633 | // TODO: This is not an optimal solution. Based on feedback received so
634 | // far; pressure is not very accurate, it may be better not to map
635 | // coefficient to range [0f, 1f] but something like [.2f, 1f] instead.
636 | // Leaving it as is until get my hands on a real device. On emulator
637 | // this doesn't work anyway.
638 | radius *= Math.max(1f - pointerPos.mPressure, 0f);
639 | // NOTE: Here we set pointerPos to mCurlPos. It might be a bit confusing
640 | // later to see e.g "mCurlPos.x - mDragStartPos.x" used. But it's
641 | // actually pointerPos we are doing calculations against. Why? Simply to
642 | // optimize code a bit with the cost of making it unreadable. Otherwise
643 | // we had to this in both of the next if-else branches.
644 | mCurlPos.set(pointerPos.mPos);
645 |
646 | // If curl happens on right page, or on left page on two page mode,
647 | // we'll calculate curl position from pointerPos.
648 | if (mCurlState == CURL_RIGHT
649 | || (mCurlState == CURL_LEFT && mViewMode == SHOW_TWO_PAGES)) {
650 |
651 | mCurlDir.x = mCurlPos.x - mDragStartPos.x;
652 | mCurlDir.y = mCurlPos.y - mDragStartPos.y;
653 | float dist = (float) Math.sqrt(mCurlDir.x * mCurlDir.x + mCurlDir.y
654 | * mCurlDir.y);
655 |
656 | // Adjust curl radius so that if page is dragged far enough on
657 | // opposite side, radius gets closer to zero.
658 | float pageWidth = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT)
659 | .width();
660 | double curlLen = radius * Math.PI;
661 | if (dist > (pageWidth * 2) - curlLen) {
662 | curlLen = Math.max((pageWidth * 2) - dist, 0f);
663 | radius = curlLen / Math.PI;
664 | }
665 |
666 | // Actual curl position calculation.
667 | if (dist >= curlLen) {
668 | double translate = (dist - curlLen) / 2;
669 | if (mViewMode == SHOW_TWO_PAGES) {
670 | mCurlPos.x -= mCurlDir.x * translate / dist;
671 | } else {
672 | float pageLeftX = mRenderer
673 | .getPageRect(CurlRenderer.PAGE_RIGHT).left;
674 | radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius),
675 | 0f);
676 | }
677 | mCurlPos.y -= mCurlDir.y * translate / dist;
678 | } else {
679 | double angle = Math.PI * Math.sqrt(dist / curlLen);
680 | double translate = radius * Math.sin(angle);
681 | mCurlPos.x += mCurlDir.x * translate / dist;
682 | mCurlPos.y += mCurlDir.y * translate / dist;
683 | }
684 | }
685 | // Otherwise we'll let curl follow pointer position.
686 | else if (mCurlState == CURL_LEFT) {
687 |
688 | // Adjust radius regarding how close to page edge we are.
689 | float pageLeftX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).left;
690 | radius = Math.max(Math.min(mCurlPos.x - pageLeftX, radius), 0f);
691 |
692 | float pageRightX = mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT).right;
693 | mCurlPos.x -= Math.min(pageRightX - mCurlPos.x, radius);
694 | mCurlDir.x = mCurlPos.x + mDragStartPos.x;
695 | mCurlDir.y = mCurlPos.y - mDragStartPos.y;
696 | }
697 |
698 | setCurlPos(mCurlPos, mCurlDir, radius);
699 | }
700 |
701 | /**
702 | * Updates given CurlPage via PageProvider for page located at index.
703 | */
704 | private void updatePage(CurlPage page, int index) {
705 | // First reset page to initial state.
706 | page.reset();
707 | // Ask page provider to fill it up with bitmaps and colors.
708 | mPageProvider.updatePage(page, mPageBitmapWidth, mPageBitmapHeight,
709 | index);
710 | }
711 |
712 | /**
713 | * Updates bitmaps for page meshes.
714 | */
715 | private void updatePages() {
716 | if (mPageProvider == null || mPageBitmapWidth <= 0
717 | || mPageBitmapHeight <= 0) {
718 | return;
719 | }
720 |
721 | // Remove meshes from renderer.
722 | mRenderer.removeCurlMesh(mPageLeft);
723 | mRenderer.removeCurlMesh(mPageRight);
724 | mRenderer.removeCurlMesh(mPageCurl);
725 |
726 | int leftIdx = mCurrentIndex - 1;
727 | int rightIdx = mCurrentIndex;
728 | int curlIdx = -1;
729 | if (mCurlState == CURL_LEFT) {
730 | curlIdx = leftIdx;
731 | --leftIdx;
732 | } else if (mCurlState == CURL_RIGHT) {
733 | curlIdx = rightIdx;
734 | ++rightIdx;
735 | }
736 |
737 | if (rightIdx >= 0 && rightIdx < mPageProvider.getPageCount()) {
738 | updatePage(mPageRight.getTexturePage(), rightIdx);
739 | mPageRight.setFlipTexture(false);
740 | mPageRight.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_RIGHT));
741 | mPageRight.reset();
742 | mRenderer.addCurlMesh(mPageRight);
743 | }
744 | if (leftIdx >= 0 && leftIdx < mPageProvider.getPageCount()) {
745 | updatePage(mPageLeft.getTexturePage(), leftIdx);
746 | mPageLeft.setFlipTexture(true);
747 | mPageLeft.setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
748 | mPageLeft.reset();
749 | if (mRenderLeftPage) {
750 | mRenderer.addCurlMesh(mPageLeft);
751 | }
752 | }
753 | if (curlIdx >= 0 && curlIdx < mPageProvider.getPageCount()) {
754 | updatePage(mPageCurl.getTexturePage(), curlIdx);
755 |
756 | if (mCurlState == CURL_RIGHT) {
757 | mPageCurl.setFlipTexture(true);
758 | mPageCurl.setRect(mRenderer
759 | .getPageRect(CurlRenderer.PAGE_RIGHT));
760 | } else {
761 | mPageCurl.setFlipTexture(false);
762 | mPageCurl
763 | .setRect(mRenderer.getPageRect(CurlRenderer.PAGE_LEFT));
764 | }
765 |
766 | mPageCurl.reset();
767 | mRenderer.addCurlMesh(mPageCurl);
768 | }
769 | }
770 |
771 | /**
772 | * Provider for feeding 'book' with bitmaps which are used for rendering
773 | * pages.
774 | */
775 | public interface PageProvider {
776 |
777 | /**
778 | * Return number of pages available.
779 | */
780 | public int getPageCount();
781 |
782 | /**
783 | * Called once new bitmaps/textures are needed. Width and height are in
784 | * pixels telling the size it will be drawn on screen and following them
785 | * ensures that aspect ratio remains. But it's possible to return bitmap
786 | * of any size though. You should use provided CurlPage for storing page
787 | * information for requested page number.
788 | *
789 | * Index is a number between 0 and getBitmapCount() - 1.
790 | */
791 | public void updatePage(CurlPage page, int width, int height, int index);
792 | }
793 |
794 | /**
795 | * Simple holder for pointer position.
796 | */
797 | private class PointerPosition {
798 | PointF mPos = new PointF();
799 | float mPressure;
800 | }
801 |
802 | /**
803 | * Observer interface for handling CurlView size changes.
804 | */
805 | public interface SizeChangedObserver {
806 |
807 | /**
808 | * Called once CurlView size changes.
809 | */
810 | public void onSizeChanged(int width, int height);
811 | }
812 |
813 | }
814 |
--------------------------------------------------------------------------------