├── .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 | ![Paper 1](https://github.com/harism/android_page_curl/blob/master/paper1.jpg?raw=true)
114 | 2. And let's say we want to curl it approximately like this.
115 | ![Paper 2](https://github.com/harism/android_page_curl/blob/master/paper2.jpg?raw=true)
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 | ![Paper 3](https://github.com/harism/android_page_curl/blob/master/paper3.jpg?raw=true)
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 | --------------------------------------------------------------------------------