Repository: MartinRGB/Animer Branch: master Commit: 0feccb729f42 Files: 122 Total size: 510.4 KB Directory structure: gitextract_xxgve15n/ ├── .gitignore ├── LICENSE ├── README.md ├── README.zh.md ├── animer/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── martinrgb/ │ │ └── animer/ │ │ ├── Animer.java │ │ ├── component/ │ │ │ ├── overscroller/ │ │ │ │ └── launcher3/ │ │ │ │ ├── FlingSpringAnim.java │ │ │ │ ├── Interpolators.java │ │ │ │ └── OverScroller.java │ │ │ ├── overscrolllayout/ │ │ │ │ └── .gitkeep │ │ │ ├── recyclerview/ │ │ │ │ └── AnRecyclerView.java │ │ │ └── scrollview/ │ │ │ ├── AnOverScroller.java │ │ │ └── AnScrollView.java │ │ ├── core/ │ │ │ ├── interpolator/ │ │ │ │ ├── AnInterpolator.java │ │ │ │ ├── AndroidNative/ │ │ │ │ │ ├── AccelerateDecelerateInterpolator.java │ │ │ │ │ ├── AccelerateInterpolator.java │ │ │ │ │ ├── AnticipateInterpolator.java │ │ │ │ │ ├── AnticipateOvershootInterpolator.java │ │ │ │ │ ├── BounceInterpolator.java │ │ │ │ │ ├── CycleInterpolator.java │ │ │ │ │ ├── DecelerateInterpolator.java │ │ │ │ │ ├── FastOutLinearInInterpolator.java │ │ │ │ │ ├── FastOutSlowInInterpolator.java │ │ │ │ │ ├── LinearInterpolator.java │ │ │ │ │ ├── LinearOutSlowInInterpolator.java │ │ │ │ │ ├── LookupTableInterpolator.java │ │ │ │ │ ├── OvershootInterpolator.java │ │ │ │ │ └── PathInterpolator.java │ │ │ │ ├── AndroidSpringInterpolator.java │ │ │ │ ├── AndroidSpringInterpolator2.java │ │ │ │ ├── CustomBounceInterpolator.java │ │ │ │ ├── CustomDampingInterpolator.java │ │ │ │ ├── CustomMocosSpringInterpolator.java │ │ │ │ ├── CustomSpringInterpolator.java │ │ │ │ └── FlingSpringAnim.java │ │ │ ├── math/ │ │ │ │ ├── calculator/ │ │ │ │ │ ├── FlingCalculator.java │ │ │ │ │ └── SpringInterpolatorCalculator.java │ │ │ │ └── converter/ │ │ │ │ ├── AnSpringConverter.java │ │ │ │ ├── AndroidSpringConverter.java │ │ │ │ ├── DHOConverter.java │ │ │ │ ├── OrigamiPOPConverter.java │ │ │ │ ├── RK4Converter.java │ │ │ │ └── UIViewSpringConverter.java │ │ │ ├── property/ │ │ │ │ └── AnProperty.java │ │ │ ├── solver/ │ │ │ │ └── AnSolver.java │ │ │ ├── state/ │ │ │ │ └── PhysicsState.java │ │ │ └── util/ │ │ │ ├── AnSpringOscillateHelper.java │ │ │ └── AnUtil.java │ │ └── monitor/ │ │ ├── AnConfigData.java │ │ ├── AnConfigMap.java │ │ ├── AnConfigRegistry.java │ │ ├── AnConfigView.java │ │ ├── AnSpinnerAdapter.java │ │ ├── fps/ │ │ │ ├── Calculation.java │ │ │ ├── FPSBuilder.java │ │ │ ├── FPSConfig.java │ │ │ ├── FPSDetector.java │ │ │ ├── FPSFrameCallback.java │ │ │ ├── Foreground.java │ │ │ └── FrameDataCallback.java │ │ └── shader/ │ │ ├── ShaderProgram.java │ │ ├── ShaderRenderer.java │ │ ├── ShaderSurfaceView.java │ │ └── util/ │ │ ├── FPSCounter.java │ │ ├── LoggerConfig.java │ │ ├── ShaderHelper.java │ │ └── TextReader.java │ └── res/ │ ├── drawable/ │ │ ├── background_spinner.xml │ │ ├── gradient.xml │ │ ├── ic_button_background.xml │ │ ├── ic_edit_border.xml │ │ ├── ic_grid.xml │ │ ├── ic_nub.xml │ │ ├── ic_nub2.xml │ │ ├── ic_spinner.xml │ │ ├── ic_thumb.xml │ │ ├── popbackground_spinner.xml │ │ └── text_cursor.xml │ ├── layout/ │ │ └── config_view.xml │ ├── raw/ │ │ ├── simplefrag.glsl │ │ └── simplevert.glsl │ └── values/ │ ├── corlors.xml │ ├── dimension.xml │ └── styles.xml ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── martinrgb/ │ │ └── animerexample/ │ │ └── ExampleInstrumentedTest.java │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── martinrgb/ │ │ │ └── animerexample/ │ │ │ ├── MainActivity.java │ │ │ ├── PrototypeActivity.java │ │ │ ├── ScrollerActivity.java │ │ │ └── SmoothCornersImage.java │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── background_elevation.xml │ │ │ ├── ic_arrow.xml │ │ │ ├── ic_launcher_background.xml │ │ │ └── myrect.xml │ │ ├── drawable-nodpi/ │ │ │ └── mute.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── activity_prototype.xml │ │ │ ├── activity_scroller.xml │ │ │ └── custom_cell_view.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── values/ │ │ ├── attr.xml │ │ ├── colors.xml │ │ ├── dimension.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test/ │ └── java/ │ └── com/ │ └── martinrgb/ │ └── animerexample/ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ *.DS_Store # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries .idea/caches .idea # Keystore files # Uncomment the following line if you do not want to check your keystore files in. #*.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Google Services (e.g. APIs or Firebase) google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md local.properties app/.DS_Store app/.DS_Store ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2019 Qiuyinyan(MartinRGB) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ## About | MonitorUI Example | Scroller Example | View Prototype | | ------------- |-------------| ----- | | | | [中文说明](https://github.com/MartinRGB/Animer/blob/master/README.zh.md) Animer is a java library which designed for a better Android animation experience.(Currently is more like a view animation controller) It contains animation curves in `Android` `iOS` `Origami(POP or Rebound in Client)` `Principle` `Protopie` `FramerJS` Unlike [Rebound](https://github.com/facebook/rebound),Animer didn't use `Choreographer` or self-building `Looper` for creating an Animator from scratch. All these animation algorithm will be translated into Android's native implementation like DynamicAnimation & TimingInterpolator,which can improve the performance of animation. It also provides a real-time controller & graph UI for tweaking parameters. Web version(Convert Any Animation tools' parameters to Android's) —— [Animer_Web](http://www.martinrgb.com/Animer_Web/#) AE Script —— [Animer AE](https://github.com/MartinRGB/Animer_AE) ## Download [ ![Download](https://api.bintray.com/packages/martinrgb/animer/animer/images/download.svg) ](https://bintray.com/martinrgb/animer/animer/_latestVersion) [Simple Demo 1](https://github.com/MartinRGB/Animer/files/3948871/app-debug_2.zip) [Simple Demo 2](https://github.com/MartinRGB/Animer/files/3948863/app-debug.zip) ## Usage Animer supports multiple ways of animating: ### Android Native Style ```java // equal to Android's TimingInterpolator Animer.AnimerSolver solver = Animer.interpolatorDroid(new AccelerateDecelerateInterpolator(),600) // similar to ObjectAnimator Animer animer = new Animer(myView,solver,Animer.TRANSLATION_Y,0,200); animer.start(); // animer.cancel(); // animer.end(); ``` ### FramerJS State Machine Style ```java // equal to Android's SpringAnimation Animer.AnimerSolver solver = Animer.springDroid(1000,0.5f); Animer animer = new Animer(); // add a solver to Animer; animer.setSolver(solver); // add value to different state; animer.setStateValue("stateA",300); animer.setStateValue("stateB",700); animer.setStateValue("stateC",200); // add a listener to observe the motion of the Animation animer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { myView1.setTranslationX(value); myView2.setScaleX(1.f+value/100); myView2.setScaleY(1.f+value/100); } }); // switch immediately animer.switchToState("stateA"); // or animate to state value // animer.animateToState("stateB"); ``` ### Facebook Rebound Style ```java // equal to Facebook Origami POP Animation Animer.AnimerSolver solver = Animer.springOrigamiPOP(5,10); Animer animer = new Animer(myView,solver,Animer.SCALE); // setup a listener to add everything you want animer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) myView.setScaleX(value); myView.setScaleY(value); } }); animer.setCurrentValue(1.f); boolean isScaled = false; myView.setOnClickListener(view -> { if(!isScaled){ animer.setEndValue(0.5); } else{ animer.setEndValue(1); } isScaled = !isScaled; }); ``` ### Add animers to configUI init in xml ```xml ``` add animers' config in java ```java AnConfigView mAnimerConfiguratorView = (AnConfigView) findViewById(R.id.an_configurator); AnConfigRegistry.getInstance().addAnimer("Card Scale Animation",cardScaleAnimer); AnConfigRegistry.getInstance().addAnimer("Card TranslationX Animation",cardTransXAnimer); mAnimerConfiguratorView.refreshAnimerConfigs(); ``` ### Add custom animation curve presets first you need clean all the solver configs,then add yourselves. ```java AnConfigRegistry.getInstance().removeAllSolverConfig(); AnConfigRegistry.getInstance().addSolver("Preset1",Animer.springDroid(500.0f,0.96f)); AnConfigRegistry.getInstance().addSolver("Preset2",Animer.flingDroid(400.f,0.95f)); ... mAnimerConfiguratorView.refreshAnimerConfigs(); ``` ### Supported View Propertys: ``` Animer.TRANSLATION_X Animer.TRANSLATION_Y Animer.TRANSLATION_Z Animer.SCALE // equal to SCALE_X + SCALE_Y Animer.SCALE_X Animer.SCALE_Y Animer.ROTATION Animer.ROTATION_X Animer.ROTATION_Y Animer.X Animer.Y Animer.Z Animer.ALPHA ``` ### Supported Animators(as AnimerSolver): ```java Animer.springDroid(stiffness,dampingratio) // Android Dynamic SpringAnimation Animer.flingDroid(velocity,friction) // Android Dynamic FlingAnimation Animer.springiOSUIView(dampingratio,duration) // iOS UIView SPring Animer.springiOSCoreAnimation(stiffness,damping) // iOS CASpringAnimation Animer.springOrigamiPOP(bounciness,speed) // Origami POP Animer.springRK4(tension,friction) // Framer-RK4 Animer.springDHO(stiffness,damping) // Framer-DHO Animer.springProtopie(tension,friction) // Protopie Animer.springPrinciple(tension,friction) // Principle // Custom Bounce Interpolator(Romain Guy's DropInMotion) Animer.interpolatorDroid(new CustomBounceInterpolator(),duration) // Custom Damping Interpolator(Romain Guy's DropInMotion) Animer.interpolatorDroid(new CustomDampingInterpolator(),duration) // MocosSpring Interpolator (https://github.com/marcioapaiva/mocos-controlator) Animer.interpolatorDroid(new CustomMocosSpringInterpolator(),duration) // Custom Spring Interpolator(https://inloop.github.io/interpolator/) Animer.interpolatorDroid(new CustomSpringInterpolator(),duration) // Android Native Interpolator Below Animer.interpolatorDroid(new PathInterpolator(),duration) // Cubic Bezier Interpolator ... Animer.interpolatorDroid(new DecelerateInterpolator(),duration) // Android Decelerate Interpolator ... ``` ## TODO - A State Machine which can control interface's state(current is only object propertyValue's state) - Redesign the API - Rewrite the glsl shader - Consider scences in activity/fragment transition - Hook mechanism - Redesign the ConfigUI,it's difficult to tweak paras in small screen,maybe consider 'adb+electron in desktop client' ## Core concpet: **Data** - State machine concpet from [FramerJS](https://github.com/koenbok/Framer/tree/master/framer) ✅ - Aniamtion converter from my [Animation Converter](https://github.com/MartinRGB/AndroidInterpolator_AE) ✅ - Support external JSON to edit animation data **Althogrim** - Physics animation concept from [Rebound](https://github.com/facebook/rebound) & Android DynamicAnimation ✅ - LookupTable Interpolation Animator + RK4 Solver + DHO Solver ✅ - Physics simulation from [Flutter Physics](https://api.flutter.dev/flutter/physics/physics-library.html) & [UIKit Dyanmic](https://developer.apple.com/documentation/uikit/animation_and_haptics/uikit_dynamics) - Momentum - Calculating mass by Elements' area - Limit SpringAnimation's by time or overshoot counts **Advanced Animation Setting** - Addtive animation (compose mulitple animation) - Chained animation (one by one) - Parallax animation (same duration but differnt transition) - Sequencing animation (same transition but different startDelay) **User-Control** - Gesture-Driven animation,you can interact with the animation even it is animating(Like iOS's `CADisplayLink` Or Rebound's `SetEndValue`) ✅ - Package a gesture animator for interactive animation,attach gesture's velocity to animation system,make a flawess experience. - Easy2use animation listener for controlling other element when the object is interacting or animating ✅ **Performance** - Use android framework native DyanmicAniamtion And TimingInterpolator ✅ - Pre-save animation's data for less calculation - Hardware Acceleration ✅ **Design Component** - Scrollview|Scroller|PageViewer Component & Example - Drag | DND Component & Example - Button Component & Example - Transition Component & Example(Maintain different element's property in state machine) - Scroll-selector Component & Example(Scroll to fixed position) - Swipe to delete Component & Example **Dev Tools** - Data-bind graph to modify and preview animation in application ✅ - Data-bind selctor to change animation-type in application ✅(Still has some bugs) **Utils** - AE Plugin for converting curves & revealing codes ✅(Will update GUI later) ## License See Apache License [here](https://github.com/MartinRGB/Animer/blob/master/LICENSE) ================================================ FILE: README.zh.md ================================================ ## 关于 | MonitorUI Example | Scroller Example | View Prototype | | ------------- |-------------| ----- | | | | Launguage:[English](https://github.com/MartinRGB/Animer) Animer 是一款致力于提升 Android 动画体验的 Java 库 你可以把 Animer 理解为`基于 View 动画` 、`强化动画控制、交互性` 的动画器,并提供了调试UI Animer 封装了: * Android 平台的 DynamicAnimation 和 Interpolator 的曲线 * iOS 平台的 CASpringAnimation 和 UIViewSpring 的曲线 * 贝塞尔函数曲线 (iOS Deafult Easing,Web Default CSS Easing,AE 的关键帧间曲线 都可以用此法实现) * Principle、Origami、Protopie、FramerJS 等动画工具的曲线 Animer 并没有像 [Rebound](https://github.com/facebook/rebound) 那样,通过 Choreographer 或者自构建 Looper ,从头构建一套动画器,而是将上述曲线的算法通过转换器,最终会转换成 Android 原生的 DynamicAnimation 或者 TimingInterpolator,进而提高动画执行性能。 Animer 提供了可实时调节的控制UI和曲线图表,以便设计师和开发者调节参数,节省编译时间。 网页版本(该网页主要功能是可以将其他平台、工具的参数转化为安卓原生动画类的参数) —— [Animer_Web](http://www.martinrgb.com/Animer_Web/#) AE 脚本 —— [Animer AE](https://github.com/MartinRGB/Animer_AE) ## 下载 ## 两个简单的 View 实现的快速原型 [ ![Download](https://api.bintray.com/packages/martinrgb/animer/animer/images/download.svg) ](https://bintray.com/martinrgb/animer/animer/_latestVersion) [View Demo 1](https://github.com/MartinRGB/Animer/files/3948871/app-debug_2.zip) [View Demo 2](https://github.com/MartinRGB/Animer/files/3948863/app-debug.zip) ## 使用方法 Animer 支持多种风格的动画方法,如果写过 FramerJS、用过 Rebound 库的朋友会非常熟悉,也模拟了安卓原生的动画方法。 ### Android 原生风格 ```java // 创建一个 Animer 解算器对象,采用了原生的插值动画类 Animer.AnimerSolver solver = Animer.interpolatorDroid(new AccelerateDecelerateInterpolator(),600) // 模仿 ObjectAnimator 的构造 Animer animer = new Animer(myView,solver,Animer.TRANSLATION_Y,0,200); animer.start(); // animer.cancel(); // animer.end(); ``` ### FramerJS 状态机风格( FramerJS 的状态机动画机制 非常便于组织、整理页面动画) ```java // 创建一个 Animer 解算器对象,采用了原生的 DynamicAnimation 动画器 Animer.AnimerSolver solver = Animer.springDroid(1000,0.5f); Animer animer = new Animer(); // 给 Animer 对象添加 solver animer.setSolver(solver); // 设置动画的几种可能状态(入场、退场、点击) animer.setStateValue("stateA",300); animer.setStateValue("stateB",700); animer.setStateValue("stateC",200); // 给动画添加监听,观察数值变化 animer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { myView1.setTranslationX(value); myView2.setScaleX(1.f+value/100); myView2.setScaleY(1.f+value/100); } }); // 立即切换到状态 animer.switchToState("stateA"); // 动画运动到状态 // animer.animateToState("stateB"); ``` ### Facebook Rebound 风格 ```java // 创建一个 Animer 解算器对象,采用了 Facebook 的 POP 弹性动画器 Animer.AnimerSolver solver = Animer.springOrigamiPOP(5,10); Animer animer = new Animer(myView,solver,Animer.SCALE); // 给动画添加监听,观察数值变化,这里将对象 view 的缩放和动画器数值绑定 animer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) myView.setScaleX(value); myView.setScaleY(value); } }); animer.setCurrentValue(1.f); boolean isScaled = false; myView.setOnClickListener(view -> { if(!isScaled){ animer.setEndValue(0.5); } else{ animer.setEndValue(1); } isScaled = !isScaled; }); ``` ### 将 Animer 对象添加到 config UI 中 添加 XML ```xml ``` 在 Java中, 将 Animer 对象添加到 AnConfigRegistry 实例中,然后刷新 UI ```java AnConfigView mAnimerConfiguratorView = (AnConfigView) findViewById(R.id.an_configurator); AnConfigRegistry.getInstance().addAnimer("Card Scale Animation",cardScaleAnimer); AnConfigRegistry.getInstance().addAnimer("Card TranslationX Animation",cardTransXAnimer); mAnimerConfiguratorView.refreshAnimerConfigs(); ``` ### 将自定义曲线预设添加到 config UI 中 先需要清空默认的所有预设,然后添加自己的动画预设。 ```java AnConfigRegistry.getInstance().removeAllSolverConfig(); AnConfigRegistry.getInstance().addSolver("Preset1",Animer.springDroid(500.0f,0.96f)); AnConfigRegistry.getInstance().addSolver("Preset2",Animer.flingDroid(400.f,0.95f)); ... mAnimerConfiguratorView.refreshAnimerConfigs(); ``` ### 支持的 View 属性 ``` Animer.TRANSLATION_X Animer.TRANSLATION_Y Animer.TRANSLATION_Z Animer.SCALE // equal to SCALE_X + SCALE_Y Animer.SCALE_X Animer.SCALE_Y Animer.ROTATION Animer.ROTATION_X Animer.ROTATION_Y Animer.X Animer.Y Animer.Z Animer.ALPHA ``` ### 支持的动画曲线 ```java Animer.springDroid(stiffness,dampingratio) // Android Dynamic SpringAnimation Animer.flingDroid(velocity,friction) // Android Dynamic FlingAnimation Animer.springiOSUIView(dampingratio,duration) // iOS UIView SPring Animer.springiOSCoreAnimation(stiffness,damping) // iOS CASpringAnimation Animer.springOrigamiPOP(bounciness,speed) // Origami POP Animer.springRK4(tension,friction) // Framer-RK4 Animer.springDHO(stiffness,damping) // Framer-DHO Animer.springProtopie(tension,friction) // Protopie Animer.springPrinciple(tension,friction) // Principle // Custom Bounce Interpolator(Romain Guy's DropInMotion) Animer.interpolatorDroid(new CustomBounceInterpolator(),duration) // Custom Damping Interpolator(Romain Guy's DropInMotion) Animer.interpolatorDroid(new CustomDampingInterpolator(),duration) // MocosSpring Interpolator (https://github.com/marcioapaiva/mocos-controlator) Animer.interpolatorDroid(new CustomMocosSpringInterpolator(),duration) // Custom Spring Interpolator(https://inloop.github.io/interpolator/) Animer.interpolatorDroid(new CustomSpringInterpolator(),duration) // Android Native Interpolator Below Animer.interpolatorDroid(new PathInterpolator(),duration) // Cubic Bezier Interpolator ... Animer.interpolatorDroid(new DecelerateInterpolator(),duration) // Android Decelerate Interpolator ... ``` ## TODO - 以界面整体状态为考量的状态机系统,而不是单个对象的状态 - 重新设计 API,重新编写文档,提高可用性 - 重写绘制图表的 shader,目前使用了太多条件分歧,参考[如何在shader中避免使用if else](https://www.bilibili.com/read/cv1469216/) - 考虑转场的使用场景 - 考虑 Hook 机制 - 考虑重新设计调试UI,因为不方便连续调试,可能考虑 adb + electron + web桌面客户端的方式 ## Animer 设计的核心理念和一些想法 下图是 Animer 大致的原理和设计思路 **数据** - FramerJS 的[动画状态机概念](https://github.com/koenbok/Framer/tree/master/framer) ✅ - Animation Converter 中的[多平台、工具动画器转换函数](https://github.com/MartinRGB/AndroidInterpolator_AE) ✅ - 支持读取外部 JSON 编辑动画(???) **算法** - Rebound 的[物理动画概念](https://github.com/facebook/rebound) 和 Android 的 DynamicAnimation ✅ - Android 原生的 查找表插值器(LookupTable Interpolator) + RK4 弹性解算器 + DHO 弹性解算器 ✅ - Flutter Physics 的[物理模拟](https://api.flutter.dev/flutter/physics/physics-library.html) & UIKit Dyanmic 的[物理模拟](https://developer.apple.com/documentation/uikit/animation_and_haptics/uikit_dynamics) - 动量传递与保存(通过状态机实现) - 通过元素面积计算质量 - 通过振荡次数或时间限制弹性动画运动时长 **高级动画** - 叠加动画(Addtive animation,将多个动画同时影响到一个对象) - 链式动画(Chained animation,动画一个接一个开始) - 视差效果(Parallax animation,相同动画触发,不同动画曲线、时间、延迟) - 序列动画(Sequencing animation,动画逐次开始) **交互动画** - 支持手势驱动的动画,动画可中断,可在动画过程中重新交互(参考 iOS `CADisplayLink` 或 Rebound 的 `SetEndValue`) ✅ - 内置封装一个手势动画器,提供手势速度自动保留,以便手势交互时动画体验更物理、更流畅。(完成一半) - 提供易用的动画监听,在动画监听中控制多个对象元素 ✅ **性能** - 所有动画最终被转换为 Android 框架原生的 DyanmicAniamtion 和 TimingInterpolator,可调试完解除依赖然后使用原生写法 ✅ - 可考虑将动画数据转换为预存数组,以便节省实时计算开销 - 硬件加速 ✅ **设计组件** - Scrollview|Scroller|PageViewer 组件跟案例,提供更好的 Overscroll 和 Fling 效果 - Drag | 拖拽组件跟案例,提供更符合物理直觉的甩手感 - Button 组件和案例 - Transition 组件和案例(考虑如何在不同 Activity 中维护一个元素的属性以便转场) - Scroll-selector 滑动选择器组件和案例(类似 iOS 的日期选择器,提供平缓的衰减且定位的滚动体验) - Swipe to delete 滑动删除组件和案例(更自然的滑动删除效果) **开发工具** - 提供 GLSL 异步绘制的图表,展示动画曲线本身 ✅ - 提供 GLSL 异步绘制的图表,展示实时属性变化曲线 - 提供数据与 View 属性绑定的控制 UI,实时切换、修改动画 ✅(目前仍有些许 bugs) **实用工具** - AE 插件,通过赋予关键帧表达式模拟上述曲线 ✅(目前仅为脚本,后面提供 Extension 级插件,增加 UI) ## 协议 采用了 Apache 许可协议,[详细](https://github.com/MartinRGB/Animer/blob/master/LICENSE) ================================================ FILE: animer/.gitignore ================================================ /build ================================================ FILE: animer/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'com.jfrog.bintray' //apply plugin: 'com.novoda.bintray-release' android { compileSdkVersion 29 defaultConfig { minSdkVersion 26 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility = "1.8" targetCompatibility = "1.8" } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02' } //publish { // userOrg = 'martinrgb' // groupId = 'com.martinrgb' // repoName = 'animer' // artifactId = 'animer' // publishVersion = '0.1.6.0' // desc = 'An animation controller for better Android Experience' // website = "https://github.com/MartinRGB/Animer" //} //项目主页 def siteUrl = 'https://github.com/MartinRGB/Animer' //项目的版本控制地址 def gitUrl = 'https://github.com/MartinRGB/Animer.git' //发布到组织名称名字,必须填写 group = "com.martinrgb" //发布到JCenter上的项目名字,必须填写 def libName = "animer" // 版本号,下次更新是只需要更改版本号即可 version = "0.1.6.3" /** 上面配置后上传至JCenter后的编译路径是这样的: compile 'group:libName:version' **/ //生成源文件 task sourcesJar(type: Jar) { from android.sourceSets.main.java.srcDirs archiveClassifier.set("sources") } //生成文档 task javadoc(type: Javadoc) { source = android.sourceSets.main.java.srcDirs classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) options.encoding "UTF-8" options.charSet 'UTF-8' options.author true options.version true options.links "https://github.com/MartinRGB/Animer" failOnError false } //文档打包成jar task javadocJar(type: Jar, dependsOn: javadoc) { archiveClassifier.set("javadoc") from javadoc.destinationDir } //拷贝javadoc文件 task copyDoc(type: Copy) { from "${buildDir}/docs/" into "docs" } //上传到jCenter所需要的源码文件 artifacts { archives javadocJar archives sourcesJar } // 配置maven库,生成POM.xml文件 install { repositories.mavenInstaller { // This generates POM.xml with proper parameters pom { project { packaging 'aar' name 'baseui' url siteUrl licenses { license { name 'The Apache Software License, Version 2.0' url 'http://www.apache.org/licenses/LICENSE-2.0.txt' } } developers { developer { id 'MartinRGB' name 'Martin Tsiu' email 'qiuyinsen@gmail.com' } } scm { connection gitUrl developerConnection gitUrl url siteUrl } } } } } //上传到JCenter Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) bintray { user = properties.getProperty("bintray.user") //读取 local.properties 文件里面的 bintray.user key = properties.getProperty("bintray.apikey") //读取 local.properties 文件里面的 bintray.apikey configurations = ['archives'] pkg { repo = "animer" name = libName //发布到JCenter上的项目名字,必须填写 desc = 'Animation Framework' //项目描述 websiteUrl = siteUrl vcsUrl = gitUrl licenses = ["Apache-2.0"] publish = true } } ================================================ FILE: animer/consumer-rules.pro ================================================ ================================================ FILE: animer/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: animer/src/main/AndroidManifest.xml ================================================ ================================================ FILE: animer/src/main/java/com/martinrgb/animer/Animer.java ================================================ package com.martinrgb.animer; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import androidx.core.view.ViewCompat; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FlingAnimation; import androidx.dynamicanimation.animation.FloatValueHolder; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.martinrgb.animer.core.interpolator.AnInterpolator; import com.martinrgb.animer.core.math.converter.AndroidSpringConverter; import com.martinrgb.animer.core.math.converter.DHOConverter; import com.martinrgb.animer.core.math.converter.OrigamiPOPConverter; import com.martinrgb.animer.core.math.converter.RK4Converter; import com.martinrgb.animer.core.math.converter.UIViewSpringConverter; import com.martinrgb.animer.core.property.AnProperty; import com.martinrgb.animer.core.solver.AnSolver; import com.martinrgb.animer.core.state.PhysicsState; import com.martinrgb.animer.monitor.AnConfigData; public class Animer { // ########################################### // Property // ########################################### private abstract static class AnimerProperty extends AnProperty { private AnimerProperty(String name) { super(name); } } public static final AnimerProperty TRANSLATION_X = new AnimerProperty("translationX") { @Override public void setValue(View view, float value) { view.setTranslationX(value); } @Override public float getValue(View view) { return view.getTranslationX(); } }; public static final AnimerProperty TRANSLATION_Y = new AnimerProperty("translationY") { @Override public void setValue(View view, float value) { view.setTranslationY(value); } @Override public float getValue(View view) { return view.getTranslationY(); } }; public static final AnimerProperty TRANSLATION_Z = new AnimerProperty("translationZ") { @Override public void setValue(View view, float value) { ViewCompat.setTranslationZ(view, value); } @Override public float getValue(View view) { return ViewCompat.getTranslationZ(view); } }; public static final AnimerProperty SCALE = new AnimerProperty("scale") { @Override public void setValue(View view, float value) { view.setScaleX(value); view.setScaleY(value); } @Override public float getValue(View view) { return view.getScaleX(); } }; public static final AnimerProperty SCALE_X = new AnimerProperty("scaleX") { @Override public void setValue(View view, float value) { view.setScaleX(value); } @Override public float getValue(View view) { return view.getScaleX(); } }; public static final AnimerProperty SCALE_Y = new AnimerProperty("scaleY") { @Override public void setValue(View view, float value) { view.setScaleY(value); } @Override public float getValue(View view) { return view.getScaleY(); } }; public static final AnimerProperty ROTATION = new AnimerProperty("rotation") { @Override public void setValue(View view, float value) { view.setRotation(value); } @Override public float getValue(View view) { return view.getRotation(); } }; public static final AnimerProperty ROTATION_X = new AnimerProperty("rotationX") { @Override public void setValue(View view, float value) { view.setRotationX(value); } @Override public float getValue(View view) { return view.getRotationX(); } }; public static final AnimerProperty ROTATION_Y = new AnimerProperty("rotationY") { @Override public void setValue(View view, float value) { view.setRotationY(value); } @Override public float getValue(View view) { return view.getRotationY(); } }; public static final AnimerProperty X = new AnimerProperty("x") { @Override public void setValue(View view, float value) { view.setX(value); } @Override public float getValue(View view) { return view.getX(); } }; public static final AnimerProperty Y = new AnimerProperty("y") { @Override public void setValue(View view, float value) { view.setY(value); } @Override public float getValue(View view) { return view.getY(); } }; public static final AnimerProperty Z = new AnimerProperty("z") { @Override public void setValue(View view, float value) { ViewCompat.setZ(view, value); } @Override public float getValue(View view) { return ViewCompat.getZ(view); } }; public static final AnimerProperty ALPHA = new AnimerProperty("alpha") { @Override public void setValue(View view, float value) { view.setAlpha(value); } @Override public float getValue(View view) { return view.getAlpha(); } }; // ########################################### // Arugment // ########################################### public Object getArgument1(){ return getCurrentSolver().getArg1(); } public void setArgument1(Object val){ getCurrentSolver().setArg1(val); } public Object getArgument2(){ return getCurrentSolver().getArg2(); } public void setArgument2(Object val){ getCurrentSolver().setArg2(val); } public AnConfigData getCurrentSolverData(){ return getCurrentSolver().getConfigSet(); } // ########################################### // Object // ########################################### private Object mTarget; private AnimerProperty mProperty; private PhysicsState mPhysicsState; private FlingAnimation mFlingAnimation; private SpringAnimation mSpringAnimation; private ObjectAnimator mTimingAnimation; private static final int FLING_SOLVER_MODE = 0; private static final int SPRING_SOLVER_MODE = 1; private static final int INTERPOLATOR_SOLVER_MODE = 2; private static AnimerSolver defaultSpringSolver = springDroid(50f,0.99f); private AnimerSolver currentSolver = springDroid(50f,0.99f); public AnimerSolver getCurrentSolver() { return currentSolver; } public void setCurrentSolver(AnimerSolver solver) { currentSolver.unBindSolverListener(); currentSolver = solver; } private static final int VALUE_ANIMATOR_MODE = 0; private static final int OBJECT_ANIMAOTR_MODE = 1; private int ANIMATOR_MODE = -1; private boolean HARDWAREACCELERATION_IS_ENABLED = false; private float velocityFactor = 1.0f; private float minimumVisValue = 0.001f; // ########################################### // Solver // ########################################### public static class AnimerSolver extends AnSolver { private AnConfigData anConfigData; private AnimerSolver(Object val1,Object val2,int mode,AnConfigData data) { super(val1,val2,mode); setConfigSet(data); } public AnConfigData getConfigSet(){ return anConfigData; } private void setConfigSet(AnConfigData data){ anConfigData = data; } } public static AnimerSolver flingDroid(float velocity,float friction){ AnConfigData configData = new AnConfigData(velocity,friction); configData.setArguments("AndroidFling","velocity",-5000,5000,"friction",0.01,10); return new AnimerSolver(velocity,friction,0,configData); } public static AnimerSolver springDroid(float stiffness,float dampingratio){ AnConfigData configData = new AnConfigData(stiffness,dampingratio); configData.setArguments("AndroidSpring","stiffness",0.01,3000,"dampingratio",0.01,1); return new AnimerSolver(stiffness,dampingratio,1,configData); } public static AnimerSolver springRK4(float tension,float friction){ AnConfigData configData = new AnConfigData(tension,friction); configData.setArguments("RK4Spring","tension",0.01,3000,"friction",0.01,100); RK4Converter rk4Converter = new RK4Converter(tension,friction); return new AnimerSolver(rk4Converter.getStiffness(),rk4Converter.getDampingRatio(),1,configData); } public static AnimerSolver springDHO(float stiffness,float damping){ AnConfigData configData = new AnConfigData(stiffness,damping); configData.setArguments("DHOSpring","stiffness",0.01,3000,"damping",0.01,100); DHOConverter dhoConverter = new DHOConverter(stiffness,damping); return new AnimerSolver(dhoConverter.getStiffness(),dhoConverter.getDampingRatio(),1,configData); } public static AnimerSolver springOrigamiPOP(float bounciness,float speed){ AnConfigData configData = new AnConfigData(bounciness,speed); configData.setArguments("OrigamiPOPSpring","bounciness",0.01,100,"speed",0.01,100); OrigamiPOPConverter origamiPOPConverter = new OrigamiPOPConverter(bounciness,speed); return new AnimerSolver(origamiPOPConverter.getStiffness(),origamiPOPConverter.getDampingRatio(),1,configData); } public static AnimerSolver springiOSUIView(float dampingratio,float duration){ AnConfigData configData = new AnConfigData(dampingratio,duration); configData.setArguments("iOSUIViewSpring","dampingratio",0.01,0.99,"duration",0.01,5); UIViewSpringConverter uiViewSpringConverter = new UIViewSpringConverter(dampingratio,duration); return new AnimerSolver(uiViewSpringConverter.getStiffness(),uiViewSpringConverter.getDampingRatio(),1,configData); } public static AnimerSolver springiOSCoreAnimation(float stiffness,float damping){ AnConfigData configData = new AnConfigData(stiffness,damping); configData.setArguments("iOSCoreAnimationSpring","stiffness",0.01,3000,"damping",0.01,100); DHOConverter dhoConverter = new DHOConverter(stiffness,damping); return new AnimerSolver(dhoConverter.getStiffness(),dhoConverter.getDampingRatio(),1,configData); } public static AnimerSolver springProtopie(float tension,float friction){ AnConfigData configData = new AnConfigData(tension,friction); configData.setArguments("ProtopieSpring","tension",0.01,3000,"friction",0.01,100); RK4Converter rk4Converter = new RK4Converter(tension,friction); return new AnimerSolver(rk4Converter.getStiffness(),rk4Converter.getDampingRatio(),1,configData); } public static AnimerSolver springPrinciple(float tension,float friction){ AnConfigData configData = new AnConfigData(tension,friction); configData.setArguments("PrincipleSpring","tension",0.01,3000,"friction",0.01,100); RK4Converter rk4Converter = new RK4Converter(tension,friction); return new AnimerSolver(rk4Converter.getStiffness(),rk4Converter.getDampingRatio(),1,configData); } public static AnimerSolver interpolatorDroid(AnInterpolator interpolator, long duration){ AnConfigData configData = new AnConfigData(interpolator,duration); configData.setArguments("AndroidInterpolator","interpolator",0.01,1,"duration",0.01,5000); return new AnimerSolver(interpolator,duration,2,configData); } // ########################################### // Constructor // ########################################### public Animer() { mTarget = null; mProperty = null; mPhysicsState = new PhysicsState(); ANIMATOR_MODE = VALUE_ANIMATOR_MODE; setupBySolver(currentSolver); } public Animer(AnimerSolver solver) { mTarget = null; mProperty = null; mPhysicsState = new PhysicsState(); ANIMATOR_MODE = VALUE_ANIMATOR_MODE; setupBySolver(solver); } public Animer(float value) { mTarget = null; mProperty = null; mPhysicsState = new PhysicsState((float)value); ANIMATOR_MODE = VALUE_ANIMATOR_MODE; setupBySolver(currentSolver); } public Animer(AnimerSolver solver,float value) { mTarget = null; mProperty = null; mPhysicsState = new PhysicsState((float) value); ANIMATOR_MODE = VALUE_ANIMATOR_MODE; setupBySolver(solver); } public Animer(K target, AnimerSolver solver, AnimerProperty property) { mTarget = target; mProperty = property; float proertyValue = mProperty.getValue((View) mTarget); mPhysicsState = new PhysicsState(proertyValue); ANIMATOR_MODE = OBJECT_ANIMAOTR_MODE; setupBySolver(solver); } public Animer(K target, AnimerSolver solver, AnimerProperty property, float end) { mTarget = target; mProperty = property; float proertyValue = mProperty.getValue((View) mTarget); mPhysicsState = new PhysicsState(proertyValue,end); ANIMATOR_MODE = OBJECT_ANIMAOTR_MODE; setupBySolver(solver); } public Animer(K target, AnimerSolver solver, AnimerProperty property, float start, float end) { mTarget = target; mProperty = property; mPhysicsState = new PhysicsState(start,end); ANIMATOR_MODE = OBJECT_ANIMAOTR_MODE; setupBySolver(solver); } public void setTarget(Object target) { this.mTarget = target; } public void setProperty(AnimerProperty mProperty) { this.mProperty = mProperty; } // ############################################ // Setup Solver // ############################################ // TODO:MORE Accuracy Setting public void setSolver(AnimerSolver solver){ cancel(); setupBySolver(solver); } // ############################################ // Setup Aniamtor // ############################################ private void setupBySolver(AnimerSolver solver) { switch(solver.getSolverMode()) { case FLING_SOLVER_MODE: setupFlingAnimator(solver); break; case SPRING_SOLVER_MODE: setupSpringAnimator(solver); break; case INTERPOLATOR_SOLVER_MODE: setupTimingAnimator(solver); break; default: break; } } private void setupFlingAnimator(AnimerSolver solver){ setCurrentSolver(solver); if(mFlingAnimation == null) { mFlingAnimation = new FlingAnimation(new FloatValueHolder()); mFlingAnimation.setMinimumVisibleChange(minimumVisValue); mFlingAnimation.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() { @Override public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) { float progress = (value - getStateValue("Start"))/(getStateValue("End") - getStateValue("Start")); updateCurrentPhysicsState(value,velocity,progress); } }); mFlingAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { @Override public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, float velocity) { endCurrentPhysicsState(value,0,true); if(triggereListener !=null ){ triggereListener.onTrigger(false); } } }); } attachSolverToFling(solver,mFlingAnimation); } private void attachSolverToFling(AnimerSolver solver, FlingAnimation flingAnimation){ final FlingAnimation flingAnim = flingAnimation; flingAnim.setStartVelocity((float) solver.getArg1()); flingAnim.setFriction((float) solver.getArg2()); solver.bindSolverListener(new AnimerSolver.SolverListener() { @Override public void onSolverUpdate(Object arg1, Object arg2) { flingAnim.setStartVelocity((float) arg1); flingAnim.setFriction((float) arg2); } }); } private void setupSpringAnimator(AnimerSolver solver){ setCurrentSolver(solver); if(mSpringAnimation == null) { mSpringAnimation = new SpringAnimation(new FloatValueHolder()); mSpringAnimation.setSpring(new SpringForce()); mSpringAnimation.setMinimumVisibleChange(minimumVisValue); mSpringAnimation.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() { @Override public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) { float progress = (value - getStateValue("Start"))/(getStateValue("End") - getStateValue("Start")); updateCurrentPhysicsState(value,velocity,progress); } }); mSpringAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { @Override public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, float velocity) { endCurrentPhysicsState(value,0,true); if(triggereListener !=null ){ triggereListener.onTrigger(false); } } }); } attachSolverToSpring(solver,mSpringAnimation); } private void attachSolverToSpring(AnimerSolver solver, SpringAnimation springAnimation){ final SpringAnimation springAnim = springAnimation; springAnim.getSpring().setStiffness( (float) solver.getArg1()); springAnim.getSpring().setDampingRatio( (float) solver.getArg2()); solver.bindSolverListener(new AnimerSolver.SolverListener() { @Override public void onSolverUpdate(Object arg1, Object arg2) { springAnim.getSpring().setStiffness((float) arg1); springAnim.getSpring().setDampingRatio((float) arg2); } }); } private void setupTimingAnimator(AnimerSolver solver){ setCurrentSolver(solver); if(mTimingAnimation == null) { mTimingAnimation = new ObjectAnimator(); mTimingAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { float mCurrentVelocity,mPrevVelocity; @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //#1 mCurrentVelocity = (float) valueAnimator.getAnimatedValue(); float value = mCurrentVelocity; float velocity = mCurrentVelocity - mPrevVelocity; float progress = (value - getStateValue("Start"))/(getStateValue("End") - getStateValue("Start")); updateCurrentPhysicsState(value,velocity,progress); mPrevVelocity = (float) valueAnimator.getAnimatedValue(); } }); mTimingAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); endCurrentPhysicsState(getCurrentPhysicsValue(),0,true); if(triggereListener !=null ){ triggereListener.onTrigger(false); } } }); } attachSolverToTiming(solver,mTimingAnimation); } private void attachSolverToTiming(AnimerSolver solver, ObjectAnimator timingAnimation){ final ObjectAnimator timingAnim = timingAnimation; timingAnim.setInterpolator( (AnInterpolator) solver.getArg1()); timingAnim.setDuration( (long) solver.getArg2()); solver.bindSolverListener(new AnimerSolver.SolverListener() { @Override public void onSolverUpdate(Object arg1, Object arg2) { timingAnim.setInterpolator((AnInterpolator) arg1); timingAnim.setDuration((long) arg2); } }); } // ############################################ // Animation Control Interface // ############################################ // ## Android Style Animaton Interface,driven by PhysicsState's State Machine public void setFrom(float start){ setStateValue("Start",start); //# Then from-to-setVelocity-start can work getCurrentPhysicsState().updatePhysicsValue(start); switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: mFlingAnimation.setStartValue(getStateValue("Start")); break; case SPRING_SOLVER_MODE: mSpringAnimation.setStartValue(getStateValue("Start")); break; case INTERPOLATOR_SOLVER_MODE: mTimingAnimation.setFloatValues(getStateValue("Start"),getStateValue("End")); break; default: break; } } public void setTo(float end){ setStateValue("End",end); switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: setupSpringAnimator(defaultSpringSolver); mSpringAnimation.getSpring().setFinalPosition(getStateValue("End")); break; case SPRING_SOLVER_MODE: mSpringAnimation.getSpring().setFinalPosition(getStateValue("End")); break; case INTERPOLATOR_SOLVER_MODE: mTimingAnimation.setFloatValues(getStateValue("Start"),getStateValue("End")); break; default: break; } } public void start(){ if(triggereListener != null){ triggereListener.onTrigger(true); //triggereListener.onTrigger(false); } setHardwareAcceleration(true); if(currentSolver.getSolverMode() == FLING_SOLVER_MODE) { mFlingAnimation.cancel(); mFlingAnimation.start(); } else { animateToState("End"); } } public void cancel(){ if(mFlingAnimation !=null && mFlingAnimation.isRunning()){ mFlingAnimation.cancel(); } if(mSpringAnimation !=null && mSpringAnimation.isRunning()){ mSpringAnimation.cancel(); } if(mTimingAnimation !=null && mTimingAnimation.isRunning()){ mTimingAnimation.cancel(); } } public boolean isRunning(){ boolean isRunning = false; switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: if(mFlingAnimation !=null) isRunning = mFlingAnimation.isRunning(); break; case SPRING_SOLVER_MODE: if(mSpringAnimation !=null) isRunning = mSpringAnimation.isRunning(); break; case INTERPOLATOR_SOLVER_MODE: if(mTimingAnimation !=null) isRunning = mTimingAnimation.isRunning(); break; default: break; } return isRunning; } public void end(){ cancel(); switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: setupSpringAnimator(defaultSpringSolver); if(mSpringAnimation.canSkipToEnd()){ mSpringAnimation.skipToEnd(); } break; case SPRING_SOLVER_MODE: if(mSpringAnimation.canSkipToEnd()){ mSpringAnimation.skipToEnd(); } break; case INTERPOLATOR_SOLVER_MODE: mTimingAnimation.end(); break; default: break; } switchToState("End"); } public void reverse(){ animateToState("Start"); } // ## FramerJS Style Animation Interface,driven by PhysicsState's State Machine public void switchToState(String state){ cancel(); float progress = (getStateValue(state) - getStateValue("Start"))/(getStateValue("End") - getStateValue("Start")); updateCurrentPhysicsState(getStateValue(state),getCurrentPhysicsVelocity(),progress); } public void animateToState(String state){ if(triggereListener != null){ triggereListener.onTrigger(true); //triggereListener.onTrigger(false); } setHardwareAcceleration(true); switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: setupSpringAnimator(defaultSpringSolver); mSpringAnimation.setStartValue(getCurrentPhysicsValue()); mSpringAnimation.setStartVelocity(getCurrentPhysicsVelocity()); mSpringAnimation.animateToFinalPosition(getStateValue(state)); break; case SPRING_SOLVER_MODE: mSpringAnimation.setStartValue(getCurrentPhysicsValue()); mSpringAnimation.setStartVelocity(getCurrentPhysicsVelocity()); mSpringAnimation.animateToFinalPosition(getStateValue(state)); break; case INTERPOLATOR_SOLVER_MODE: mTimingAnimation.setFloatValues(getCurrentPhysicsValue(),getStateValue(state)); mTimingAnimation.start(); break; default: break; } } // ## Origami-POP-Rebound Style Animation Interface,driven by PhysicsState's Value // # Equal to [setCurrentValue] public void setCurrentValue(float value){ float velocity = value - getCurrentPhysicsValue(); float progress = (value - getStateValue("Start"))/(getStateValue("End") - getStateValue("Start")); updateCurrentPhysicsState(value,velocity*velocityFactor,progress); } // # Equal to [setEndValue] public void setEndValue(float value){ if(triggereListener != null){ triggereListener.onTrigger(true); //triggereListener.onTrigger(false); } setHardwareAcceleration(true); switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: setupSpringAnimator(defaultSpringSolver); mSpringAnimation.setStartValue(getCurrentPhysicsValue()); mSpringAnimation.setStartVelocity(getCurrentPhysicsVelocity()); mSpringAnimation.animateToFinalPosition(value); break; case SPRING_SOLVER_MODE: mSpringAnimation.setStartValue(getCurrentPhysicsValue()); mSpringAnimation.setStartVelocity(getCurrentPhysicsVelocity()); mSpringAnimation.animateToFinalPosition(value); break; case INTERPOLATOR_SOLVER_MODE: mTimingAnimation.setFloatValues(getCurrentPhysicsValue(),value); mTimingAnimation.start(); break; default: break; } } // ############################################ // Setup Velocity // ############################################ // ## State public void setVelocity(float velocity){ setCurrentPhysicsVelocity(velocity); } public void setVelocityInfluence(float factor){ velocityFactor = factor; } // ############################################ // minVisChange // ############################################ public void setMinimumVisibleChange(float minimumVisValue) { this.minimumVisValue = minimumVisValue; switch(currentSolver.getSolverMode()) { case FLING_SOLVER_MODE: mFlingAnimation.setMinimumVisibleChange(minimumVisValue); break; case SPRING_SOLVER_MODE: mSpringAnimation.setMinimumVisibleChange(minimumVisValue); break; case INTERPOLATOR_SOLVER_MODE: break; default: break; } } // ############################################ // PhysicsState's Getter & Setter // ############################################ public void setStateValue(String key,float value){ mPhysicsState.setStateValue(key,value); } public float getStateValue(String state){ return mPhysicsState.getStateValue(state); } // ## Value private void setCurrentPhysicsVelocity(float velocity){ mPhysicsState.updatePhysicsVelocity(velocity); } private void setCurrenetPhysicsValue(float value){ mPhysicsState.updatePhysicsValue(value); } private float getCurrentPhysicsVelocity(){ return mPhysicsState.getPhysicsVelocity(); } private float getCurrentPhysicsValue(){ return mPhysicsState.getPhysicsValue(); } private void endCurrentPhysicsState(float value,float velocity,boolean canceled){ setCurrenetPhysicsValue(value); setCurrentPhysicsVelocity(velocity); setHardwareAcceleration(false); if (endListener != null) { endListener.onEnd(value,velocity,canceled); } } // ## Mainly use this for listener-mode private void updateCurrentPhysicsState(float value,float velocity,float progress){ setCurrenetPhysicsValue(value); setCurrentPhysicsVelocity(velocity); if (ANIMATOR_MODE != VALUE_ANIMATOR_MODE) { mProperty.setValue((View) mTarget, value); } if (updateListener != null) { updateListener.onUpdate(value,velocity,progress); } } public PhysicsState getCurrentPhysicsState(){ return mPhysicsState; } // ############################################ // Hardware Acceleration // ############################################ private void setHardwareAcceleration(boolean bool){ if(HARDWAREACCELERATION_IS_ENABLED && mTarget !=null){ if(bool){ if(((View)mTarget).getLayerType() == View.LAYER_TYPE_NONE){ ((View)mTarget).setLayerType(View.LAYER_TYPE_HARDWARE,null); } } else{ if(((View)mTarget).getLayerType() == View.LAYER_TYPE_HARDWARE){ ((View)mTarget).setLayerType(View.LAYER_TYPE_NONE,null); } } } } public boolean isHardwareAccelerationEnabled() { return HARDWAREACCELERATION_IS_ENABLED; } public void enableHardwareAcceleration(boolean enable){ HARDWAREACCELERATION_IS_ENABLED = enable; } // ############################################ // TODO: Optim Actioner // ############################################ private boolean ENABLE_ACTIONER_POSTION_VELOCITY = false; private Object mActioner; public void enableActionerInfluenceOnVelocity(boolean boo){ ENABLE_ACTIONER_POSTION_VELOCITY = boo; } private boolean getActionerInfluenceOnVelocity(){ return ENABLE_ACTIONER_POSTION_VELOCITY; } public void setActionerAndListener(K actioner,ActionTouchListener listener){ mActioner = actioner; setActionTouchListener(listener); ((View)mActioner).setOnTouchListener(new View.OnTouchListener() { VelocityTracker velocityTracker; float initX = 0,initY = 0; float startX,startY,posX = 0,posY = 0,velX = 0,velY = 0; @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } else { velocityTracker.clear(); } velocityTracker.addMovement(motionEvent); startX = view.getX() - motionEvent.getRawX(); startY = view.getY() - motionEvent.getRawY(); actionListener.onDown(view,motionEvent); break; } case MotionEvent.ACTION_MOVE: { velocityTracker.addMovement(motionEvent); velocityTracker.computeCurrentVelocity(1000); velX = velocityTracker.getXVelocity(); velY = velocityTracker.getYVelocity(); posX = motionEvent.getRawX() + startX; posY = motionEvent.getRawY() + startY; actionListener.onMove(view,motionEvent,velX,velY); break; } case MotionEvent.ACTION_UP: { Log.e("velX",String.valueOf(velX)); if(getActionerInfluenceOnVelocity()){ setVelocity(velX); } actionListener.onUp(view,motionEvent); break; } case MotionEvent.ACTION_CANCEL: { actionListener.onCancel(view,motionEvent); break; } } return true; } }); } // ############################################ // Animation Listener // ############################################ private UpdateListener updateListener; private EndListener endListener; private ActionTouchListener actionListener; private TriggeredListener triggereListener; public void setUpdateListener(UpdateListener listener) { updateListener = listener; } public void setEndListener(EndListener listener) { endListener = listener; } public TriggeredListener getTriggerListener() { if(triggereListener !=null){ return triggereListener; } else{ return null; } } public void setTriggerListener(TriggeredListener listener) { triggereListener = listener; } public void removeTriggerListener() { triggereListener = null; } public void setActionTouchListener(ActionTouchListener listener) { actionListener = listener; } public interface UpdateListener { void onUpdate(float value, float velocity,float progress); } public interface EndListener{ void onEnd(float value, float velocity,boolean canceled); } public interface TriggeredListener{ void onTrigger(boolean triggered); } public interface ActionTouchListener{ void onDown(View view,MotionEvent event); void onMove(View view,MotionEvent event,float velocityX,float velocityY); void onUp(View view,MotionEvent event); abstract void onCancel(View view,MotionEvent event); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/overscroller/launcher3/FlingSpringAnim.java ================================================ /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.martinrgb.animer.component.overscroller.launcher3; import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener; import androidx.dynamicanimation.animation.FlingAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; /** * Given a property to animate and a target value and starting velocity, first apply friction to * the fling until we pass the target, then apply a spring force to pull towards the target. */ public class FlingSpringAnim { private static final float FLING_FRICTION = 1.5f; private static final float SPRING_STIFFNESS = 400f; private static final float SPRING_DAMPING = 0.8f; private final FlingAnimation mFlingAnim; private SpringAnimation mSpringAnim; private float mTargetPosition; public FlingSpringAnim(K object, FloatPropertyCompat property, float startPosition, float targetPosition, float startVelocity, float minVisChange, float minValue, float maxValue, float springVelocityFactor, OnAnimationEndListener onEndListener) { mFlingAnim = new FlingAnimation(object, property) .setFriction(FLING_FRICTION) // Have the spring pull towards the target if we've slowed down too much before // reaching it. .setMinimumVisibleChange(minVisChange) .setStartVelocity(startVelocity) .setMinValue(minValue) .setMaxValue(maxValue); mTargetPosition = targetPosition; mFlingAnim.addEndListener(((animation, canceled, value, velocity) -> { mSpringAnim = new SpringAnimation(object, property) .setStartValue(value) .setStartVelocity(velocity * springVelocityFactor) .setSpring(new SpringForce(mTargetPosition) .setStiffness(SPRING_STIFFNESS) .setDampingRatio(SPRING_DAMPING)); mSpringAnim.addEndListener(onEndListener); mSpringAnim.animateToFinalPosition(mTargetPosition); })); } public float getTargetPosition() { return mTargetPosition; } public void updatePosition(float startPosition, float targetPosition) { mFlingAnim.setMinValue(Math.min(startPosition, targetPosition)) .setMaxValue(Math.max(startPosition, targetPosition)); mTargetPosition = targetPosition; if (mSpringAnim != null) { mSpringAnim.animateToFinalPosition(mTargetPosition); } } public void start() { mFlingAnim.start(); } public void end() { mFlingAnim.cancel(); if (mSpringAnim.canSkipToEnd()) { mSpringAnim.skipToEnd(); } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/overscroller/launcher3/Interpolators.java ================================================ package com.martinrgb.animer.component.overscroller.launcher3; //import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs; import android.content.Context; import android.graphics.Path; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.OvershootInterpolator; import android.view.animation.PathInterpolator; /** * Common interpolators used in Launcher */ public class Interpolators { public static final Interpolator LINEAR = new LinearInterpolator(); public static final Interpolator ACCEL = new AccelerateInterpolator(); public static final Interpolator ACCEL_1_5 = new AccelerateInterpolator(1.5f); public static final Interpolator ACCEL_2 = new AccelerateInterpolator(2); public static final Interpolator DEACCEL = new DecelerateInterpolator(); public static final Interpolator DEACCEL_1_5 = new DecelerateInterpolator(1.5f); public static final Interpolator DEACCEL_1_7 = new DecelerateInterpolator(1.7f); public static final Interpolator DEACCEL_2 = new DecelerateInterpolator(2); public static final Interpolator DEACCEL_2_5 = new DecelerateInterpolator(2.5f); public static final Interpolator DEACCEL_3 = new DecelerateInterpolator(3f); public static final Interpolator ACCEL_DEACCEL = new AccelerateDecelerateInterpolator(); public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); public static final Interpolator AGGRESSIVE_EASE = new PathInterpolator(0.2f, 0f, 0f, 1f); public static final Interpolator AGGRESSIVE_EASE_IN_OUT = new PathInterpolator(0.6f,0, 0.4f, 1); public static final Interpolator EXAGGERATED_EASE; public static final Interpolator INSTANT = t -> 1; private static final int MIN_SETTLE_DURATION = 200; private static final float OVERSHOOT_FACTOR = 0.9f; static { Path exaggeratedEase = new Path(); exaggeratedEase.moveTo(0, 0); exaggeratedEase.cubicTo(0.05f, 0f, 0.133333f, 0.08f, 0.166666f, 0.4f); exaggeratedEase.cubicTo(0.225f, 0.94f, 0.5f, 1f, 1f, 1f); EXAGGERATED_EASE = new PathInterpolator(exaggeratedEase); } public static final Interpolator OVERSHOOT_1_2 = new OvershootInterpolator(1.2f); public static final Interpolator OVERSHOOT_1_7 = new OvershootInterpolator(1.7f); public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR = new PathInterpolator(0.3f, 0f, 0.1f, 1f); /** * Inversion of ZOOM_OUT, compounded with an ease-out. */ public static final Interpolator ZOOM_IN = new Interpolator() { @Override public float getInterpolation(float v) { return DEACCEL_3.getInterpolation(1 - ZOOM_OUT.getInterpolation(1 - v)); } }; public static final Interpolator ZOOM_OUT = new Interpolator() { private static final float FOCAL_LENGTH = 0.35f; @Override public float getInterpolation(float v) { return zInterpolate(v); } /** * This interpolator emulates the rate at which the perceived scale of an object changes * as its distance from a camera increases. When this interpolator is applied to a scale * animation on a view, it evokes the sense that the object is shrinking due to moving away * from the camera. */ private float zInterpolate(float input) { return (1.0f - FOCAL_LENGTH / (FOCAL_LENGTH + input)) / (1.0f - FOCAL_LENGTH / (FOCAL_LENGTH + 1.0f)); } }; public static final Interpolator SCROLL = new Interpolator() { @Override public float getInterpolation(float t) { t -= 1.0f; return t*t*t*t*t + 1; } }; public static final Interpolator SCROLL_CUBIC = new Interpolator() { @Override public float getInterpolation(float t) { t -= 1.0f; return t*t*t + 1; } }; private static final float FAST_FLING_PX_MS = 10; public static Interpolator scrollInterpolatorForVelocity(float velocity) { return Math.abs(velocity) > FAST_FLING_PX_MS ? SCROLL : SCROLL_CUBIC; } /** * Create an OvershootInterpolator with tension directly related to the velocity (in px/ms). * @param velocity The start velocity of the animation we want to overshoot. */ public static Interpolator overshootInterpolatorForVelocity(float velocity) { return new OvershootInterpolator(Math.min(Math.abs(velocity), 3f)); } /** * Runs the given interpolator such that the entire progress is set between the given bounds. * That is, we set the interpolation to 0 until lowerBound and reach 1 by upperBound. */ public static Interpolator clampToProgress(Interpolator interpolator, float lowerBound, float upperBound) { if (upperBound <= lowerBound) { throw new IllegalArgumentException(String.format( "lowerBound (%f) must be less than upperBound (%f)", lowerBound, upperBound)); } return t -> { if (t < lowerBound) { return 0; } if (t > upperBound) { return 1; } return interpolator.getInterpolation((t - lowerBound) / (upperBound - lowerBound)); }; } /** * Runs the given interpolator such that the interpolated value is mapped to the given range. * This is useful, for example, if we only use this interpolator for part of the animation, * such as to take over a user-controlled animation when they let go. */ public static Interpolator mapToProgress(Interpolator interpolator, float lowerBound, float upperBound) { return t -> mapRange(interpolator.getInterpolation(t), lowerBound, upperBound); } /** * Computes parameters necessary for an overshoot effect. */ private static int singleFrameMs = 1; public static class OvershootParams { public Interpolator interpolator; public float start; public float end; public long duration; /** * Given the input params, sets OvershootParams variables to be used by the caller. * @param startProgress The progress from 0 to 1 that the overshoot starts from. * @param overshootPastProgress The progress from 0 to 1 where we overshoot past (should * either be equal to startProgress or endProgress, depending on if we want to * overshoot immediately or only once we reach the end). * @param endProgress The final progress from 0 to 1 that we will settle to. * @param velocityPxPerMs The initial velocity that causes this overshoot. * @param totalDistancePx The distance against which progress is calculated. */ public OvershootParams(float startProgress, float overshootPastProgress, float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) { velocityPxPerMs = Math.abs(velocityPxPerMs); start = startProgress; int startPx = (int) (start * totalDistancePx); // Overshoot by about half a frame. float overshootBy = OVERSHOOT_FACTOR * velocityPxPerMs * singleFrameMs / totalDistancePx / 2; //getSingleFrameMs(context) overshootBy = boundToRange(overshootBy, 0.02f, 0.15f); end = overshootPastProgress + overshootBy; int endPx = (int) (end * totalDistancePx); int overshootDistance = endPx - startPx; // Calculate deceleration necessary to reach overshoot distance. // Formula: velocityFinal^2 = velocityInitial^2 + 2 * acceleration * distance // 0 = v^2 + 2ad (velocityFinal == 0) // a = v^2 / -2d float decelerationPxPerMs = velocityPxPerMs * velocityPxPerMs / (2 * overshootDistance); // Calculate time necessary to reach peak of overshoot. // Formula: acceleration = velocity / time // time = velocity / acceleration duration = (long) (velocityPxPerMs / decelerationPxPerMs); // Now that we're at the top of the overshoot, need to settle back to endProgress. float settleDistance = end - endProgress; int settleDistancePx = (int) (settleDistance * totalDistancePx); // Calculate time necessary for the settle. // Formula: distance = velocityInitial * time + 1/2 * acceleration * time^2 // d = 1/2at^2 (velocityInitial = 0, since we just stopped at the top) // t = sqrt(2d/a) // Above formula assumes constant acceleration. Since we use ACCEL_DEACCEL, we actually // have acceleration to halfway then deceleration the rest. So the formula becomes: // t = sqrt(d/a) * 2 (half the distance for accel, half for deaccel) long settleDuration = (long) Math.sqrt(settleDistancePx / decelerationPxPerMs) * 4; settleDuration = Math.max(MIN_SETTLE_DURATION, settleDuration); // How much of the animation to devote to playing the overshoot (the rest is for settle). float overshootFraction = (float) duration / (duration + settleDuration); duration += settleDuration; // Finally, create the interpolator, composed of two interpolators: an overshoot, which // reaches end > 1, and then a settle to endProgress. Interpolator overshoot = Interpolators.clampToProgress(DEACCEL, 0, overshootFraction); // The settle starts at 1, where 1 is the top of the overshoot, and maps to a fraction // such that final progress is endProgress. For example, if we overshot to 1.1 but want // to end at 1, we need to map to 1/1.1. Interpolator settle = Interpolators.clampToProgress(Interpolators.mapToProgress( ACCEL_DEACCEL, 1, (endProgress - start) / (end - start)), overshootFraction, 1); interpolator = t -> t <= overshootFraction ? overshoot.getInterpolation(t) : settle.getInterpolation(t); } } public static float mapRange(float value, float min, float max) { return min + (value * (max - min)); } public static float boundToRange(float value, float lowerBound, float upperBound) { return Math.max(lowerBound, Math.min(value, upperBound)); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/overscroller/launcher3/OverScroller.java ================================================ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.martinrgb.animer.component.overscroller.launcher3; import static com.martinrgb.animer.component.overscroller.launcher3.Interpolators.SCROLL; import android.animation.TimeInterpolator; import android.content.Context; import android.hardware.SensorManager; import android.util.Log; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; /** * Based on {@link android.widget.OverScroller} supporting only 1-d scrolling and with more * customization options. */ public class OverScroller { private int mMode; private final SplineOverScroller mScroller; private TimeInterpolator mInterpolator; private final boolean mFlywheel; private static final int DEFAULT_DURATION = 250; private static final int SCROLL_MODE = 0; private static final int FLING_MODE = 1; /** * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel. * @param context */ public OverScroller(Context context) { this(context, null); } /** * Creates an OverScroller with flywheel enabled. * @param context The context of this application. * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will * be used. */ public OverScroller(Context context, Interpolator interpolator) { this(context, interpolator, true); } /** * Creates an OverScroller. * @param context The context of this application. * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will * be used. * @param flywheel If true, successive fling motions will keep on increasing scroll speed. */ public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { if (interpolator == null) { mInterpolator = SCROLL; } else { mInterpolator = interpolator; } mFlywheel = flywheel; mScroller = new SplineOverScroller(context); } public void setInterpolator(TimeInterpolator interpolator) { if (interpolator == null) { mInterpolator = SCROLL; } else { mInterpolator = interpolator; } } /** * The amount of friction applied to flings. The default value * is {@link ViewConfiguration#getScrollFriction}. * * @param friction A scalar dimension-less value representing the coefficient of * friction. */ public final void setFriction(float friction) { mScroller.setFriction(friction); } /** * * Returns whether the scroller has finished scrolling. * * @return True if the scroller has finished scrolling, false otherwise. */ public final boolean isFinished() { return mScroller.mFinished; } /** * Force the finished field to a particular value. Contrary to * {@link #abortAnimation()}, forcing the animation to finished * does NOT cause the scroller to move to the final x and y * position. * * @param finished The new finished value. */ public final void forceFinished(boolean finished) { mScroller.mFinished = finished; } /** * Returns the current offset in the scroll. * * @return The new offset as an absolute distance from the origin. */ public final int getCurrPos() { return mScroller.mCurrentPosition; } /** * Returns the absolute value of the current velocity. * * @return The original velocity less the deceleration, norm of the X and Y velocity vector. */ public float getCurrVelocity() { return mScroller.mCurrVelocity; } /** * Returns the start offset in the scroll. * * @return The start offset as an absolute distance from the origin. */ public final int getStartPos() { return mScroller.mStart; } /** * Returns where the scroll will end. Valid only for "fling" scrolls. * * @return The final offset as an absolute distance from the origin. */ public final int getFinalPos() { return mScroller.mFinal; } /** * Returns how long the scroll event will take, in milliseconds. * * @return The duration of the scroll in milliseconds. */ public final int getDuration() { return mScroller.mDuration; } /** * Extend the scroll animation. This allows a running animation to scroll * further and longer, when used with {@link #setFinalPos(int)}. * * @param extend Additional time to scroll in milliseconds. * @see #setFinalPos(int) */ public void extendDuration(int extend) { mScroller.extendDuration(extend); } /** * Sets the final position for this scroller. * * @param newPos The new offset as an absolute distance from the origin. * @see #extendDuration(int) */ public void setFinalPos(int newPos) { mScroller.setFinalPosition(newPos); } /** * Call this when you want to know the new location. If it returns true, the * animation is not yet finished. */ public boolean computeScrollOffset() { if (isFinished()) { return false; } switch (mMode) { case SCROLL_MODE: if (isSpringing()) { return true; } long time = AnimationUtils.currentAnimationTimeMillis(); // Any scroller can be used for time, since they were started // together in scroll mode. We use X here. final long elapsedTime = time - mScroller.mStartTime; final int duration = mScroller.mDuration; if (elapsedTime < duration) { final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScroller.updateScroll(q); } else { abortAnimation(); } break; case FLING_MODE: if (!mScroller.mFinished) { if (!mScroller.update()) { if (!mScroller.continueWhenFinished()) { mScroller.finish(); } } } break; } return true; } /** * Start scrolling by providing a starting point and the distance to travel. * The scroll will use the default value of 250 milliseconds for the * duration. * * @param start Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param delta Distance to travel. Positive numbers will scroll the * content to the left. */ public void startScroll(int start, int delta) { startScroll(start, delta, DEFAULT_DURATION); } /** * Start scrolling by providing a starting point and the distance to travel. * * @param start Starting scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param delta Distance to travel. Positive numbers will scroll the * content to the left. * @param duration Duration of the scroll in milliseconds. */ public void startScroll(int start, int delta, int duration) { mMode = SCROLL_MODE; mScroller.startScroll(start, delta, duration); } /** * Start scrolling using a spring by providing a starting point and the distance to travel. * * @param start Starting scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param delta Distance to travel. Positive numbers will scroll the * content to the left. * @param duration Duration of the scroll in milliseconds. * @param velocity The starting velocity for the spring in px per ms. */ public void startScrollSpring(int start, int delta, int duration, float velocity) { mMode = SCROLL_MODE; mScroller.mState = mScroller.SPRING; mScroller.startScroll(start, delta, duration, velocity); } /** * Call this when you want to 'spring back' into a valid coordinate range. * * @param start Starting X coordinate * @param min Minimum valid X value * @param max Maximum valid X value * @return true if a springback was initiated, false if startX and startY were * already within the valid range. */ public boolean springBack(int start, int min, int max) { mMode = FLING_MODE; return mScroller.springback(start, min, max); } public void fling(int start, int velocity, int min, int max) { fling(start, velocity, min, max, 0); } /** * Start scrolling based on a fling gesture. The distance traveled will * depend on the initial velocity of the fling. * @param start Starting point of the scroll (X) * @param velocity Initial velocity of the fling (X) measured in pixels per * second. * @param min Minimum X value. The scroller will not scroll past this point * unless overX > 0. If overfling is allowed, it will use minX as * a springback boundary. * @param max Maximum X value. The scroller will not scroll past this point * unless overX > 0. If overfling is allowed, it will use maxX as * a springback boundary. * @param over Overfling range. If > 0, horizontal overfling in either * direction will be possible. */ public void fling(int start, int velocity, int min, int max, int over) { // Continue a scroll or fling in progress if (mFlywheel && !isFinished()) { float oldVelocityX = mScroller.mCurrVelocity; if (Math.signum(velocity) == Math.signum(oldVelocityX)) { velocity += oldVelocityX; } } mMode = FLING_MODE; mScroller.fling(start, velocity, min, max, over); } /** * Notify the scroller that we've reached a horizontal boundary. * Normally the information to handle this will already be known * when the animation is started, such as in a call to one of the * fling functions. However there are cases where this cannot be known * in advance. This function will transition the current motion and * animate from startX to finalX as appropriate. * @param start Starting/current X position * @param finalPos Desired final X position * @param over Magnitude of overscroll allowed. This should be the maximum */ public void notifyEdgeReached(int start, int finalPos, int over) { mScroller.notifyEdgeReached(start, finalPos, over); } /** * Returns whether the current Scroller is currently returning to a valid position. * Valid bounds were provided by the * {@link #fling(int, int, int, int, int)} method. * * One should check this value before calling * {@link #startScroll(int, int)} as the interpolation currently in progress * to restore a valid position will then be stopped. The caller has to take into account * the fact that the started scroll will start from an overscrolled position. * * @return true when the current position is overscrolled and in the process of * interpolating back to a valid value. */ public boolean isOverScrolled() { return (!mScroller.mFinished && mScroller.mState != SplineOverScroller.SPLINE); } /** * Stops the animation. Contrary to {@link #forceFinished(boolean)}, * aborting the animating causes the scroller to move to the final x and y * positions. * * @see #forceFinished(boolean) */ public void abortAnimation() { mScroller.finish(); } /** * Returns the time elapsed since the beginning of the scrolling. * * @return The elapsed time in milliseconds. * * @hide */ public int timePassed() { final long time = AnimationUtils.currentAnimationTimeMillis(); return (int) (time - mScroller.mStartTime); } public boolean isSpringing() { return mScroller.mState == SplineOverScroller.SPRING && !isFinished(); } static class SplineOverScroller { // Initial position private int mStart; // Current position private int mCurrentPosition; // Final position private int mFinal; // Initial velocity private int mVelocity; // Current velocity private float mCurrVelocity; // Constant current deceleration private float mDeceleration; // Animation starting time, in system milliseconds private long mStartTime; // Animation duration, in milliseconds private int mDuration; // Duration to complete spline component of animation private int mSplineDuration; // Distance to travel along spline animation private int mSplineDistance; // Whether the animation is currently in progress private boolean mFinished; // The allowed overshot distance before boundary is reached. private int mOver; // Fling friction private float mFlingFriction = ViewConfiguration.getScrollFriction(); // Current state of the animation. private int mState = SPLINE; private SpringAnimation mSpring; // Constant gravity value, used in the deceleration phase. private static final float GRAVITY = 2000.0f; // A context-specific coefficient adjusted to physical values. private float mPhysicalCoeff; private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) private static final float START_TENSION = 0.5f; private static final float END_TENSION = 1.0f; private static final float P1 = START_TENSION * INFLEXION; private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); private static final int NB_SAMPLES = 100; private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; private static final int SPLINE = 0; private static final int CUBIC = 1; private static final int BALLISTIC = 2; private static final int SPRING = 3; private static final FloatPropertyCompat SPRING_PROPERTY = new FloatPropertyCompat("splineOverScrollerSpring") { @Override public float getValue(SplineOverScroller scroller) { return scroller.mCurrentPosition; } @Override public void setValue(SplineOverScroller scroller, float value) { scroller.mCurrentPosition = (int) value; } }; static { float x_min = 0.0f; float y_min = 0.0f; for (int i = 0; i < NB_SAMPLES; i++) { final float alpha = (float) i / NB_SAMPLES; float x_max = 1.0f; float x, tx, coef; while (true) { x = x_min + (x_max - x_min) / 2.0f; coef = 3.0f * x * (1.0f - x); tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; if (Math.abs(tx - alpha) < 1E-5) break; if (tx > alpha) x_max = x; else x_min = x; } SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; float y_max = 1.0f; float y, dy; while (true) { y = y_min + (y_max - y_min) / 2.0f; coef = 3.0f * y * (1.0f - y); dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; if (Math.abs(dy - alpha) < 1E-5) break; if (dy > alpha) y_max = y; else y_min = y; } SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; } SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; } void setFriction(float friction) { mFlingFriction = friction; } SplineOverScroller(Context context) { mFinished = true; final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) * 39.37f // inch/meter * ppi * 0.84f; // look and feel tuning } void updateScroll(float q) { if (mState == SPRING) { return; } mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); } /* * Get a signed deceleration that will reduce the velocity. */ static private float getDeceleration(int velocity) { return velocity > 0 ? -GRAVITY : GRAVITY; } /* * Modifies mDuration to the duration it takes to get from start to newFinal using the * spline interpolation. The previous duration was needed to get to oldFinal. */ private void adjustDuration(int start, int oldFinal, int newFinal) { final int oldDistance = oldFinal - start; final int newDistance = newFinal - start; final float x = Math.abs((float) newDistance / oldDistance); final int index = (int) (NB_SAMPLES * x); if (index < NB_SAMPLES) { final float x_inf = (float) index / NB_SAMPLES; final float x_sup = (float) (index + 1) / NB_SAMPLES; final float t_inf = SPLINE_TIME[index]; final float t_sup = SPLINE_TIME[index + 1]; final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); mDuration *= timeCoef; } } void startScroll(int start, int distance, int duration) { startScroll(start, distance, duration, 0); } void startScroll(int start, int distance, int duration, float velocity) { mFinished = false; mCurrentPosition = mStart = start; mFinal = start + distance; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = duration; if (mState == SPRING) { if (mSpring != null) { mSpring.cancel(); } mSpring = new SpringAnimation(this, SPRING_PROPERTY); mSpring.setSpring(new SpringForce(mFinal) .setStiffness(SpringForce.STIFFNESS_LOW) .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); mSpring.setStartVelocity(velocity); mSpring.animateToFinalPosition(mFinal); mSpring.addEndListener((animation, canceled, value, velocity1) -> { finish(); mState = SPLINE; mSpring = null; }); } // Unused mDeceleration = 0.0f; mVelocity = 0; } void finish() { if (mSpring != null && mSpring.isRunning()) mSpring.cancel(); mCurrentPosition = mFinal; // Not reset since WebView relies on this value for fast fling. // TODO: restore when WebView uses the fast fling implemented in this class. // mCurrVelocity = 0.0f; mFinished = true; } void setFinalPosition(int position) { mFinal = position; if (mState == SPRING && mSpring != null) { mSpring.animateToFinalPosition(mFinal); } mSplineDistance = mFinal - mStart; mFinished = false; } void extendDuration(int extend) { final long time = AnimationUtils.currentAnimationTimeMillis(); final int elapsedTime = (int) (time - mStartTime); mDuration = mSplineDuration = elapsedTime + extend; mFinished = false; } boolean springback(int start, int min, int max) { mFinished = true; mCurrentPosition = mStart = mFinal = start; mVelocity = 0; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = 0; if (start < min) { startSpringback(start, min, 0); } else if (start > max) { startSpringback(start, max, 0); } return !mFinished; } private void startSpringback(int start, int end, int velocity) { // mStartTime has been set mFinished = false; mState = CUBIC; mCurrentPosition = mStart = start; mFinal = end; final int delta = start - end; mDeceleration = getDeceleration(delta); // TODO take velocity into account mVelocity = -delta; // only sign is used mOver = Math.abs(delta); mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); } void fling(int start, int velocity, int min, int max, int over) { mOver = over; mFinished = false; mCurrVelocity = mVelocity = velocity; mDuration = mSplineDuration = 0; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mCurrentPosition = mStart = start; if (start > max || start < min) { startAfterEdge(start, min, max, velocity); return; } mState = SPLINE; double totalDistance = 0.0; if (velocity != 0) { mDuration = mSplineDuration = getSplineFlingDuration(velocity); totalDistance = getSplineFlingDistance(velocity); } mSplineDistance = (int) (totalDistance * Math.signum(velocity)); mFinal = start + mSplineDistance; // Clamp to a valid final position if (mFinal < min) { adjustDuration(mStart, mFinal, min); mFinal = min; } if (mFinal > max) { adjustDuration(mStart, mFinal, max); mFinal = max; } } private double getSplineDeceleration(int velocity) { return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff)); } private double getSplineFlingDistance(int velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); } /* Returns the duration, expressed in milliseconds */ private int getSplineFlingDuration(int velocity) { final double l = getSplineDeceleration(velocity); final double decelMinusOne = DECELERATION_RATE - 1.0; return (int) (1000.0 * Math.exp(l / decelMinusOne)); } private void fitOnBounceCurve(int start, int end, int velocity) { // Simulate a bounce that started from edge final float durationToApex = - velocity / mDeceleration; // The float cast below is necessary to avoid integer overflow. final float velocitySquared = (float) velocity * velocity; final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration); final float distanceToEdge = Math.abs(end - start); final float totalDuration = (float) Math.sqrt( 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); mCurrentPosition = mStart = end; mVelocity = (int) (- mDeceleration * totalDuration); } private void startBounceAfterEdge(int start, int end, int velocity) { mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); fitOnBounceCurve(start, end, velocity); onEdgeReached(); } private void startAfterEdge(int start, int min, int max, int velocity) { if (start > min && start < max) { Log.e("OverScroller", "startAfterEdge called from a valid position"); mFinished = true; return; } final boolean positive = start > max; final int edge = positive ? max : min; final int overDistance = start - edge; boolean keepIncreasing = overDistance * velocity >= 0; if (keepIncreasing) { // Will result in a bounce or a to_boundary depending on velocity. startBounceAfterEdge(start, edge, velocity); } else { final double totalDistance = getSplineFlingDistance(velocity); if (totalDistance > Math.abs(overDistance)) { fling(start, velocity, positive ? min : start, positive ? start : max, mOver); } else { startSpringback(start, edge, velocity); } } } void notifyEdgeReached(int start, int end, int over) { // mState is used to detect successive notifications if (mState == SPLINE) { mOver = over; mStartTime = AnimationUtils.currentAnimationTimeMillis(); // We were in fling/scroll mode before: current velocity is such that distance to // edge is increasing. This ensures that startAfterEdge will not start a new fling. startAfterEdge(start, end, end, (int) mCurrVelocity); } } private void onEdgeReached() { // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. // The float cast below is necessary to avoid integer overflow. final float velocitySquared = (float) mVelocity * mVelocity; float distance = velocitySquared / (2.0f * Math.abs(mDeceleration)); final float sign = Math.signum(mVelocity); if (distance > mOver) { // Default deceleration is not sufficient to slow us down before boundary mDeceleration = - sign * velocitySquared / (2.0f * mOver); distance = mOver; } mOver = (int) distance; mState = BALLISTIC; mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); mDuration = - (int) (1000.0f * mVelocity / mDeceleration); } boolean continueWhenFinished() { switch (mState) { case SPLINE: // Duration from start to null velocity if (mDuration < mSplineDuration) { // If the animation was clamped, we reached the edge mCurrentPosition = mStart = mFinal; // TODO Better compute speed when edge was reached mVelocity = (int) mCurrVelocity; mDeceleration = getDeceleration(mVelocity); mStartTime += mDuration; onEdgeReached(); } else { // Normal stop, no need to continue return false; } break; case BALLISTIC: mStartTime += mDuration; startSpringback(mFinal, mStart, 0); break; case CUBIC: return false; } update(); return true; } /* * Update the current position and velocity for current time. Returns * true if update has been done and false if animation duration has been * reached. */ boolean update() { if (mState == SPRING) { return mFinished; } final long time = AnimationUtils.currentAnimationTimeMillis(); final long currentTime = time - mStartTime; if (currentTime == 0) { // Skip work but report that we're still going if we have a nonzero duration. return mDuration > 0; } if (currentTime > mDuration) { return false; } double distance = 0.0; switch (mState) { case SPLINE: { final float t = (float) currentTime / mSplineDuration; final int index = (int) (NB_SAMPLES * t); float distanceCoef = 1.f; float velocityCoef = 0.f; if (index < NB_SAMPLES) { final float t_inf = (float) index / NB_SAMPLES; final float t_sup = (float) (index + 1) / NB_SAMPLES; final float d_inf = SPLINE_POSITION[index]; final float d_sup = SPLINE_POSITION[index + 1]; velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); distanceCoef = d_inf + (t - t_inf) * velocityCoef; } distance = distanceCoef * mSplineDistance; mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; break; } case BALLISTIC: { final float t = currentTime / 1000.0f; mCurrVelocity = mVelocity + mDeceleration * t; distance = mVelocity * t + mDeceleration * t * t / 2.0f; break; } case CUBIC: { final float t = (float) (currentTime) / mDuration; final float t2 = t * t; final float sign = Math.signum(mVelocity); distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); mCurrVelocity = sign * mOver * 6.0f * (- t + t2); break; } } mCurrentPosition = mStart + (int) Math.round(distance); return true; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/overscrolllayout/.gitkeep ================================================ ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/recyclerview/AnRecyclerView.java ================================================ package com.martinrgb.animer.component.recyclerview; import android.content.Context; import android.content.res.Resources; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import com.martinrgb.animer.Animer; public class AnRecyclerView extends RecyclerView { private Context mContext; private RecyclerView mRecyclerView; private RecyclerView.Adapter mAdapter; private LayoutManager mLayoutManager; private boolean mIsRootOnTouch = false; private int mTouchSlop,mMinFlingVelocity; private Animer mFlingAnimer, mSpringAnimer; private boolean mShouldFling = true,mShouldSpringBack = true; private float mRootCurrentTransValue,mDragStartValue,mCurrentVelocity,mPrevFrameValue,mFlingStartVelocity,mFlingCurrentVelocity; private VelocityTracker mRootVelocityTracker; public AnRecyclerView(Context context) { super(context); init(context, null); } public AnRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context, attrs); } public AnRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } @Override public void setAdapter(RecyclerView.Adapter adapter) { mAdapter = adapter; super.setAdapter(adapter); } private static final int LINEAR_LAYOUT = 0; private static final int GRID_LAYOUT = 1; private static final int STAGGERED_LAYOUT=2; private int LAYOUT_MODE = 0; @Override public void setLayoutManager(LayoutManager layout) { if(layout instanceof LinearLayoutManager){ LAYOUT_MODE = LINEAR_LAYOUT; } if(layout instanceof GridLayoutManager){ LAYOUT_MODE = GRID_LAYOUT; } if(layout instanceof StaggeredGridLayoutManager){ LAYOUT_MODE = STAGGERED_LAYOUT; } mLayoutManager = layout; super.setLayoutManager(layout); } @Override public void scrollToPosition(int position) { super.scrollToPosition(position); } @Override public void smoothScrollToPosition(int position) { super.smoothScrollToPosition(position); } private void init(Context context, AttributeSet attributeSet) { // Custom Attribute in XML // if (context != null && attributeSet != null) { // TypedArray a = context.getTheme().obtainStyledAttributes( // attributeSet, R.styleable.RecyclerViewBouncy, // 0, 0 // ); // } setOverScrollMode(OVER_SCROLL_NEVER); mContext = context; mRecyclerView = this; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); scrollToPosition(0); initOnScrollListener(); initTouchListener(); initAnimer(); } private void initAnimer(){ mFlingAnimer = new Animer(); mFlingAnimer.setSolver(Animer.flingDroid(0,0.5f)); // velocity is 1000ms velocity/60 is frame velocity; mFlingAnimer.setMinimumVisibleChange(1f); mFlingAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { if(directionVertical()){ if(mRecyclerView.getTranslationY() < 0){ mRecyclerView.setTranslationY(Math.max(0,mRecyclerView.getTranslationY() - velocity/(1000/16))); } else if(mRecyclerView.getTranslationY() > 0){ mRecyclerView.setTranslationY(Math.min(0,mRecyclerView.getTranslationY() - velocity/(1000/16))); } else { mRecyclerView.scrollBy( 0, (int) velocity/(1000/16)); } } if(!directionVertical()) { if(mRecyclerView.getTranslationX() < 0){ mRecyclerView.setTranslationX(Math.max(0,mRecyclerView.getTranslationX() - velocity/(1000/16))); } else if(mRecyclerView.getTranslationX() > 0){ mRecyclerView.setTranslationX(Math.min(0,mRecyclerView.getTranslationX() - velocity/(1000/16))); } else { mRecyclerView.scrollBy((int) velocity/(1000/16), 0); } } mFlingCurrentVelocity = velocity; } }); mSpringAnimer = new Animer(); mSpringAnimer.setSolver(Animer.springDroid(150,0.99f)); mSpringAnimer.setMinimumVisibleChange(1f); mSpringAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { if(directionVertical()){ mRecyclerView.setTranslationY(value); } else { mRecyclerView.setTranslationX(value); } } }); } private void initOnScrollListener() { mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { // top springback if(directionVertical() && !recyclerView.canScrollVertically(-1)){ if(mShouldFling){ mSpringAnimer.setVelocity(-mFlingCurrentVelocity); mFlingAnimer.cancel(); mSpringAnimer.setEndValue(0); mShouldFling = false; } } // bottom springBack if(directionVertical() && !recyclerView.canScrollVertically(1)){ if(mShouldFling){ mSpringAnimer.setVelocity(-mFlingCurrentVelocity); mFlingAnimer.cancel(); mSpringAnimer.setEndValue(0); mShouldFling = false; } } // left springBack if(!directionVertical() && !recyclerView.canScrollHorizontally(-1)){ if(mShouldFling){ mSpringAnimer.setVelocity(-mFlingCurrentVelocity); mFlingAnimer.cancel(); mSpringAnimer.setEndValue(0); mShouldFling = false; } } // right springBack if(!directionVertical() && !recyclerView.canScrollHorizontally(1)){ if(mShouldFling){ mSpringAnimer.setVelocity(-mFlingCurrentVelocity); mFlingAnimer.cancel(); mSpringAnimer.setEndValue(0); mShouldFling = false; } } } }); } private void initTouchListener() { mRecyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() { @Override public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { switch (e.getAction()) { case MotionEvent.ACTION_DOWN: // add vt if (mRootVelocityTracker == null) { mRootVelocityTracker = VelocityTracker.obtain(); } else { mRootVelocityTracker.clear();} mRootVelocityTracker.addMovement(e); // reset val mDragStartValue = (directionVertical())?e.getRawY():e.getRawX(); mPrevFrameValue = (directionVertical())?e.getRawY():e.getRawX(); mCurrentVelocity = 0; mFlingStartVelocity = 0; mRootCurrentTransValue = (directionVertical())?mRecyclerView.getTranslationY():mRecyclerView.getTranslationX(); rv.stopScroll(); // Method-I ,but when spring is not easy to click, // detect is interact on root or item // if(!mFlingAnimer.isRunning() && !mSpringAnimer.isRunning()){ // Log.e("isNotRunning","isNotRunning"); // mIsRootOnTouch = false; // } // // if(mFlingAnimer.isRunning() || mSpringAnimer.isRunning()){ // Log.e("isRunning","is Running"); // mFlingAnimer.cancel(); // mSpringAnimer.cancel(); // mIsRootOnTouch = true; // } // Method-II if(!mFlingAnimer.isRunning()){ mIsRootOnTouch = false; } if(mFlingAnimer.isRunning()){ mFlingAnimer.cancel(); mIsRootOnTouch = true; } if(mSpringAnimer.isRunning()){ mSpringAnimer.cancel(); // disable this for rapid click when spring is running //mIsRootOnTouch = true; } break; case MotionEvent.ACTION_MOVE: float mCurrentVal = ((directionVertical())?e.getRawY():e.getRawX()); float mAbsTransValue = Math.abs( mCurrentVal - mPrevFrameValue ); // if TouchMove bigger than slop,group action // Method -I - mMinFlingVelocity , Method - II - mTouchSlop if( mAbsTransValue > mTouchSlop){ mIsRootOnTouch = true; } // if TouchMove smaller than slop,group action else { mIsRootOnTouch = false; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // OverRangeScroll -> SpringBack mSpringAnimer.setFrom((directionVertical())?mRecyclerView.getTranslationY():mRecyclerView.getTranslationX()); mSpringAnimer.setEndValue(0); break; } return mIsRootOnTouch; } @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { mIsRootOnTouch = false; switch (e.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: float dragValue = overDragFucntion(((directionVertical())?e.getRawY():e.getRawX()) - mDragStartValue); float mTransValue = mRootCurrentTransValue + dragValue; mRootVelocityTracker.addMovement(e); mRootVelocityTracker.computeCurrentVelocity(1000); mFlingStartVelocity = (directionVertical())? mRootVelocityTracker.getYVelocity(): mRootVelocityTracker.getXVelocity(); // top overscroll if(directionVertical() && mTransValue > 0 && !rv.canScrollVertically(-1)){ mRecyclerView.setTranslationY(mTransValue);//3 mShouldSpringBack = (mFlingStartVelocity >= 0)? true:false; } // bottom overscroll else if(directionVertical() && mTransValue < 0 && !rv.canScrollVertically(1)){ mRecyclerView.setTranslationY(mTransValue); //3 mShouldSpringBack = (mFlingStartVelocity <= 0)? true:false; } // left overscroll else if(!directionVertical() && mTransValue > 0 && !rv.canScrollHorizontally(-1)){ mRecyclerView.setTranslationX(mTransValue); //3 mShouldSpringBack = (mFlingStartVelocity >= 0)? true:false; } // right overscroll else if(!directionVertical() && mTransValue < 0 && !rv.canScrollHorizontally(1)){ mRecyclerView.setTranslationX(mTransValue); //3 mShouldSpringBack = (mFlingStartVelocity <= 0)? true:false; } // normal scroll else{ scrollBy((int)-mCurrentVelocity); mShouldSpringBack = false; } mCurrentVelocity = ((directionVertical())?e.getRawY():e.getRawX()) - mPrevFrameValue; mPrevFrameValue = ((directionVertical())?e.getRawY():e.getRawX()); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // OverRangeScroll -> SpringBack if(mShouldSpringBack){ mSpringAnimer.setFrom((directionVertical())?mRecyclerView.getTranslationY():mRecyclerView.getTranslationX()); mSpringAnimer.setEndValue(0); } // Scroll -> Fling else { // if velocity greter than Min Fling Vel,then Fling if(Math.abs(mFlingStartVelocity) > mMinFlingVelocity){ // Method-II if(mSpringAnimer.isRunning()){ mSpringAnimer.cancel(); } mShouldFling = true; mFlingAnimer.setArgument1(-mFlingStartVelocity); mFlingAnimer.start(); } // otherwise SpringBack else { mSpringAnimer.setFrom((directionVertical()) ? mRecyclerView.getTranslationY() : mRecyclerView.getTranslationX()); mSpringAnimer.setEndValue(0); } } break; } } }); } private float overDragFucntion(float value){ return value/3; } private void scrollBy(int dist) { if (directionVertical()) { mRecyclerView.scrollBy(0, dist); } else { mRecyclerView.scrollBy(dist, 0); } } private boolean directionVertical() { switch (LAYOUT_MODE){ case LINEAR_LAYOUT: return ((LinearLayoutManager)mLayoutManager).getOrientation() == RecyclerView.VERTICAL; case GRID_LAYOUT: return ((GridLayoutManager)mLayoutManager).getOrientation() == RecyclerView.VERTICAL; case STAGGERED_LAYOUT: return ((StaggeredGridLayoutManager)mLayoutManager).getOrientation() == RecyclerView.VERTICAL; } return true; } private double dpToPx(double dp) { Resources resources = mContext.getResources(); DisplayMetrics metrics = resources.getDisplayMetrics(); return dp * ((double) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/scrollview/AnOverScroller.java ================================================ package com.martinrgb.animer.component.scrollview; import android.content.Context; import android.util.Log; import android.view.animation.Interpolator; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FlingAnimation; import androidx.dynamicanimation.animation.FloatValueHolder; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.martinrgb.animer.Animer; import com.martinrgb.animer.core.math.calculator.FlingCalculator; public class AnOverScroller { private FlingAnimation flingAnimation; private SpringAnimation springAnimation; private Animer flingAnimer,springAnimer,flingSpringAnimer,scrollAnimer; private FloatValueHolder scrollValue,scrollSpeed; private boolean isDyanmicFling = false,isVertScroll = true,isFixedScroll = false; private float fixedCellWidth = 0; private final Animer.AnimerSolver defaultSpring = Animer.springDroid(150,0.99f); private final Animer.AnimerSolver defaultFling = Animer.flingDroid(4000,0.8f); private final Animer.AnimerSolver springAsFling = Animer.springDroid(50,0.99f); private boolean isFling = true; private boolean isSpringBack = true; private boolean isAnimerDriven = true; // ############################################ // Constructor // ############################################ public AnOverScroller(Context context) { this(context, null); } public AnOverScroller(Context context, Interpolator interpolator) {this(context, interpolator, true);} public AnOverScroller(Context context, Interpolator interpolator,float bounceCoefficientX, float bounceCoefficientY) {this(context, interpolator, true); } public AnOverScroller(Context context, Interpolator interpolator, float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) { this(context, interpolator, flywheel); } public AnOverScroller(Context context, Interpolator interpolator, boolean flywheel) { if(!isAnimerDriven()){ springAnimer = new Animer(); springAnimer.setSolver(defaultSpring); flingAnimer = new Animer(); flingAnimer.setSolver(defaultFling); flingSpringAnimer = new Animer(); flingSpringAnimer.setSolver(springAsFling); scrollValue = new FloatValueHolder(); scrollSpeed = new FloatValueHolder(); scrollValue.setValue(0); flingAnimation = new FlingAnimation(scrollValue); flingAnimation.setFriction((float)flingAnimer.getArgument2()); flingAnimation.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() { @Override public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) { scrollSpeed.setValue(velocity); } }); springAnimation = new SpringAnimation(scrollValue); springAnimation.setSpring(new SpringForce()); springAnimation.getSpring().setStiffness((float)springAnimer.getArgument1()); springAnimation.getSpring().setDampingRatio((float)springAnimer.getArgument2()); springAnimation.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() { @Override public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) { if(isSpringBack()){ setSpringBack(false); } scrollSpeed.setValue(velocity); } }); springAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { @Override public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, float velocity) { setSpringBack(true); scrollSpeed.setValue(0); } }); } if(isAnimerDriven()){ scrollValue = new FloatValueHolder(); scrollSpeed = new FloatValueHolder(); scrollValue.setValue(0); springAnimer = new Animer(); springAnimer.setSolver(defaultSpring); flingSpringAnimer = new Animer(); flingSpringAnimer.setSolver(springAsFling); flingAnimer = new Animer(); flingAnimer.setSolver(defaultFling); scrollAnimer = new Animer(scrollValue.getValue()); scrollAnimer.setArgument1(springAnimer.getArgument1()); scrollAnimer.setArgument2(springAnimer.getArgument2()); scrollAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { if(isSpringBack()){ setSpringBack(false); } scrollValue.setValue(value); scrollSpeed.setValue(velocity); } }); scrollAnimer.setEndListener(new Animer.EndListener() { @Override public void onEnd(float value, float velocity, boolean canceled) { setSpringBack(true); scrollValue.setValue(value); scrollSpeed.setValue(0); } }); } } // ############################################ // Value Getter // ############################################ public final int getCurrX() { return (isVertScroll())?0:Math.round(scrollValue.getValue()); } public final int getCurrY() { return (isVertScroll())?Math.round(scrollValue.getValue()):0; } public float getCurrVelocityY() { return (isVertScroll())?scrollSpeed.getValue():0; } public float getCurrVelocityX() { return (isVertScroll())?0:scrollSpeed.getValue(); } public void startScroll(int startX, int startY, int dx, int dy) { scrollValue.setValue((isVertScroll())?startY:startX); } public boolean computeScrollOffset() { return (!isAnimerDriven())?(flingAnimation.isRunning() || springAnimation.isRunning()):scrollAnimer.isRunning(); } public final boolean isFinished() { return (!isAnimerDriven())?!(flingAnimation.isRunning() || springAnimation.isRunning()):!scrollAnimer.isRunning(); } public void abortAnimation() { if(!isAnimerDriven()){ springAnimation.cancel(); flingAnimation.cancel(); } if(isAnimerDriven()) { scrollAnimer.cancel(); } } // ############################################ // SpringBack Functions // ############################################ public boolean springBack(int startX, int startY, int velocityX,int velocityY,int minX, int maxX, int minY, int maxY) { float start = (isVertScroll())?startY:startX; float min = (isVertScroll())?minY:minX; float max = (isVertScroll())?maxY:maxX; if (start > max || start < min) { if (!isAnimerDriven() && !isFixedScroll()) { if (isFling()) { flingAnimation.cancel(); setFling(false); } if (isSpringBack()) { springFunctions(start, min, max); } } if (!isAnimerDriven() && isFixedScroll()) { if (isFling()) { float tempSpeed = scrollSpeed.getValue(); springAnimation.cancel(); scrollSpeed.setValue(tempSpeed); setFling(false); } if (isSpringBack()) { springFunctions(start, min, max); } } if (isAnimerDriven() && !isFixedScroll()) { if (isFling()) { float tempSpeed = scrollSpeed.getValue(); scrollAnimer.cancel(); scrollSpeed.setValue(tempSpeed); setFling(false); } if (isSpringBack()) { springFunctions(start, min, max); } } if (isAnimerDriven() && isFixedScroll()) { if (isFling()) { float tempSpeed = scrollSpeed.getValue(); scrollAnimer.cancel(); scrollSpeed.setValue(tempSpeed); setFling(false); } if (isSpringBack()) { springFunctions(start, min, max); } } return true; } return true; } private void springFunctions(float val,float min,float max){ if(!isAnimerDriven()){ scrollValue.setValue(val); springAnimation.setStartValue(val); springAnimation.setStartVelocity(scrollSpeed.getValue()); springAnimation.getSpring().setFinalPosition(((val > max))?max:min); springAnimation.getSpring().setStiffness((float)springAnimer.getArgument1()); springAnimation.getSpring().setDampingRatio((float)springAnimer.getArgument2()); springAnimation.start(); }else { scrollAnimer.setSolver(springAnimer.getCurrentSolver()); // setFrom - setCurrenetPhysicsValue(); // setVelocity - setCurrenetPhysicsVelocity(); // start() - setStartValue(getCurrentPhysicsValue())|setStartVelocity(getCurrentPhysicsVelocity()) scrollAnimer.setFrom(val); scrollAnimer.setTo(((val > max))?max:min); scrollAnimer.setVelocity(scrollSpeed.getValue()); scrollAnimer.start(); } } // ############################################ // Fling Functions // ############################################ public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) { setFling(true); setSpringBack(true); float startVelocity = (isVertScroll())?velocityY:velocityX; float startValue = (isVertScroll())?startY:startX; if (!isAnimerDriven() && !isFixedScroll()) { scrollValue.setValue(startValue); flingAnimation.setStartVelocity(startVelocity); if (isDynamicFlingFriction()) { float dynamicDamping = (isVertScroll()) ? (float) mapValueFromRangeToRange(Math.abs(velocityY), 0, 24000, 1.35, 0.5) : (float) mapValueFromRangeToRange(Math.abs(velocityX), 0, 24000, 1.35, 0.5); flingAnimation.setFriction(dynamicDamping); } else { flingAnimation.setFriction((float) flingAnimer.getArgument2()); } flingAnimation.start(); } if (!isAnimerDriven() && isFixedScroll()) { FlingCalculator flingCalculator = new FlingCalculator(startVelocity, (float) flingAnimer.getArgument2()); float flingTransition = flingCalculator.getTransiton(); springAnimation.setStartVelocity(startVelocity); scrollValue.setValue(startValue); springAnimation.setStartValue(startValue); float roundValue = Math.round(((startValue + flingTransition) / fixedCellWidth)) * fixedCellWidth; springAnimation.getSpring().setFinalPosition(roundValue); springAnimation.getSpring().setStiffness((float) flingSpringAnimer.getArgument1()); springAnimation.getSpring().setDampingRatio((float) flingSpringAnimer.getArgument2()); springAnimation.start(); } //TODO if (isAnimerDriven() && !isFixedScroll()) { scrollAnimer.setSolver(flingAnimer.getCurrentSolver()); scrollAnimer.setFrom(startValue); scrollAnimer.setVelocity(startVelocity); scrollAnimer.setArgument1((float)startVelocity); if(isDynamicFlingFriction()){ float dynamicDamping = (isVertScroll()) ? (float) mapValueFromRangeToRange(Math.abs(velocityY), 0, 24000, 1.35, 0.5) : (float) mapValueFromRangeToRange(Math.abs(velocityX), 0, 24000, 1.35, 0.5);; scrollAnimer.setArgument2((float)dynamicDamping); } else { //flingAnimation.setFriction((float)flingAnimer.getArgument2()); } scrollAnimer.start(); } //TODO if (isAnimerDriven() && isFixedScroll()) { scrollAnimer.cancel(); scrollAnimer.setSolver(flingSpringAnimer.getCurrentSolver()); FlingCalculator flingCalculator = new FlingCalculator(startVelocity,(float)flingAnimer.getArgument2()); float flingTransition = flingCalculator.getTransiton(); float roundValue = Math.round(((startValue + flingTransition)/fixedCellWidth))*fixedCellWidth; scrollAnimer.setFrom(startValue); scrollAnimer.setTo(roundValue); scrollAnimer.setVelocity(startVelocity); scrollAnimer.start(); } } // ############################################ // Utils // ############################################ private boolean isAnimerDriven(){ return isAnimerDriven; } private boolean isFling(){ return isFling; } private void setFling(boolean boo){ isFling = boo; } private boolean isSpringBack() { return isSpringBack; } private void setSpringBack(boolean boo) { isSpringBack = boo; } private static double mapValueFromRangeToRange(double value,double fromLow, double fromHigh, double toLow, double toHigh) { double fromRangeSize = fromHigh - fromLow; double toRangeSize = toHigh - toLow; double valueScale = (value - fromLow) / fromRangeSize; return toLow + (valueScale * toRangeSize); } public void setDynamicFlingFriction(boolean dynamicDampingState){ isDyanmicFling = dynamicDampingState; } public boolean isDynamicFlingFriction(){ return isDyanmicFling; } public Animer getSpringAnimer(){ return springAnimer; } public Animer getFlingAnimer(){ return flingAnimer; } public Animer getFakeFlingAnimer(){ return flingSpringAnimer; } public void setVertScroll(boolean isVertical) { isVertScroll = isVertical; } public boolean isVertScroll(){ return isVertScroll; } public void setFixedScroll(boolean fixedScroll,float cellWidth){ isFixedScroll = fixedScroll; fixedCellWidth = cellWidth; } private boolean isFixedScroll(){ return isFixedScroll; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/component/scrollview/AnScrollView.java ================================================ package com.martinrgb.animer.component.scrollview; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import com.martinrgb.animer.Animer; import java.util.List; public class AnScrollView extends FrameLayout { static final int ANIMATED_SCROLL_GAP = 500; static final float MAX_SCROLL_FACTOR = 0.5f; // clampedY 回弹 init is 0.25f; // 0.4 static final float FOLLOW_HAND_FACTOR = 0.25f; //0.25f private static final String TAG = "ScrollView"; private long mLastScroll; private final Rect mTempRect = new Rect(); public AnOverScroller mScroller; /** * Position of the last motion event. */ private int mLastMotionY; private int mLastMotionX; /** * True when the layout has changed but the traversal has not come through yet. * Ideally the view hierarchy would keep track of this for us. */ private boolean mIsLayoutDirty = true; /** * The child to give focus to in the event that a child has requested focus while the * layout is dirty. This prevents the scroll from being wrong if the child has not been * laid out before requesting focus. */ private View mChildToScrollTo = null; /** * True if the user is currently dragging this ScrollView around. This is * not the same as 'is being flinged', which can be checked by * mScroller.isFinished() (flinging begins when the user lifts his finger). */ private boolean mIsBeingDragged = false; /** * Determines speed during touch scrolling */ private VelocityTracker mVelocityTracker; /** * When set to true, the scroll view measure its child to make it fill the currently * visible area. */ @ViewDebug.ExportedProperty(category = "layout") private boolean mFillViewport; /** * Whether arrow scrolling is animated. */ private boolean mSmoothScrollingEnabled = true; private int mTouchSlop; private int mMinimumVelocity; private int mMaximumVelocity; private int mOverscrollDistance; private int mOverflingDistance; /** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */ private int mActivePointerId = INVALID_POINTER; /** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */ private static final int INVALID_POINTER = -1; private SavedState mSavedState; public AnScrollView(Context context) { this(context, null); } public AnScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); //TODO: com.android.internal.R.attr.scrollViewStyle); } public AnScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initScrollView(); } @Override public int getOverScrollMode() { return OVER_SCROLL_ALWAYS; } @Override public boolean shouldDelayChildPressedState() { return true; } @Override protected float getTopFadingEdgeStrength() { return 0; } @Override protected float getBottomFadingEdgeStrength() { return 0; } /** * @return The maximum amount this scroll view will scroll in response to * an arrow event. */ public int getMaxScrollAmount() { return (int) (MAX_SCROLL_FACTOR * (getBottom() - getTop())); } private void initScrollView() { mScroller = new AnOverScroller(getContext()); //mScroller.setVertScroll(isVertScroll()); setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mOverscrollDistance = configuration.getScaledOverscrollDistance(); mOverflingDistance = configuration.getScaledOverflingDistance(); } @Override public void addView(View child) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child); } @Override public void addView(View child, int index) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, index); } @Override public void addView(View child, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, params); } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (getChildCount() > 0) { throw new IllegalStateException("ScrollView can host only one direct child"); } super.addView(child, index, params); } /** * @return Returns true this ScrollView can be scrolled */ private boolean canScroll() { View child = getChildAt(0); if (child != null) { if(isVertScroll()){ int childHeight = child.getHeight(); return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); } else{ int childWidth = child.getWidth(); return getWidth() < childWidth + getPaddingLeft() + getPaddingRight(); } } return false; } /** * Indicates whether this ScrollView's content is stretched to fill the viewport. * * @return True if the content fills the viewport, false otherwise. * * @attr ref android.R.styleable#ScrollView_fillViewport */ public boolean isFillViewport() { return mFillViewport; } /** * Indicates this ScrollView whether it should stretch its content height to fill * the viewport or not. * * @param fillViewport True to stretch the content's height to the viewport's * boundaries, false otherwise. * * @attr ref android.R.styleable#ScrollView_fillViewport */ public void setFillViewport(boolean fillViewport) { if (fillViewport != mFillViewport) { mFillViewport = fillViewport; requestLayout(); } } /** * @return Whether arrow scrolling will animate its transition. */ public boolean isSmoothScrollingEnabled() { return mSmoothScrollingEnabled; } /** * Set whether arrow scrolling will animate its transition. * @param smoothScrollingEnabled whether arrow scrolling will animate its transition */ public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { mSmoothScrollingEnabled = smoothScrollingEnabled; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (!mFillViewport) { return; } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode == MeasureSpec.UNSPECIFIED) { return; } Log.e("totalHeight",String.valueOf(heightMeasureSpec)); if (getChildCount() > 0) { final View child = getChildAt(0); int height = getMeasuredHeight(); int width = getMeasuredWidth(); if(isVertScroll()) { if (child.getMeasuredHeight() < height) { final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); height -= getPaddingTop(); height -= getPaddingBottom(); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } }else{ if (child.getMeasuredWidth() < width) { final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height); width -= getPaddingLeft(); width -= getPaddingRight(); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } } @Override public boolean dispatchKeyEvent(KeyEvent event) { // Let the focused view and/or our descendants get the key first return super.dispatchKeyEvent(event) || executeKeyEvent(event); } /** * You can call this function yourself to have the scroll view perform * scrolling from a key event, just as if the event had been dispatched to * it by the view hierarchy. * * @param event The key event to execute. * @return Return true if the event was handled, else false. */ public boolean executeKeyEvent(KeyEvent event) { mTempRect.setEmpty(); if (!canScroll()) { if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { View currentFocused = findFocus(); if (currentFocused == this) currentFocused = null; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); } return false; } boolean handled = false; if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_UP: if (!event.isAltPressed()) { handled = arrowScroll(View.FOCUS_UP); } else { handled = fullScroll(View.FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (!event.isAltPressed()) { handled = arrowScroll(View.FOCUS_DOWN); } else { handled = fullScroll(View.FOCUS_DOWN); } break; case KeyEvent.KEYCODE_SPACE: pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); break; } } return handled; } private boolean inChild(int x, int y) { if (getChildCount() > 0) { final int scrollY = getScrollY(); final int scrollX = getScrollX(); final View child = getChildAt(0); if(isVertScroll()){ return !(y < child.getTop() - scrollY || y >= child.getBottom() - scrollY || x < child.getLeft() || x >= child.getRight()); } else { return !(y < child.getTop() || y >= child.getBottom() || x < child.getLeft() - scrollX || x >= child.getRight() - scrollX); } } return false; } private void initOrResetVelocityTracker() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } } private void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } } private void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ /* * Shortcut the most recurring case: the user is in the dragging * state and he is moving his finger. We want to intercept this * motion. */ final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } /* * Don't try to intercept touch if we can't scroll anyway. */ if (getScrollY() == 0 && !canScrollVertically(1) && isVertScroll()) { return false; } if (getScrollX() == 0 && !canScrollVertically(1) && !isVertScroll()) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } if(isVertScroll()) { final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } else{ final int x = (int) ev.getX(pointerIndex); final int xDiff = Math.abs(x - mLastMotionX); if (xDiff > mTouchSlop) { mIsBeingDragged = true; mLastMotionX = x; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } break; } case MotionEvent.ACTION_DOWN: { if(isVertScroll()) { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), (int) y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; } else{ final int x = (int) ev.getX(); if (!inChild((int) x, (int) ev.getY())) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionX = x; } mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. */ mIsBeingDragged = !mScroller.isFinished(); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if(isVertScroll()){ if (mScroller.springBack(getScrollX(), getScrollY(), 0,0,0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } } else { if (mScroller.springBack(getScrollX(), getScrollY(), 0,0,0, getScrollRange(), 0, 0)) { postInvalidateOnAnimation(); } } break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0) { return false; } if ((mIsBeingDragged = !mScroller.isFinished())) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } /* * If being flinged and user touches, stop the fling. isFinished * will be false if being flinged. */ if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // Remember where the motion event started if(isVertScroll()){ mLastMotionY = (int) ev.getY(); } else { mLastMotionX = (int) ev.getX(); } mActivePointerId = ev.getPointerId(0); break; } case MotionEvent.ACTION_MOVE: final int activePointerIndex = ev.findPointerIndex(mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } if(isVertScroll()) { final int y = (int) ev.getY(activePointerIndex); int deltaY = mLastMotionY - y; if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaY > 0) { deltaY -= mTouchSlop; } else { deltaY += mTouchSlop; } } if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionY = y; final int oldX = getScrollX(); final int oldY = getScrollY(); final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); // Calling overScrollBy will call onOverScrolled, which // calls onScrollChanged if applicable. if (overScrollBy(0, deltaY, 0, getScrollY(), 0, range, 0, mOverscrollDistance, true)) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } } } else{ final int x = (int) ev.getX(activePointerIndex); int deltaX = mLastMotionX - x; if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; if (deltaX > 0) { deltaX -= mTouchSlop; } else { deltaX += mTouchSlop; } } if (mIsBeingDragged) { // Scroll to follow the motion event mLastMotionX = x; final int oldX = getScrollX(); final int oldY = getScrollY(); final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); // Calling overScrollBy will call onOverScrolled, which // calls onScrollChanged if applicable. if (overScrollBy(deltaX, 0, getScrollX(), 0, range, 0, mOverscrollDistance, 0, true)) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); } } } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocityY = (int) velocityTracker.getYVelocity(mActivePointerId); int initialVelocityX = (int) velocityTracker.getXVelocity(mActivePointerId); if (getChildCount() > 0) { if(isVertScroll()) { if ((Math.abs(initialVelocityY) > mMinimumVelocity)) { fling(-initialVelocityY); } else { if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } } } else { if ((Math.abs(initialVelocityX) > mMinimumVelocity)) { fling(-initialVelocityX); } else{ if (mScroller.springBack(getScrollX(), getScrollY(),0,0, 0, getScrollRange(), 0, 0)) { postInvalidateOnAnimation(); } } } } mActivePointerId = INVALID_POINTER; endDrag(); } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) { if(isVertScroll()){ if (mScroller.springBack(getScrollX(), getScrollY(),0,0, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } } else { if (mScroller.springBack(getScrollX(), getScrollY(),0,0, 0, getScrollRange(), 0, 0)) { postInvalidateOnAnimation(); } } mActivePointerId = INVALID_POINTER; endDrag(); } break; case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); if(isVertScroll()){ mLastMotionY = (int) ev.getY(index); } else { mLastMotionX = (int) ev.getX(index); } mActivePointerId = ev.getPointerId(index); break; } case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); if(isVertScroll()){ mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); } else { mLastMotionY = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); } break; } return true; } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. // TODO: Make this decision more intelligent. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; if(isVertScroll()){ mLastMotionY = (int) ev.getY(newPointerIndex); } else { mLastMotionX = (int) ev.getX(newPointerIndex); } mActivePointerId = ev.getPointerId(newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); } } } // @Override // public boolean onGenericMotionEvent(MotionEvent event) { // if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { // switch (event.getAction()) { // case MotionEvent.ACTION_SCROLL: { // if (!mIsBeingDragged) { // final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); // if (vscroll != 0) { // final int delta = (int) (vscroll * getVerticalScrollFactor()); // final int range = getScrollRange(); // int oldScrollY = getScrollY(); // int newScrollY = oldScrollY - delta; // if (newScrollY < 0) { // newScrollY = 0; // } else if (newScrollY > range) { // newScrollY = range; // } // if (newScrollY != oldScrollY) { // super.scrollTo(getScrollX(), newScrollY); // return true; // } // } // } // } // } // } // return super.onGenericMotionEvent(event); // } // 滑动后的松手 @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { // Treat animating scrolls differently; see #computeScroll() for why. if (!mScroller.isFinished()) { final int oldX = getScrollX(); final int oldY = getScrollY(); setScrollX(scrollX); setScrollY(scrollY); invalidateParentIfNeeded(); onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); if(isVertScroll()){ if (clampedY) { //松手后 OverScroll 的滑动 mScroller.springBack(getScrollX(), getScrollY(), 0,0,0, 0, 0, getScrollRange()); } } else { if(clampedX){ mScroller.springBack(getScrollX(), getScrollY(), 0,0,0, getScrollRange(), 0, 0); } } } else { //跟手的滑动(包括 OverScroll) super.scrollTo(scrollX, scrollY); } awakenScrollBars(); } private void invalidateParentIfNeeded() { if (isHardwareAccelerated() && getParent() instanceof View) { ((View) getParent()).invalidate(); } } @Override public boolean performAccessibilityAction(int action, Bundle arguments) { if (super.performAccessibilityAction(action, arguments)) { return true; } if (!isEnabled()) { return false; } switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { if(isVertScroll()){ final int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop(); final int targetScrollY = Math.min(getScrollY() + viewportHeight, getScrollRange()); if (targetScrollY != getScrollY()) { smoothScrollTo(0, targetScrollY); return true; } } else { final int viewportWidth = getWidth() - getPaddingRight() - getPaddingLeft(); final int targetScrollX = Math.min(getScrollX() + viewportWidth, getScrollRange()); if (targetScrollX != getScrollX()) { smoothScrollTo(targetScrollX, 0); return true; } } } return false; case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { if(isVertScroll()){ final int viewportHeight = getHeight() - getPaddingBottom() - getPaddingTop(); final int targetScrollY = Math.max(getScrollY() - viewportHeight, 0); if (targetScrollY != getScrollY()) { smoothScrollTo(0, targetScrollY); return true; } } else { final int viewportWidth = getWidth() - getPaddingRight() - getPaddingLeft(); final int targetScrollX = Math.max(getScrollX() - viewportWidth, 0); if (targetScrollX != getScrollX()) { smoothScrollTo(targetScrollX, 0); return true; } } } return false; } return false; } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(AnScrollView.class.getName()); if (isEnabled()) { final int scrollRange = getScrollRange(); if (scrollRange > 0) { info.setScrollable(true); if(isVertScroll()){ if (getScrollY() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } if (getScrollY() < scrollRange) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } } else { if (getScrollX() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } if (getScrollX() < scrollRange) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } } } } } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(AnScrollView.class.getName()); final boolean scrollable = getScrollRange() > 0; event.setScrollable(scrollable); event.setScrollX(getScrollX()); event.setScrollY(getScrollY()); if(isVertScroll()){ event.setMaxScrollX(getScrollX()); event.setMaxScrollY(getScrollRange()); } else { event.setMaxScrollX(getScrollRange()); event.setMaxScrollY(getScrollY()); } } private int getScrollRange() { int scrollRange = 0; if (getChildCount() > 0) { View child = getChildAt(0); if(isVertScroll()){ scrollRange = Math.max(0,child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); } else { scrollRange = Math.max(0,child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight())); } } return scrollRange; } /** *

* Finds the next focusable component that fits in the specified bounds. *

* * @param topFocus look for a candidate is the one at the top of the bounds * if topFocus is true, or at the bottom of the bounds if topFocus is * false * @param top the top offset of the bounds in which a focusable must be * found * @param bottom the bottom offset of the bounds in which a focusable must * be found * @return the next focusable component in the bounds or null if none can * be found */ private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { List focusables = getFocusables(View.FOCUS_FORWARD); View focusCandidate = null; /* * A fully contained focusable is one where its top is below the bound's * top, and its bottom is above the bound's bottom. A partially * contained focusable is one where some part of it is within the * bounds, but it also has some part that is not within bounds. A fully contained * focusable is preferred to a partially contained focusable. */ boolean foundFullyContainedFocusable = false; int count = focusables.size(); if(isVertScroll()){ for (int i = 0; i < count; i++) { View view = focusables.get(i); int viewTop = view.getTop(); int viewBottom = view.getBottom(); if (top < viewBottom && viewTop < bottom) { /* * the focusable is in the target area, it is a candidate for * focusing */ final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); if (focusCandidate == null) { /* No candidate, take this one */ focusCandidate = view; foundFullyContainedFocusable = viewIsFullyContained; } else { final boolean viewIsCloserToBoundary = (topFocus && viewTop < focusCandidate.getTop()) || (!topFocus && viewBottom > focusCandidate .getBottom()); if (foundFullyContainedFocusable) { if (viewIsFullyContained && viewIsCloserToBoundary) { /* * We're dealing with only fully contained views, so * it has to be closer to the boundary to beat our * candidate */ focusCandidate = view; } } else { if (viewIsFullyContained) { /* Any fully contained view beats a partially contained view */ focusCandidate = view; foundFullyContainedFocusable = true; } else if (viewIsCloserToBoundary) { /* * Partially contained view beats another partially * contained view if it's closer */ focusCandidate = view; } } } } } } else { for (int i = 0; i < count; i++) { View view = focusables.get(i); int viewLeft = view.getLeft(); int viewRight = view.getRight(); if (top < viewRight && viewLeft < bottom) { /* * the focusable is in the target area, it is a candidate for * focusing */ final boolean viewIsFullyContained = (top < viewLeft) && (viewRight < bottom); if (focusCandidate == null) { /* No candidate, take this one */ focusCandidate = view; foundFullyContainedFocusable = viewIsFullyContained; } else { final boolean viewIsCloserToBoundary = (topFocus && viewLeft < focusCandidate.getLeft()) || (!topFocus && viewRight > focusCandidate.getRight()); if (foundFullyContainedFocusable) { if (viewIsFullyContained && viewIsCloserToBoundary) { /* * We're dealing with only fully contained views, so * it has to be closer to the boundary to beat our * candidate */ focusCandidate = view; } } else { if (viewIsFullyContained) { /* Any fully contained view beats a partially contained view */ focusCandidate = view; foundFullyContainedFocusable = true; } else if (viewIsCloserToBoundary) { /* * Partially contained view beats another partially * contained view if it's closer */ focusCandidate = view; } } } } } } return focusCandidate; } /** *

Handles scrolling in response to a "page up/down" shortcut press. This * method will scroll the view by one page up or down and give the focus * to the topmost/bottommost component in the new visible area. If no * component is a good candidate for focus, this scrollview reclaims the * focus.

* * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} * to go one page up or * {@link android.view.View#FOCUS_DOWN} to go one page down * @return true if the key event is consumed by this method, false otherwise */ public boolean pageScroll(int direction) { boolean down = direction == View.FOCUS_DOWN; if(isVertScroll()) { int height = getHeight(); if (down) { mTempRect.top = getScrollY() + height; int count = getChildCount(); if (count > 0) { View view = getChildAt(count - 1); if (mTempRect.top + height > view.getBottom()) { mTempRect.top = view.getBottom() - height; } } } else { mTempRect.top = getScrollY() - height; if (mTempRect.top < 0) { mTempRect.top = 0; } } mTempRect.bottom = mTempRect.top + height; return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); } else{ int width = getWidth(); if (down) { mTempRect.left = getScrollX() + width; int count = getChildCount(); if (count > 0) { View view = getChildAt(count - 1); if (mTempRect.left + width > view.getRight()) { mTempRect.left = view.getRight() - width; } } } else { mTempRect.left = getScrollX() - width; if (mTempRect.left < 0) { mTempRect.left = 0; } } mTempRect.right = mTempRect.left + width; return scrollAndFocus(direction, mTempRect.left,mTempRect.right); } } /** *

Handles scrolling in response to a "home/end" shortcut press. This * method will scroll the view to the top or bottom and give the focus * to the topmost/bottommost component in the new visible area. If no * component is a good candidate for focus, this scrollview reclaims the * focus.

* * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} * to go the top of the view or * {@link android.view.View#FOCUS_DOWN} to go the bottom * @return true if the key event is consumed by this method, false otherwise */ public boolean fullScroll(int direction) { boolean down = direction == View.FOCUS_DOWN; int height = getHeight(); mTempRect.top = 0; mTempRect.bottom = height; if (down) { int count = getChildCount(); if (count > 0) { View view = getChildAt(count - 1); mTempRect.bottom = view.getBottom() + getPaddingBottom(); mTempRect.top = mTempRect.bottom - height; } } return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); } /** *

Scrolls the view to make the area defined by top and * bottom visible. This method attempts to give the focus * to a component visible in this area. If no component can be focused in * the new visible area, the focus is reclaimed by this ScrollView.

* * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} * to go upward, {@link android.view.View#FOCUS_DOWN} to downward * @param top the top offset of the new area to be made visible * @param bottom the bottom offset of the new area to be made visible * @return true if the key event is consumed by this method, false otherwise */ private boolean scrollAndFocus(int direction, int top, int bottom) { boolean handled = true; if(isVertScroll()){ int height = getHeight(); int containerTop = getScrollY(); int containerBottom = containerTop + height; boolean up = direction == View.FOCUS_UP; View newFocused = findFocusableViewInBounds(up, top, bottom); if (newFocused == null) { newFocused = this; } if (top >= containerTop && bottom <= containerBottom) { handled = false; } else { int delta = up ? (top - containerTop) : (bottom - containerBottom); doScrollY(delta); } if (newFocused != findFocus()) newFocused.requestFocus(direction); } else { int width = getWidth(); int containerLeft = getScrollX(); int containerRight = containerLeft + width; boolean up = direction == View.FOCUS_UP; View newFocused = findFocusableViewInBounds(up, top, bottom); if (newFocused == null) { newFocused = this; } if (top >= containerLeft && bottom <= containerRight) { handled = false; } else { int delta = up ? (top - containerLeft) : (bottom - containerRight); doScrollX(delta); } if (newFocused != findFocus()) newFocused.requestFocus(direction); } return handled; } /** * Handle scrolling in response to an up or down arrow click. * * @param direction The direction corresponding to the arrow key that was * pressed * @return True if we consumed the event, false otherwise */ public boolean arrowScroll(int direction) { View currentFocused = findFocus(); if (currentFocused == this) currentFocused = null; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); final int maxJump = getMaxScrollAmount(); if(isVertScroll()){ if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { nextFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(nextFocused, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); doScrollY(scrollDelta); nextFocused.requestFocus(direction); } else { // no new focus int scrollDelta = maxJump; if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { scrollDelta = getScrollY(); } else if (direction == View.FOCUS_DOWN) { if (getChildCount() > 0) { int daBottom = getChildAt(0).getBottom(); int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); if (daBottom - screenBottom < maxJump) { scrollDelta = daBottom - screenBottom; } } } if (scrollDelta == 0) { return false; } doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); } } else { if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getWidth())) { nextFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(nextFocused, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); doScrollX(scrollDelta); nextFocused.requestFocus(direction); } else { // no new focus int scrollDelta = maxJump; if (direction == View.FOCUS_UP && getScrollX() < scrollDelta) { scrollDelta = getScrollX(); } else if (direction == View.FOCUS_DOWN) { if (getChildCount() > 0) { int daRight = getChildAt(0).getRight(); int screenRight = getScrollX() + getWidth() - getPaddingRight(); if (daRight - screenRight < maxJump) { scrollDelta = daRight - screenRight; } } } if (scrollDelta == 0) { return false; } doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); } } if (currentFocused != null && currentFocused.isFocused() && isOffScreen(currentFocused)) { // previously focused item still has focus and is off screen, give // it up (take it back to ourselves) // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are // sure to // get it) final int descendantFocusability = getDescendantFocusability(); // save setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); requestFocus(); setDescendantFocusability(descendantFocusability); // restore } return true; } /** * @return whether the descendant of this scroll view is scrolled off * screen. */ private boolean isOffScreen(View descendant) { if(isVertScroll()){ return !isWithinDeltaOfScreen(descendant, 0, getHeight()); } else { return !isWithinDeltaOfScreen(descendant, 0, getWidth()); } } /** * @return whether the descendant of this scroll view is within delta * pixels of being on the screen. */ private boolean isWithinDeltaOfScreen(View descendant, int delta, int value) { descendant.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(descendant, mTempRect); if(isVertScroll()){ return (mTempRect.bottom + delta) >= getScrollY() && (mTempRect.top - delta) <= (getScrollY() + value); } else { return (mTempRect.right + delta) >= getScrollX() && (mTempRect.left - delta) <= (getScrollX() + value); } } /** * Smooth scroll by a Y delta * * @param delta the number of pixels to scroll by on the Y axis */ private void doScrollY(int delta) { if (delta != 0) { if (mSmoothScrollingEnabled) { smoothScrollBy(0, delta); } else { scrollBy(0, delta); } } } private void doScrollX(int delta) { if (delta != 0) { if (mSmoothScrollingEnabled) { smoothScrollBy(delta,0); } else { scrollBy(delta, 0); } } } /** * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. * * @param dx the number of pixels to scroll by on the X axis * @param dy the number of pixels to scroll by on the Y axis */ public final void smoothScrollBy(int dx, int dy) { if (getChildCount() == 0) { // Nothing to do. return; } long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; if (duration > ANIMATED_SCROLL_GAP) { if(isVertScroll()){ final int height = getHeight() - getPaddingBottom() - getPaddingTop(); final int bottom = getChildAt(0).getHeight(); final int maxY = Math.max(0, bottom - height); final int scrollY = getScrollY(); dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; mScroller.startScroll(getScrollX(), scrollY, 0, dy); } else { final int width = getWidth() - getPaddingRight() - getPaddingLeft(); final int right = getChildAt(0).getRight(); final int maxX = Math.max(0, right - width); final int scrollX = getScrollX(); dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX; mScroller.startScroll(scrollX, getScrollY(), dx, 0); } postInvalidateOnAnimation(); } else { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } scrollBy(dx, dy); } mLastScroll = AnimationUtils.currentAnimationTimeMillis(); } /** * Like {@link #scrollTo}, but scroll smoothly instead of immediately. * * @param x the position where to scroll on the X axis * @param y the position where to scroll on the Y axis */ public final void smoothScrollTo(int x, int y) { smoothScrollBy(x - getScrollX(), y - getScrollY()); } /** *

The scroll range of a scroll view is the overall height of all of its * children.

*/ @Override protected int computeVerticalScrollRange() { final int count = getChildCount(); final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft(); if(isVertScroll()){ if (count == 0) { return contentHeight; } } else { if (count == 0) { return contentWidth; } } if(isVertScroll()){ int scrollRange = getChildAt(0).getBottom(); final int scrollY = getScrollY(); final int overscrollBottom = Math.max(0, scrollRange - contentHeight); if (scrollY < 0) { scrollRange -= scrollY; } else if (scrollY > overscrollBottom) { scrollRange += scrollY - overscrollBottom; } return scrollRange; } else { int scrollRange = getChildAt(0).getRight(); final int scrollX = getScrollX(); final int overscrollRight = Math.max(0, scrollRange - contentWidth); if (scrollX < 0) { scrollRange -= scrollX; } else if (scrollX > overscrollRight) { scrollRange += scrollX - overscrollRight; } return scrollRange; } } @Override protected int computeVerticalScrollOffset() { return Math.max(0, super.computeVerticalScrollOffset()); } @Override protected int computeHorizontalScrollOffset() { return Math.max(0, super.computeHorizontalScrollOffset()); } @Override protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { ViewGroup.LayoutParams lp = child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; if(isVertScroll()){ childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } else { childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; if (isVertScroll()) { childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); } else { childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override public void computeScroll() { //滚动尚未完成 if (mScroller.computeScrollOffset()) { // This is called at drawing time by ViewGroup. We don't want to // re-show the scrollbars at this point, which scrollTo will do, // so we replicate most of scrollTo here. // // It's a little odd to call onScrollChanged from inside the drawing. // // It is, except when you remember that computeScroll() is used to // animate scrolling. So unless we want to defer the onScrollChanged() // until the end of the animated scrolling, we don't really have a // choice here. // // I agree. The alternative, which I think would be worse, is to post // something and tell the subclasses later. This is bad because there // will be a window where getScrollX()/Y is different from what the app // thinks it is. // int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { final int range = getScrollRange(); final int overscrollMode = getOverScrollMode(); final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); if(isVertScroll()){ overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 0, mOverflingDistance, false); }else { overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0, mOverflingDistance,0, false); } onScrollChanged(getScrollX(), getScrollY(), oldX, oldY); } if (!awakenScrollBars()) { // Keep on drawing until the animation has finished. postInvalidateOnAnimation(); } } } /** * Scrolls the view to the given child. * * @param child the View to scroll to */ private void scrollToChild(View child) { child.getDrawingRect(mTempRect); /* Offset from child's local coordinates to ScrollView coordinates */ offsetDescendantRectToMyCoords(child, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); if (scrollDelta != 0) { if(isVertScroll()){ scrollBy(0, scrollDelta); } else { scrollBy(scrollDelta, 0); } } } /** * If rect is off screen, scroll just enough to get it (or at least the * first screen size chunk of it) on screen. * * @param rect The rectangle. * @param immediate True to scroll immediately without animation * @return true if scrolling was performed */ private boolean scrollToChildRect(Rect rect, boolean immediate) { final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); final boolean scroll = delta != 0; if (scroll) { if (immediate) { if(isVertScroll()){ scrollBy(0, delta); } else { scrollBy(delta, 0); } } else { if(isVertScroll()){ smoothScrollBy(0, delta); } else { smoothScrollBy(delta, 0); } } } return scroll; } /** * Compute the amount to scroll in the Y direction in order to get * a rectangle completely on the screen (or, if taller than the screen, * at least the first screen size chunk of it). * * @param rect The rect. * @return The scroll delta. */ protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { if (getChildCount() == 0) return 0; int height = getHeight(); int screenTop = getScrollY(); int screenBottom = screenTop + height; int width = getWidth(); int screenLeft = getScrollX(); int screenRight = screenLeft + width; int fadingEdgeVertical = getVerticalFadingEdgeLength(); int fadingEdgeHorizontal = getHorizontalFadingEdgeLength(); if(isVertScroll()){ // leave room for top fading edge as long as rect isn't at very top if (rect.top > 0) { screenTop += fadingEdgeVertical; } // leave room for bottom fading edge as long as rect isn't at very bottom if (rect.bottom < getChildAt(0).getHeight()) { screenBottom -= fadingEdgeVertical; } int scrollYDelta = 0; if (rect.bottom > screenBottom && rect.top > screenTop) { // need to move down to get it in view: move down just enough so // that the entire rectangle is in view (or at least the first // screen size chunk). if (rect.height() > height) { // just enough to get screen size chunk on scrollYDelta += (rect.top - screenTop); } else { // get entire rect at bottom of screen scrollYDelta += (rect.bottom - screenBottom); } // make sure we aren't scrolling beyond the end of our content int bottom = getChildAt(0).getBottom(); int distanceToBottom = bottom - screenBottom; scrollYDelta = Math.min(scrollYDelta, distanceToBottom); } else if (rect.top < screenTop && rect.bottom < screenBottom) { // need to move up to get it in view: move up just enough so that // entire rectangle is in view (or at least the first screen // size chunk of it). if (rect.height() > height) { // screen size chunk scrollYDelta -= (screenBottom - rect.bottom); } else { // entire rect at top scrollYDelta -= (screenTop - rect.top); } // make sure we aren't scrolling any further than the top our content scrollYDelta = Math.max(scrollYDelta, -getScrollY()); } return scrollYDelta; }else { // leave room for top fading edge as long as rect isn't at very top if (rect.left > 0) { screenLeft += fadingEdgeHorizontal; } // leave room for bottom fading edge as long as rect isn't at very bottom if (rect.right < getChildAt(0).getWidth()) { screenRight -= fadingEdgeHorizontal; } int scrollXDelta = 0; if (rect.right > screenRight && rect.left > screenLeft) { // need to move down to get it in view: move down just enough so // that the entire rectangle is in view (or at least the first // screen size chunk). if (rect.width() > width) { // just enough to get screen size chunk on scrollXDelta += (rect.left - screenLeft); } else { // get entire rect at bottom of screen scrollXDelta += (rect.right - screenRight); } // make sure we aren't scrolling beyond the end of our content int right = getChildAt(0).getRight(); int distanceToRight = right - screenRight; scrollXDelta = Math.min(scrollXDelta, distanceToRight); } else if (rect.left < screenLeft && rect.right < screenRight) { // need to move up to get it in view: move up just enough so that // entire rectangle is in view (or at least the first screen // size chunk of it). if (rect.width() > width) { // screen size chunk scrollXDelta -= (screenRight - rect.right); } else { // entire rect at top scrollXDelta -= (screenLeft - rect.left); } // make sure we aren't scrolling any further than the top our content scrollXDelta = Math.max(scrollXDelta, -getScrollX()); } return scrollXDelta; } } @Override public void requestChildFocus(View child, View focused) { if (!mIsLayoutDirty) { scrollToChild(focused); } else { // The child may not be laid out yet, we can't compute the scroll yet mChildToScrollTo = focused; } super.requestChildFocus(child, focused); } /** * When looking for focus in children of a scroll view, need to be a little * more careful not to give focus to something that is scrolled off screen. * * This is more expensive than the default {@link android.view.ViewGroup} * implementation, otherwise this behavior might have been made the default. */ @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { // convert from forward / backward notation to up / down / left / right // (ugh). if (direction == View.FOCUS_FORWARD) { direction = View.FOCUS_DOWN; } else if (direction == View.FOCUS_BACKWARD) { direction = View.FOCUS_UP; } final View nextFocus = previouslyFocusedRect == null ? FocusFinder.getInstance().findNextFocus(this, null, direction) : FocusFinder.getInstance().findNextFocusFromRect(this, previouslyFocusedRect, direction); if (nextFocus == null) { return false; } if (isOffScreen(nextFocus)) { return false; } return nextFocus.requestFocus(direction, previouslyFocusedRect); } @Override public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) { // offset into coordinate space of this scroll view rectangle.offset(child.getLeft() - child.getScrollX(),child.getTop() - child.getScrollY()); return scrollToChildRect(rectangle, immediate); } @Override public void requestLayout() { mIsLayoutDirty = true; super.requestLayout(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); mIsLayoutDirty = false; // Give a child focus if it needs it if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { scrollToChild(mChildToScrollTo); } mChildToScrollTo = null; if (!isLaidOut()) { if (mSavedState != null) { if(isVertScroll()){ setScrollY(mSavedState.scrollPosition); } else { setScrollX(mSavedState.scrollPosition); } mSavedState = null; } // getScrollY() default value is "0" final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; final int scrollRangeY = Math.max(0, childHeight - (b - t - getPaddingBottom() - getPaddingTop())); final int childWidth = (getChildCount() > 0) ? getChildAt(0).getMeasuredWidth() : 0; final int scrollRangeX = Math.max(0, childWidth - (r - l - getPaddingRight() - getPaddingLeft())); if(isVertScroll()){ // Don't forget to clamp if (getScrollY() > scrollRangeY) { setScrollY(scrollRangeY); } else if (getScrollY() < 0) { //Original is setScaleY setScrollY(0); } } else { // Don't forget to clamp if (getScrollX() > scrollRangeX) { setScrollX(scrollRangeX); } else if (getScrollX() < 0) { setScrollX(0); } } } // Calling this with the present values causes it to re-claim them scrollTo(getScrollX(), getScrollY()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); View currentFocused = findFocus(); if (null == currentFocused || this == currentFocused) return; // If the currently-focused view was visible on the screen when the // screen was at the old height, then scroll the screen to make that // view visible with the new screen height. if(isVertScroll()){ if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { currentFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(currentFocused, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); doScrollY(scrollDelta); } } else { if (isWithinDeltaOfScreen(currentFocused, 0, oldw)) { currentFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(currentFocused, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); doScrollX(scrollDelta); } } } /** * Return true if child is a descendant of parent, (or equal to the parent). */ private static boolean isViewDescendantOf(View child, View parent) { if (child == parent) { return true; } final ViewParent theParent = child.getParent(); return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); } /** * Fling the scroll view * * @param velocity The initial velocity in the Y direction. Positive * numbers mean that the finger/cursor is moving down the screen, * which means we want to scroll towards the top. */ public void fling(int velocity) { if (getChildCount() > 0) { int height = getHeight() - getPaddingBottom() - getPaddingTop(); int bottom = getChildAt(0).getHeight(); int width = getWidth() - getPaddingRight() - getPaddingLeft(); int right = getChildAt(0).getRight(); if(isVertScroll()) { mScroller.fling(getScrollX(), getScrollY(), 0, velocity, 0, 0, 0, Math.max(0, bottom - height), 0, height / 2); } else { mScroller.fling(getScrollX(), getScrollY(), velocity, 0, 0, Math.max(0, right - width), 0, 0, width/2, 0); } postInvalidateOnAnimation(); } } private void endDrag() { mIsBeingDragged = false; recycleVelocityTracker(); } /** * {@inheritDoc} * *

This version also clamps the scrolling to the bounds of our child. */ @Override public void scrollTo(int x, int y) { // we rely on the fact the View.scrollBy calls scrollTo. if (getChildCount() > 0) { if (x != getScrollX() || y != getScrollY()) { super.scrollTo(x, y); } } } @Override public void setOverScrollMode(int mode) { } @Override public void draw(Canvas canvas) { super.draw(canvas); } @Override protected void onRestoreInstanceState(Parcelable state) { if (getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) { // Some old apps reused IDs in ways they shouldn't have. // Don't break them, but they don't get scroll state restoration. super.onRestoreInstanceState(state); return; } SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); mSavedState = ss; requestLayout(); } @Override protected Parcelable onSaveInstanceState() { if (getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) { // Some old apps reused IDs in ways they shouldn't have. // Don't break them, but they don't get scroll state restoration. return super.onSaveInstanceState(); } Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.scrollPosition = getScrollY(); return ss; } static class SavedState extends BaseSavedState { public int scrollPosition; SavedState(Parcelable superState) { super(superState); } public SavedState(Parcel source) { super(source); scrollPosition = source.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(scrollPosition); } @Override public String toString() { return "HorizontalScrollView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " scrollPosition=" + scrollPosition + "}"; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { final int overScrollMode = OVER_SCROLL_ALWAYS; final boolean canScrollHorizontal = computeHorizontalScrollRange() > computeHorizontalScrollExtent(); final boolean canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent(); final boolean overScrollHorizontal = overScrollMode == OVER_SCROLL_ALWAYS || (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); final boolean overScrollVertical = overScrollMode == OVER_SCROLL_ALWAYS || (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); // 不涉及 Drag + Overscroll 的 Fling int newScrollX = scrollX + deltaX; if (!overScrollHorizontal) { maxOverScrollX = 0; } int newScrollY = scrollY + deltaY; if (!overScrollVertical) { maxOverScrollY = 0; } // Clamp values if at the limits and record int padding = 0; final int left = -maxOverScrollX - padding; final int right = maxOverScrollX + scrollRangeX + padding; final int top = -maxOverScrollY - padding; final int bottom = maxOverScrollY + scrollRangeY + padding; boolean clampedX = false; if (newScrollX > right) { //newScrollX = right; clampedX = true; } else if (newScrollX < left) { //newScrollX = left; clampedX = true; } boolean clampedY = false; if (newScrollY > bottom) { //newScrollY = bottom; clampedY = true; } else if (newScrollY < top) { //newScrollY = top; clampedY = true; } if (mIsBeingDragged && clampedX) { newScrollX = (int)(scrollX + deltaX * FOLLOW_HAND_FACTOR); } if (mIsBeingDragged && clampedY) { newScrollY = (int)(scrollY + deltaY * FOLLOW_HAND_FACTOR); } onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); return clampedX || clampedY; } private boolean isVertScroll(){ return mScroller.isVertScroll(); } public AnOverScroller getScroller(){ return mScroller; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AnInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator; import android.animation.TimeInterpolator; /** * An interpolator defines the rate of change of an animation. This allows * the basic animation effects (alpha, scale, translate, rotate) to be * accelerated, decelerated, repeated, etc. */ public abstract class AnInterpolator implements TimeInterpolator { // A new interface, TimeInterpolator, was introduced for the new android.animation // package. This older Interpolator interface extends TimeInterpolator so that users of // the new Animator-based animations can use either the old Interpolator implementations or // new classes that implement TimeInterpolator directly. //TODO Use reflection public float arg1,arg2,arg3,arg4 = -1f; public String string1,string2,string3,string4 = "NULL"; public float min1,min2,min3,min4,max1,max2,max3,max4 = -1f; public int argNum = 0; public void initArgData(int i, float val, String name, float min, float max){ if(i == 0){ //arg1 = val; string1 = name; min1 = min; max1 = max; } else if(i == 1){ //arg2 = val; string2 = name; min2 = min; max2 = max; } else if(i == 2){ //arg3 = val; string3 = name; min3 = min; max3 = max; } else if(i == 3){ //arg4 = val; string4 = name; min4 = min; max4 = max; } setArgValue(i,val); argNum++; } public void resetArgValue(int i, float value){ } public void setArgValue(int i,float val){ if(i == 0){ arg1 = val; } else if(i == 1){ arg2 = val; } else if(i == 2){ arg3 = val; } else if(i == 3){ arg4 = val; } } public int getArgNum(){ return argNum; } public float getArgValue(int i) { if(i == 0){ if(arg1 != -1) return (float) arg1; else return -1; } else if(i == 1){ if(arg2 != -1) return (float) arg2; else return -1; } else if(i == 2){ if(arg3 != -1) return (float) arg3; else return -1; } else if(i == 3){ if(arg4 != -1) return (float) arg4; else return -1; } return -1; } public String getArgString(int i) { if(i == 0){ if(string1 != "NULL") return string1; else return "NULL"; } else if(i == 1){ if(string2 != "NULL") return string2; else return "NULL"; } else if(i == 2){ if(string3 != "NULL") return string3; else return "NULL"; } else if(i == 3){ if(string4 != "NULL") return string4; else return "NULL"; } return "NULL"; } public float getArgMin(int i) { if(i == 0){ if(min1 != -1) return min1; else return -1; } else if(i == 1){ if(min2 != -1) return min2; else return -1; } else if(i == 2){ if(min3 != -1) return min3; else return -1; } else if(i == 3){ if(min4 != -1) return min4; else return -1; } return -1; } public float getArgMax(int i) { if(i == 0){ if(max1 != -1) return max1; else return -1; } else if(i == 1){ if(max2 != -1) return max2; else return -1; } else if(i == 2){ if(max3 != -1) return max3; else return -1; } else if(i == 3){ if(max4 != -1) return max4; else return -1; } return -1; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/AccelerateDecelerateInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class AccelerateDecelerateInterpolator extends AnInterpolator { public AccelerateDecelerateInterpolator() { } @SuppressWarnings({"UnusedDeclaration"}) public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) { } public float getInterpolation(float input) { return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/AccelerateInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class AccelerateInterpolator extends AnInterpolator { private float mFactor; private double mDoubleFactor; public AccelerateInterpolator() { mFactor = 1.0f; mDoubleFactor = 2.0; initArgData(0,1,"factor",0,10); } public AccelerateInterpolator(float factor) { mFactor = factor; mDoubleFactor = 2 * mFactor; initArgData(0,factor,"factor",0,10); } public float getInterpolation(float input) { if (mFactor == 1.0f) { return input * input; } else { return (float)Math.pow(input, mDoubleFactor); } } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mFactor = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/AnticipateInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class AnticipateInterpolator extends AnInterpolator { private float mTension; public AnticipateInterpolator() { mTension = 2.0f; initArgData(0,2,"factor",0,10); } /** * @param tension Amount of anticipation. When tension equals 0.0f, there is * no anticipation and the interpolator becomes a simple * acceleration interpolator. */ public AnticipateInterpolator(float tension) { mTension = tension; initArgData(0,tension,"factor",0,10); } public float getInterpolation(float t) { // a(t) = t * t * ((tension + 1) * t - tension) return t * t * ((mTension + 1) * t - mTension); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mTension = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/AnticipateOvershootInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class AnticipateOvershootInterpolator extends AnInterpolator { private float mTension; public AnticipateOvershootInterpolator() { mTension = 2.0f * 1.5f; initArgData(0,2,"factor",0,10); } public AnticipateOvershootInterpolator(float tension) { mTension = tension * 1.5f; initArgData(0,tension,"factor",0,10); } public AnticipateOvershootInterpolator(float tension, float extraTension) { mTension = tension * extraTension; initArgData(0,tension,"factor",0,10); } private static float a(float t, float s) { return t * t * ((s + 1) * t - s); } private static float o(float t, float s) { return t * t * ((s + 1) * t + s); } public float getInterpolation(float t) { // a(t, s) = t * t * ((s + 1) * t - s) // o(t, s) = t * t * ((s + 1) * t + s) // f(t) = 0.5 * a(t * 2, tension * extraTension), when t < 0.5 // f(t) = 0.5 * (o(t * 2 - 2, tension * extraTension) + 2), when t <= 1.0 if (t < 0.5f) return 0.5f * a(t * 2.0f, mTension); else return 0.5f * (o(t * 2.0f - 2.0f, mTension) + 2.0f); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mTension = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/BounceInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class BounceInterpolator extends AnInterpolator { public BounceInterpolator() { } private static float bounce(float t) { return t * t * 8.0f; } public float getInterpolation(float t) { // _b(t) = t * t * 8 // bs(t) = _b(t) for t < 0.3535 // bs(t) = _b(t - 0.54719) + 0.7 for t < 0.7408 // bs(t) = _b(t - 0.8526) + 0.9 for t < 0.9644 // bs(t) = _b(t - 1.0435) + 0.95 for t <= 1.0 // b(t) = bs(t * 1.1226) t *= 1.1226f; if (t < 0.3535f) return bounce(t); else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f; else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f; else return bounce(t - 1.0435f) + 0.95f; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/CycleInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class CycleInterpolator extends AnInterpolator { public CycleInterpolator(float cycles) { mCycles = cycles; initArgData(0,cycles,"factor",0,10); } public float getInterpolation(float input) { return (float)(Math.sin(2 * mCycles * Math.PI * input)); } private float mCycles; @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mCycles = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/DecelerateInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class DecelerateInterpolator extends AnInterpolator { private float mFactor = 1.0f; public DecelerateInterpolator() { initArgData(0,1,"factor",0,10); } public DecelerateInterpolator(float factor) { mFactor = factor; initArgData(0,factor,"factor",0,10); } public float getInterpolation(float input) { float result; if (mFactor == 1.0f) { result = (float)(1.0f - (1.0f - input) * (1.0f - input)); } else { result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor)); } return result; } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mFactor = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/FastOutLinearInInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class FastOutLinearInInterpolator extends LookupTableInterpolator { private static final float[] VALUES = new float[] { 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0008f, 0.0013f, 0.0018f, 0.0024f, 0.0032f, 0.0040f, 0.0049f, 0.0059f, 0.0069f, 0.0081f, 0.0093f, 0.0106f, 0.0120f, 0.0135f, 0.0151f, 0.0167f, 0.0184f, 0.0201f, 0.0220f, 0.0239f, 0.0259f, 0.0279f, 0.0300f, 0.0322f, 0.0345f, 0.0368f, 0.0391f, 0.0416f, 0.0441f, 0.0466f, 0.0492f, 0.0519f, 0.0547f, 0.0574f, 0.0603f, 0.0632f, 0.0662f, 0.0692f, 0.0722f, 0.0754f, 0.0785f, 0.0817f, 0.0850f, 0.0884f, 0.0917f, 0.0952f, 0.0986f, 0.1021f, 0.1057f, 0.1093f, 0.1130f, 0.1167f, 0.1205f, 0.1243f, 0.1281f, 0.1320f, 0.1359f, 0.1399f, 0.1439f, 0.1480f, 0.1521f, 0.1562f, 0.1604f, 0.1647f, 0.1689f, 0.1732f, 0.1776f, 0.1820f, 0.1864f, 0.1909f, 0.1954f, 0.1999f, 0.2045f, 0.2091f, 0.2138f, 0.2184f, 0.2232f, 0.2279f, 0.2327f, 0.2376f, 0.2424f, 0.2473f, 0.2523f, 0.2572f, 0.2622f, 0.2673f, 0.2723f, 0.2774f, 0.2826f, 0.2877f, 0.2929f, 0.2982f, 0.3034f, 0.3087f, 0.3141f, 0.3194f, 0.3248f, 0.3302f, 0.3357f, 0.3412f, 0.3467f, 0.3522f, 0.3578f, 0.3634f, 0.3690f, 0.3747f, 0.3804f, 0.3861f, 0.3918f, 0.3976f, 0.4034f, 0.4092f, 0.4151f, 0.4210f, 0.4269f, 0.4329f, 0.4388f, 0.4448f, 0.4508f, 0.4569f, 0.4630f, 0.4691f, 0.4752f, 0.4814f, 0.4876f, 0.4938f, 0.5000f, 0.5063f, 0.5126f, 0.5189f, 0.5252f, 0.5316f, 0.5380f, 0.5444f, 0.5508f, 0.5573f, 0.5638f, 0.5703f, 0.5768f, 0.5834f, 0.5900f, 0.5966f, 0.6033f, 0.6099f, 0.6166f, 0.6233f, 0.6301f, 0.6369f, 0.6436f, 0.6505f, 0.6573f, 0.6642f, 0.6710f, 0.6780f, 0.6849f, 0.6919f, 0.6988f, 0.7059f, 0.7129f, 0.7199f, 0.7270f, 0.7341f, 0.7413f, 0.7484f, 0.7556f, 0.7628f, 0.7700f, 0.7773f, 0.7846f, 0.7919f, 0.7992f, 0.8066f, 0.8140f, 0.8214f, 0.8288f, 0.8363f, 0.8437f, 0.8513f, 0.8588f, 0.8664f, 0.8740f, 0.8816f, 0.8892f, 0.8969f, 0.9046f, 0.9124f, 0.9201f, 0.9280f, 0.9358f, 0.9437f, 0.9516f, 0.9595f, 0.9675f, 0.9755f, 0.9836f, 0.9918f, 1.0000f }; public FastOutLinearInInterpolator() { super(VALUES); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/FastOutSlowInInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class FastOutSlowInInterpolator extends LookupTableInterpolator { private static final float[] VALUES = new float[] { 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0009f, 0.0014f, 0.0020f, 0.0027f, 0.0036f, 0.0046f, 0.0058f, 0.0071f, 0.0085f, 0.0101f, 0.0118f, 0.0137f, 0.0158f, 0.0180f, 0.0205f, 0.0231f, 0.0259f, 0.0289f, 0.0321f, 0.0355f, 0.0391f, 0.0430f, 0.0471f, 0.0514f, 0.0560f, 0.0608f, 0.0660f, 0.0714f, 0.0771f, 0.0830f, 0.0893f, 0.0959f, 0.1029f, 0.1101f, 0.1177f, 0.1257f, 0.1339f, 0.1426f, 0.1516f, 0.1610f, 0.1707f, 0.1808f, 0.1913f, 0.2021f, 0.2133f, 0.2248f, 0.2366f, 0.2487f, 0.2611f, 0.2738f, 0.2867f, 0.2998f, 0.3131f, 0.3265f, 0.3400f, 0.3536f, 0.3673f, 0.3810f, 0.3946f, 0.4082f, 0.4217f, 0.4352f, 0.4485f, 0.4616f, 0.4746f, 0.4874f, 0.5000f, 0.5124f, 0.5246f, 0.5365f, 0.5482f, 0.5597f, 0.5710f, 0.5820f, 0.5928f, 0.6033f, 0.6136f, 0.6237f, 0.6335f, 0.6431f, 0.6525f, 0.6616f, 0.6706f, 0.6793f, 0.6878f, 0.6961f, 0.7043f, 0.7122f, 0.7199f, 0.7275f, 0.7349f, 0.7421f, 0.7491f, 0.7559f, 0.7626f, 0.7692f, 0.7756f, 0.7818f, 0.7879f, 0.7938f, 0.7996f, 0.8053f, 0.8108f, 0.8162f, 0.8215f, 0.8266f, 0.8317f, 0.8366f, 0.8414f, 0.8461f, 0.8507f, 0.8551f, 0.8595f, 0.8638f, 0.8679f, 0.8720f, 0.8760f, 0.8798f, 0.8836f, 0.8873f, 0.8909f, 0.8945f, 0.8979f, 0.9013f, 0.9046f, 0.9078f, 0.9109f, 0.9139f, 0.9169f, 0.9198f, 0.9227f, 0.9254f, 0.9281f, 0.9307f, 0.9333f, 0.9358f, 0.9382f, 0.9406f, 0.9429f, 0.9452f, 0.9474f, 0.9495f, 0.9516f, 0.9536f, 0.9556f, 0.9575f, 0.9594f, 0.9612f, 0.9629f, 0.9646f, 0.9663f, 0.9679f, 0.9695f, 0.9710f, 0.9725f, 0.9739f, 0.9753f, 0.9766f, 0.9779f, 0.9791f, 0.9803f, 0.9815f, 0.9826f, 0.9837f, 0.9848f, 0.9858f, 0.9867f, 0.9877f, 0.9885f, 0.9894f, 0.9902f, 0.9910f, 0.9917f, 0.9924f, 0.9931f, 0.9937f, 0.9944f, 0.9949f, 0.9955f, 0.9960f, 0.9964f, 0.9969f, 0.9973f, 0.9977f, 0.9980f, 0.9984f, 0.9986f, 0.9989f, 0.9991f, 0.9993f, 0.9995f, 0.9997f, 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f }; public FastOutSlowInInterpolator() { super(VALUES); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/LinearInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class LinearInterpolator extends AnInterpolator { public LinearInterpolator() { } public LinearInterpolator(Context context, AttributeSet attrs) { } public float getInterpolation(float input) { return input; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/LinearOutSlowInInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class LinearOutSlowInInterpolator extends LookupTableInterpolator { private static final float[] VALUES = new float[] { 0.0000f, 0.0222f, 0.0424f, 0.0613f, 0.0793f, 0.0966f, 0.1132f, 0.1293f, 0.1449f, 0.1600f, 0.1747f, 0.1890f, 0.2029f, 0.2165f, 0.2298f, 0.2428f, 0.2555f, 0.2680f, 0.2802f, 0.2921f, 0.3038f, 0.3153f, 0.3266f, 0.3377f, 0.3486f, 0.3592f, 0.3697f, 0.3801f, 0.3902f, 0.4002f, 0.4100f, 0.4196f, 0.4291f, 0.4385f, 0.4477f, 0.4567f, 0.4656f, 0.4744f, 0.4831f, 0.4916f, 0.5000f, 0.5083f, 0.5164f, 0.5245f, 0.5324f, 0.5402f, 0.5479f, 0.5555f, 0.5629f, 0.5703f, 0.5776f, 0.5847f, 0.5918f, 0.5988f, 0.6057f, 0.6124f, 0.6191f, 0.6257f, 0.6322f, 0.6387f, 0.6450f, 0.6512f, 0.6574f, 0.6635f, 0.6695f, 0.6754f, 0.6812f, 0.6870f, 0.6927f, 0.6983f, 0.7038f, 0.7093f, 0.7147f, 0.7200f, 0.7252f, 0.7304f, 0.7355f, 0.7406f, 0.7455f, 0.7504f, 0.7553f, 0.7600f, 0.7647f, 0.7694f, 0.7740f, 0.7785f, 0.7829f, 0.7873f, 0.7917f, 0.7959f, 0.8002f, 0.8043f, 0.8084f, 0.8125f, 0.8165f, 0.8204f, 0.8243f, 0.8281f, 0.8319f, 0.8356f, 0.8392f, 0.8429f, 0.8464f, 0.8499f, 0.8534f, 0.8568f, 0.8601f, 0.8634f, 0.8667f, 0.8699f, 0.8731f, 0.8762f, 0.8792f, 0.8823f, 0.8852f, 0.8882f, 0.8910f, 0.8939f, 0.8967f, 0.8994f, 0.9021f, 0.9048f, 0.9074f, 0.9100f, 0.9125f, 0.9150f, 0.9174f, 0.9198f, 0.9222f, 0.9245f, 0.9268f, 0.9290f, 0.9312f, 0.9334f, 0.9355f, 0.9376f, 0.9396f, 0.9416f, 0.9436f, 0.9455f, 0.9474f, 0.9492f, 0.9510f, 0.9528f, 0.9545f, 0.9562f, 0.9579f, 0.9595f, 0.9611f, 0.9627f, 0.9642f, 0.9657f, 0.9672f, 0.9686f, 0.9700f, 0.9713f, 0.9726f, 0.9739f, 0.9752f, 0.9764f, 0.9776f, 0.9787f, 0.9798f, 0.9809f, 0.9820f, 0.9830f, 0.9840f, 0.9849f, 0.9859f, 0.9868f, 0.9876f, 0.9885f, 0.9893f, 0.9900f, 0.9908f, 0.9915f, 0.9922f, 0.9928f, 0.9934f, 0.9940f, 0.9946f, 0.9951f, 0.9956f, 0.9961f, 0.9966f, 0.9970f, 0.9974f, 0.9977f, 0.9981f, 0.9984f, 0.9987f, 0.9989f, 0.9992f, 0.9994f, 0.9995f, 0.9997f, 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f }; public LinearOutSlowInInterpolator() { super(VALUES); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/LookupTableInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.AttributeSet; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class LookupTableInterpolator extends AnInterpolator { private final float[] mValues; private final float mStepSize; protected LookupTableInterpolator(float[] values) { mValues = values; mStepSize = 1f / (mValues.length - 1); } @Override public float getInterpolation(float input) { if (input >= 1.0f) { return 1.0f; } if (input <= 0f) { return 0f; } // Calculate index - We use min with length - 2 to avoid IndexOutOfBoundsException when // we lerp (linearly interpolate) in the return statement int position = Math.min((int) (input * (mValues.length - 1)), mValues.length - 2); // Calculate values to account for small offsets as the lookup table has discrete values float quantized = position * mStepSize; float diff = input - quantized; float weight = diff / mStepSize; // Linearly interpolate between the table values return mValues[position] + weight * (mValues[position + 1] - mValues[position]); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/OvershootInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class OvershootInterpolator extends AnInterpolator { private float mTension; public OvershootInterpolator() { mTension = 2.0f; initArgData(0,2,"factor",0,10); } public OvershootInterpolator(float tension) { mTension = tension; initArgData(0,tension,"factor",0,10); } public float getInterpolation(float t) { // _o(t) = t * t * ((tension + 1) * t + tension) // o(t) = _o(t - 1) + 1 t -= 1.0f; return t * t * ((mTension + 1) * t + mTension) + 1.0f; } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mTension = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidNative/PathInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator.AndroidNative; import android.graphics.Path; import com.martinrgb.animer.core.interpolator.AnInterpolator; public class PathInterpolator extends AnInterpolator { private static final float PRECISION = 0.002f; private float[] mX; // x coordinates in the line private float[] mY; // y coordinates in the line private float x1,y1,x2,y2; public PathInterpolator(float controlX1, float controlY1, float controlX2, float controlY2) { x1= controlX1; y1 = controlY1; x2 = controlX2; y2 = controlY2; initCubic(x1, y1, x2, y2); initArgData(0,x1,"x1",0.0f,1.0f); initArgData(1,y1,"y1",0.0f,1.0f); initArgData(2,x2,"x2",0.0f,1.0f); initArgData(3,y2,"y2",0.0f,1.0f); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ x1 = value; } if(i == 1){ y1 = value; } if(i == 2){ x2 = value; } if(i == 3){ y2 = value; } initCubic(x1, y1, x2, y2); } private void initCubic(float x1, float y1, float x2, float y2) { Path path = new Path(); path.moveTo(0, 0); path.cubicTo(x1, y1, x2, y2, 1f, 1f); initPath(path); } private void initPath(Path path) { float[] pointComponents = path.approximate(PRECISION); int numPoints = pointComponents.length / 3; if (pointComponents[1] != 0 || pointComponents[2] != 0 || pointComponents[pointComponents.length - 2] != 1 || pointComponents[pointComponents.length - 1] != 1) { throw new IllegalArgumentException("The Path must start at (0,0) and end at (1,1)"); } mX = new float[numPoints]; mY = new float[numPoints]; float prevX = 0; float prevFraction = 0; int componentIndex = 0; for (int i = 0; i < numPoints; i++) { float fraction = pointComponents[componentIndex++]; float x = pointComponents[componentIndex++]; float y = pointComponents[componentIndex++]; if (fraction == prevFraction && x != prevX) { throw new IllegalArgumentException( "The Path cannot have discontinuity in the X axis."); } if (x < prevX) { throw new IllegalArgumentException("The Path cannot loop back on itself."); } mX[i] = x; mY[i] = y; prevX = x; prevFraction = fraction; } } @Override public float getInterpolation(float t) { if (t <= 0) { return 0; } else if (t >= 1) { return 1; } // Do a binary search for the correct x to interpolate between. int startIndex = 0; int endIndex = mX.length - 1; while (endIndex - startIndex > 1) { int midIndex = (startIndex + endIndex) / 2; if (t < mX[midIndex]) { endIndex = midIndex; } else { startIndex = midIndex; } } float xRange = mX[endIndex] - mX[startIndex]; if (xRange == 0) { return mY[startIndex]; } float tInRange = t - mX[startIndex]; float fraction = tInRange / xRange; float startY = mY[startIndex]; float endY = mY[endIndex]; return startY + (fraction * (endY - startY)); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidSpringInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator; // Interpolator Version of Android's SpringAnimation import android.util.Log; import com.martinrgb.animer.core.math.calculator.SpringInterpolatorCalculator; public class AndroidSpringInterpolator extends AnInterpolator{ //Parameters private float mStiffness = 1500.f; private float mDampingRatio = 0.5f; private float mVelocity = 0.f; private float mDuration = 1.f; private float mLastPlacement = 0.f; public AndroidSpringInterpolator(float stiffness, float dampingratio,float velocity,float duration) { this.mStiffness = stiffness; this.mDampingRatio = dampingratio; this.mVelocity = velocity; this.mDuration = duration/1000.f; //this.mDuration = new SpringInterpolatorCalculator(stiffness,dampingratio).getDuration(); initArgData(0,(float) stiffness,"stiffness",0.01f,3000); initArgData(1,(float) dampingratio,"dampingratio",0.01f,3000); initArgData(2,(float) velocity,"velocity",-5000,5000); initArgData(3,(float) duration,"duration",0,5000); } public AndroidSpringInterpolator(float stiffness, float dampingratio,float duration) { this.mStiffness = stiffness; this.mDampingRatio = dampingratio; this.mVelocity = 0.f; this.mDuration = duration/1000.f; //this.mDuration = new SpringInterpolatorCalculator(stiffness,dampingratio).getDuration(); initArgData(0,(float) stiffness,"stiffness",0.01f,3000); initArgData(1,(float) dampingratio,"dampingratio",0.01f,1); initArgData(2,(float) 0,"velocity",-5000,5000); initArgData(3,(float) duration,"duration",0,5000); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ mStiffness = value; } if(i == 1){ mDampingRatio = value; } if(i == 2){ mVelocity = value; } if(i == 3){ mDuration = value/1000; } } @Override public float getInterpolation(float ratio) { if (ratio == 0.0f || ratio == 1.0f) return ratio; else { float deltaT = ratio * mDuration; float starVal = 0; float endVal = 1; float mNaturalFreq = (float) Math.sqrt(mStiffness); float mDampedFreq = (float)(mNaturalFreq*Math.sqrt(1.0 - mDampingRatio* mDampingRatio)); float lastVelocity = mVelocity; //float lastDisplacement = ratio - endVal* deltaT/60 - endVal; float lastDisplacement = ratio - endVal; float coeffB = (float) (1.0 / mDampedFreq * (mDampingRatio * mNaturalFreq * lastDisplacement + lastVelocity)); float displacement = (float) (Math.pow(Math.E,-mDampingRatio * mNaturalFreq * deltaT) * (lastDisplacement * Math.cos(mDampedFreq * deltaT) + coeffB * Math.sin(mDampedFreq * deltaT))); float mValue = displacement + endVal; Log.e("ratio",String.valueOf(displacement)); if(mDuration == 0){ return starVal; } else{ return mValue; } } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/AndroidSpringInterpolator2.java ================================================ package com.martinrgb.animer.core.interpolator; import android.animation.TimeInterpolator; import android.util.Log; public class AndroidSpringInterpolator2 implements TimeInterpolator { //Parameters private float mStiffness; private float mDampingRatio; private float mVelocity; private float mDuration; private boolean canGetDuration = true; public AndroidSpringInterpolator2(float stiffness, float dampingratio,float velocity) { this.mStiffness = stiffness; this.mDampingRatio = dampingratio; this.mVelocity = velocity/1000.f; } @Override public float getInterpolation(float ratio) { if(canGetDuration){ getDuration(ratio); } float starVal = 0; float endVal = 1; float mDeltaT = ratio * mDuration; float lastDisplacement = ratio - endVal; float mNaturalFreq = (float) Math.sqrt(mStiffness); float mDampedFreq = (float)(mNaturalFreq*Math.sqrt(1.0 - mDampingRatio* mDampingRatio)); float cosCoeff = lastDisplacement; float sinCoeff = (float) (1.0 / mDampedFreq * (mDampingRatio * mNaturalFreq * lastDisplacement + mVelocity)); float displacement = (float) (Math.pow(Math.E,-mDampingRatio * mNaturalFreq * mDeltaT) * (cosCoeff * Math.cos(mDampedFreq * mDeltaT) + sinCoeff * Math.sin(mDampedFreq * mDeltaT))); mVelocity = (float) (displacement * (-mNaturalFreq) * mDampingRatio + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * mDeltaT) * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * mDeltaT) + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * mDeltaT))); float mValue = displacement + endVal; Log.e("mValue",String.valueOf(mValue)); return mValue; } public void setVelocityInSeconds(float velocity){ mVelocity = velocity/1000.f; } public void setDampingRatio(float dampingRatio){ mDampingRatio = dampingRatio; } private void getDuration(float ratio){ if(ratio !=0){ float oneFrameRatio = ratio - 0; float timeInMs = (1.f/oneFrameRatio)*(1000.f/60.f); mDuration = timeInMs/1000.f; canGetDuration = false; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/CustomBounceInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator; public class CustomBounceInterpolator extends AnInterpolator{ //Parameters private static final float maxStifness = 50.f; private static final float maxFrictionMultipler = 1.f; private float mTension = 0.f; private float mFriction = 0.f; //Curve Position parameters(No Adjust) private static final float amplitude = 1.f; private static final float phase = 0.f; //Original Scale parameters(Better No Adjust) private static final float originalStiffness = 12.f; private static final float originalFrictionMultipler = 0.3f; private static final float mass = 0.058f; //Internal parameters private float pulsation; private float friction; private void computePulsation() { this.pulsation = (float) Math.sqrt((originalStiffness + mTension) / mass); } private void computeFriction() { this.friction = (originalFrictionMultipler + mFriction) * pulsation; } private void computeInternalParameters() { // never call computeFriction() without // updating the pulsation computePulsation(); computeFriction(); } public CustomBounceInterpolator( float tension, float friction) { this.mTension = Math.min(Math.max(tension,0.f),100.f) * (maxStifness- originalStiffness)/100.f; this.mFriction = Math.min(Math.max(friction,0.f),100.f) * (maxFrictionMultipler - originalFrictionMultipler)/100.f; computeInternalParameters(); initArgData(0,tension,"tension",0,100); initArgData(1,friction,"friction",0,100); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ this.mTension = Math.min(Math.max(value,0.f),100.f) * (maxStifness- originalStiffness)/100.f; } if(i == 1){ this.mFriction = Math.min(Math.max(friction,0.f),100.f) * (maxFrictionMultipler - originalFrictionMultipler)/100.f; } computeInternalParameters(); } public CustomBounceInterpolator() { computeInternalParameters(); } @Override public float getInterpolation(float ratio) { if (ratio == 0.0f || ratio == 1.0f) return ratio; else { float value = amplitude * (float) Math.exp(-friction * ratio) * (float) Math.cos(pulsation * ratio + phase) ; return -Math.abs(value)+1.f; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/CustomDampingInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator; public class CustomDampingInterpolator extends AnInterpolator{ //Parameters private static final float maxStifness = 50.f; private static final float maxFrictionMultipler = 1.f; private float mTension = 0.f; private float mFriction = 0.f; //Curve Position parameters(No Adjust) private static final float amplitude = 1.f; private static final float phase = 0.f; //Original Scale parameters(Better No Adjust) private static final float originalStiffness = 12.f; private static final float originalFrictionMultipler = 0.3f; private static final float mass = 0.058f; //Internal parameters private float pulsation; private float friction; private void computePulsation() { this.pulsation = (float) Math.sqrt((originalStiffness + mTension) / mass); } private void computeFriction() { this.friction = (originalFrictionMultipler + mFriction) * pulsation; } private void computeInternalParameters() { // never call computeFriction() without // updating the pulsation computePulsation(); computeFriction(); } public CustomDampingInterpolator( float tension, float friction) { this.mTension = Math.min(Math.max(tension,0.f),100.f) * (maxStifness- originalStiffness)/100.f; this.mFriction = Math.min(Math.max(friction,0.f),100.f) * (maxFrictionMultipler - originalFrictionMultipler)/100.f; // this.mFriction = friction; // this.mTension = tension; computeInternalParameters(); initArgData(0,tension,"tension",0,100); initArgData(1,friction,"friction",0,100); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ this.mTension = Math.min(Math.max(value,0.f),100.f) * (maxStifness- originalStiffness)/100.f; } if(i == 1){ this.mFriction = Math.min(Math.max(friction,0.f),100.f) * (maxFrictionMultipler - originalFrictionMultipler)/100.f; } computeInternalParameters(); } public CustomDampingInterpolator() { computeInternalParameters(); } @Override public float getInterpolation(float ratio) { if (ratio == 0.0f || ratio == 1.0f) return ratio; else { float value = amplitude * (float) Math.exp(-friction * ratio) * (float) Math.cos(pulsation * ratio + phase) ; return -value+1; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/CustomMocosSpringInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator; public class CustomMocosSpringInterpolator extends AnInterpolator{ private double mGamma, mVDiv2; private boolean mOscilative; private double mEps; private double mA, mB; private double mDuration; private double tension,damping,velocity; public CustomMocosSpringInterpolator(double tension, double damping) { this(tension, damping, 0.001); this.setInitialVelocity(20.); initArgData(0,(float) tension,"tension",0,200); initArgData(1,(float) damping,"damping",0,100); initArgData(2,(float) 20,"velocity",0,1000); } public CustomMocosSpringInterpolator(double tension, double damping, double velocity) { //mEps = eps; this.tension = tension; this.damping = damping; this.velocity = velocity; init(); initArgData(0,(float) tension,"tension",0,200); initArgData(1,(float) damping,"damping",0,100); initArgData(2,(float) velocity,"velocity",0,1000); } private void init(){ mEps = 0.001; mOscilative = (4 * this.tension - this.damping * this.damping > 0); if (mOscilative) { mGamma = Math.sqrt(4 * this.tension - this.damping * this.damping) / 2; mVDiv2 = this.damping / 2; } else { mGamma = Math.sqrt(this.damping * this.damping - 4 * this.tension) / 2; mVDiv2 = this.damping / 2; } this.setInitialVelocity(velocity); } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ this.tension = (double) value; init(); } if(i == 1){ this.damping = (double) value; init(); } if(i == 2){ this.velocity = (double) value; init(); } } public void setInitialVelocity(double v0) { if (mOscilative) { mB = Math.atan(-mGamma / (v0 - mVDiv2)); mA = -1 / Math.sin(mB); mDuration = Math.log(Math.abs(mA) / mEps) / mVDiv2; } else { mA = (v0 - (mGamma + mVDiv2)) / (2 * mGamma); mB = -1 - mA; mDuration = Math.log(Math.abs(mA) / mEps) / (mVDiv2 - mGamma); } } public double getDesiredDuration() { return mDuration; } @Override public float getInterpolation(float input) { if (input >= 1) { return 1; } double t = input * mDuration; return (float) (mOscilative ? (mA * Math.exp(-mVDiv2 * t) * Math.sin(mGamma * t + mB) + 1) : (mA * Math.exp((mGamma - mVDiv2) * t) + mB * Math.exp(-(mGamma + mVDiv2) * t) + 1)); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/CustomSpringInterpolator.java ================================================ package com.martinrgb.animer.core.interpolator; public class CustomSpringInterpolator extends AnInterpolator{ private float factor = 0.5f; public CustomSpringInterpolator(float factor) { this.factor = factor; initArgData(0,(float) factor,"factor",0,10); } public CustomSpringInterpolator() { initArgData(0,(float) 0.5,"factor",0,10); } @Override public float getInterpolation(float ratio) { if (ratio == 0.0f || ratio == 1.0f) return ratio; else { float value = (float) (Math.pow(2, -10 * ratio) * Math.sin((ratio - factor / 4.0d) * (2.0d * Math.PI) / factor) + 1); return value; } } @Override public void resetArgValue(int i, float value){ setArgValue(i,value); if(i == 0){ factor = value; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/interpolator/FlingSpringAnim.java ================================================ /* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.anim; import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener; import androidx.dynamicanimation.animation.FlingAnimation; import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; /** * Given a property to animate and a target value and starting velocity, first apply friction to * the fling until we pass the target, then apply a spring force to pull towards the target. */ public class FlingSpringAnim { private static final float FLING_FRICTION = 1.5f; private static final float SPRING_STIFFNESS = 400f; private static final float SPRING_DAMPING = 0.8f; private final FlingAnimation mFlingAnim; private SpringAnimation mSpringAnim; private float mTargetPosition; public FlingSpringAnim(K object, FloatPropertyCompat property, float startPosition, float targetPosition, float startVelocity, float minVisChange, float minValue, float maxValue, float springVelocityFactor, OnAnimationEndListener onEndListener) { mFlingAnim = new FlingAnimation(object, property) .setFriction(FLING_FRICTION) // Have the spring pull towards the target if we've slowed down too much before // reaching it. .setMinimumVisibleChange(minVisChange) .setStartVelocity(startVelocity) .setMinValue(minValue) .setMaxValue(maxValue); mTargetPosition = targetPosition; mFlingAnim.addEndListener(((animation, canceled, value, velocity) -> { mSpringAnim = new SpringAnimation(object, property) .setStartValue(value) .setStartVelocity(velocity * springVelocityFactor) .setSpring(new SpringForce(mTargetPosition) .setStiffness(SPRING_STIFFNESS) .setDampingRatio(SPRING_DAMPING)); mSpringAnim.addEndListener(onEndListener); mSpringAnim.animateToFinalPosition(mTargetPosition); })); } public float getTargetPosition() { return mTargetPosition; } public void updatePosition(float startPosition, float targetPosition) { mFlingAnim.setMinValue(Math.min(startPosition, targetPosition)) .setMaxValue(Math.max(startPosition, targetPosition)); mTargetPosition = targetPosition; if (mSpringAnim != null) { mSpringAnim.animateToFinalPosition(mTargetPosition); } } public void start() { mFlingAnim.start(); } public void end() { mFlingAnim.cancel(); if (mSpringAnim.canSkipToEnd()) { mSpringAnim.skipToEnd(); } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/calculator/FlingCalculator.java ================================================ package com.martinrgb.animer.core.math.calculator; public class FlingCalculator { private float mFriction; private float mVelocity; private float mDuration; private float mTransiton; public FlingCalculator(float velocity,float friction) { mFriction = friction*-4.2f; mVelocity = velocity; mDuration = calculate()[0]; mTransiton = calculate()[1]; } private float[] calculate() { float sampleScale = 1.5f; float maxItertation = 0; float maxValue = 0; float sampeScale = 1.5f; for (float i = 1 / (60 * sampleScale); i < 20.; i += 1 / (60 * sampleScale)) { float currentVelocity = mVelocity * (float) Math.exp(i * mFriction); float currentTransition = (mVelocity / mFriction) * (float) (Math.exp(mFriction * i) - 1); float speedThereshold = 2.3f; if (Math.abs(currentVelocity) <= speedThereshold) { maxItertation = i; maxValue = (currentTransition); } else{ } } return new float[]{maxItertation, maxValue}; } public float getDuration() { return mDuration; } public float getTransiton() { return mTransiton; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/calculator/SpringInterpolatorCalculator.java ================================================ package com.martinrgb.animer.core.math.calculator; public class SpringInterpolatorCalculator{ private double mStiffness,mMass,mDamping,mFactor,mTime; private float mEpsilon,mDuration,mVelocity; public SpringInterpolatorCalculator(double stiffness,double dampingratio) { mStiffness = stiffness; mMass = 1; mDamping = computeDamping(stiffness,dampingratio,mMass); mVelocity = 0; mEpsilon = 1/1000f; } public double computeDamping(double stiffness,double dampingRatio,double mass){ //double mass = mMass; return dampingRatio * (2 * Math.sqrt(mass * stiffness)); } private double computeMaxValue() { float time = 0; float value = 0; float velocity = mVelocity; float maxValue = 0; while (!(time > 0 && Math.abs(velocity) < mEpsilon)) { time += mEpsilon; float k = 0 - (float) mStiffness; float b = 0 - (float) mDamping; float F_spring = k * ((value) - 1); float F_damper = b * (velocity); velocity += ((F_spring + F_damper) / mMass) * mEpsilon; value += velocity * mEpsilon; if (maxValue < value) { maxValue = value; } } mDuration = time; return maxValue; } private double computeSpringMax(double factor) { double maxValue = 0; double epsilon = mEpsilon; double count = 1 / epsilon; for (int i = 0; i < count; i++) { double x = i * mEpsilon; double result = Math.pow(2, -10 * x) * Math.sin((x - factor / 4) * (2 * Math.PI) / factor) + 1; if (maxValue < result) { maxValue = result; } } return maxValue; } private double findCloseNum(double num) { int arraySize= (int) (1/mEpsilon); double[] arr = new double[arraySize]; for (int i = 0; i < 1/mEpsilon - 1; i++) { arr[i] = this.computeSpringMax(i * mEpsilon); } int index = 0; double d_value = 10; for (int i = 0; i < arr.length; i++) { double new_d_value = Math.abs(arr[i] - num); if (new_d_value <= d_value) { if (new_d_value == d_value && arr[i] < arr[index]) { continue; } index = i; d_value = new_d_value; } } return index / (1/mEpsilon); } public float getFactor(){ return (float) findCloseNum(computeMaxValue()); } public float getDuration(){ return mDuration; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/converter/AnSpringConverter.java ================================================ package com.martinrgb.animer.core.math.converter; abstract class AnSpringConverter { public double mMass = 1; public double mStiffness; public double mDamping; public double mDampingRatio; public double mTension; public double mFriction; public double mBouncyTension; public double mBouncyFriction; public double mDuration; public double mS; public double mB; public double mBounciness; public double mSpeed; public double mVelocity; public AnSpringConverter() { } public double computeDamping(double stiffness,double dampingRatio,double mass){ //double mass = mMass; return dampingRatio * (2 * Math.sqrt(mass * stiffness)); } public double computeDampingRatio(double tension,double friction,double mass) { //double mass = mMass; return friction / (2 * Math.sqrt(mass * tension)); } public double computeDuration(double tension, double friction,double mass) { double epsilon = 0.001; double velocity = 0.0; //double mass = mMass; double dampingRatio = this.computeDampingRatio(tension, friction,mass); double undampedFrequency = Math.sqrt(tension / mass); if (dampingRatio < 1) { double a = Math.sqrt(1 - Math.pow(dampingRatio, 2)); double b = velocity / (a * undampedFrequency); double c = dampingRatio / a; double d = -((b - c) / epsilon); if (d <= 0) { return 0.0; } return Math.log(d) / (dampingRatio * undampedFrequency); } else { return 0.0; } } public double computeTension(double dampingratio,double duration,double mass) { //let mass = this.mass; double a = Math.sqrt(1 - Math.pow(dampingratio, 2)); double d = (dampingratio/a)*1000.; double tension = Math.pow(Math.log(d)/(dampingratio * duration),2)*mass; return tension; } public double computeFriction(double dampingratio,double tension,double mass){ //let mass = this.mass; double a = (2 * Math.sqrt(mass * tension)); double friction = dampingratio * a; return friction; } public double tensionConversion(double oValue) { return (oValue - 30.0) * 3.62 + 194.0; } public double frictionConversion(double oValue) { return (oValue - 8.0) * 3.0 + 25.0; } public double bouncyTesnionConversion(double tension){ return (tension - 194.0)/3.62 + 30.; } public double bouncyFrictionConversion(double friction){ return (friction - 25.)/3. + 8.; } public double getParaS(double n,double start,double end){ return (n - start)/(end - start); } public double getParaB(double finalVal, double start,double end) { double a = 1; double b = -2; double c = (finalVal - start)/(end-start); double root_part = Math.sqrt(b * b - 4 * a * c); double denom = 2 * a; double root1 = ( -b + root_part ) / denom; double root2 = ( -b - root_part ) / denom; if(root2 <0) return root1; if(root1 <0) return root2; return Math.min(root1,root2); } public double computeSpeed(double value,double startValue,double endValue){ return (value * (endValue - startValue) + startValue)*1.7 ; } public double normalize(double value, double startValue, double endValue) { return (value - startValue) / (endValue - startValue); } public double projectNormal(double n, double start, double end) { return start + (n * (end - start)); } public double linearInterpolation(double t, double start, double end) { return t * end + (1.0 - t) * start; } public double quadraticOutInterpolation(double t, double start, double end) { return linearInterpolation(2 * t - t * t, start, end); } public double b3Friction1(double x) { return (0.0007 * Math.pow(x, 3)) - (0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28; } public double b3Friction2(double x) { return (0.000044 * Math.pow(x, 3)) - (0.006 * Math.pow(x, 2)) + 0.36 * x + 2.; } public double b3Friction3(double x) { return (0.00000045 * Math.pow(x, 3)) - (0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84; } public double b3Nobounce(double tension) { double friction = 0; if (tension <= 18) { friction = this.b3Friction1(tension); } else if (tension > 18 && tension <= 44) { friction = this.b3Friction2(tension); } else { friction = this.b3Friction3(tension); } return friction; } public double computeDuration(double stiffness, double dampingratio) { double epsilon = 0.001; double velocity = 0.0; double mass = 1; double dampingRatio = dampingratio; double undampedFrequency = Math.sqrt(stiffness / mass); if (dampingRatio < 1) { double a = Math.sqrt(1 - Math.pow(dampingRatio, 2)); double b = velocity / (a * undampedFrequency); double c = dampingRatio / a; double d = -((b - c) / epsilon); if (d <= 0) { return 0.0; } return Math.log(d) / (dampingRatio * undampedFrequency); } else { return 0.0; } } public float getStiffness() { return (float) mStiffness; } public float getDampingRatio() { return (float) mDampingRatio; } public float getDuration(){ return (float) computeDuration(mStiffness,mDampingRatio); } public float getArg(int i) { if(i == 0){ return (float) mStiffness; } else if(i == 1){ return (float) mDampingRatio; } return -1; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/converter/AndroidSpringConverter.java ================================================ package com.martinrgb.animer.core.math.converter; public class AndroidSpringConverter extends AnSpringConverter { private boolean otherParaCalculation = false; public AndroidSpringConverter(double stiffness,double dampingratio) { super(); calculate(stiffness,dampingratio,1,0); } public AndroidSpringConverter(double stiffness,double dampingratio,double mass,double velocity) { super(); calculate(stiffness,dampingratio,mass,velocity); } private void calculate(double s,double d,double m,double v){ mStiffness = s; mDampingRatio = d; mMass = m; mVelocity = v; mDamping = this.computeDamping(mStiffness, mDampingRatio,mMass); mTension = mStiffness; mFriction = mDamping; if(otherParaCalculation){ mBouncyTension = this.bouncyTesnionConversion(mTension); mBouncyFriction = this.bouncyFrictionConversion(mFriction); mDuration = this.computeDuration(mTension, mFriction,mMass); mS = this.getParaS(mBouncyTension,0.5,200); mSpeed = this.computeSpeed(this.getParaS(mBouncyTension,0.5,200),0.,20.); mB = this.getParaB(mBouncyFriction,this.b3Nobounce(mBouncyTension), 0.01); mBounciness = 20*1.7*mB/0.8; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/converter/DHOConverter.java ================================================ package com.martinrgb.animer.core.math.converter; public class DHOConverter extends AnSpringConverter { private boolean otherParaCalculation = false; public DHOConverter(double stiffness,double damping) { super(); calculate(stiffness,damping,1,0); } public DHOConverter(double stiffness,double damping,double mass,double velocity) { super(); calculate(stiffness,damping,mass,velocity); } private void calculate(double s,double d,double m,double v){ mStiffness = s; mDamping = d; mMass = m; mVelocity = v; mTension = mStiffness; mFriction = mDamping; mDampingRatio = this.computeDampingRatio(mStiffness, mDamping,mMass); if(otherParaCalculation){ mBouncyTension = this.bouncyTesnionConversion(mTension); mBouncyFriction = this.bouncyFrictionConversion(mFriction); mDuration = this.computeDuration(mTension, mFriction,mMass); mS = this.getParaS(mBouncyTension,0.5,200); mSpeed = this.computeSpeed(this.getParaS(mBouncyTension,0.5,200),0.,20.); mB = this.getParaB(mBouncyFriction,this.b3Nobounce(mBouncyTension), 0.01); mBounciness = 20*1.7*mB/0.8; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/converter/OrigamiPOPConverter.java ================================================ package com.martinrgb.animer.core.math.converter; public class OrigamiPOPConverter extends AnSpringConverter { private boolean otherParaCalculation = false; public OrigamiPOPConverter(double bounciness,double speed) { super(); calculate(bounciness,speed,1,0); } public OrigamiPOPConverter(double bounciness,double speed,double mass,double velocity) { super(); calculate(bounciness,speed,mass,velocity); } private void calculate(double b,double s,double m,double v){ mBounciness = b; mSpeed = s; mMass = m; mVelocity = v; mB = this.normalize(mBounciness / 1.7, 0, 20.0); mB = this.projectNormal(mB, 0.0, 0.8); mS = this.normalize(mSpeed / 1.7, 0, 20.0); mBouncyTension = this.projectNormal(mS, 0.5, 200); mBouncyFriction = this.quadraticOutInterpolation(mB, this.b3Nobounce(this.mBouncyTension), 0.01); mTension = this.tensionConversion(mBouncyTension); mFriction = this.frictionConversion(mBouncyFriction); mStiffness = mTension; mDamping = mFriction; mDampingRatio = this.computeDampingRatio(mTension, mFriction,mMass); if(otherParaCalculation){ mDuration = this.computeDuration(mTension, mFriction,mMass); } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/converter/RK4Converter.java ================================================ package com.martinrgb.animer.core.math.converter; public class RK4Converter extends AnSpringConverter { private boolean otherParaCalculation = false; public RK4Converter(double tension,double friction) { super(); calculate(tension,friction,1,0); } public RK4Converter(double tension,double friction,double mass,double velocity) { super(); calculate(tension,friction,mass,velocity); } private void calculate(double t,double f,double m,double v){ mStiffness = t; mDamping = f; mMass = m; mVelocity = v; mTension = mStiffness; mFriction = mDamping; mDampingRatio = this.computeDampingRatio(mStiffness, mDamping,mMass); if(otherParaCalculation){ mBouncyTension = this.bouncyTesnionConversion(mTension); mBouncyFriction = this.bouncyFrictionConversion(mFriction); mDuration = this.computeDuration(mTension, mFriction,mMass); mS = this.getParaS(mBouncyTension,0.5,200); mSpeed = this.computeSpeed(this.getParaS(mBouncyTension,0.5,200),0.,20.); mB = this.getParaB(mBouncyFriction,this.b3Nobounce(mBouncyTension), 0.01); mBounciness = 20*1.7*mB/0.8; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/math/converter/UIViewSpringConverter.java ================================================ package com.martinrgb.animer.core.math.converter; public class UIViewSpringConverter extends AnSpringConverter { private boolean otherParaCalculation = false; public UIViewSpringConverter(double dampingratio,double duration) { super(); calculate(dampingratio,duration,1,0); } public UIViewSpringConverter(double dampingratio,double duration,double mass,double velocity) { super(); calculate(dampingratio,duration,mass,velocity); } private void calculate(double dampingRatio,double duration,double m,double v){ mDampingRatio = dampingRatio; mDuration = duration; mMass = m; mVelocity = v; mTension = this.computeTension(mDampingRatio,mDuration,mMass); mStiffness = mTension; if(otherParaCalculation){ mFriction = this.computeFriction(mDampingRatio,mTension,mMass); mDamping = mFriction; mBouncyTension = this.bouncyTesnionConversion(mTension); mBouncyFriction = this.bouncyFrictionConversion(mFriction); mS = this.getParaS(mBouncyTension,0.5,200); mSpeed = this.computeSpeed(this.getParaS(mBouncyTension,0.5,200),0.,20.); mB = this.getParaB(mBouncyFriction,this.b3Nobounce(mBouncyTension), 0.01); mBounciness = 20*1.7*mB/0.8; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/property/AnProperty.java ================================================ package com.martinrgb.animer.core.property; public abstract class AnProperty { final String mPropertyName; public AnProperty(String name) { mPropertyName = name; } public abstract float getValue(T object); public abstract void setValue(T object, float value); } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/solver/AnSolver.java ================================================ package com.martinrgb.animer.core.solver; import android.animation.TimeInterpolator; import android.util.Log; import android.view.animation.LinearInterpolator; public class AnSolver extends Object{ // ############################################ // Construct // ############################################ private Object arg1,arg2; private SolverListener mListener = null; private int SOLVER_MODE = -1; public AnSolver(Object val1,Object val2,int mode){ unBindSolverListener(); setSolverMode(mode); setArg1(val1); setArg2(val2); } public interface SolverListener { void onSolverUpdate(Object arg1, Object arg2); } public void bindSolverListener(SolverListener listener) { mListener = listener; } public void unBindSolverListener(){ if(mListener !=null){ mListener = null; } } public void setArg1(Object val){ arg1 = val; if(mListener !=null){ mListener.onSolverUpdate(arg1,arg2); } } public Object getArg1(){ return arg1; } public void setArg2(Object val){ arg2 = val; if(mListener !=null){ mListener.onSolverUpdate(arg1,arg2); } } public Object getArg2(){ return arg2; } public int getSolverMode() { return SOLVER_MODE; } public void setSolverMode(int solverMode) { if(getSolverMode() != solverMode){ unBindSolverListener(); SOLVER_MODE = solverMode; } } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/state/PhysicsState.java ================================================ package com.martinrgb.animer.core.state; import android.util.Log; import java.util.HashMap; import java.util.Map; public class PhysicsState { private float value; private float velocity; Map kvMap = new HashMap<>(); // ############################################ // Constructor // ############################################ public PhysicsState() { updatePhysics(0,0); setStateValue("Start",0); setStateValue("End",1); } public PhysicsState(float start) { updatePhysics(start,0); setStateValue("Start",start); setStateValue("End",0); } public PhysicsState(float start,float end) { updatePhysics(start,0); setStateValue("Start",start); setStateValue("End",end); } // ############################################ // PhysicsState Value's Getter & Setter // ############################################ public void updatePhysics(float val,float vel){ updatePhysicsValue(val); updatePhysicsVelocity(vel); } public void updatePhysicsVelocity(float vel){ velocity = vel; } public void updatePhysicsValue(float val){ value = val; } public float getPhysicsValue() { return value; } public float getPhysicsVelocity() { return velocity; } // ############################################ // PhysicsState State's Getter & Setter // ############################################ public void setStateValue(String key,float value){ kvMap.put(key,value); } public float getStateValue(String key){ try { return kvMap.get(key); } catch (Exception e) { e.printStackTrace(); Log.e("setStateValue first", Log.getStackTraceString(e)); } return -1; } //TODO: Prev State } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/util/AnSpringOscillateHelper.java ================================================ package com.martinrgb.animer.core.util; import android.os.SystemClock; import androidx.dynamicanimation.animation.SpringAnimation; //Examples // //final AnSpringOscillateHelper anSpringOscillateHelper = new AnSpringOscillateHelper(AnSpringOscillateHelper.TIME,500); //final AnSpringOscillateHelper anSpringOscillateHelper = new AnSpringOscillateHelper(AnSpringOscillateHelper.COUNT,4); // //springAnimation.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() { // @Override // public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) { // anSpringOscillateHelper.observe(springAnimation_2,velocity,value,startValue,endValue); // } //}); // //springAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() { // @Override // public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, float velocity) { // anSpringOscillateHelper.reset(); // } //}); // for rapid stop spring animation public class AnSpringOscillateHelper { public abstract static class OscillateLimitedMode{ } public static final OscillateLimitedMode TIME = new OscillateLimitedMode() {}; public static final OscillateLimitedMode COUNT = new OscillateLimitedMode() {}; private OscillateLimitedMode mLimitedMode; private int mOscillateCounter = 0; private int mLimitedCounts ; private long mOscillateTimer = 0; private long mLimitedTime ; private boolean mShouldTriggerOnce = true; private float prevStiffness,prevDampingRatio; private static final float ACCELERATION_STIFFNESS = 3000f; private static final float ACCELERATION_DAMPINGRATIO = 0.99f; public AnSpringOscillateHelper(OscillateLimitedMode oscillateLimitedMode,int value){ if(oscillateLimitedMode == TIME){ setLimitedMode(TIME); setLimitedTime(value); } else if(oscillateLimitedMode == COUNT){ setLimitedMode(COUNT); setLimitedCounts(value); } } public void observe(SpringAnimation springAnimation,float currentVelocity,float currentValue,float startValue,float endValue){ if(mShouldTriggerOnce){ mOscillateTimer = SystemClock.elapsedRealtime(); resetAccelerationAnimation(springAnimation); mShouldTriggerOnce = false; } if(getLimitedMode() == TIME){ float animationElapsedTime = SystemClock.elapsedRealtime() - mOscillateTimer; if(animationElapsedTime > getLimitedTime()){ springAnimation.getSpring().setStiffness(ACCELERATION_STIFFNESS); springAnimation.getSpring().setDampingRatio(ACCELERATION_DAMPINGRATIO); } } if(getLimitedMode() == COUNT){ float finalPos = springAnimation.getSpring().getFinalPosition(); // from -> to if(finalPos == endValue){ if(mOscillateCounter %2==0 && currentValue < finalPos){ mOscillateCounter++; } if(mOscillateCounter %2!=0 && currentValue > finalPos){ mOscillateCounter++; } if((mOscillateCounter +1)/2 == getLimitedCounts() && currentValue > finalPos){ springAnimation.getSpring().setStiffness(ACCELERATION_STIFFNESS); springAnimation.getSpring().setDampingRatio(ACCELERATION_DAMPINGRATIO); } } // to -> from if(finalPos == startValue){ if(mOscillateCounter %2==0 && currentValue > finalPos){ mOscillateCounter++; } if(mOscillateCounter %2!=0 && currentValue < finalPos){ mOscillateCounter++; } if((mOscillateCounter +1)/2 == getLimitedCounts() && currentValue < finalPos){ springAnimation.getSpring().setStiffness(ACCELERATION_STIFFNESS); springAnimation.getSpring().setDampingRatio(ACCELERATION_DAMPINGRATIO); } } } } public void reset(){ mOscillateCounter = 0; mShouldTriggerOnce = true; } private void resetAccelerationAnimation(SpringAnimation springAnimation){ if(springAnimation.getSpring().getStiffness() == ACCELERATION_STIFFNESS){ springAnimation.getSpring().setStiffness(prevStiffness); } else{ prevStiffness = springAnimation.getSpring().getStiffness(); } if(springAnimation.getSpring().getDampingRatio() == ACCELERATION_DAMPINGRATIO){ springAnimation.getSpring().setDampingRatio(prevDampingRatio); } else{ prevDampingRatio = springAnimation.getSpring().getDampingRatio(); } } // getter & setter public void setLimitedCounts(int counts) { this.mLimitedCounts = counts; } public float getLimitedCounts(){return this.mLimitedCounts; } public void setLimitedTime(long time) { this.mLimitedTime = time; } public long getLimitedTime(){return this.mLimitedTime; } private void setLimitedMode(OscillateLimitedMode oscillateLimitMode) { this.mLimitedMode = oscillateLimitMode; } private OscillateLimitedMode getLimitedMode() { return this.mLimitedMode; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/core/util/AnUtil.java ================================================ package com.martinrgb.animer.core.util; // From Facebook Rebound public class AnUtil { /** * Map a value within a given range to another range. * @param value the value to map * @param fromLow the low end of the range the value is within * @param fromHigh the high end of the range the value is within * @param toLow the low end of the range to map to * @param toHigh the high end of the range to map to * @return the mapped value */ public static float mapValueFromRangeToRange( float value, float fromLow, float fromHigh, float toLow, float toHigh) { float fromRangeSize = fromHigh - fromLow; float toRangeSize = toHigh - toLow; float valueScale = (value - fromLow) / fromRangeSize; return toLow + (valueScale * toRangeSize); } public static float mapClampedValueFromRangeToRange( float value, float fromLow, float fromHigh, float toLow, float toHigh) { float fromRangeSize = fromHigh - fromLow; float toRangeSize = toHigh - toLow; float valueScale = (value - fromLow) / fromRangeSize; return toLow + (Math.max(0,Math.min(1,valueScale)) * toRangeSize); } /** * Clamp a value to be within the provided range. * @param value the value to clamp * @param low the low end of the range * @param high the high end of the range * @return the clamped value */ public static double clamp(double value, double low, double high) { return Math.min(Math.max(value, low), high); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/AnConfigData.java ================================================ package com.martinrgb.animer.monitor; import android.util.Log; import java.util.LinkedHashMap; public class AnConfigData { private int DATA_MODE = 0; LinkedHashMap configMap = new LinkedHashMap(); public AnConfigData(Object o1,Object o2,int mode) { setMode(mode); addConfig("arg1",o1); addConfig("arg2",o2); } public AnConfigData(Object o1,Object o2) { addConfig("arg1",o1); addConfig("arg2",o2); } public void setArguments(String type,String arg1,Object arg1_min,Object arg1_max,String arg2,Object arg2_min,Object arg2_max){ addConfig("converter_type",type); addConfig("arg1_name",arg1); addConfig("arg1_min",arg1_min); addConfig("arg1_max",arg1_max); addConfig("arg2_name",arg2); addConfig("arg2_min",arg2_min); addConfig("arg2_max",arg2_max); } // ############################################ // PhysicsState State's Getter & Setter // ############################################ public void addConfig(String key, Object value){ configMap.put(key,value); } public LinkedHashMap getConfigs(){ return configMap; } public void clearConfigs(){ configMap.clear(); } public void cloneConfigFrom(LinkedHashMap targetMap){ if(targetMap instanceof LinkedHashMap){ configMap.clear(); configMap = (LinkedHashMap) targetMap.clone(); } else{ } } public Object getKeyByString(String key){ try { return configMap.get(key); } catch (Exception e) { e.printStackTrace(); Log.e("setStateValue first", Log.getStackTraceString(e)); } return -1; } // TODO:Cannot use this,only String works public Object[] getConfigByIndex(int index){ Object key = configMap.keySet().toArray()[index]; Object value = configMap.get(key); Log.e("index: ",String.valueOf(index)); Log.e("key: ",String.valueOf(key)); Log.e("value: ",String.valueOf(value)); //return valueForFirstKey; return new Object[]{key,value}; } public int getMode() { return DATA_MODE; } public void setMode(int mode) { DATA_MODE = mode; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/AnConfigMap.java ================================================ package com.martinrgb.animer.monitor; import java.util.LinkedHashMap; public class AnConfigMap extends LinkedHashMap { private AnConfigMap mLinkedHashMap; public AnConfigMap() { mLinkedHashMap = this; } public Object getValue(int index){ Object key = mLinkedHashMap.keySet().toArray()[index]; Object value = mLinkedHashMap.get(key); return value; } public Object getKey(int index){ Object key = mLinkedHashMap.keySet().toArray()[index]; return key; } public void resetIndex(int index,Object value){ Object key = mLinkedHashMap.keySet().toArray()[index]; mLinkedHashMap.replace(key,value); } public int getIndexByString(String string){ for(int i = 0;i< mLinkedHashMap.size();i++){ Object key = mLinkedHashMap.keySet().toArray()[i]; Object value = mLinkedHashMap.get(key); if(string.equals(key.toString())){ return i; } } return -1; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/AnConfigRegistry.java ================================================ package com.martinrgb.animer.monitor; import com.martinrgb.animer.Animer; import com.martinrgb.animer.core.interpolator.AndroidNative.AccelerateDecelerateInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.AccelerateInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.AnticipateInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.AnticipateOvershootInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.BounceInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.FastOutLinearInInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.FastOutSlowInInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.LinearInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.LinearOutSlowInInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.PathInterpolator; import com.martinrgb.animer.core.interpolator.AndroidSpringInterpolator; import com.martinrgb.animer.core.interpolator.CustomBounceInterpolator; import com.martinrgb.animer.core.interpolator.CustomDampingInterpolator; import com.martinrgb.animer.core.interpolator.CustomMocosSpringInterpolator; import com.martinrgb.animer.core.interpolator.CustomSpringInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.CycleInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.DecelerateInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.OvershootInterpolator; public class AnConfigRegistry { private static final AnConfigRegistry INSTANCE = new AnConfigRegistry(); public static AnConfigRegistry getInstance() { return INSTANCE; } private final AnConfigMap mAnimerMap; private AnConfigMap mSolverMap; AnConfigRegistry() { mAnimerMap = new AnConfigMap(); initSolverConfig(); } private void initSolverConfig(){ mSolverMap= new AnConfigMap(); mSolverMap.put("AndroidSpring",Animer.springDroid(1500,0.5f)); mSolverMap.put("AndroidFling",Animer.flingDroid(4000,0.8f)); mSolverMap.put("iOSUIViewSpring",Animer.springiOSUIView(0.5f,0.5f)); mSolverMap.put("iOSCoreAnimationSpring",Animer.springiOSCoreAnimation(100,10)); mSolverMap.put("OrigamiPOPSpring",Animer.springOrigamiPOP(5,10)); mSolverMap.put("RK4Spring",Animer.springRK4(200,25)); mSolverMap.put("DHOSpring",Animer.springDHO(50,2f)); mSolverMap.put("ProtopieSpring",Animer.springProtopie(300,15f)); mSolverMap.put("PrincipleSpring",Animer.springPrinciple(380,20f)); mSolverMap.put("CubicBezier",Animer.interpolatorDroid(new PathInterpolator(0.5f,0.5f,0.5f,0.5f),500)); mSolverMap.put("LinearInterpolator",Animer.interpolatorDroid(new LinearInterpolator(),500)); mSolverMap.put("AccelerateDecelerateInterpolator",Animer.interpolatorDroid(new AccelerateDecelerateInterpolator(),500)); mSolverMap.put("AccelerateInterpolator",Animer.interpolatorDroid(new AccelerateInterpolator(2),500)); mSolverMap.put("DecelerateInterpolator",Animer.interpolatorDroid(new DecelerateInterpolator(2),500)); mSolverMap.put("AnticipateInterpolator",Animer.interpolatorDroid(new AnticipateInterpolator(2),500)); mSolverMap.put("OvershootInterpolator",Animer.interpolatorDroid(new OvershootInterpolator(2),500)); mSolverMap.put("AnticipateOvershootInterpolator",Animer.interpolatorDroid(new AnticipateOvershootInterpolator(2),500)); mSolverMap.put("BounceInterpolator",Animer.interpolatorDroid(new BounceInterpolator(),500)); mSolverMap.put("CycleInterpolator",Animer.interpolatorDroid(new CycleInterpolator(2),500)); mSolverMap.put("FastOutSlowInInterpolator",Animer.interpolatorDroid(new FastOutSlowInInterpolator(),500)); mSolverMap.put("LinearOutSlowInInterpolator",Animer.interpolatorDroid(new LinearOutSlowInInterpolator(),500)); mSolverMap.put("FastOutLinearInInterpolator",Animer.interpolatorDroid(new FastOutLinearInInterpolator(),500)); mSolverMap.put("CustomMocosSpringInterpolator",Animer.interpolatorDroid(new CustomMocosSpringInterpolator(100,15,0),500)); mSolverMap.put("CustomSpringInterpolator",Animer.interpolatorDroid(new CustomSpringInterpolator(0.5f),500)); mSolverMap.put("CustomBounceInterpolator",Animer.interpolatorDroid(new CustomBounceInterpolator(0,0),500)); mSolverMap.put("CustomDampingInterpolator",Animer.interpolatorDroid(new CustomDampingInterpolator(0,0),500)); mSolverMap.put("AndroidSpringInterpolator",Animer.interpolatorDroid(new AndroidSpringInterpolator(1500,0.5f,500),500)); } public void removeAllSolverConfig(){ mSolverMap.clear(); } public boolean addSolver(String configName, Animer.AnimerSolver animerSolver) { if (animerSolver == null) { throw new IllegalArgumentException("animerSolver is required"); } if (configName == null) { throw new IllegalArgumentException("configName is required"); } if (mSolverMap.containsKey(animerSolver)) { return false; } mSolverMap.put(configName,animerSolver); return true; } public boolean addAnimer(String configName,Animer animer) { if (animer == null) { throw new IllegalArgumentException("animer is required"); } if (configName == null) { throw new IllegalArgumentException("configName is required"); } if (mAnimerMap.containsKey(animer)) { return false; } mAnimerMap.put(configName,animer); return true; } public boolean removeAnimerConfig(Animer animer) { if (animer == null) { throw new IllegalArgumentException("animer is required"); } return mAnimerMap.remove(animer) != null; } public void removeAllAnimerConfig() { mAnimerMap.clear(); } public AnConfigMap getAllAnimer() { return mAnimerMap; } public AnConfigMap getAllSolverTypes(){ // AnConfigMap map = new AnConfigMap(); // map.put("AndroidSpring",Animer.springDroid(1500,0.5f)); // map.put("AndroidFling",Animer.flingDroid(4000,0.8f)); // map.put("iOSUIViewSpring",Animer.springiOSUIView(0.5f,0.5f)); // map.put("iOSCoreAnimationSpring",Animer.springiOSCoreAnimation(100,10)); // map.put("OrigamiPOPSpring",Animer.springOrigamiPOP(5,10)); // map.put("RK4Spring",Animer.springRK4(200,25)); // map.put("DHOSpring",Animer.springDHO(50,2f)); // map.put("ProtopieSpring",Animer.springProtopie(300,15f)); // map.put("PrincipleSpring",Animer.springPrinciple(380,20f)); // map.put("CubicBezier",Animer.interpolatorDroid(new PathInterpolator(0.5f,0.5f,0.5f,0.5f),500)); // map.put("LinearInterpolator",Animer.interpolatorDroid(new LinearInterpolator(),500)); // map.put("AccelerateDecelerateInterpolator",Animer.interpolatorDroid(new AccelerateDecelerateInterpolator(),500)); // map.put("AccelerateInterpolator",Animer.interpolatorDroid(new AccelerateInterpolator(2),500)); // map.put("DecelerateInterpolator",Animer.interpolatorDroid(new DecelerateInterpolator(2),500)); // map.put("AnticipateInterpolator",Animer.interpolatorDroid(new AnticipateInterpolator(2),500)); // map.put("OvershootInterpolator",Animer.interpolatorDroid(new OvershootInterpolator(2),500)); // map.put("AnticipateOvershootInterpolator",Animer.interpolatorDroid(new AnticipateOvershootInterpolator(2),500)); // map.put("BounceInterpolator",Animer.interpolatorDroid(new BounceInterpolator(),500)); // map.put("CycleInterpolator",Animer.interpolatorDroid(new CycleInterpolator(2),500)); // map.put("FastOutSlowInInterpolator",Animer.interpolatorDroid(new FastOutSlowInInterpolator(),500)); // map.put("LinearOutSlowInInterpolator",Animer.interpolatorDroid(new LinearOutSlowInInterpolator(),500)); // map.put("FastOutLinearInInterpolator",Animer.interpolatorDroid(new FastOutLinearInInterpolator(),500)); // map.put("CustomMocosSpringInterpolator",Animer.interpolatorDroid(new CustomMocosSpringInterpolator(100,15,0),500)); // map.put("CustomSpringInterpolator",Animer.interpolatorDroid(new CustomSpringInterpolator(0.5f),500)); // map.put("CustomBounceInterpolator",Animer.interpolatorDroid(new CustomBounceInterpolator(0,0),500)); // map.put("CustomDampingInterpolator",Animer.interpolatorDroid(new CustomDampingInterpolator(0,0),500)); // map.put("AndroidSpringInterpolator",Animer.interpolatorDroid(new AndroidSpringInterpolator(1500,0.5f,500),500)); // return map; return mSolverMap; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/AnConfigView.java ================================================ package com.martinrgb.animer.monitor; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.opengl.GLSurfaceView; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.AdapterView; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TableLayout; import android.widget.TextView; import androidx.core.content.ContextCompat; import com.martinrgb.animer.Animer; import com.martinrgb.animer.R; import com.martinrgb.animer.core.interpolator.AnInterpolator; import com.martinrgb.animer.core.math.converter.DHOConverter; import com.martinrgb.animer.core.math.converter.OrigamiPOPConverter; import com.martinrgb.animer.core.math.converter.RK4Converter; import com.martinrgb.animer.core.math.converter.UIViewSpringConverter; import com.martinrgb.animer.monitor.fps.FPSDetector; import com.martinrgb.animer.monitor.fps.FrameDataCallback; import com.martinrgb.animer.monitor.shader.ShaderSurfaceView; import java.text.DecimalFormat; public class AnConfigView extends FrameLayout { private Spinner mSolverObjectSelectorSpinner,mSolverTypeSelectorSpinner; private AnSpinnerAdapter solverObjectSpinnerAdapter,solverTypeSpinnerAdapter; private Animer currentAnimer,mRevealAnimer,mFPSAnimer; private AnConfigRegistry anConfigRegistry; private LinearLayout listLayout; private SeekbarListener seekbarListener; private SolverSelectedListener solverSelectedListener; private ShaderSurfaceView shaderSurfaceView; //private final int mTextColor = Color.argb(255, 255, 255, 255); private int mainColor; private int secondaryColor; private int backgroundColor; private int fontSize; private Typeface typeface; private String currentObjectType = "NULL"; private int listSize = 2; private static int SEEKBAR_START_ID = 15000; private static int SEEKLABEL_START_ID_START_ID = 20000; private static int EDITTEXT_START_ID_START_ID = 25000; private static final int MAX_SEEKBAR_VAL = 100000; private static final int MIN_SEEKBAR_VAL = 1; //TODO private static final DecimalFormat DECIMAL_FORMAT_2 = new DecimalFormat("#.##"); private static final DecimalFormat DECIMAL_FORMAT_1 = new DecimalFormat("#.#"); private static final DecimalFormat DECIMAL_FORMAT_3 = new DecimalFormat("#.###"); private float MAX_VAL1,MAX_VAL2,MAX_VAL3,MAX_VAL4,MAX_VAL5; private float[] MAX_VALUES = new float[]{MAX_VAL1,MAX_VAL2,MAX_VAL3,MAX_VAL4,MAX_VAL5}; private float MIN_VAL1,MIN_VAL2,MIN_VAL3,MIN_VAL4,MIN_VAL5; private float[] MIN_VALUES = new float[]{MIN_VAL1,MIN_VAL2,MIN_VAL3,MIN_VAL4,MIN_VAL5}; private float RANGE_VAL1,RANGE_VAL2,RANGE_VAL3,RANGE_VAL4,RANGE_VAL5; private float[] RANGE_VALUES = new float[]{RANGE_VAL1,RANGE_VAL2,RANGE_VAL3,RANGE_VAL4,RANGE_VAL5}; private float seekBarValue1,seekBarValue2,seekBarValue3,seekBarValue4,seekBarValue5; private Object[] SEEKBAR_VALUES = new Object[]{seekBarValue1,seekBarValue2,seekBarValue3,seekBarValue4,seekBarValue5}; private TextView mArgument1SeekLabel,mArgument2SeekLabel,mArgument3SeekLabel,mArgument4SeekLabel,mArgument5SeekLabel; private TextView[] SEEKBAR_LABElS = new TextView[]{mArgument1SeekLabel,mArgument2SeekLabel,mArgument3SeekLabel,mArgument4SeekLabel,mArgument5SeekLabel}; private EditText mArgument1EditText,mArgument2EditText,mArgument3EditText,mArgument4EditText,mArgument5EditText; private EditText[] EDITTEXTS = new EditText[]{mArgument1EditText,mArgument2EditText,mArgument3EditText,mArgument4EditText,mArgument5EditText}; private SeekBar mArgument1SeekBar,mArgument2SeekBar,mArgument3SeekBar,mArgument4SeekBar,mArgument5SeekBar; private SeekBar[] SEEKBARS = new SeekBar[]{mArgument1SeekBar,mArgument2SeekBar,mArgument3SeekBar,mArgument4SeekBar,mArgument5SeekBar}; private final int MARGIN_SIZE = (int) getResources().getDimension(R.dimen.margin_size); private final int PADDING_SIZE = (int) getResources().getDimension(R.dimen.padding_size); private final int PX_120 = dpToPx(120, getResources()); private TextView fpsView; private AnConfigMap mSolverTypesMap; private AnConfigMap mAnimerObjectsMap; private Animer.TriggeredListener triggeredListener; private Context mContext; public AnConfigView(Context context) { this(context, null); } public AnConfigView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AnConfigView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(context); } private boolean hadInited = false; private void initView(Context context) { typeface = Typeface.createFromAsset(context.getAssets(), "Montserrat-SemiBold.ttf"); secondaryColor = ContextCompat.getColor(context, R.color.secondaryColor); mainColor = ContextCompat.getColor(context,R.color.mainColor); backgroundColor = ContextCompat.getColor(context,R.color.backgroundColor); fontSize = getResources().getDimensionPixelSize(R.dimen.font_size); View view = inflate(getContext(), R.layout.config_view, null); addView(view); //Log.e("r: ",String.valueOf() + "g:" + String.valueOf(Color.green(mainColor)) + "b:" + String.valueOf(String.valueOf(Color.blue(mainColor))) ); fpsView = findViewById(R.id.fps_view); fpsView.setTypeface(typeface); fpsView.setTextSize(fontSize); fpsView.setOnTouchListener(new OnFPSTouchListener()); shaderSurfaceView = findViewById(R.id.shader_surfaceview); shaderSurfaceView.setFactorInput(1500,0); shaderSurfaceView.setFactorInput(0.5f,1); shaderSurfaceView.setMainColor((float)Color.red(mainColor)/255.f,(float) Color.green(mainColor)/255.f,(float) Color.blue(mainColor)/255.f ); Log.e("rgb-r:", String.valueOf((float)Color.red(mainColor)/255.f)); Log.e("rgb-g:", String.valueOf((float)Color.green(mainColor)/255.f)); Log.e("rgb-b:", String.valueOf((float)Color.blue(mainColor)/255.f)); shaderSurfaceView.setSecondaryColor((float)Color.red(secondaryColor)/255.f,(float) Color.green(secondaryColor)/255.f,(float) Color.blue(secondaryColor)/255.f ); mContext = context; // ## Spinner anConfigRegistry = AnConfigRegistry.getInstance(); triggeredListener = new Animer.TriggeredListener() { @Override public void onTrigger(boolean triggered) { shaderSurfaceView.resetTime(); //TODO ResetWhen Request if(triggered){ shaderSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); } else{ shaderSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); } } }; solverObjectSpinnerAdapter = new AnSpinnerAdapter(context,getResources()); solverTypeSpinnerAdapter = new AnSpinnerAdapter(context,getResources()); mSolverObjectSelectorSpinner = findViewById(R.id.object_spinner); mSolverTypeSelectorSpinner = findViewById(R.id.type_spinner); solverSelectedListener = new SolverSelectedListener(); seekbarListener = new SeekbarListener(); mSolverObjectSelectorSpinner.setAdapter(solverObjectSpinnerAdapter); mSolverObjectSelectorSpinner.setOnItemSelectedListener(solverSelectedListener); mSolverTypeSelectorSpinner.setAdapter(solverTypeSpinnerAdapter); mSolverTypeSelectorSpinner.setOnItemSelectedListener(solverSelectedListener); refreshAnimerConfigs(); // ## List listLayout = findViewById(R.id.list_layout); // ## Nub View nub = findViewById(R.id.nub); nub.setOnTouchListener(new OnNubTouchListener()); ViewTreeObserver vto = view.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // Put your code here. if(!hadInited) { mRevealAnimer = new Animer(); mRevealAnimer.setSolver(Animer.springDroid(500, 0.95f)); mRevealAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { AnConfigView.this.setTranslationY(value); } }); mFPSAnimer = new Animer(); mFPSAnimer.setSolver(Animer.springDroid(600, 0.7f)); mFPSAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { fpsView.setScaleX(value); fpsView.setScaleY(value); } }); mRevealAnimer.setCurrentValue(-(AnConfigView.this.getMeasuredHeight() - getResources().getDimension(R.dimen.nub_height))); mFPSAnimer.setCurrentValue(1); hadInited = true; float maxValue = 0; float minValue = -(AnConfigView.this.getMeasuredHeight() - getResources().getDimension(R.dimen.nub_height)); if(setRevealed){ mRevealAnimer.setCurrentValue(maxValue); nub.setVisibility(INVISIBLE); } else{ mRevealAnimer.setCurrentValue(minValue); nub.setVisibility(VISIBLE); } } } }); this.setElevation(1000); } public void refreshAnimerConfigs() { mAnimerObjectsMap = anConfigRegistry.getAllAnimer(); solverObjectSpinnerAdapter.clear(); for(int i = 0; i< mAnimerObjectsMap.size(); i++){ solverObjectSpinnerAdapter.add(String.valueOf(mAnimerObjectsMap.getKey(i))); } solverObjectSpinnerAdapter.notifyDataSetChanged(); if (solverObjectSpinnerAdapter.getCount() > 0) { // object first time selection mSolverObjectSelectorSpinner.setSelection(0); initTypeConfigs(); } mSolverTypesMap = anConfigRegistry.getAllSolverTypes(); solverTypeSpinnerAdapter.clear(); for(int i = 0; i< mSolverTypesMap.size(); i++){ solverTypeSpinnerAdapter.add(String.valueOf(mSolverTypesMap.getKey(i))); } solverTypeSpinnerAdapter.notifyDataSetChanged(); if (solverObjectSpinnerAdapter.getCount() > 0) { // solver first time selection if(currentAnimer !=null && currentAnimer.getTriggerListener() !=null){ currentAnimer.removeTriggerListener(); } currentAnimer = (Animer) mAnimerObjectsMap.getValue(0); currentAnimer.setTriggerListener(triggeredListener); //shaderSurfaceView.requestRender(); recreateList(); int typeIndex = 0; // select the right interpolator if(String.valueOf(currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("converter_type")) == "AndroidInterpolator"){ typeIndex = mSolverTypesMap.getIndexByString(String.valueOf(currentAnimer.getCurrentSolver().getArg1().getClass().getSimpleName())); } // select the right animator else{ typeIndex = mSolverTypesMap.getIndexByString(String.valueOf(currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("converter_type"))); } mSolverTypeSelectorSpinner.setSelection(typeIndex,false); } } private void initTypeConfigs() { mSolverTypesMap = anConfigRegistry.getAllSolverTypes(); solverTypeSpinnerAdapter.clear(); for(int i = 0; i< mSolverTypesMap.size(); i++){ solverTypeSpinnerAdapter.add(String.valueOf(mSolverTypesMap.getKey(i))); } solverTypeSpinnerAdapter.notifyDataSetChanged(); if (solverObjectSpinnerAdapter.getCount() > 0) { // solver first time selection if(currentAnimer !=null && currentAnimer.getTriggerListener() !=null){ currentAnimer.removeTriggerListener(); } currentAnimer = (Animer) mAnimerObjectsMap.getValue(0); currentAnimer.setTriggerListener(triggeredListener); //shaderSurfaceView.requestRender(); recreateList(); int typeIndex = 0; // select the right interpolator if(String.valueOf(currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("converter_type")) == "AndroidInterpolator"){ typeIndex = mSolverTypesMap.getIndexByString(String.valueOf(currentAnimer.getCurrentSolver().getArg1().getClass().getSimpleName())); } // select the right animator else{ typeIndex = mSolverTypesMap.getIndexByString(String.valueOf(currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("converter_type"))); } mSolverTypeSelectorSpinner.setSelection(typeIndex,false); } } private void recreateList(){ FrameLayout.LayoutParams params; LinearLayout seekWrapper; TableLayout.LayoutParams tableLayoutParams = new TableLayout.LayoutParams(0,ViewGroup.LayoutParams.WRAP_CONTENT,1f); tableLayoutParams.setMargins(MARGIN_SIZE, MARGIN_SIZE, MARGIN_SIZE, MARGIN_SIZE); listLayout.removeAllViews(); if(currentAnimer.getCurrentSolverData().getKeyByString("converter_type").toString() != "AndroidInterpolator") { listSize =2; } else{ AnInterpolator mInterpolator = (AnInterpolator) currentAnimer.getCurrentSolver().getArg1(); listSize = 1 + (mInterpolator.getArgNum()) ; } for (int i = 0;i adapterView, View view, int i, long l) { if(adapterView == mSolverObjectSelectorSpinner){ // get animer from Map solverObjectSpinnerAdapter.setSelectedItemIndex(i); if(currentAnimer !=null && currentAnimer.getTriggerListener() !=null){ currentAnimer.removeTriggerListener(); } if(typeIndex !=-1){ prevTypeIndex = typeIndex; } currentAnimer = (Animer) mAnimerObjectsMap.getValue(i); currentAnimer.setTriggerListener(triggeredListener); //shaderSurfaceView.requestRender(); recreateList(); redefineMinMax(currentAnimer.getCurrentSolver()); updateSeekBars(currentAnimer.getCurrentSolver()); // will not excute in init if(objectChecker > 0){ typeSpinnerIsFixedSelection = true; // select the right interpolator if(String.valueOf(currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("converter_type")).toString().contains("AndroidInterpolator")){ typeIndex = mSolverTypesMap.getIndexByString(String.valueOf(currentAnimer.getCurrentSolver().getArg1().getClass().getSimpleName())); } // select the right animator else{ typeIndex = mSolverTypesMap.getIndexByString(currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("converter_type").toString()); } // when aniamtor type is equal,fix bugs if(mSolverTypeSelectorSpinner.getSelectedItemPosition() == typeIndex){ typeSpinnerIsFixedSelection = false; } mSolverTypeSelectorSpinner.setSelection(typeIndex,false); } objectChecker++; } else if (adapterView == mSolverTypeSelectorSpinner){ // will not excute in init solverTypeSpinnerAdapter.setSelectedItemIndex(i); if(typeChecker > 0) { if(typeSpinnerIsFixedSelection){ typeSpinnerIsFixedSelection = false; } //TODO Remeber Parameters Before else{ // reset animer from Map Animer.AnimerSolver seltectedSolver = (Animer.AnimerSolver) mSolverTypesMap.getValue(i); currentAnimer.setSolver(seltectedSolver); recreateList(); redefineMinMax(currentAnimer.getCurrentSolver()); updateSeekBars(currentAnimer.getCurrentSolver()); } } typeChecker++; } } @Override public void onNothingSelected(AdapterView adapterView) { } } private void redefineMinMax(Animer.AnimerSolver animerSolver){ currentObjectType = animerSolver.getConfigSet().getKeyByString("converter_type").toString(); if(currentObjectType != "AndroidInterpolator"){ for (int index = 0;index (float) mMax){ convertedValue = mMax; EDITTEXTS[mIndex].setText(String.valueOf(mMax)); } else if(convertedValue < (float) mMin) { convertedValue = mMin; EDITTEXTS[mIndex].setText(String.valueOf(mMin)); } float calculatedProgress = (convertedValue - MIN_VALUES[mIndex])/ RANGE_VALUES[mIndex]* (MAX_SEEKBAR_VAL - MIN_SEEKBAR_VAL) + MIN_SEEKBAR_VAL; canSetEditText = false; SEEKBARS[mIndex].setProgress((int) calculatedProgress); canSetEditText = true; } } } } public static boolean isNumeric(String strNum) { if (strNum == null) { return false; } try { double d = Double.parseDouble(strNum); } catch (NumberFormatException nfe) { return false; } return true; } private class SeekbarListener implements SeekBar.OnSeekBarChangeListener { @Override public void onProgressChanged(SeekBar seekBar, int val, boolean b) { //TODO Request Renderer if(currentObjectType != "AndroidInterpolator") { for (int i = 0; i < listSize; i++) { if (seekBar == SEEKBARS[i]) { SEEKBAR_VALUES[i] = ((float) (val - MIN_SEEKBAR_VAL) / (MAX_SEEKBAR_VAL - MIN_SEEKBAR_VAL)) * RANGE_VALUES[i] + MIN_VALUES[i]; if (i == 0) { String roundedValue1Label = DECIMAL_FORMAT_2.format(SEEKBAR_VALUES[i]); SEEKBAR_LABElS[i].setText((String) currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("arg" + String.valueOf(i + 1) + "_name") + ": "); if(canSetEditText){ EDITTEXTS[i].setText(roundedValue1Label); } currentAnimer.getCurrentSolver().getConfigSet().addConfig("arg" + String.valueOf(i + 1) + "", Float.valueOf(roundedValue1Label)); } else if (i == 1) { String roundedValue1Label = DECIMAL_FORMAT_3.format(SEEKBAR_VALUES[i]); SEEKBAR_LABElS[i].setText((String) currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("arg" + String.valueOf(i + 1) + "_name") + ": "); if(canSetEditText){ EDITTEXTS[i].setText(roundedValue1Label); } currentAnimer.getCurrentSolver().getConfigSet().addConfig("arg" + String.valueOf(i + 1) + "", Float.valueOf(roundedValue1Label)); } } } // Seekbar in Fling not works at all if (currentObjectType != "AndroidFling") { Object val1 = getConvertValueByIndexAndType(0, currentObjectType); Object val2 = getConvertValueByIndexAndType(1, currentObjectType); currentAnimer.getCurrentSolver().setArg1(val1); currentAnimer.getCurrentSolver().setArg2(val2); float convertVal1 = Float.valueOf(String.valueOf(val1)); float convertVal2 = Float.valueOf(String.valueOf(val2)); shaderSurfaceView.setCurveMode(1); shaderSurfaceView.setFactorInput(convertVal1,0); shaderSurfaceView.setFactorInput(convertVal2,1); } else{ Object val1 = getConvertValueByIndexAndType(0, currentObjectType); Object val2 = getConvertValueByIndexAndType(1, currentObjectType); float convertVal1 = Float.valueOf(String.valueOf(val1)); float convertVal2 = Float.valueOf(String.valueOf(val2)); currentAnimer.getCurrentSolver().setArg2(Math.max(0.01f,(float)val2)); shaderSurfaceView.setCurveMode(0); shaderSurfaceView.setFactorInput(convertVal1,0); shaderSurfaceView.setFactorInput(convertVal2,1); } } else{ getCurveModeByString(); // Interpolator Factor for (int i = 0; i < listSize - 1; i++) { if (seekBar == SEEKBARS[i]) { SEEKBAR_VALUES[i] = ((float) (val - MIN_SEEKBAR_VAL) / (MAX_SEEKBAR_VAL - MIN_SEEKBAR_VAL)) * RANGE_VALUES[i] + MIN_VALUES[i]; String roundedValue1Label = DECIMAL_FORMAT_3.format(SEEKBAR_VALUES[i]); SEEKBAR_LABElS[i].setText(((AnInterpolator) currentAnimer.getCurrentSolver().getArg1()).getArgString(i) + ": "); if(canSetEditText){ EDITTEXTS[i].setText(roundedValue1Label); } ((AnInterpolator) currentAnimer.getCurrentSolver().getArg1()).resetArgValue(i,Float.valueOf(roundedValue1Label)); shaderSurfaceView.setFactorInput(Float.valueOf(roundedValue1Label),i); if(currentAnimer.getCurrentSolver().getArg1().getClass().getSimpleName().contains("PathInterpolator")){ } } } // Interpolator Duration if (seekBar == SEEKBARS[listSize - 1]) { SEEKBAR_VALUES[listSize - 1] = ((float) (val - MIN_SEEKBAR_VAL) / (MAX_SEEKBAR_VAL - MIN_SEEKBAR_VAL)) * RANGE_VALUES[listSize - 1] + MIN_VALUES[listSize - 1]; String roundedValue1Label = DECIMAL_FORMAT_1.format(SEEKBAR_VALUES[listSize - 1]); SEEKBAR_LABElS[listSize - 1].setText((String) currentAnimer.getCurrentSolver().getConfigSet().getKeyByString("arg" + String.valueOf(2) + "_name") + ": "); if(canSetEditText){ EDITTEXTS[listSize - 1].setText(roundedValue1Label); } currentAnimer.getCurrentSolver().getConfigSet().addConfig("arg" + String.valueOf(2) + "", Float.valueOf(roundedValue1Label)); float floatVal = Float.valueOf(roundedValue1Label); currentAnimer.getCurrentSolver().setArg2( (long) floatVal); shaderSurfaceView.setDuration(floatVal/1000); } } shaderSurfaceView.requestRender(); } @Override public void onStartTrackingTouch(SeekBar seekBar) { isEditListenerWork = false; canSetEditText = true; } @Override public void onStopTrackingTouch(SeekBar seekBar) { isEditListenerWork = true; canSetEditText = false; } } private String[] interpolatorArray = new String[] { "PathInterpolator","LinearInterpolator","AccelerateDecelerateInterpolator","AccelerateInterpolator", "DecelerateInterpolator","AnticipateInterpolator","OvershootInterpolator","AnticipateOvershootInterpolator", "BounceInterpolator","CycleInterpolator","FastOutSlowInInterpolator","LinearOutSlowInInterpolator", "FastOutLinearInInterpolator","CustomMocosSpringInterpolator","CustomSpringInterpolator","CustomBounceInterpolator", "CustomDampingInterpolator","AndroidSpringInterpolator" }; private void getCurveModeByString(){ for(int i=0;i minValue && nubDragMoveY + currViewTransY< maxValue){ mRevealAnimer.setCurrentValue(nubDragMoveY + currViewTransY); } break; case MotionEvent.ACTION_UP: if( Math.abs(nubDragMoveY) > (maxValue - minValue)/3){ mRevealAnimer.setEndValue((currViewTransY == minValue)?maxValue:minValue); } else{ mRevealAnimer.setEndValue((currViewTransY == minValue)?minValue:maxValue); } break; case MotionEvent.ACTION_CANCEL: if( Math.abs(nubDragMoveY) > (maxValue - minValue)/3){ mRevealAnimer.setEndValue((currViewTransY == minValue)?maxValue:minValue); } else{ mRevealAnimer.setEndValue((currViewTransY == minValue)?minValue:maxValue); } break; } return true; } } private class OnFPSTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: mFPSAnimer.setEndValue(0.8f); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: mFPSAnimer.setEndValue(1f); if (String.valueOf(fpsView.getText()).contains("FPS")) { FPSDetector.create().addFrameDataCallback(new FrameDataCallback() { @Override public void doFrame(long previousFrameNS, long currentFrameNS, int droppedFrames, float currentFPS) { if(currentFPS <50 && currentFPS > 30){ fpsView.setTextColor(Color.YELLOW); } else if(currentFPS>=50){ fpsView.setTextColor(Color.GREEN); } else if(currentFPS < 30){ fpsView.setTextColor(Color.RED); } fpsView.setText(String.valueOf(currentFPS)); } }).show(mContext); } else { FPSDetector.hide(mContext); fpsView.setTextColor(backgroundColor); fpsView.setText("FPS"); } break; case MotionEvent.ACTION_CANCEL: mFPSAnimer.setEndValue(1); if (String.valueOf(fpsView.getText()).contains("FPS")) { FPSDetector.create().addFrameDataCallback(new FrameDataCallback() { @Override public void doFrame(long previousFrameNS, long currentFrameNS, int droppedFrames, float currentFPS) { if(currentFPS <50 && currentFPS > 30){ fpsView.setTextColor(Color.YELLOW); } else if(currentFPS>=50){ fpsView.setTextColor(Color.GREEN); } else if(currentFPS < 30){ fpsView.setTextColor(Color.RED); } fpsView.setText(String.valueOf(currentFPS)); } }).show(mContext); } else { FPSDetector.hide(mContext); fpsView.setTextColor(backgroundColor); fpsView.setText("FPS"); } break; } return true; } } public static int dpToPx(float dp, Resources res) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,res.getDisplayMetrics()); } public static FrameLayout.LayoutParams createLayoutParams(int width, int height) { return new FrameLayout.LayoutParams(width, height); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/AnSpinnerAdapter.java ================================================ package com.martinrgb.animer.monitor; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.TextView; import androidx.core.content.ContextCompat; import com.martinrgb.animer.R; import java.util.ArrayList; import java.util.List; public class AnSpinnerAdapter extends BaseAdapter { private final Context mContext; private final List mStrings; private final Resources mResources; private int mFontSize; //private final int mTextColor = Color.argb(255, 255, 255, 255); private int mTextColor; public AnSpinnerAdapter(Context context,Resources resources) { mTextColor = ContextCompat.getColor(context, R.color.secondaryColor); mContext = context; mResources = resources; mStrings = new ArrayList(); mFontSize = resources.getDimensionPixelSize(R.dimen.font_size); } @Override public int getCount() { return mStrings.size(); } @Override public Object getItem(int position) { return mStrings.get(position); } @Override public long getItemId(int position) { return position; } public void add(String string) { mStrings.add(string); notifyDataSetChanged(); } public void clear() { mStrings.clear(); notifyDataSetChanged(); } @Override public View getView(int position, View convertView, ViewGroup parent) { TextView textView; if (convertView == null) { textView = new TextView(mContext); AbsListView.LayoutParams params = new AbsListView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); textView.setLayoutParams(params); int twelvePx = dpToPx(12, mResources); int ninePx = dpToPx(9, mResources); textView.setPadding(dpToPx(32,mResources), ninePx, dpToPx(32,mResources),ninePx); textView.setTextColor(mTextColor); textView.setTextSize(mFontSize); textView.setTypeface( Typeface.createFromAsset(mContext.getAssets(), "Montserrat-SemiBold.ttf")); } else { textView = (TextView) convertView; } textView.setText(mStrings.get(position)); return textView; } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { View v = null; v = super.getDropDownView(position, null, parent); // If this is the selected item position if (position == seletecedIndex) { v.setAlpha(1.f); } else { v.setAlpha(0.5f); } return v; } private int seletecedIndex = -1; public void setSelectedItemIndex(int i){ seletecedIndex = i; } public static int dpToPx(float dp, Resources res) { return (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp, res.getDisplayMetrics()); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/Calculation.java ================================================ package com.martinrgb.animer.monitor.fps; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; public class Calculation { public enum Metric {GOOD, BAD, MEDIUM}; public static List getDroppedSet(FPSConfig fpsConfig, List dataSet) { List droppedSet = new ArrayList<>(); long start = -1; for (Long value : dataSet) { if (start == -1) { start = value; continue; } int droppedCount = droppedCount(start, value, fpsConfig.deviceRefreshRateInMs); if (droppedCount > 0) { droppedSet.add(droppedCount); } start = value; } return droppedSet; } public static int droppedCount(long start, long end, float devRefreshRate){ int count = 0; long diffNs = end - start; long diffMs = TimeUnit.MILLISECONDS.convert(diffNs, TimeUnit.NANOSECONDS); long dev = Math.round(devRefreshRate); if (diffMs > dev) { long droppedCount = (diffMs / dev); count = (int) droppedCount; } return count; } public static AbstractMap.SimpleEntry calculateMetric(FPSConfig fpsConfig, List dataSet, List droppedSet) { long timeInNS = dataSet.get(dataSet.size() - 1) - dataSet.get(0); long size = getNumberOfFramesInSet(timeInNS, fpsConfig); //metric int runningOver = 0; // total dropped int dropped = 0; for(Integer k : droppedSet){ dropped+=k; if (k >=2) { runningOver+=k; } } float multiplier = fpsConfig.refreshRate / size; float answer = multiplier * (size - dropped); long realAnswer = Math.round(answer); // calculate metric float percentOver = (float)runningOver/(float)size; Metric metric = Metric.GOOD; if (percentOver >= fpsConfig.redFlagPercentage) { metric = Metric.BAD; } else if (percentOver >= fpsConfig.yellowFlagPercentage) { metric = Metric.MEDIUM; } return new AbstractMap.SimpleEntry(metric, realAnswer); } protected static long getNumberOfFramesInSet(long realSampleLengthNs, FPSConfig fpsConfig) { float realSampleLengthMs = TimeUnit.MILLISECONDS.convert(realSampleLengthNs, TimeUnit.NANOSECONDS); float size = realSampleLengthMs/fpsConfig.deviceRefreshRateInMs; return Math.round(size); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/FPSBuilder.java ================================================ package com.martinrgb.animer.monitor.fps; import android.app.Application; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.provider.Settings; import android.view.Choreographer; import android.view.Display; import android.view.WindowManager; /** * Created by brianplummer on 8/29/15. */ public class FPSBuilder { private static FPSConfig fpsConfig; private static FPSFrameCallback fpsFrameCallback; //private static TinyCoach tinyCoach; private static Foreground.Listener foregroundListener = new Foreground.Listener() { @Override public void onBecameForeground() { //tinyCoach.show(); } @Override public void onBecameBackground() { //tinyCoach.hide(false); } }; protected FPSBuilder(){ fpsConfig = new FPSConfig(); } /** * configures the fpsConfig to the device's hardware * refreshRate ex. 60fps and deviceRefreshRateInMs ex. 16.6ms * @param context */ private void setFrameRate(Context context){ Display display = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); fpsConfig.deviceRefreshRateInMs = 1000f/display.getRefreshRate(); fpsConfig.refreshRate = display.getRefreshRate(); } /** * stops the frame callback and foreground listener * nulls out static variables * called from FPSLibrary in a static context * @param context */ protected static void hide(Context context) { // tell callback to stop registering itself fpsFrameCallback.setEnabled(false); Foreground.get(context).removeListener(foregroundListener); // remove the view from the window //tinyCoach.destroy(); // null it all out //tinyCoach = null; fpsFrameCallback = null; fpsConfig = null; } // PUBLIC BUILDER METHODS /** * show fps meter, this regisers the frame callback that * collects the fps info and pushes it to the ui * @param context */ public void show(Context context) { // if (overlayPermRequest(context)) { // //once permission is granted then you must call show() again // return; // } //are we running? if so, call tinyCoach.show() and return // if (tinyCoach != null) { // tinyCoach.show(); // return; // } // set device's frame rate info into the config setFrameRate(context); // create the presenter that updates the view // tinyCoach = new TinyCoach((Application) context.getApplicationContext(), fpsConfig); // create our choreographer callback and register it fpsFrameCallback = new FPSFrameCallback(fpsConfig); Choreographer.getInstance().postFrameCallback(fpsFrameCallback); //set activity background/foreground listener Foreground.init((Application) context.getApplicationContext()).addListener(foregroundListener); } /** * this adds a frame callback that the library will invoke on the * each time the choreographer calls us, we will send you the frame times * and number of dropped frames. * @param callback * @return */ public FPSBuilder addFrameDataCallback(FrameDataCallback callback) { fpsConfig.frameDataCallback = callback; return this; } /** * set red flag percent, default is 20% * * @param percentage * @return */ public FPSBuilder redFlagPercentage(float percentage) { fpsConfig.redFlagPercentage = percentage; return this; } /** * set red flag percent, default is 5% * @param percentage * @return */ public FPSBuilder yellowFlagPercentage(float percentage) { fpsConfig.yellowFlagPercentage = percentage; return this; } /** * starting x position of fps meter default is 200px * @param xPosition * @return */ public FPSBuilder startingXPosition(int xPosition) { fpsConfig.startingXPosition = xPosition; fpsConfig.xOrYSpecified = true; return this; } /** * starting y positon of fps meter default is 600px * @param yPosition * @return */ public FPSBuilder startingYPosition(int yPosition) { fpsConfig.startingYPosition = yPosition; fpsConfig.xOrYSpecified = true; return this; } /** * starting gravity of fps meter default is Gravity.TOP | Gravity.START; * @param gravity * @return */ public FPSBuilder startingGravity(int gravity) { fpsConfig.startingGravity = gravity; fpsConfig.gravitySpecified = true; return this; } /** * request overlay permission when api >= 23 * @param context * @return */ private boolean overlayPermRequest(Context context) { boolean permNeeded = false; if(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(context)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName())); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); permNeeded = true; } } return permNeeded; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/FPSConfig.java ================================================ package com.martinrgb.animer.monitor.fps; import android.view.Gravity; import java.io.Serializable; import java.util.concurrent.TimeUnit; /** * Created by brianplummer on 8/29/15. */ public class FPSConfig implements Serializable { public static int DEFAULT_GRAVITY = Gravity.TOP | Gravity.START; public float redFlagPercentage = 0.2f; // public float yellowFlagPercentage = 0.05f; // public float refreshRate = 60; //60fps public float deviceRefreshRateInMs = 16.6f; //value from device ex 16.6 ms // starting coordinates public int startingXPosition = 200; public int startingYPosition = 600; public int startingGravity = DEFAULT_GRAVITY; public boolean xOrYSpecified = false; public boolean gravitySpecified = false; // client facing callback that provides frame info public FrameDataCallback frameDataCallback = null; // making final for now.....want to be solid on the math before we allow an // arbitrary value public final long sampleTimeInMs = 736;//928;//736; // default sample time protected FPSConfig() {} public long getSampleTimeInNs() { return TimeUnit.NANOSECONDS.convert(sampleTimeInMs, TimeUnit.MILLISECONDS); } public long getDeviceRefreshRateInNs() { float value = deviceRefreshRateInMs * 1000000f; return (long) value; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/FPSDetector.java ================================================ package com.martinrgb.animer.monitor.fps; import android.content.Context; /** * Created by brianplummer on 8/29/15. */ public class FPSDetector { public static FPSBuilder create(){ return new FPSBuilder(); } public static void hide(Context context) { FPSBuilder.hide(context.getApplicationContext()); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/FPSFrameCallback.java ================================================ package com.martinrgb.animer.monitor.fps; import android.util.Log; import android.view.Choreographer; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; /** * Created by brianplummer on 8/29/15. */ public class FPSFrameCallback implements Choreographer.FrameCallback { private FPSConfig fpsConfig; //private TinyCoach tinyCoach; private List dataSet; //holds the frame times of the sample set private boolean enabled = true; private long startSampleTimeInNs = 0; public FPSFrameCallback(FPSConfig fpsConfig) { this.fpsConfig = fpsConfig; //this.tinyCoach = tinyCoach; dataSet = new ArrayList<>(); } public void setEnabled(boolean enabled) { this.enabled = enabled; } @Override public void doFrame(long frameTimeNanos) { //if not enabled then we bail out now and don't register the callback if (!enabled){ destroy(); return; } //initial case if (startSampleTimeInNs == 0){ startSampleTimeInNs = frameTimeNanos; } // only invoked for callbacks.... else if (fpsConfig.frameDataCallback != null) { long start = dataSet.get(dataSet.size()-1); int droppedCount = Calculation.droppedCount(start, frameTimeNanos, fpsConfig.deviceRefreshRateInMs); fpsConfig.frameDataCallback.doFrame(start, frameTimeNanos, droppedCount,currentFPS); } //we have exceeded the sample length ~700ms worth of data...we should push results and save current //frame time in new list if (isFinishedWithSample(frameTimeNanos)) { collectSampleAndSend(frameTimeNanos); } // add current frame time to our list dataSet.add(frameTimeNanos); //we need to register for the next frame callback Choreographer.getInstance().postFrameCallback(this); } private float currentFPS = 0.0f; private void collectSampleAndSend(long frameTimeNanos) { //this occurs only when we have gathered over the sample time ~700ms List dataSetCopy = new ArrayList<>(); dataSetCopy.addAll(dataSet); //push data to the presenter //tinyCoach.showData(fpsConfig, dataSetCopy); List droppedSet = Calculation.getDroppedSet(fpsConfig, dataSet); AbstractMap.SimpleEntry answer = Calculation.calculateMetric(fpsConfig, dataSet, droppedSet); currentFPS = answer.getValue(); //Log.e("FPS",String.valueOf(answer.getValue())); // clear data dataSet.clear(); //reset sample timer to last frame startSampleTimeInNs = frameTimeNanos; } /** * returns true when sample length is exceed * @param frameTimeNanos current frame time in NS * @return */ private boolean isFinishedWithSample(long frameTimeNanos) { return frameTimeNanos-startSampleTimeInNs > fpsConfig.getSampleTimeInNs(); } private void destroy() { dataSet.clear(); fpsConfig = null; //tinyCoach = null; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/Foreground.java ================================================ package com.martinrgb.animer.monitor.fps; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import android.app.Activity; import android.app.Application; import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.util.Log; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; // COPIED FROM: https://gist.github.com/steveliles/11116937 /** * Usage: * * 1. Get the Foreground Singleton, passing a Context or Application object unless you * are sure that the Singleton has definitely already been initialised elsewhere. * * 2.a) Perform a direct, synchronous check: Foreground.isForeground() / .isBackground() * * or * * 2.b) Register to be notified (useful in Service or other non-UI components): * * Foreground.Listener myListener = new Foreground.Listener(){ * public void onBecameForeground(){ * // ... whatever you want to do * } * public void onBecameBackground(){ * // ... whatever you want to do * } * } * * public void onCreate(){ * super.onCreate(); * Foreground.get(this).addListener(listener); * } * * public void onDestroy(){ * super.onCreate(); * Foreground.get(this).removeListener(listener); * } */ public class Foreground implements Application.ActivityLifecycleCallbacks { public static final long CHECK_DELAY = 600; public static final String TAG = Foreground.class.getName(); public interface Listener { public void onBecameForeground(); public void onBecameBackground(); } private static Foreground instance; private boolean foreground = true, paused = true; private Handler handler = new Handler(); private List listeners = new CopyOnWriteArrayList(); private Runnable check; /** * Its not strictly necessary to use this method - _usually_ invoking * get with a Context gives us a path to retrieve the Application and * initialise, but sometimes (e.g. in test harness) the ApplicationContext * is != the Application, and the docs make no guarantees. * * @param application * @return an initialised Foreground instance */ public static Foreground init(Application application){ if (instance == null) { instance = new Foreground(); application.registerActivityLifecycleCallbacks(instance); } return instance; } public static Foreground get(Application application){ if (instance == null) { init(application); } return instance; } public static Foreground get(Context ctx){ if (instance == null) { Context appCtx = ctx.getApplicationContext(); if (appCtx instanceof Application) { init((Application)appCtx); } throw new IllegalStateException( "Foreground is not initialised and " + "cannot obtain the Application object"); } return instance; } public static Foreground get(){ if (instance == null) { throw new IllegalStateException( "Foreground is not initialised - invoke " + "at least once with parameterised init/get"); } return instance; } public boolean isForeground(){ return foreground; } public boolean isBackground(){ return !foreground; } public void addListener(Listener listener){ listeners.add(listener); } public void removeListener(Listener listener){ listeners.remove(listener); } @Override public void onActivityResumed(Activity activity) { paused = false; boolean wasBackground = !foreground; foreground = true; if (check != null) handler.removeCallbacks(check); if (wasBackground){ Log.i(TAG, "went foreground"); for (Listener l : listeners) { try { l.onBecameForeground(); } catch (Exception exc) { Log.e(TAG, "Listener threw exception!", exc); } } } else { Log.i(TAG, "still foreground"); } } @Override public void onActivityPaused(Activity activity) { paused = true; if (check != null) handler.removeCallbacks(check); handler.postDelayed(check = new Runnable(){ @Override public void run() { if (foreground && paused) { foreground = false; Log.i(TAG, "went background"); for (Listener l : listeners) { try { l.onBecameBackground(); } catch (Exception exc) { Log.e(TAG, "Listener threw exception!", exc); } } } else { Log.i(TAG, "still foreground"); } } }, CHECK_DELAY); } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} @Override public void onActivityStarted(Activity activity) {} @Override public void onActivityStopped(Activity activity) {} @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} @Override public void onActivityDestroyed(Activity activity) {} } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/fps/FrameDataCallback.java ================================================ package com.martinrgb.animer.monitor.fps; /** * Created by brianplummer on 11/12/15. */ public interface FrameDataCallback { /** * this is called for every doFrame() on the choreographer callback * use this very judiciously. Logging synchronously from here is a bad * idea as doFrame will be called every 16-32ms. * @param previousFrameNS previous vsync frame time in NS * @param currentFrameNS current vsync frame time in NS * @param droppedFrames number of dropped frames between current and previous times */ void doFrame(long previousFrameNS, long currentFrameNS, int droppedFrames,float currentFPS); } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/ShaderProgram.java ================================================ package com.martinrgb.animer.monitor.shader; import android.content.Context; import android.opengl.GLES20; import android.opengl.Matrix; import com.martinrgb.animer.R; import com.martinrgb.animer.monitor.shader.util.ShaderHelper; import com.martinrgb.animer.monitor.shader.util.TextReader; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import static android.opengl.GLES20.GL_TRIANGLE_STRIP; public class ShaderProgram { private float[] mModelMatrix = new float[16]; private float[] mViewMatrix = new float[16]; private float[] mProjectionMatrix = new float[16]; private float[] mMVPMatrix = new float[16]; private final int mPositionDataSize = 4; private final int mBytesPerFloat = 4; private FloatBuffer vertexBuffer; public static final String ATTRIBUTE_POSITION = "a_position"; public static final String UNIFORM_RESOLUTION = "u_resolution"; public static final String UNIFORM_TIME = "u_time"; public static final String UNIFORM_FACTOR = "u_factor"; public static final String UNIFORM_MVP = "u_MVPMatrix"; public static final String UNIFORM_MODE = "u_mode"; public static final String UNIFORM_DURATION = "u_duration"; public static final String UNIFORM_MAIN_COLOR = "u_mainColor"; public static final String UNIFORM_SECONDARY_COLOR = "u_secondaryColor"; private int program = 0; private int positionLoc = -1; private int resolutionLoc = -1; private int modeLoc = -1; private int timeLoc = -1; private int durationLoc = -1; private int mvpLoc = -1; private int secondaryColorLoc = -1; private int mainColorLoc = -1; private static int factorLength = 5; private final int factorLocs[] = new int[32]; public ShaderProgram(Context context) { if (program != 0) { program = 0; } program = ShaderHelper.buildProgram( TextReader.readTextFileFromResource(context,R.raw.simplevert), TextReader.readTextFileFromResource(context,R.raw.simplefrag)); } public void setOnCreate(){ positionLoc = GLES20.glGetAttribLocation(program, ATTRIBUTE_POSITION); resolutionLoc = GLES20.glGetUniformLocation(program, UNIFORM_RESOLUTION); timeLoc = GLES20.glGetUniformLocation(program, UNIFORM_TIME); mvpLoc = GLES20.glGetUniformLocation(program, UNIFORM_MVP); modeLoc = GLES20.glGetUniformLocation(program, UNIFORM_MODE); durationLoc = GLES20.glGetUniformLocation(program,UNIFORM_DURATION); mainColorLoc = GLES20.glGetUniformLocation(program,UNIFORM_MAIN_COLOR); secondaryColorLoc = GLES20.glGetUniformLocation(program,UNIFORM_SECONDARY_COLOR); // for (int i = 0;i < factorLength;i++ ) { // factorLocs[i] = GLES20.glGetUniformLocation(program,UNIFORM_FACTOR + (i+1)); // } final float[] verticesData = { 1.0f,-1.0f,0.0f,1.0f, -1.0f,-1.0f,0.0f,1.0f, 1.0f,1.0f,0.0f,1.0f, -1.0f,1.0f,0.0f,1.0f}; vertexBuffer = ByteBuffer.allocateDirect(verticesData.length * mBytesPerFloat).order(ByteOrder.nativeOrder()).asFloatBuffer(); vertexBuffer.put(verticesData).position(0); GLES20.glEnableVertexAttribArray(positionLoc); final float eyeX = 0.0f; final float eyeY = 0.0f; final float eyeZ = 1.0f; final float lookX = 0.0f; final float lookY = 0.0f; final float lookZ = -5.0f; final float upX = 0.0f; final float upY = 1.0f; final float upZ = 0.0f; Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ); } public void setOnChange(int width, int height){ final float ratio = (float) width / height; final float left = -ratio; final float right = ratio; final float bottom = -1.0f; final float top = 1.0f; final float near = 1.0f; final float far = 10.0f; Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far); } public void setOnDrawFrame(float[] resolution,float time,float[] factors,float mode,float duration,float[] mainColor,float[] secondaryColor){ // ######################### clear the canvas ######################### GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); // ######################### first program ######################### GLES20.glUseProgram(program); if (positionLoc > -1){ GLES20.glVertexAttribPointer(positionLoc, mPositionDataSize, GLES20.GL_FLOAT, false,0, vertexBuffer); } if (resolutionLoc > -1) { GLES20.glUniform2fv(resolutionLoc,1,resolution,0); } if (timeLoc > -1) { GLES20.glUniform1f(timeLoc,time); } if (modeLoc > -1) { GLES20.glUniform1f(modeLoc,mode); } if (durationLoc > -1) { GLES20.glUniform1f(durationLoc,duration); } if (mainColorLoc > -1) { GLES20.glUniform3f(mainColorLoc,mainColor[0],mainColor[1],mainColor[2]); } if (secondaryColorLoc > -1) { GLES20.glUniform3f(secondaryColorLoc,secondaryColor[0],secondaryColor[1],secondaryColor[2]); } //5 Factors for (int i = 0; i < factorLength; ++i) { //factorLocs.length factorLocs[i] = GLES20.glGetUniformLocation(program,UNIFORM_FACTOR + (i+1)); GLES20.glUniform1f(factorLocs[i],factors[i]); } //Draw the triangle facing straight on. if(mvpLoc > -1){ Matrix.setIdentityM(mModelMatrix, 0); Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0); Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0); GLES20.glUniformMatrix4fv(mvpLoc, 1, false, mMVPMatrix, 0); } GLES20.glViewport(0,0,(int) resolution[0],(int) resolution[1]); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GL_TRIANGLE_STRIP,0,4); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/ShaderRenderer.java ================================================ package com.martinrgb.animer.monitor.shader; import android.content.Context; import android.opengl.GLES20; import android.opengl.GLSurfaceView; import com.martinrgb.animer.monitor.shader.util.FPSCounter; import com.martinrgb.animer.monitor.shader.util.LoggerConfig; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; public class ShaderRenderer implements GLSurfaceView.Renderer { private float resolution[] = new float[]{0,0}; private float secondaryColor[] = new float[]{0,0,0}; private float mainColor[] = new float[]{0,0,0}; private long startTime; private static final float NS_PER_SECOND = 1000000000f; private final Context context; private ShaderProgram shaderProgram; public ShaderRenderer(Context context) { this.context = context; } @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES20.glDisable(GLES20.GL_CULL_FACE); GLES20.glDisable(GLES20.GL_BLEND); GLES20.glDisable(GLES20.GL_DEPTH_TEST); GLES20.glClearColor(0f, 0f, 0f, 0f); shaderProgram = new ShaderProgram(context); shaderProgram.setOnCreate(); // setFactorInput(mFactor1,0); // setFactorInput(mFactor2,1); } @Override public void onSurfaceChanged(GL10 gl, int width, int height) { startTime = System.nanoTime(); resolution[0] = width; resolution[1] = height; shaderProgram.setOnChange(width,height); } @Override public void onDrawFrame(GL10 gl) { //float time = (System.nanoTime() - startTime) / NS_PER_SECOND; float time = (isInteraction)? (System.nanoTime() - startTime) / NS_PER_SECOND:0; shaderProgram.setOnDrawFrame(resolution,time,factors,mode,duration,mainColor,secondaryColor); if(LoggerConfig.ON == true){ FPSCounter.logFrameRate(); } } private float factors[] = new float[32]; private float mode; private float duration; // private float mFactor1 = 1500; // private float mFactor2 = 0.5f; public void setFactorInput(float factor,int i){ factors[i] = factor; } public void setMainColor(float r,float g,float b){ mainColor[0] = r; mainColor[1] = g; mainColor[2] = b; } public void setSecondaryColor(float r,float g,float b){ secondaryColor[0] = r; secondaryColor[1] = g; secondaryColor[2] = b; } public void setCurveMode(float i){ mode = i ; } public void setDuration(float i){ duration = i ; } private boolean isInteraction = false; public void resetTime(){ isInteraction = true; startTime = System.nanoTime(); } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/ShaderSurfaceView.java ================================================ package com.martinrgb.animer.monitor.shader; import android.content.Context; import android.graphics.PixelFormat; import android.opengl.GLSurfaceView; import android.util.AttributeSet; import android.view.MotionEvent; public class ShaderSurfaceView extends GLSurfaceView { private ShaderRenderer renderer; public ShaderSurfaceView(Context context) { super(context); setRenderer(context); } public ShaderSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); setRenderer(context); } private void setRenderer(Context context) { renderer = new ShaderRenderer(context); setEGLContextClientVersion(2); setZOrderOnTop(true); setEGLConfigChooser(8, 8, 8, 8, 16, 0); getHolder().setFormat(PixelFormat.RGBA_8888); setRenderer(renderer); //TODO Request Renderer setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); } public ShaderRenderer getRenderer() { return renderer; } @Override public boolean onTouchEvent(MotionEvent event) { return true; } public void setFactorInput(float factor,int i){ renderer.setFactorInput(factor,i); } public void setMainColor(float r,float g,float b){ renderer.setMainColor(r,g,b); } public void setSecondaryColor(float r,float g,float b){ renderer.setSecondaryColor(r,g,b); } public void setCurveMode(float i){ renderer.setCurveMode(i); } public void setDuration(float i){ renderer.setDuration(i); } public void resetTime(){renderer.resetTime();} } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/util/FPSCounter.java ================================================ package com.martinrgb.animer.monitor.shader.util; import android.os.SystemClock; import android.util.Log; public class FPSCounter { private static long startTimeMs; private static int frameCount; public static void logFrameRate(){ final long elapsedRealtimeMs = SystemClock.elapsedRealtime(); final double elapsedSeconds = (elapsedRealtimeMs - startTimeMs)/1000.0; if(elapsedSeconds >= 1.0){ Log.v("Current FPS is ",frameCount/elapsedSeconds + "fps"); startTimeMs = SystemClock.elapsedRealtime(); frameCount = 0; } frameCount ++; } public static int getFPS() { return frameCount; } } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/util/LoggerConfig.java ================================================ package com.martinrgb.animer.monitor.shader.util; public class LoggerConfig { public static final boolean ON = true; } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/util/ShaderHelper.java ================================================ package com.martinrgb.animer.monitor.shader.util; import android.opengl.GLES20; import android.util.Log; import static android.opengl.GLES20.GL_COMPILE_STATUS; import static android.opengl.GLES20.GL_LINK_STATUS; import static android.opengl.GLES20.GL_VALIDATE_STATUS; import static android.opengl.GLES20.glAttachShader; import static android.opengl.GLES20.glCreateProgram; import static android.opengl.GLES20.glDeleteProgram; import static android.opengl.GLES20.glGetProgramInfoLog; import static android.opengl.GLES20.glGetProgramiv; import static android.opengl.GLES20.glGetShaderiv; import static android.opengl.GLES20.glLinkProgram; import static android.opengl.GLES20.glValidateProgram; public class ShaderHelper { private static final String TAG = "ShaderHelper"; //构建顶点着色器对象 public static int complieVertexShader(String shaderCode){ return compileShader(GLES20.GL_VERTEX_SHADER,shaderCode); } //构建片段着色器对象 public static int complieFragmentShader(String shaderCode){ return compileShader(GLES20.GL_FRAGMENT_SHADER,shaderCode); } //构建单个着色器对象 private static int compileShader(int type,String shaderCode){ //glCreateShader 构建了着色器对象,并把 ID 存入shaderObjectID final int shaderObjectId = GLES20.glCreateShader(type); if(shaderObjectId == 0){ if(LoggerConfig.ON){ Log.w(TAG,"Could not create new shader"); } return 0; } //把着色器源代码传入着色器对象里 GLES20.glShaderSource(shaderObjectId, shaderCode); //编译着色器 GLES20.glCompileShader(shaderObjectId); //检测编译是否成功 final int[] compileStatus = new int[1]; glGetShaderiv(shaderObjectId,GL_COMPILE_STATUS,compileStatus,0); //检测连接成功失败的日志 //如果编译好了,给出ID if (LoggerConfig.ON) { Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:" + GLES20.glGetShaderInfoLog( shaderObjectId)); } //如果编译失败 if (compileStatus[0] == 0) { GLES20.glDeleteShader(shaderObjectId); if (LoggerConfig.ON) { Log.w(TAG, "Compilation of shader failed"); } } return shaderObjectId; }; //将顶点着色器和片段着色器连接,并构建对象 public static int linkProgram(int vertexShaderId,int fragmentShaderId){ //创建新的程序对象 final int programObjectId = glCreateProgram(); //如果对象没有创建 if(programObjectId == 0){ if(LoggerConfig.ON){ Log.w(TAG,"Could not create new program"); } return 0; } //程序对象附着上着色器 glAttachShader(programObjectId,vertexShaderId); glAttachShader(programObjectId,fragmentShaderId); //链接程序 glLinkProgram(programObjectId); //检测连接成功失败的日志 final int[] linkStatus = new int[1]; glGetProgramiv(programObjectId,GL_LINK_STATUS,linkStatus,0); //如果创建好了,给出ID if (LoggerConfig.ON) { // Print the program info log to the Android log output. Log.v(TAG, "Results of linking program:\n" + glGetProgramInfoLog(programObjectId)); } //如果创建失败 if(linkStatus[0] == 0){ glDeleteProgram(programObjectId); if(LoggerConfig.ON){ Log.w(TAG,"Linking of program failed"); } return 0; } return programObjectId; }; public static boolean validateProgram(int programObjectId){ glValidateProgram(programObjectId); final int[] validateStatus = new int[1]; glGetProgramiv(programObjectId,GL_VALIDATE_STATUS,validateStatus,0); Log.v(TAG, "Results of validating program: " + validateStatus[0] + "\nLog:" + glGetProgramInfoLog(programObjectId)); return validateStatus[0] != 0; }; public static int buildProgram(String vertexShaderSource,String fragmentShaderSource){ int program; int vertexShader = complieVertexShader(vertexShaderSource); int fragmentShader = complieFragmentShader(fragmentShaderSource); program = linkProgram(vertexShader,fragmentShader); if(LoggerConfig.ON){ validateProgram(program); } return program; }; } ================================================ FILE: animer/src/main/java/com/martinrgb/animer/monitor/shader/util/TextReader.java ================================================ package com.martinrgb.animer.monitor.shader.util; import android.content.Context; import android.content.res.Resources; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; public class TextReader { public static String readTextFileFromResource(Context context, int resourceID){ StringBuilder body = new StringBuilder(); try{ InputStream inputStream = context.getResources().openRawResource(resourceID); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String nextLine; while((nextLine = bufferedReader.readLine()) != null){ body.append(nextLine); body.append('\n'); } } catch (IOException e){ throw new RuntimeException("Could not open resource:" + resourceID,e); } catch (Resources.NotFoundException nfe){ throw new RuntimeException("Resource not found:" + resourceID,nfe); } return body.toString(); }; } ================================================ FILE: animer/src/main/res/drawable/background_spinner.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/gradient.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_button_background.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_edit_border.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_grid.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_nub.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_nub2.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_spinner.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/ic_thumb.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/popbackground_spinner.xml ================================================ ================================================ FILE: animer/src/main/res/drawable/text_cursor.xml ================================================ ================================================ FILE: animer/src/main/res/layout/config_view.xml ================================================ ================================================ FILE: animer/src/main/res/raw/simplefrag.glsl ================================================ #ifdef GL_ES precision highp float; #endif //#extension GL_OES_standard_derivatives : enable #define PI 3.14159265359 #define E 2.718281828459045 uniform vec2 u_resolution; uniform float u_time; varying vec2 vFlingCalc; varying float vSpringCalc; varying float vDuration; varying float vMode; varying vec2 vUv; varying float v_factor1; varying float v_factor2; varying float v_factor3; varying float v_factor4; varying float v_factor5; varying vec3 v_secondaryColor; varying vec3 v_mainColor; const float lineJitter = 0.5; const float lineWidth = 7.0; const float gridWidth = 1.7; const float scale = 0.0013; const float Samples = 3.; float timeProgress; vec2 circlePos = vec2(0.); float circleRadius = 0.0008; bool reset = false; int duration_mode = 0; float FlingSimulator(in float time){ float startVal = 0.; float deltaT = time * vDuration; float mRealFriction = v_factor2*(-4.2); float valTransition = (0. - v_factor1/mRealFriction) + ( v_factor1/ mRealFriction) * (exp(mRealFriction * deltaT ) ); float mLastVal = valTransition/vFlingCalc[1]; return mLastVal/2.; } float SpringSimulator(in float time){ float deltaT = time*1. * vDuration; float starVal = 0.; float endVal = 1.; float mNaturalFreq = sqrt(v_factor1); float mDampedFreq = mNaturalFreq*sqrt(1.0 - v_factor2* v_factor2); //TODO vSpringVelocity float lastVelocity = 0.; float lastDisplacement = time*1. - endVal; float coeffB = 1.0 / mDampedFreq * (v_factor2 * mNaturalFreq * lastDisplacement + lastVelocity); float displacement = pow(E,-v_factor2 * mNaturalFreq * deltaT) * (lastDisplacement * cos(mDampedFreq * deltaT) + coeffB * sin(mDampedFreq * deltaT)); float mValue = displacement + endVal; return mValue/2.+0.; } float CubicBezierSimulator(in float time,vec4 bezierPoint) { if (time > 1.0) { return 1.; } else if(time < 0.){ return 0.; } float x = time; float z; vec2 c,b,a; for (int i = 1; i < 20; i++) { c.x = 3. * bezierPoint[0]; b.x = 3. * (bezierPoint[2] - bezierPoint[0]) - c.x; a.x = 1. - c.x - b.x; z = x * (c.x + x * (b.x + x * a.x)) - time; if (abs(z) < 1e-3) { break; } x -= z / (c.x + x * (2. * b.x + 3. * a.x * x)); } c.y = 3. * bezierPoint[1]; b.y = 3. * (bezierPoint[3] - bezierPoint[1]) - c.y; a.y = 1. - c.y - b.y; float mValue = x * (c.y + x * (b.y + x * a.y)); return mValue/1.; } float AccelerateInterpolator(in float time){ if (time <= 0.0) { return 0.; } if (v_factor1 == 1.0) { return time * time; } else { return pow(time, 2.*v_factor1); } } float DecelerateInterpolator(in float time) { if(time >=1.0){ return 1.; } if (v_factor1 == 1.0) { return 1.0 - (1.0 - time) * (1.0 - time); } else { return (1.0 - pow((1.0 - time), 2.0 * v_factor1)); } } float LinearInterpolator(in float time){ return time; } float AccelerateDecelerateInterpolator(in float time){ return (cos((time + 1.) * PI) / 2.0) + 0.5; } float AnticipateInterpolator(in float time){ return time * time * ((v_factor1 + 1.) * time - v_factor1); } float OvershootInterpolator(float time) { time -= 1.0; return time * time * ((v_factor1 + 1.) * time + v_factor1) + 1.0; } float AOSIA(float t, float s) { return t * t * ((s + 1.) * t - s); } float AOSIO(float t, float s) { return t * t * ((s + 1.) * t + s); } float AnticipateOvershootInterpolator(float time) { float t = time; if (t < 0.5) return 0.5 * AOSIA(t * 2.0, v_factor1*1.5); else return 0.5 * (AOSIO(t * 2.0 - 2.0, v_factor1*1.5) + 2.0); } float BounceInterpolator(in float time){ float t= time; t *= 1.1226; if (t < 0.3535) return t * t * 8.0; else if (t < 0.7408) return (t - 0.54719)*(t - 0.54719)*8. + 0.7; else if (t < 0.9644) return (t - 0.8526)*(t - 0.8526)*8. + 0.9; else return (t - 1.0435)*(t - 1.0435)*8. + 0.95; } float CycleInterpolator(in float time) { float mValue = sin(2. * v_factor1 * PI * time); return mValue/2. + 0.5; } float FastOutLinearInInterpolator(in float time){ return CubicBezierSimulator(time,vec4(0.40,0.00,1.00,1.00)); } float FastOutSlowInInterpolator(in float time){ return CubicBezierSimulator(time,vec4(0.40,0.00,0.20,1.00)); } float LinearOutSlowInInterpolator(in float time){ return CubicBezierSimulator(time,vec4(0.00,0.00,0.20,1.00)); } float CustomSpringInterpolator(in float ratio) { if (ratio == 0.0 || ratio == 1.0) return ratio/2.; else { float value = (pow(2., -10. * ratio) * sin((ratio - v_factor1 / 4.0) * (2.0 * PI) / v_factor1) + 1.); return value/2.; } } float CustomBounceInterpolator(in float ratio){ float amplitude = 1.; float phase = 0.; float originalStiffness = 12.; float originalFrictionMultipler = 0.3; float mass = 0.058; float maxStifness = 50.; float maxFrictionMultipler = 1.; float aTension = min(max(v_factor1,0.),100.) * (maxStifness- originalStiffness)/100.; float aFriction = min(max(v_factor2,0.),100.) * (maxFrictionMultipler - originalFrictionMultipler)/100.; float pulsation = sqrt((originalStiffness + aTension) / mass); float friction = (originalFrictionMultipler + aFriction) * pulsation; if (ratio == 0.0 || ratio == 1.0) return ratio/2.; else { float value = amplitude * exp(-friction * ratio) * cos(pulsation * ratio + phase) ; return (-abs(value)+1.)/2.; } } float CustomDampingInterpolator(in float ratio){ float amplitude = 1.; float phase = 0.; float originalStiffness = 12.; float originalFrictionMultipler = 0.3; float mass = 0.058; float maxStifness = 50.; float maxFrictionMultipler = 1.; float aTension = min(max(v_factor1,0.),100.) * (maxStifness- originalStiffness)/100.; float aFriction = min(max(v_factor2,0.),100.) * (maxFrictionMultipler - originalFrictionMultipler)/100.; float pulsation = sqrt((originalStiffness + aTension) / mass); float friction = (originalFrictionMultipler + aFriction) * pulsation; if (ratio == 0.0 || ratio == 1.0) return ratio/2.; else { float value = amplitude * exp(-friction * ratio) * cos(pulsation * ratio + phase) ; return (-(value)+1.)/2.; } } float AndroidSpringInterpolator(float ratio) { if (ratio == 0.0 || ratio == 1.0) return ratio/2.; else { float deltaT = ratio * vDuration; float starVal = 0.; float endVal = 1.; float mNaturalFreq = sqrt(v_factor1); float mDampedFreq = (mNaturalFreq*sqrt(1.0 - v_factor2* v_factor2)); //TODO vSpringVelocity float lastVelocity = 0.; //float lastDisplacement = ratio - endVal* deltaT/60 - endVal; float lastDisplacement = ratio - endVal; float coeffB = (1.0 / mDampedFreq * (v_factor2 * mNaturalFreq * lastDisplacement + lastVelocity)); float displacement = (pow(E,-v_factor2 * mNaturalFreq * deltaT) * (lastDisplacement * cos(mDampedFreq * deltaT) + coeffB * sin(mDampedFreq * deltaT))); float mValue = displacement + endVal; if(vDuration == 0.){ return starVal/2.; } else{ return mValue/2.; } } } float CustomMocosSpringInterpolator(in float ratio) { if (ratio >= 1.) { return 1./2.; } float tension = v_factor1; float damping = v_factor2; float velocity = v_factor3; float mEps = 0.001; float mGamma,mVDiv2,mB,mA,mMocosDuration; bool mOscilative = (4. * tension - damping * damping > 0.); if (mOscilative) { mGamma = sqrt(4. * tension - damping * damping) / 2.; mVDiv2 = damping / 2.; mB = atan(-mGamma / (velocity - mVDiv2)); mA = -1. / sin(mB); mMocosDuration = log(abs(mA) / mEps) / mVDiv2; } else { mGamma = sqrt(damping * damping - 4. * tension) / 2.; mVDiv2 = damping / 2.; mA = (velocity - (mGamma + mVDiv2)) / (2. * mGamma); mB = -1. - mA; mMocosDuration = log(abs(mA) / mEps) / (mVDiv2 - mGamma); } float t = ratio * mMocosDuration; if(mOscilative){ return (mA * exp(-mVDiv2 * t) * sin(mGamma * t + mB) + 1.)/2.; } else{ return (mA * exp((mGamma - mVDiv2) * t) + mB * exp(-(mGamma + mVDiv2) * t) + 1.)/2.; } } float plot(vec2 st, float progress){ float pct; if(vMode == 0.0){ pct = FlingSimulator(progress); } else if(vMode == 1.0){ pct = SpringSimulator(progress); } else if(vMode == 2.0){ pct = CubicBezierSimulator(progress,vec4(v_factor1,v_factor2,v_factor3,v_factor4)); } else if(vMode == 2.01){ pct = LinearInterpolator(progress); } else if(vMode == 2.02){ pct = AccelerateDecelerateInterpolator(progress); } else if(vMode == 2.03){ pct = AccelerateInterpolator(progress); } else if(vMode == 2.04){ pct = DecelerateInterpolator(progress); } else if(vMode == 2.05){ pct = AnticipateInterpolator(progress); } else if(vMode == 2.06){ pct = OvershootInterpolator(progress); } else if(vMode == 2.07){ pct = AnticipateOvershootInterpolator(progress); } else if(vMode == 2.08){ pct = BounceInterpolator(progress); } else if(vMode == 2.09){ pct = CycleInterpolator(progress); } else if(vMode == 2.10){ pct = FastOutSlowInInterpolator(progress); } else if(vMode == 2.11){ pct = LinearOutSlowInInterpolator(progress); } else if(vMode == 2.12){ pct = FastOutLinearInInterpolator(progress); } else if(vMode == 2.13){ pct = CustomMocosSpringInterpolator(progress); } else if(vMode == 2.14){ pct = CustomSpringInterpolator(progress); } else if(vMode == 2.15){ pct = CustomBounceInterpolator(progress); } else if(vMode == 2.16){ pct = CustomDampingInterpolator(progress); } else if(vMode == 2.17){ pct = BounceInterpolator(progress); } return smoothstep( pct - 0.025, pct, st.y) - smoothstep( pct, pct + 0.025, st.y); } vec4 plot2D(in vec2 _st, in float _width ,vec3 initColor) { const float samples = float(Samples); vec2 steping = _width*vec2(scale)/samples; float count = 0.0; float mySamples = 0.0; for (float i = 0.0; i < samples; i++) { for (float j = 0.0;j < samples; j++) { if (i*i+j*j>samples*samples) continue; mySamples++; float ii = i; float jj = j; float f = 0.; if(vMode == 0.0){ f = FlingSimulator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 1.0){ f = SpringSimulator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.0){ f = CubicBezierSimulator((_st.x+ ii*steping.x),vec4(v_factor1,v_factor2,v_factor3,v_factor4))-(_st.y+ jj*steping.y); } else if(vMode == 2.01){ f = LinearInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.02){ f = AccelerateDecelerateInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.03){ f = AccelerateInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.04){ f = DecelerateInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.05){ f = AnticipateInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.06){ f = OvershootInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.07){ f = AnticipateOvershootInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.08){ f = BounceInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.09){ f = CycleInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.10){ f = FastOutSlowInInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.11){ f = LinearOutSlowInInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.12){ f = FastOutLinearInInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.13){ f = CustomMocosSpringInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.14){ f = CustomSpringInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.15){ f = CustomBounceInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.16){ f = CustomDampingInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } else if(vMode == 2.17){ f = AndroidSpringInterpolator((_st.x+ ii*steping.x) )-(_st.y+ jj*steping.y); } count += (f>0.0) ? 1. : -1.0 ; } } if (abs(count)!=mySamples) return vec4( vec3(abs(float(count*50.))/float(mySamples))*initColor ,1.); // count*10. make stroke soilder return vec4(0.); } float circle(in vec2 _st, in float _radius){ vec2 dist = _st-vec2(0.5); return 1.-smoothstep(_radius-(_radius*0.01),_radius+(_radius*0.01),dot(dist,dist)*4.0); } vec2 circle2D(in float progress){ if(vMode == 0.0){ return vec2(progress,FlingSimulator(progress)); } else if(vMode == 1.0){ return vec2(progress,SpringSimulator(progress)); } else if(vMode == 2.0){ return vec2(progress,CubicBezierSimulator(progress,vec4(v_factor1,v_factor2,v_factor3,v_factor4))); } else if(vMode == 2.01){ return vec2(progress,LinearInterpolator(progress)); } else if(vMode == 2.02){ return vec2(progress,AccelerateDecelerateInterpolator(progress)); } else if(vMode == 2.03){ return vec2(progress,AccelerateInterpolator(progress)); } else if(vMode == 2.04){ return vec2(progress,DecelerateInterpolator(progress)); } else if(vMode == 2.05){ return vec2(progress,AnticipateInterpolator(progress)); } else if(vMode == 2.06){ return vec2(progress,OvershootInterpolator(progress)); } else if(vMode == 2.07){ return vec2(progress,AnticipateOvershootInterpolator(progress)); } else if(vMode == 2.08){ return vec2(progress,BounceInterpolator(progress)); } else if(vMode == 2.09){ return vec2(progress,CycleInterpolator(progress)); } else if(vMode == 2.10){ return vec2(progress,FastOutSlowInInterpolator(progress)); } else if(vMode == 2.11){ return vec2(progress,LinearOutSlowInInterpolator(progress)); } else if(vMode == 2.12){ return vec2(progress,FastOutLinearInInterpolator(progress)); } else if(vMode == 2.13){ return vec2(progress,CustomMocosSpringInterpolator(progress)); } else if(vMode == 2.14){ return vec2(progress,CustomSpringInterpolator(progress)); } else if(vMode == 2.15){ return vec2(progress,CustomBounceInterpolator(progress)); } else if(vMode == 2.16){ return vec2(progress,CustomDampingInterpolator(progress)); } else if(vMode == 2.17){ return vec2(progress,AndroidSpringInterpolator(progress)); } } float when_gt(float x, float y) { return max(sign(x - y), 0.0); } float when_lt(float x, float y) { return max(sign(y - x), 0.0); } void main(){ vec2 st = vUv; //#Scale float mScale = 0.8; st *= 1./mScale; st -= (1./mScale - 1.)/2.; timeProgress = min(u_time/vDuration,1.); vec4 color = vec4(0.,0.,0.,0.); if(st.x>-0.01 &&st.y>-0.5 && st.y<1.5 && st.x<1.01){ // ### Plot2D color = plot2D(st,lineWidth,v_secondaryColor); // ### Plot //float v2 = plot(st, st.x); //color = mix(color, vec4(v_secondaryColor,1.), v2); if(st.x #09CDFF #FFFFFF #4DFFFFFF #000000 #00000000 #4C4C4C #B0B0B0 #19181D #111112 #19181D #1A191E ================================================ FILE: animer/src/main/res/values/dimension.xml ================================================ 4dp 4dp 46dp 13px ================================================ FILE: animer/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 29 defaultConfig { applicationId "com.martinrgb.animerexample_1" minSdkVersion 27 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility = '1.8' targetCompatibility = '1.8' } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation project(':animer') //implementation 'com.martinrgb:animer:0.1.6.0' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/androidTest/java/com/martinrgb/animerexample/ExampleInstrumentedTest.java ================================================ package com.martinrgb.animerexample; import android.content.Context; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumented test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.martinrgb.swipeexample", appContext.getPackageName()); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/martinrgb/animerexample/MainActivity.java ================================================ package com.martinrgb.animerexample; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.ImageView; import android.widget.TextView; import com.martinrgb.animer.Animer; import com.martinrgb.animer.core.interpolator.AndroidNative.AccelerateDecelerateInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.DecelerateInterpolator; import com.martinrgb.animer.monitor.AnConfigRegistry; import com.martinrgb.animer.monitor.AnConfigView; public class MainActivity extends AppCompatActivity { private ImageView iv1,iv2,iv3,iv4; private Animer animer1,animer2,animer3,animer4,animer5,animer6,animer7,animerNew; private boolean isOpen,isOpen2,isOpen3,isOpen4 = false; private AnConfigView mAnimerConfiguratorView; private ImageView ivNew; private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); deleteBars(); setContentView(R.layout.activity_main); iv1 = findViewById(R.id.iv); iv2 = findViewById(R.id.iv2);; iv3 = findViewById(R.id.iv3); iv4 = findViewById(R.id.iv4); ivNew = findViewById(R.id.iv5); tv = findViewById(R.id.tv); Animer.AnimerSolver solverB = Animer.springDroid(1000,0.5f); animer1 = new Animer(iv1,solverB,Animer.TRANSLATION_X,0,500); animer2 = new Animer(iv2,Animer.interpolatorDroid(new AccelerateDecelerateInterpolator(),1500),Animer.TRANSLATION_X,0,500); animer3 = new Animer(iv3,Animer.interpolatorDroid(new DecelerateInterpolator(2),1200),Animer.TRANSLATION_X,0,720); animer4 = new Animer(iv1,Animer.springRK4(100,10),Animer.ROTATION,1,1.2f); animer5 = new Animer(iv2,Animer.springDHO(200,20),Animer.ROTATION,0,720); animer6 = new Animer(iv3,Animer.springOrigamiPOP(30,10),Animer.ROTATION,200,800); animer7 = new Animer(iv4,Animer.springRK4(230,15),Animer.SCALE,1,0.5f); ivNew.getLayoutParams().width = 44 * 3; animerNew = new Animer(); animerNew.setSolver(Animer.springDroid(600,0.99f)); animerNew.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { ivNew.getLayoutParams().height = (int) (44*3 + progress*(200-44)*3); ivNew.requestLayout(); } }); animerNew.setCurrentValue(0); animer1.setCurrentValue(200); animer2.setCurrentValue(200); animer3.setCurrentValue(200); mAnimerConfiguratorView = (AnConfigView) findViewById(R.id.an_configurator); AnConfigRegistry.getInstance().addAnimer("Image Scale Animation",animer7); AnConfigRegistry.getInstance().addAnimer("Red TranslationX",animer1); AnConfigRegistry.getInstance().addAnimer("Blue TranslationX",animer2); AnConfigRegistry.getInstance().addAnimer("Green TranslationX",animer3); AnConfigRegistry.getInstance().addAnimer("Red Rotation",animer4); AnConfigRegistry.getInstance().addAnimer("Blue Rotation",animer5); AnConfigRegistry.getInstance().addAnimer("Green Rotation",animer6); AnConfigRegistry.getInstance().addAnimer("Volume Simulation",animerNew); mAnimerConfiguratorView.refreshAnimerConfigs(); iv1.setOnClickListener(view -> { if(!isOpen){ animer1.setEndValue(800); //animer1.animateToState("stateA"); animer4.setEndValue(720); } else{ animer1.setEndValue(200); //animer1.animateToState("stateB"); animer4.setEndValue(0); } isOpen = !isOpen; }); iv2.setOnClickListener(view -> { if(!isOpen2){ animer2.setEndValue(800); animer5.setEndValue(720); } else{ animer2.setEndValue(200); animer5.setEndValue(0); } isOpen2 = !isOpen2; }); iv3.setOnClickListener(view -> { if(!isOpen3){ animer3.setEndValue(800); animer6.setEndValue(720); } else{ animer3.setEndValue(200); animer6.setEndValue(0); } isOpen3 = !isOpen3; }); iv4.setOnClickListener(view -> { if(!isOpen4){ animer7.setEndValue(0.5f); } else{ animer7.setEndValue(1f); } isOpen4 = !isOpen4; }); ivNew.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: animerNew.setEndValue(1); tv.setText("手势状态:Down"); Log.i("TAG", "touched down"); break; case MotionEvent.ACTION_MOVE: Log.i("TAG", "touched move"); break; case MotionEvent.ACTION_UP: animerNew.setEndValue(0); tv.setText("手势状态:Up"); Log.i("TAG", "touched up"); break; case MotionEvent.ACTION_CANCEL: Log.i("TAG", "touched cancel"); break; } return true; } }); } private void deleteBars() { this.requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); } } ================================================ FILE: app/src/main/java/com/martinrgb/animerexample/PrototypeActivity.java ================================================ package com.martinrgb.animerexample; import android.graphics.Point; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import androidx.appcompat.app.AppCompatActivity; import com.martinrgb.animer.Animer; import com.martinrgb.animer.core.interpolator.AndroidNative.AccelerateDecelerateInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.FastOutSlowInInterpolator; import com.martinrgb.animer.core.interpolator.AndroidNative.LinearInterpolator; import com.martinrgb.animer.core.util.AnUtil; import com.martinrgb.animer.monitor.AnConfigRegistry; import com.martinrgb.animer.monitor.AnConfigView; public class PrototypeActivity extends AppCompatActivity { private SmoothCornersImage smoothCornersImage; private AnConfigView mAnimerConfiguratorView; private static final Animer.AnimerSolver solverRect = Animer.springRK4(650,45); private static final Animer.AnimerSolver solverScale = Animer.springRK4(400,30f); private static final Animer.AnimerSolver solverButtonScale = Animer.springRK4(500,30f); private static final Animer.AnimerSolver solverRadius = Animer.interpolatorDroid(new FastOutSlowInInterpolator(),600); private static final Animer.AnimerSolver solverTrans = Animer.springDroid(800,0.95f); private static final Animer.AnimerSolver solverAlpha = Animer.interpolatorDroid(new LinearInterpolator(),100); private static final Animer.AnimerSolver solverNav = Animer.interpolatorDroid(new AccelerateDecelerateInterpolator(),150); private Animer mRectAnimer,mTransAnimer,mRadiusAnimer,mAlphaAnimer, mNavAnimer,mScaleAnimer,mScaleButtonAnimer; private boolean isShowDetail = false; private float initW, initH, initR, initTranslationX; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); deleteBars(); setContentView(R.layout.activity_prototype); smoothCornersImage = findViewById(R.id.smooth_iv); ViewTreeObserver vto = smoothCornersImage.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { smoothCornersImage.getViewTreeObserver().removeOnGlobalLayoutListener(this); initTranslationX = smoothCornersImage.getTranslationX(); initW = smoothCornersImage.getMeasuredWidth(); initH = smoothCornersImage.getMeasuredHeight(); initR = smoothCornersImage.getRoundRadius(); } }); getDisplayPoint(); setAnimerSystem(); findViewById(R.id.smooth_iv).setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: // if (!isShowDetail) { // mScaleAnimer.setEndValue(0.95f); // } mScaleAnimer.setEndValue(0.95f); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: mRectAnimer.setEndValue(1); mTransAnimer.setEndValue(1); mRadiusAnimer.setEndValue(0); mAlphaAnimer.setEndValue(1); mNavAnimer.setEndValue(1); mScaleAnimer.setEndValue(1f); // if (!isShowDetail) { // mRectAnimer.setEndValue(1); // mTransAnimer.setEndValue(1); // mRadiusAnimer.setEndValue(0); // mAlphaAnimer.setEndValue(1); // mNavAnimer.setEndValue(1); // mScaleAnimer.setEndValue(1f); // } else { // mRectAnimer.setEndValue(0); // mTransAnimer.setEndValue(0); // mRadiusAnimer.setEndValue(initR); // mAlphaAnimer.setEndValue(0); // mNavAnimer.setEndValue(0); // } isShowDetail = !isShowDetail; break; case MotionEvent.ACTION_CANCEL: break; } return true; } }); findViewById(R.id.arrow_iv).setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: // if (isShowDetail) { // mScaleAnimer.setEndValue(0.95f); // } mScaleButtonAnimer.setEndValue(0.95f); break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: mRectAnimer.setEndValue(0); mTransAnimer.setEndValue(0); mRadiusAnimer.setEndValue(initR); mAlphaAnimer.setEndValue(0); mNavAnimer.setEndValue(0); mScaleButtonAnimer.setEndValue(1); //isShowDetail = !isShowDetail; break; case MotionEvent.ACTION_CANCEL: break; } return true; } }); } private void setAnimerSystem() { mRectAnimer = new Animer(); mRectAnimer.setSolver(solverRect); mRectAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { float XVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, initTranslationX, 0); float YVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, 0, -918); float widthVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, initW, sWidth); float heightVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, initH, sWidth); float reverseHeightVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, (sWidth - 420) / 2, 0); smoothCornersImage.setRectSize(widthVal, heightVal); smoothCornersImage.setTranslationX(XVal); smoothCornersImage.setTranslationY(YVal); findViewById(R.id.smooth_text_iv).setTranslationY(-(heightVal - 420) / 2); findViewById(R.id.detail_text_iv).setTranslationY(reverseHeightVal); findViewById(R.id.arrow_iv).setTranslationY(reverseHeightVal); } }); mTransAnimer = new Animer(); mTransAnimer.setSolver(solverTrans); mTransAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { float transVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, 0, 1170); findViewById(R.id.top_iv).setTranslationY(-transVal); findViewById(R.id.bottom_iv).setTranslationY(transVal); } }); mRadiusAnimer = new Animer(); mRadiusAnimer.setSolver(solverRadius); mRadiusAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { smoothCornersImage.setRoundRadius(value); } }); mScaleAnimer = new Animer(); mScaleAnimer.setSolver(solverScale); mScaleAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { smoothCornersImage.setScaleX(value); smoothCornersImage.setScaleY(value); smoothCornersImage.setAlpha(value); } }); mScaleButtonAnimer = new Animer(); mScaleButtonAnimer.setSolver(solverButtonScale); mScaleButtonAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { findViewById(R.id.arrow_iv).setScaleX(value); findViewById(R.id.arrow_iv).setScaleY(value); } }); mScaleButtonAnimer.setCurrentValue(1); mScaleAnimer.setCurrentValue(1); mAlphaAnimer = new Animer(); mAlphaAnimer.setSolver(solverAlpha); mAlphaAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { float alphaVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, 1, 0); float reverseAlpha = (float) AnUtil.mapValueFromRangeToRange(value, 0.5f, 1, 0, 1); findViewById(R.id.top_iv).setAlpha(alphaVal); findViewById(R.id.bottom_iv).setAlpha(alphaVal); findViewById(R.id.smooth_text_iv).setAlpha(alphaVal); findViewById(R.id.arrow_iv).setAlpha(reverseAlpha); findViewById(R.id.detail_text_iv).setAlpha(reverseAlpha); } }); mNavAnimer = new Animer(); mNavAnimer.setSolver(solverNav); mNavAnimer.setUpdateListener(new Animer.UpdateListener() { @Override public void onUpdate(float value, float velocity, float progress) { float transVal = (float) AnUtil.mapValueFromRangeToRange(value, 0, 1, 0, 300); findViewById(R.id.nav_iv).setTranslationY(transVal); } }); mAnimerConfiguratorView = (AnConfigView) findViewById(R.id.an_configurator); AnConfigRegistry.getInstance().addAnimer("Image Rect Animation",mRectAnimer); AnConfigRegistry.getInstance().addAnimer("Image Scale Animation",mScaleAnimer); AnConfigRegistry.getInstance().addAnimer("Image Radius Animation",mRadiusAnimer); AnConfigRegistry.getInstance().addAnimer("Button Scale Animation",mScaleButtonAnimer); AnConfigRegistry.getInstance().addAnimer("List Trans Animation",mTransAnimer); AnConfigRegistry.getInstance().addAnimer("Total Alpha Animation",mAlphaAnimer); AnConfigRegistry.getInstance().addAnimer("Navigation Animation",mNavAnimer); mAnimerConfiguratorView.refreshAnimerConfigs(); } private float sWidth, sHeight; private void getDisplayPoint() { Point point = new Point(); getWindowManager().getDefaultDisplay().getSize(point); sWidth = (float) point.x; sHeight = (float) point.y; } private void deleteBars() { this.requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); } } ================================================ FILE: app/src/main/java/com/martinrgb/animerexample/ScrollerActivity.java ================================================ package com.martinrgb.animerexample; import android.content.Context; import android.content.res.Resources; import android.opengl.Visibility; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.Display; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import com.martinrgb.animer.component.scrollview.AnScrollView; import com.martinrgb.animer.monitor.AnConfigRegistry; import com.martinrgb.animer.monitor.AnConfigView; public class ScrollerActivity extends AppCompatActivity { private final int ROW_COUNT = 20; private int[] imageViews = new int[]{R.drawable.img_1,R.drawable.img_2,R.drawable.img_3,R.drawable.img_4}; private AnConfigView mAnimerConfiguratorView; private int cellSize; private float screenWidth,screenHeight; private AnScrollView customScrollViewV,customScrollViewH; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); deleteBars(); setContentView(R.layout.activity_scroller); measureDisplay(); createLayout(); addAnimerConfig(); } private void createLayout(){ // Vertical content ViewGroup contentV = (ViewGroup) findViewById(R.id.content_view_v); for (int i = 0; i < ROW_COUNT; i++) { ExampleRowView exampleRowView = new ExampleRowView(getApplicationContext()); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, (int) LinearLayout.LayoutParams.WRAP_CONTENT ); exampleRowView.getRootLayout().setLayoutParams(params); exampleRowView.setHeader("Header " + i); exampleRowView.setSub("Sub " + i); exampleRowView.setImage(imageViews[i%4]); contentV.addView(exampleRowView); } // Horizontal content ViewGroup contentH = (ViewGroup) findViewById(R.id.content_view_h); for (int i = 0; i < ROW_COUNT; i++) { ExampleRowView exampleRowView = new ExampleRowView(getApplicationContext()); exampleRowView.setHeader("Header " + i); exampleRowView.setSub("Sub " + i); exampleRowView.setImage(imageViews[i%4]); if(i == 0){ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( cellSize, LinearLayout.LayoutParams.WRAP_CONTENT ); params.setMargins( (int)(screenWidth - cellSize)/2, 0, 0, 0); exampleRowView.setLayoutParams(params); } else if(i == ROW_COUNT - 1){ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( cellSize, LinearLayout.LayoutParams.WRAP_CONTENT ); params.setMargins( 0, 0, (int)(screenWidth - cellSize)/2, 0); exampleRowView.setLayoutParams(params); } contentH.addView(exampleRowView); } // Vertical sv customScrollViewV = findViewById(R.id.scrollView_v); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, (int)RelativeLayout.LayoutParams.MATCH_PARENT ); params.addRule(RelativeLayout.ALIGN_PARENT_TOP); customScrollViewV.setLayoutParams(params); customScrollViewV.getScroller().setVertScroll(true); // Horizontal sv customScrollViewH = findViewById(R.id.scrollView_h); params = new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, (int) screenHeight/2 ); params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); customScrollViewH.setLayoutParams(params); customScrollViewH.getScroller().setVertScroll(false); customScrollViewH.getScroller().setFixedScroll(true,cellSize); customScrollViewH.setVisibility(View.INVISIBLE); } private void addAnimerConfig(){ mAnimerConfiguratorView = (AnConfigView) findViewById(R.id.an_configurator); AnConfigRegistry.getInstance().addAnimer("V_Fling",customScrollViewV.getScroller().getFlingAnimer()); AnConfigRegistry.getInstance().addAnimer("V_SpringBack",customScrollViewV.getScroller().getSpringAnimer()); AnConfigRegistry.getInstance().addAnimer("V_FakeFling When Fixed Scroll",customScrollViewV.getScroller().getFakeFlingAnimer()); AnConfigRegistry.getInstance().addAnimer("H_Fling",customScrollViewH.getScroller().getFlingAnimer()); AnConfigRegistry.getInstance().addAnimer("H_SpringBack",customScrollViewH.getScroller().getSpringAnimer()); AnConfigRegistry.getInstance().addAnimer("H_FakeFling When Fixed Scroll",customScrollViewH.getScroller().getFakeFlingAnimer()); mAnimerConfiguratorView.refreshAnimerConfigs(); } private class ExampleRowView extends LinearLayout { private final TextView mHeaderView; private final TextView mSubView; private final SmoothCornersImage mImageView; private final LinearLayout root; public ExampleRowView(Context context) { super(context); LayoutInflater inflater = LayoutInflater.from(context); ViewGroup view = (ViewGroup) inflater.inflate(R.layout.custom_cell_view, this, false); root = view.findViewById(R.id.root); mHeaderView = (TextView) view.findViewById(R.id.head_view); mSubView = (TextView) view.findViewById(R.id.sub_view); mImageView = view.findViewById(R.id.img_view); mImageView.setRoundRadius(60); addView(view); } public void setHeader(String text) { mHeaderView.setText(text); } public void setSub(String text) { mSubView.setText(text); } public void setImage(int id) { mImageView.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(),id)); mImageView.setScaleType(ImageView.ScaleType.MATRIX); } public LinearLayout getRootLayout(){ return root; } } private void deleteBars() { this.requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().requestFeature(Window.FEATURE_ACTION_BAR); getSupportActionBar().hide(); } private void measureDisplay() { Display display = getWindowManager().getDefaultDisplay(); DisplayMetrics outMetrics = new DisplayMetrics (); display.getMetrics(outMetrics); float density = getResources().getDisplayMetrics().density; float dpHeight = outMetrics.heightPixels / density; float dpWidth = outMetrics.widthPixels / density; screenHeight = dpToPx(dpHeight,getResources()); screenWidth = dpToPx(dpWidth,getResources()); cellSize = (int) getResources().getDimension(R.dimen.cell_size_dp); Log.e("inDP","doHeight"+ dpHeight + "dpWidth" + dpWidth); } public static int dpToPx(float dp, Resources res) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,res.getDisplayMetrics()); } } ================================================ FILE: app/src/main/java/com/martinrgb/animerexample/SmoothCornersImage.java ================================================ package com.martinrgb.animerexample; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Xfermode; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import androidx.appcompat.widget.AppCompatImageView; public class SmoothCornersImage extends AppCompatImageView { private final Paint paint = new Paint(); private final Xfermode mode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN); private Bitmap imageBitmap; private Rect dstRect; private Matrix matrix; float cx, cy; private float WIDTH = 400; private float HEIGHT = 400; private float SKETCH_ROUND_RECT_RADIUS = 100f; private boolean ROUND_TL = true,ROUND_TR = true,ROUND_BL = true,ROUND_BR = true; private boolean isSquare = false; public SmoothCornersImage(Context context) { super(context); getAttributes(null); } public SmoothCornersImage(Context context, AttributeSet attrs) { super(context, attrs); getAttributes(attrs); } public SmoothCornersImage(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); getAttributes(attrs); } private void getAttributes(AttributeSet attr) { if(attr == null){ return; } else{ TypedArray typedArray = getContext() .getTheme() .obtainStyledAttributes(attr, R.styleable.SmoothCornersImage, 0, 0); try { SKETCH_ROUND_RECT_RADIUS = typedArray.getInteger(R.styleable.SmoothCornersImage_smooth_radius, 20); isSquare = typedArray.getBoolean(R.styleable.SmoothCornersImage_is_square, false); } finally { typedArray.recycle(); } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if(w != 0 && h != 0){ this.cx = w / 2.0f; this.cy = h / 2.0f; dstRect = new Rect(0, 0, w, h); this.WIDTH = isSquare()?Math.min(w,h):w; this.HEIGHT = isSquare()?Math.min(w,h):h; matrix = new Matrix(); Point canvasCenter = new Point((int)this.cx,(int)this.cy); Point bmpCenter = new Point(this.imageBitmap.getWidth() / 2, this.imageBitmap.getHeight() / 2); float xRatio = bmpCenter.x/this.cx; float yRatio = bmpCenter.y/this.cy; float ratio; if(xRatio > yRatio){ ratio = 1/yRatio; } else { ratio = 1/xRatio; } Log.e("ratio", String.valueOf(ratio)); matrix.postTranslate(cx - bmpCenter.x, cy - bmpCenter.y); matrix.postScale(ratio,ratio, cx, cy); } } @Override public void setImageBitmap(Bitmap bm) { super.setImageBitmap(bm); this.imageBitmap = bm; } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); if(drawable instanceof BitmapDrawable) this.imageBitmap = ((BitmapDrawable)drawable).getBitmap(); else { int width = drawable.getIntrinsicWidth(); int height = drawable.getIntrinsicHeight(); if(width != -1 && height != -1) this.imageBitmap = Bitmap.createBitmap(width , height , Bitmap.Config.ARGB_8888); else this.imageBitmap = Bitmap.createBitmap(1 , 1 , Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(this.imageBitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight() ); drawable.draw(canvas); } } //############ onDraw ############ @Override protected void onDraw(Canvas canvas) { if(imageBitmap != null){ canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), paint, Canvas.ALL_SAVE_FLAG); paint.setAntiAlias(true); paint.setFilterBitmap(true); paint.setDither(true); Path path; if(isSquare){ if(SKETCH_ROUND_RECT_RADIUS == WIDTH/2){ canvas.translate(cx-WIDTH/2, cy-HEIGHT/2); drawAndroidRoundRect(0,0,WIDTH,HEIGHT,SKETCH_ROUND_RECT_RADIUS,SKETCH_ROUND_RECT_RADIUS,ROUND_TL,ROUND_TR,ROUND_BL,ROUND_BR,canvas); } else{ path = SketchRealSmoothRect(0, 0, WIDTH , WIDTH , SKETCH_ROUND_RECT_RADIUS,SKETCH_ROUND_RECT_RADIUS, ROUND_TL,ROUND_TR,ROUND_BL,ROUND_BR); canvas.drawPath(path,paint); } } else{ if(SKETCH_ROUND_RECT_RADIUS == Math.min(WIDTH,HEIGHT)/2){ canvas.translate(cx-WIDTH/2, cy-HEIGHT/2); drawAndroidRoundRect(0,0,WIDTH,HEIGHT,SKETCH_ROUND_RECT_RADIUS,SKETCH_ROUND_RECT_RADIUS,ROUND_TL,ROUND_TR,ROUND_BL,ROUND_BR,canvas); } else{ path = SketchRealSmoothRect(0, 0, WIDTH , HEIGHT , SKETCH_ROUND_RECT_RADIUS,SKETCH_ROUND_RECT_RADIUS, ROUND_TL,ROUND_TR,ROUND_BL,ROUND_BR); canvas.drawPath(path,paint); } } paint.setXfermode(mode); if(imageBitmap.getWidth() != 1){ //canvas.drawBitmap(this.imageBitmap, -(this.imageBitmap.getWidth() - canvas.getWidth())/2, -(this.imageBitmap.getHeight() - canvas.getHeight())/2, paint); //canvas.drawBitmap(this.imageBitmap, 0,0, paint); canvas.drawBitmap(this.imageBitmap,matrix,paint); } else{ canvas.drawBitmap(this.imageBitmap, null, dstRect, paint); } paint.reset(); } } //############ Define Path ############ public Path SketchRealSmoothRect( float left, float top, float right, float bottom, float rx, float ry, boolean tl, boolean tr, boolean bl, boolean br ){ Path path = new Path(); if (rx < 0) rx = 0; if (ry < 0) ry = 0; float width = right - left; float height = bottom - top; float posX = cx - width/2; float posY = cy - height/2; float r = rx; float vertexRatio; if(r/Math.min(width/2,height/2) > 0.5){ float percentage = ((r/Math.min(width/2,height/2)) - 0.5f)/0.4f; float clampedPer = Math.min(1,percentage); vertexRatio = 1.f - (1 - 1.104f/1.2819f)*clampedPer; } else{ vertexRatio = 1.f; } float controlRatio; if(r/Math.min(width/2,height/2) > 0.6){ float percentage = ((r/Math.min(width/2,height/2)) - 0.6f)/0.3f; float clampedPer = Math.min(1,percentage); controlRatio = 1 + (0.8717f/0.8362f - 1)*clampedPer; } else{ controlRatio = 1; } path.moveTo(posX + width/2 , posY); if(!tr){ path.lineTo(posX + width, posY); } else{ path.lineTo(posX + Math.max(width/2,width - r/100.0f*128.19f*vertexRatio), posY); path.cubicTo(posX + width - r/100.f*83.62f*controlRatio, posY,posX + width - r/100.f*67.45f,posY + r/100.f*4.64f, posX + width - r/100.f*51.16f, posY + r/100.f*13.36f); path.cubicTo(posX + width - r/100.f*34.86f, posY + r/100.f*22.07f,posX + width - r/100.f*22.07f,posY + r/100.f*34.86f, posX + width - r/100.f*13.36f, posY + r/100.f*51.16f); path.cubicTo(posX + width - r/100.f*4.64f, posY + r/100.f*67.45f,posX + width,posY + r/100.f*83.62f*controlRatio, posX + width, posY + Math.min(height/2,r/100.f*128.19f*vertexRatio)); } if(!br){ path.lineTo(posX + width, posY + height); } else{ path.lineTo(posX + width, posY + Math.max(height/2,height - r/100.f*128.19f*vertexRatio)); path.cubicTo(posX + width, posY + height - r/100.f*83.62f*controlRatio,posX + width - r/100.f*4.64f,posY + height - r/100.f*67.45f, posX + width - r/100.f*13.36f, posY + height - r/100.f*51.16f); path.cubicTo(posX + width - r/100.f*22.07f, posY + height - r/100.f*34.86f,posX + width - r/100.f*34.86f,posY + height - r/100.f*22.07f, posX + width - r/100.f*51.16f, posY + height - r/100.f*13.36f); path.cubicTo(posX + width - r/100.f*67.45f, posY + height - r/100.f*4.64f,posX + width - r/100.f*83.62f*controlRatio,posY + height, posX + Math.max(width/2,width - r/100.f*128.19f*vertexRatio), posY + height); } if(!bl){ path.lineTo(posX, posY + height); } else{ path.lineTo(posX + Math.min(width/2,r/100.f*128.19f*vertexRatio), posY + height); path.cubicTo(posX + r/100.f*83.62f*controlRatio, posY + height,posX + r/100.f*67.45f,posY + height - r/100.f*4.64f, posX + r/100.f*51.16f, posY + height - r/100.f*13.36f); path.cubicTo(posX + r/100.f*34.86f, posY + height - r/100.f*22.07f,posX + r/100.f*22.07f,posY + height - r/100.f*34.86f, posX + r/100.f*13.36f, posY + height - r/100.f*51.16f); path.cubicTo(posX + r/100.f*4.64f, posY + height - r/100.f*67.45f,posX ,posY + height - r/100.f*83.62f*controlRatio, posX , posY + Math.max(height/2,height - r/100.f*128.19f*vertexRatio)); } if(!tl){ path.lineTo(posX, posY); } else{ path.lineTo(posX, posY + Math.min(height/2,r/100.f*128.19f*vertexRatio)); path.cubicTo(posX, posY + r/100.f*83.62f*controlRatio,posX + r/100.f*4.64f,posY + r/100.f*67.45f, posX + r/100.f*13.36f, posY + r/100.f*51.16f); path.cubicTo(posX + r/100.f*22.07f, posY + r/100.f*34.86f,posX + r/100.f*34.86f,posY + r/100.f*22.07f, posX + r/100.f*51.16f, posY + r/100.f*13.36f); path.cubicTo(posX + r/100.f*67.45f, posY + r/100.f*4.64f,posX + r/100.f*83.62f*controlRatio,posY, posX + Math.min(width/2,r/100.f*128.19f*vertexRatio), posY); } path.close(); return path; } public void drawAndroidRoundRect( float left, float top, float right, float bottom, float rx, float ry, boolean tl, boolean tr, boolean bl, boolean br,Canvas canvas ){ float w = right - left; float h = bottom - top; final Rect rect = new Rect((int)left, (int)top, (int)right,(int)bottom); final RectF rectF = new RectF(rect); canvas.drawRoundRect(rectF,rx,ry, paint); if(!tl){ canvas.drawRect(0, 0, w/2, h/2, paint); Path path = new Path(); path.moveTo(0,h/2); path.lineTo(0,0); path.lineTo(w/2,0); canvas.drawPath(path,paint); } if(!tr){ canvas.drawRect(w/2, 0, w, h/2, paint); Path path = new Path(); path.moveTo(w/2,0); path.lineTo(w,0); path.lineTo(w,h/2); canvas.drawPath(path,paint); } if(!bl){ canvas.drawRect(0, h/2, w/2, h, paint); Path path = new Path(); path.moveTo(0,h/2); path.lineTo(0,h); path.lineTo(w/2,h); canvas.drawPath(path,paint); } if(!br){ canvas.drawRect(w/2, h/2, w, h, paint); Path path = new Path(); path.moveTo(w/2,h); path.lineTo(w,h); path.lineTo(w,h/2); canvas.drawPath(path,paint); } } //############ Getter & Setter ############ public float getRoundRadius() { return SKETCH_ROUND_RECT_RADIUS; } public void setRoundRadius(float radius){ this.SKETCH_ROUND_RECT_RADIUS = Math.max(0,getRadiusInMaxRange(WIDTH,HEIGHT,radius)); this.invalidate(); } public float getMAXRadius(float width,float height){ float minBorder; if(width > height){ minBorder = height; } else{ minBorder = width; } return minBorder/2; } private float getRadiusInMaxRange(float width,float height,float radius) { float realRadius = Math.min(radius, getMAXRadius(width, height)); return realRadius; } public PointF getRectSize(){ return new PointF(getRectWidth(),getRectWidth()); } public void setRectSize(float width,float height){ setRectWidth(width); setRectHeight(height); requestLayout(); //this.invalidate(); } private float getRectWidth() { return WIDTH; } private void setRectWidth(float WIDTH) { this.WIDTH = WIDTH; getLayoutParams().width = (int)WIDTH; } private float getRectHeight() { return HEIGHT; } private void setRectHeight(float HEIGHT) { this.HEIGHT = HEIGHT; getLayoutParams().height = (int)HEIGHT; } public boolean isSquare() { return isSquare; } public void setIsSquare(boolean square) { isSquare = square; this.invalidate(); } public void setRectRoundEnable(boolean tl,boolean tr,boolean bl,boolean br){ this.ROUND_TL = tl; this.ROUND_TR = tr; this.ROUND_BL = bl; this.ROUND_BR = br; this.invalidate(); } } ================================================ FILE: app/src/main/res/drawable/background_elevation.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/myrect.xml ================================================ ================================================ FILE: app/src/main/res/drawable-nodpi/mute.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_prototype.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_scroller.xml ================================================ ================================================ FILE: app/src/main/res/layout/custom_cell_view.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/attr.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #008577 #00574B #D81B60 ================================================ FILE: app/src/main/res/values/dimension.xml ================================================ 100dp ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Animer Example ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/test/java/com/martinrgb/animerexample/ExampleUnitTest.java ================================================ package com.martinrgb.animerexample; import org.junit.Test; import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ public class ExampleUnitTest { @Test public void addition_isCorrect() { assertEquals(4, 2 + 2); } } ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.6.2' // classpath 'com.novoda:bintray-release:0.9.2' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() mavenCentral() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Tue May 26 17:09:15 CST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip d ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ include ':app', ':animer'