├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── dictionaries │ ├── Administrator.xml │ └── pro.xml └── encodings.xml ├── LICENSE ├── README-ZH.md ├── README.md ├── app ├── build.gradle ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── juziml │ │ │ │ └── read │ │ │ │ ├── base │ │ │ │ └── BaseActivity.kt │ │ │ │ ├── business │ │ │ │ ├── BookMockData.java │ │ │ │ ├── BookPaperAdapter.kt │ │ │ │ ├── SimpleBookAct.kt │ │ │ │ ├── StartupAct.kt │ │ │ │ └── read │ │ │ │ │ ├── anim │ │ │ │ │ ├── AnimHelper.java │ │ │ │ │ ├── CoverAnimationEffecter.java │ │ │ │ │ ├── IAnimationEffecter.java │ │ │ │ │ └── SimulationAnimationEffecter.java │ │ │ │ │ └── view │ │ │ │ │ ├── AnimParentView.java │ │ │ │ │ ├── BookLayoutManager.java │ │ │ │ │ ├── BookRecyclerView.java │ │ │ │ │ ├── BookView.java │ │ │ │ │ ├── EventProxy.java │ │ │ │ │ ├── FPoint.java │ │ │ │ │ ├── PaperLayout.java │ │ │ │ │ ├── PuppetView.java │ │ │ │ │ ├── RVInnerItemFunction.java │ │ │ │ │ └── RVOuterFunction.java │ │ │ │ ├── core │ │ │ │ ├── App.java │ │ │ │ ├── ArchApplication.kt │ │ │ │ └── ArchConfig.kt │ │ │ │ └── utils │ │ │ │ ├── DLog.java │ │ │ │ └── TraceUtils.java │ │ └── res │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_add_chat.png │ │ │ ├── ic_chat_gpt.png │ │ │ ├── ic_chat_sakura_flower.png │ │ │ ├── ic_close_pink_128.png │ │ │ ├── ic_empty_new.png │ │ │ ├── ic_head.png │ │ │ ├── ic_launcher_20.xml │ │ │ ├── ic_launcher_xx.xml │ │ │ ├── ic_loading_cycle.png │ │ │ ├── ic_send.png │ │ │ ├── ic_voice_dark.png │ │ │ ├── ic_voice_light.png │ │ │ └── icon_net_error.png │ │ │ ├── drawable │ │ │ ├── bg_app_window.xml │ │ │ ├── ic_launcher.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── shadow.xml │ │ │ ├── sp_chat_item_item_bg_bot.xml │ │ │ ├── sp_chat_item_item_bg_user.xml │ │ │ ├── sp_chat_session_list_item_bg.xml │ │ │ ├── sp_item_bg.xml │ │ │ └── test_img.jpg │ │ │ ├── layout │ │ │ ├── act_simple.xml │ │ │ └── item_read.xml │ │ │ ├── values-sw1024dp │ │ │ └── dimens.xml │ │ │ ├── values-sw1280dp │ │ │ └── dimens.xml │ │ │ ├── values-sw1365dp │ │ │ └── dimens.xml │ │ │ ├── values-sw240dp │ │ │ └── dimens.xml │ │ │ ├── values-sw320dp │ │ │ └── dimens.xml │ │ │ ├── values-sw384dp │ │ │ └── dimens.xml │ │ │ ├── values-sw392dp │ │ │ └── dimens.xml │ │ │ ├── values-sw400dp │ │ │ └── dimens.xml │ │ │ ├── values-sw410dp │ │ │ └── dimens.xml │ │ │ ├── values-sw411dp │ │ │ └── dimens.xml │ │ │ ├── values-sw432dp │ │ │ └── dimens.xml │ │ │ ├── values-sw480dp │ │ │ └── dimens.xml │ │ │ ├── values-sw533dp │ │ │ └── dimens.xml │ │ │ ├── values-sw592dp │ │ │ └── dimens.xml │ │ │ ├── values-sw600dp │ │ │ └── dimens.xml │ │ │ ├── values-sw640dp │ │ │ └── dimens.xml │ │ │ ├── values-sw662dp │ │ │ └── dimens.xml │ │ │ ├── values-sw720dp │ │ │ └── dimens.xml │ │ │ ├── values-sw768dp │ │ │ └── dimens.xml │ │ │ ├── values-sw800dp │ │ │ └── dimens.xml │ │ │ ├── values-sw811dp │ │ │ └── dimens.xml │ │ │ ├── values-sw820dp │ │ │ └── dimens.xml │ │ │ ├── values-sw960dp │ │ │ └── dimens.xml │ │ │ ├── values-sw961dp │ │ │ └── dimens.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── ids.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ └── release │ │ └── res │ │ └── xml │ │ └── strings.xml └── test.jks ├── build.gradle ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── buildsrc │ └── kts │ ├── AndroidConfig.kt │ ├── DebugThirdSdkConfig.kt │ ├── Dependencies.kt │ ├── EnterpriseWeChatConfig.kt │ ├── GlobalConfig.kt │ ├── PropertiesUtils.kt │ ├── Repositories.kt │ ├── ThirdSdkConfig.kt │ └── Utils.kt ├── gpu_test ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── juziml │ │ └── content │ │ └── gpu_test │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── juziml │ │ │ └── content │ │ │ └── gpu_test │ │ │ ├── FPoint.java │ │ │ ├── GpuTestAct.java │ │ │ └── GpuTestCurlAnimView.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── act_gputest.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── juziml │ └── content │ └── gpu_test │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Book_view_desc.jpg └── demo_pic.png ├── screenMatch.properties ├── settings.gradle └── signing.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # //////////////////////通用 且必须的忽略////////////////////// 2 | 3 | # 所有的iml 文件 4 | *.iml 5 | 6 | # .idea 下的所有文件与目录,可与!互斥 7 | .idea/* 8 | 9 | # 避免忽略 codeStyle 10 | !codeStyles/ 11 | 12 | # 避免忽略 dictionaries //代码字典,屏蔽拼写错误提示 13 | !dictionaries/ 14 | 15 | # 避免忽略 encodings.xml 编码 16 | !encodings.xml 17 | 18 | # 所有的build 文件夹 19 | build 20 | 21 | # 内存快照 22 | *.hprof 23 | 24 | # 截图快照 25 | /captures 26 | # MAC 磁盘文件 具体是啥不知道 27 | .DS_Store 28 | 29 | # native 编译缓存 30 | .externalNativeBuild 31 | .cxx 32 | 33 | # 整个 .gradle 文件夹 34 | .gradle 35 | 36 | ## 1 2级 目录的 release文件夹 37 | /release 38 | */release 39 | ## 1 2级 目录的 debug文件夹 40 | /debug 41 | */debug 42 | 43 | #本地配置参数 44 | local.properties 45 | 46 | # //////////////////////第三方////////////////////// -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 124 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dictionaries/Administrator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | priv 5 | prouter 6 | zhihu 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/dictionaries/pro.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | appid 5 | btns 6 | cheng 7 | datas 8 | gson 9 | shence 10 | unknow 11 | workorder 12 | xpxh 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Western-parotia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README-ZH.md: -------------------------------------------------------------------------------- 1 | [English Doc](./README.md) 2 | 3 | # 介绍 4 | 5 | ![simple_view](./images/demo_pic.png) 6 | 7 | * app module:阅读器demo 8 | * gpu_test module:上图纯色demo,作为独立的仿真动画实现最小demo,就一个类,包含完整的仿真动画算法与标点 9 | 10 | # 已知问题反馈 11 | 12 | 提供一种阅读器的实现思路,注意是实现思路不是完整的解决方案。一些反馈的问题我会先记录在这里,最近忙完了我会更新。 13 | 在这之前欢迎提PR处理 14 | 15 | * 内存增长问题,可通过增加bitmap缓存解决。bitmap创建的代码位置:2023-10-29 添加缓存策略 16 | 17 | # 实现思路介绍 18 | 19 | 采用木偶View将渲染, Paper页面布局、事件、动画完全分离。PaperLayout继承成自LinearLayout, 20 | 支持放入图片,视频等元素,但完全无需关心翻页动画的渲染。(不包含文字的处理,以后应该也不会添加) 21 | 22 | https://user-images.githubusercontent.com/13959965/230751166-a72e1f4b-317b-47a7-aa1c-bbc70ca34f13.mp4 23 | 24 | 核心类就4个,它们的职责跟它们的名字很相近。 25 | 26 | * BookView 摆放 RecyclerView 与 PuppetView 27 | * BookRecyclerView 作为底层容器,接受滑动事件,完成页面更换,接管无动画的事件 28 | * PaperLayout 作为页面卡片根布局,也就是设置给 RecyclerView.Adapter 加载的布局,接管仿真动画与覆盖动画的事件 29 | * PuppetView 本身不处理任何事件,只是展示动画 30 | 31 | ## 手绘一张图,呈现实现原理 32 | 33 | > 如果用文字来阐述原理难免要长篇大论,何况这里涉及到Z轴View堆叠,借此机会展示下我的绘画能力吧🐶(瞎搞) 34 | 35 | 36 | ![BookView](./images/Book_view_desc.jpg) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文文档](./README-ZH.md) 2 | 3 | This is a reader project for the Android platform. 4 | It is based on a custom RecyclerView within a ViewGroup and uses a custom LayoutManager. 5 | By arranging elements along the Z-axis, it achieves complete separation of reader animations and 6 | data binding, 7 | allowing for full customization of the ItemView. 8 | 9 | It also supports embedding image and video advertisements. Please see the video below for an example 10 | of the effects: 11 | 12 | https://user-images.githubusercontent.com/13959965/230751166-a72e1f4b-317b-47a7-aa1c-bbc70ca34f13.mp4 13 | 14 | ![simple_view](./images/demo_pic.png) 15 | 16 | # Usage 17 | 18 | Just like using a regular RecyclerView: 19 | 20 | ```kotlin 21 | bookView.setAdapter(RecyclerView.Adapter) 22 | bookView.setFlipMode(@BookLayoutManager.BookFlipMode) 23 | 24 | ``` 25 | 26 | # Directory structure 27 | 28 | * app module:BookView demo 29 | 30 | * gpu_test module:A demo of pure curl animations 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | import com.buildsrc.kts.AndroidConfig 2 | import com.buildsrc.kts.Dependencies 3 | 4 | apply plugin: 'com.android.application' 5 | apply plugin: 'kotlin-android' 6 | apply plugin: "kotlin-parcelize" 7 | 8 | Properties properties = new Properties() 9 | properties.load(new FileInputStream(file(rootProject.file("signing.properties")))) 10 | 11 | android { 12 | compileSdkVersion AndroidConfig.compileSdkVersion 13 | 14 | useLibrary 'org.apache.http.legacy' 15 | viewBinding { 16 | enabled = true 17 | } 18 | signingConfigs { 19 | release { 20 | keyAlias properties['keyAlias'] 21 | keyPassword properties['keyPassword'] 22 | storeFile file(properties['storeFile']) 23 | storePassword properties['storePassword'] 24 | } 25 | } 26 | 27 | defaultConfig { 28 | setApplicationId(AndroidConfig.AppInfo.applicationId) 29 | setVersionCode(AndroidConfig.AppInfo.versionCode) 30 | setVersionName(AndroidConfig.AppInfo.versionName) 31 | minSdkVersion AndroidConfig.minSdkVersion 32 | targetSdkVersion AndroidConfig.targetSdkVersion 33 | ndk { 34 | //flutter 目前只支持:'armeabi-v7a', 'arm64-v8a', 'x86_64' 35 | abiFilters "arm64-v8a", 'armeabi-v7a' //, 'x86', 'mips', 36 | } 37 | multiDexEnabled true 38 | 39 | javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } } 40 | 41 | //app名 42 | resValue("string", "app_name", AndroidConfig.AppInfo.appName) 43 | } 44 | 45 | buildTypes { 46 | release { 47 | minifyEnabled AndroidConfig.AppInfo.releaseObfuscate 48 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 49 | debuggable false 50 | renderscriptDebuggable true 51 | zipAlignEnabled true 52 | signingConfig signingConfigs.release 53 | } 54 | 55 | debug { 56 | minifyEnabled false 57 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 58 | debuggable true 59 | renderscriptDebuggable true 60 | zipAlignEnabled false 61 | signingConfig signingConfigs.release 62 | } 63 | 64 | debugProguard { 65 | //从release出会导致一些key是release的 66 | initWith(debug) 67 | minifyEnabled AndroidConfig.AppInfo.debugProguardObfuscate 68 | zipAlignEnabled true 69 | } 70 | } 71 | 72 | compileOptions { 73 | sourceCompatibility JavaVersion.VERSION_1_8 74 | targetCompatibility JavaVersion.VERSION_1_8 75 | } 76 | kotlinOptions { 77 | jvmTarget = JavaVersion.VERSION_1_8.toString() 78 | //为java继承的kotlin interface default方法添加实现 79 | freeCompilerArgs += ["-Xjvm-default=all-compatibility"] 80 | } 81 | 82 | dexOptions { 83 | javaMaxHeapSize "4G" 84 | // //关闭预编译 85 | // preDexLibraries false 86 | // dexInProcess = false 87 | } 88 | 89 | lintOptions { 90 | //禁用掉丢失多国语言的错误提示 91 | disable 'MissingTranslation' 92 | //禁用掉manifest仅支持竖屏的错误提示 93 | disable 'LockedOrientationActivity' 94 | //禁用掉代码setText必须使用string res的警告 95 | disable 'SetTextI18n' 96 | //禁用掉xml的text必须使用string res的警告 97 | disable 'HardcodedText' 98 | //禁用掉xml的ImageView必须添加描述的警告 99 | disable 'ContentDescription' 100 | //禁用掉text使用dp的警告 101 | disable 'SpUsage' 102 | 103 | abortOnError false 104 | } 105 | 106 | } 107 | 108 | 109 | dependencies { 110 | implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') 111 | 112 | implementation Dependencies.AndroidX.appcompat 113 | implementation Dependencies.OpenSourceLibrary.junit 114 | implementation Dependencies.OpenSourceLibrary.glide 115 | implementation Dependencies.Kotlin.kotlin_stdlib 116 | implementation Dependencies.AndroidX.constraintLayout 117 | api Dependencies.AndroidX.core_ktx 118 | implementation(Dependencies.Material.material) 119 | implementation(Dependencies.OpenSourceLibrary.recyclerView) 120 | implementation(Dependencies.OpenSourceLibrary.chadAdapter) 121 | implementation(Dependencies.Foundation.activityFragment) 122 | implementation(Dependencies.Foundation.view_binding_helper) 123 | implementation(Dependencies.Foundation.rv_adapter) 124 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.juziml.read.base 2 | 3 | import android.os.Bundle 4 | import com.foundation.app.arc.activity.BaseFragmentManagerActivity 5 | 6 | abstract class BaseActivity : BaseFragmentManagerActivity() { 7 | override fun afterSuperOnCreate(savedInstanceState: Bundle?) { 8 | } 9 | 10 | override fun beforeSuperOnCreate(savedInstanceState: Bundle?) { 11 | } 12 | 13 | override fun initViewModel() { 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/BookMockData.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business; 2 | 3 | /** 4 | * create by zhusw on 2020-03-27 14:51 5 | */ 6 | public class BookMockData { 7 | public String content = "由于国外大部分公司仍在远程办公,所以部分服务与疫情之前相比有所下降,比如 Netflix、YouTube、Facebook 先后宣布下调欧洲视频流质量,以帮助缓解新冠病毒疫情期间任何潜在的网络拥堵,谷歌此前宣布暂停 Chrome 浏览器更新等。然而,黑客在此期间却动作频频,先后曝出世卫组织遭到不明黑客攻击、GitHub pages 被攻击等消息。" + 8 | "\n此前,业内就有专家预测近期可能会有很多攻击出现,尤其是医疗行业。医疗机构一般防御措施有限,一旦被攻击会造成严重后果。随着更多的黑客组织在此刻关注到相关热点,很有可能会有更多攻击手法出现。如果再出现像去年针对医疗行业的针对性勒索攻击的话,将会对相关目标造成灾难性后果,可直接造成整个医院业务停摆。如果相关的攻击发生在疫情严重地区的话,其后果不堪想象。建议采取以下防范措施:" + 9 | "1、及时升级操作系统以及应用软件,打全补丁,尤其是 MS17-010、CVE-2019-0708 等高危漏洞的补丁。由于 Windows 7 操作系统已经停止推送更新补丁,建议有条件的更新到 Windows 10 操作系统。\n" + 10 | "\n" + 11 | "2、及时更新已部署的终端、边界防护产品规则。\n" + 12 | "\n" + 13 | "3、尽量减少各种外部服务的暴露面(如 RDP,VNC 等远程服务),如果一定要开启的话,需要设置白名单访问策略,设置足够强壮的登陆密码,避免黑客利用远程服务攻入。\n" + 14 | "\n" + 15 | "4、增强人员的网络安全意识,不打开不明邮件,邮件中的不明链接、附件等。\n" + 16 | "\n" + 17 | "5、常用办公软件应保持严格的安全策略,如禁止运行 Office 宏等。\n" + 18 | "\n" + 19 | "很多企业纷纷开始远程办公模式,需要对暴露在互联网上的主机应采取一些必要的防范措施。\n" + 20 | "\n" + 21 | "1、及时升级相关主机的补丁,尤其是 2019 年出现的 CVE-2019-0708 等一系列 RDP 漏洞。\n" + 22 | "\n" + 23 | "2、设置足够强壮的登陆密码,并开启强制身份认证,避免黑客利用远程服务攻入。"; 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/BookPaperAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business 2 | 3 | import com.foundation.widget.crvadapter.viewbinding.ViewBindingQuickAdapter 4 | import com.foundation.widget.crvadapter.viewbinding.ViewBindingViewHolder 5 | import com.juziml.read.R 6 | import com.juziml.read.databinding.ItemReadBinding 7 | 8 | class BookPaperAdapter : ViewBindingQuickAdapter() { 9 | 10 | override fun convertVB( 11 | holder: ViewBindingViewHolder, 12 | vb: ItemReadBinding, 13 | item: BookMockData 14 | ) { 15 | vb.tvContent.text = item.content 16 | vb.tvContent2.text = item.content 17 | 18 | holder.addOnClickListener(R.id.ir2d_iv) 19 | holder.addOnClickListener(R.id.ir2d_btn1) 20 | holder.addOnClickListener(R.id.ir2d_btn2) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/SimpleBookAct.kt: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import com.foundation.app.arc.utils.ext.lazyAtomic 6 | import com.juziml.read.R 7 | import com.juziml.read.base.BaseActivity 8 | import com.juziml.read.business.read.view.BookLayoutManager 9 | import com.juziml.read.databinding.ActSimpleBinding 10 | 11 | private const val DATA_SIZE = 20 12 | 13 | class SimpleBookAct : BaseActivity() { 14 | private val vb by lazyAndSetRoot() 15 | var position = 0 16 | private val bookPaperAdapter by lazyAtomic { 17 | BookPaperAdapter() 18 | } 19 | 20 | override fun init(savedInstanceState: Bundle?) { 21 | bookPaperAdapter.apply { 22 | setOnItemChildClickListener { _, view, position -> 23 | when (view.id) { 24 | R.id.ir2d_iv -> { 25 | showToast("点击图片:ir2d_iv") 26 | } 27 | R.id.ir2d_btn1 -> { 28 | showToast("点击按钮:ir2d_btn1") 29 | } 30 | R.id.ir2d_btn2 -> { 31 | showToast("点击按钮:ir2d_btn2") 32 | } 33 | } 34 | } 35 | 36 | } 37 | vb.bookView.apply { 38 | setAdapter(bookPaperAdapter) 39 | setOnPositionChangedListener { _, curPosition -> 40 | position = curPosition 41 | } 42 | setFlipMode(BookLayoutManager.BookFlipMode.MODE_CURL) 43 | setOnClickMenuListener { 44 | showToast("点击菜单") 45 | } 46 | 47 | } 48 | 49 | vb.btnCover.setOnClickListener { 50 | vb.bookView.setFlipMode(BookLayoutManager.BookFlipMode.MODE_COVER) 51 | } 52 | vb.btnCurl.setOnClickListener { 53 | vb.bookView.setFlipMode(BookLayoutManager.BookFlipMode.MODE_CURL) 54 | } 55 | vb.btnNormal.setOnClickListener { 56 | vb.bookView.setFlipMode(BookLayoutManager.BookFlipMode.MODE_NORMAL) 57 | } 58 | vb.btnPrevious.setOnClickListener { 59 | if (position > 0) { 60 | position -= 1 61 | vb.bookView.scrollToPosition(position) 62 | } 63 | } 64 | vb.btnNext.setOnClickListener { 65 | if (position < DATA_SIZE - 1) { 66 | position += 1 67 | vb.bookView.scrollToPosition(position) 68 | } 69 | } 70 | } 71 | 72 | override fun bindData() { 73 | bookPaperAdapter.setNewData(createData(DATA_SIZE)) 74 | } 75 | 76 | private fun createData(paperSize: Int): List { 77 | var size = paperSize 78 | if (size <= 0) size = 5 79 | val data: MutableList = ArrayList() 80 | for (i in 0 until size) { 81 | val book = BookMockData() 82 | book.content = buildString(i) 83 | data.add(book) 84 | } 85 | return data 86 | } 87 | 88 | private fun buildString(i: Int): String? { 89 | val builder = StringBuilder() 90 | for (f in 0..999) { 91 | builder.append(i) 92 | builder.append("-") 93 | } 94 | return builder.toString() 95 | } 96 | 97 | private fun showToast(text: String) { 98 | Toast.makeText(this@SimpleBookAct, text, Toast.LENGTH_LONG).show() 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/StartupAct.kt: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import com.juziml.read.base.BaseActivity 6 | 7 | class StartupAct : BaseActivity() { 8 | override fun bindData() { 9 | } 10 | 11 | override fun init(savedInstanceState: Bundle?) { 12 | startActivity(Intent(this, SimpleBookAct::class.java)) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/anim/AnimHelper.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.anim; 2 | 3 | import android.graphics.drawable.GradientDrawable; 4 | 5 | /** 6 | * create by zhusw on 2020-08-06 15:53 7 | */ 8 | public final class AnimHelper { 9 | 10 | public final static float MOVE_SLOP = 1; 11 | 12 | public final static int SLID_DIRECTION_UNKNOWN = 0; 13 | 14 | public final static int SLID_DIRECTION_LEFT = 1; 15 | 16 | public final static int SLID_DIRECTION_RIGHT = 2; 17 | 18 | public final static int RELAY_ANIM_DURATION = 400; 19 | public final static int CANCEL_ANIM_DURATION = 200; 20 | 21 | private GradientDrawable topLeftGradientDrawable; 22 | private GradientDrawable topRightGradientDrawable; 23 | 24 | private GradientDrawable bottomLeftGradientDrawable; 25 | private GradientDrawable bottomRightGradientDrawable; 26 | 27 | private GradientDrawable topBGradientDrawable; 28 | private GradientDrawable bottomBGradientDrawable; 29 | 30 | private GradientDrawable topCGradientDrawable; 31 | private GradientDrawable bottomCGradientDrawable; 32 | 33 | private GradientDrawable coverGradientDrawable; 34 | 35 | public AnimHelper() { 36 | initGradient(); 37 | } 38 | 39 | private void initGradient() { 40 | int deepColor = 0x33333333; 41 | int lightColor = 0x01333333; 42 | int[] gradientColors = new int[]{lightColor, deepColor};//渐变颜色数组 43 | topLeftGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors); 44 | topLeftGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 45 | bottomLeftGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, gradientColors); 46 | bottomLeftGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 47 | 48 | deepColor = 0x22333333; 49 | lightColor = 0x01333333; 50 | gradientColors = new int[]{deepColor, lightColor, lightColor}; 51 | topRightGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, gradientColors); 52 | topRightGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 53 | bottomRightGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, gradientColors); 54 | bottomRightGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 55 | 56 | deepColor = 0x55111111; 57 | lightColor = 0x00111111; 58 | gradientColors = new int[]{deepColor, lightColor}; 59 | topBGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors); 60 | topBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 61 | bottomBGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, gradientColors); 62 | bottomBGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 63 | 64 | 65 | deepColor = 0x55333333; 66 | lightColor = 0x00333333; 67 | gradientColors = new int[]{lightColor, deepColor}; 68 | topCGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors); 69 | topCGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 70 | bottomCGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, gradientColors); 71 | bottomCGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 72 | 73 | deepColor = 0x55333333; 74 | lightColor = 0x00333333; 75 | gradientColors = new int[]{deepColor, lightColor}; 76 | coverGradientDrawable = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, gradientColors); 77 | coverGradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT); 78 | } 79 | 80 | 81 | public GradientDrawable getTopLeftGradientDrawable() { 82 | return topLeftGradientDrawable; 83 | } 84 | 85 | public GradientDrawable getTopRightGradientDrawable() { 86 | return topRightGradientDrawable; 87 | } 88 | 89 | public GradientDrawable getBottomLeftGradientDrawable() { 90 | return bottomLeftGradientDrawable; 91 | } 92 | 93 | public GradientDrawable getBottomRightGradientDrawable() { 94 | return bottomRightGradientDrawable; 95 | } 96 | 97 | public GradientDrawable getTopBGradientDrawable() { 98 | return topBGradientDrawable; 99 | } 100 | 101 | public GradientDrawable getBottomBGradientDrawable() { 102 | return bottomBGradientDrawable; 103 | } 104 | 105 | public GradientDrawable getTopCGradientDrawable() { 106 | return topCGradientDrawable; 107 | } 108 | 109 | public GradientDrawable getBottomCGradientDrawable() { 110 | return bottomCGradientDrawable; 111 | } 112 | 113 | public GradientDrawable getCoverGradientDrawable() { 114 | return coverGradientDrawable; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/anim/CoverAnimationEffecter.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.anim; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.Path; 6 | import android.graphics.RectF; 7 | import android.graphics.drawable.GradientDrawable; 8 | import android.view.MotionEvent; 9 | import android.view.animation.AccelerateDecelerateInterpolator; 10 | import android.widget.Scroller; 11 | 12 | import com.juziml.read.business.read.view.PuppetView; 13 | import com.juziml.read.utils.DLog; 14 | 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | 18 | /** 19 | * create by zhusw on 2020-08-24 14:06 20 | */ 21 | public class CoverAnimationEffecter implements IAnimationEffecter { 22 | private final static int DOWN_AREA_NONE = -1; 23 | private final static int DOWN_AREA_MENU = 1; 24 | private final static int DOWN_AREA_LEFT = 2; 25 | private final static int DOWN_AREA_RIGHT = 3; 26 | 27 | int vWidth = 1; 28 | int vHeight = 1; 29 | private final PuppetView puppetView; 30 | 31 | private boolean isCancelFlip = false; 32 | 33 | private boolean coverAnimationRunning = false; 34 | 35 | private boolean isTouching = false; 36 | 37 | private final Scroller scroller; 38 | private final ScrollRunnable scrollRunnable; 39 | private final RectF menuBounds; 40 | 41 | private final Path pathA; 42 | private final Path pathB; 43 | private final Paint paint; 44 | private final int shadowWidth; 45 | 46 | public CoverAnimationEffecter(PuppetView readAnimView) { 47 | this.puppetView = readAnimView; 48 | scroller = new Scroller(readAnimView.getContext(), new AccelerateDecelerateInterpolator()); 49 | scrollRunnable = new ScrollRunnable(); 50 | menuBounds = new RectF(); 51 | pathA = new Path(); 52 | pathB = new Path(); 53 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 54 | shadowWidth = 20; 55 | } 56 | 57 | private int downArea = DOWN_AREA_NONE; 58 | private float downX = 0F; 59 | private int coverSlideDirection = AnimHelper.SLID_DIRECTION_UNKNOWN; 60 | private final List moveSampling = new LinkedList<>(); 61 | private final int MAX_COUNT = 5; 62 | private boolean prepareDrawCoverAnimEffect = false; 63 | 64 | private float currentX = -1; 65 | 66 | @Override 67 | public void handlerEvent(MotionEvent event) { 68 | if (coverAnimationRunning) return; 69 | float x = event.getRawX(); 70 | float y = event.getY(); 71 | switch (event.getAction()) { 72 | case MotionEvent.ACTION_DOWN: 73 | moveSampling.clear(); 74 | downX = x; 75 | prepareDrawCoverAnimEffect = false; 76 | isTouching = true; 77 | currentX = -1; 78 | downArea = DOWN_AREA_NONE; 79 | coverSlideDirection = AnimHelper.SLID_DIRECTION_UNKNOWN; 80 | if (x > menuBounds.left && y > menuBounds.top 81 | && x < menuBounds.right && y < menuBounds.bottom) { 82 | downArea = DOWN_AREA_MENU; 83 | } else if (x < vWidth / 2F) { 84 | downArea = DOWN_AREA_LEFT; 85 | } else { 86 | downArea = DOWN_AREA_RIGHT; 87 | } 88 | break; 89 | case MotionEvent.ACTION_MOVE: 90 | isTouching = true; 91 | float curDistance = x - downX; 92 | if (coverSlideDirection == AnimHelper.SLID_DIRECTION_UNKNOWN && checkDownArea(downArea)) { 93 | if (curDistance > 0) { 94 | coverSlideDirection = AnimHelper.SLID_DIRECTION_RIGHT; 95 | } else { 96 | coverSlideDirection = AnimHelper.SLID_DIRECTION_LEFT; 97 | } 98 | puppetView.buildBitmap(coverSlideDirection); 99 | prepareDrawCoverAnimEffect = checkAnimCondition(coverSlideDirection); 100 | } 101 | 102 | if (prepareDrawCoverAnimEffect) { 103 | if (moveSampling.size() == 0 104 | || x != moveSampling.get(moveSampling.size() - 1)) { 105 | moveSampling.add(x); 106 | } 107 | if (moveSampling.size() > MAX_COUNT) { 108 | moveSampling.remove(0); 109 | } 110 | currentX = x; 111 | invalidate(); 112 | } 113 | break; 114 | case MotionEvent.ACTION_CANCEL: 115 | isTouching = false; 116 | break; 117 | case MotionEvent.ACTION_UP: 118 | currentX = x; 119 | if (prepareDrawCoverAnimEffect) { 120 | if (moveSampling.size() > 0) { 121 | float lastMoveX = moveSampling.get(moveSampling.size() - 1); 122 | float firstMoveX = moveSampling.get(0); 123 | float finallyMoveX = lastMoveX - firstMoveX; 124 | if (coverSlideDirection == AnimHelper.SLID_DIRECTION_LEFT) { 125 | boolean lastFingerLeftSlop = finallyMoveX < 10; 126 | touchUp(lastFingerLeftSlop); 127 | } else if (coverSlideDirection == AnimHelper.SLID_DIRECTION_RIGHT) { 128 | finallyMoveX = lastMoveX - firstMoveX; 129 | touchUp(finallyMoveX < 0); 130 | } 131 | } else { 132 | touchUp(false); 133 | } 134 | } else if (downArea == DOWN_AREA_MENU) { 135 | if (x > menuBounds.left && x < menuBounds.right 136 | && y > menuBounds.top && y < menuBounds.bottom) { 137 | puppetView.onClickMenuArea(); 138 | } 139 | } else if (downArea != DOWN_AREA_NONE) { 140 | if (x == downX && downX >= vWidth / 2F) {//下一页 141 | coverSlideDirection = AnimHelper.SLID_DIRECTION_LEFT; 142 | puppetView.buildBitmap(coverSlideDirection); 143 | if (checkAnimCondition(coverSlideDirection)) { 144 | touchUp(true); 145 | } 146 | } else if (x == downX && downX < vWidth / 2F) {//上一页 147 | coverSlideDirection = AnimHelper.SLID_DIRECTION_RIGHT; 148 | puppetView.buildBitmap(coverSlideDirection); 149 | if (checkAnimCondition(coverSlideDirection)) { 150 | touchUp(false); 151 | } 152 | } 153 | } 154 | moveSampling.clear(); 155 | isTouching = false; 156 | break; 157 | default: 158 | break; 159 | } 160 | } 161 | 162 | private void touchUp(boolean lastFingerLeftSlop) { 163 | DLog.log("touchUp coverAnimationRunning=%s", coverAnimationRunning); 164 | coverAnimationRunning = true; 165 | isCancelFlip = (coverSlideDirection == AnimHelper.SLID_DIRECTION_RIGHT && lastFingerLeftSlop) 166 | || (coverSlideDirection == AnimHelper.SLID_DIRECTION_LEFT && !lastFingerLeftSlop); 167 | 168 | int duration = isCancelFlip ? AnimHelper.CANCEL_ANIM_DURATION : AnimHelper.RELAY_ANIM_DURATION; 169 | duration = (int) (duration * 0.7F); 170 | int startX = (int) currentX; 171 | int startY = 0; 172 | int dy = 0; 173 | int dx; 174 | 175 | if (lastFingerLeftSlop) { 176 | 177 | dx = (int) -(vWidth - (downX - currentX)); 178 | } else { 179 | dx = vWidth - (int) currentX; 180 | } 181 | scroller.startScroll(startX, startY, dx, dy, duration); 182 | invalidate(); 183 | } 184 | 185 | 186 | @Override 187 | public void draw(Canvas canvas) { 188 | if (currentX == -1) { 189 | DLog.log("CoverAnimationEffect draw 1"); 190 | return; 191 | } 192 | if (coverSlideDirection != AnimHelper.SLID_DIRECTION_LEFT && coverSlideDirection != AnimHelper.SLID_DIRECTION_RIGHT) { 193 | DLog.log("CoverAnimationEffect draw 2"); 194 | return; 195 | } 196 | if (coverSlideDirection == AnimHelper.SLID_DIRECTION_LEFT 197 | && (null == puppetView.getCurrentBitmap() || null == puppetView.getNextBitmap())) { 198 | DLog.log("CoverAnimationEffect draw 3"); 199 | return; 200 | } 201 | if (coverSlideDirection == AnimHelper.SLID_DIRECTION_RIGHT && null == puppetView.getPreviousBitmap()) { 202 | DLog.log("CoverAnimationEffect draw 4"); 203 | return; 204 | } 205 | DLog.log("CoverAnimationEffect draw 5"); 206 | if (coverSlideDirection == AnimHelper.SLID_DIRECTION_LEFT) { 207 | float offset = downX - currentX; 208 | offset = Math.max(0, offset); 209 | canvas.save(); 210 | canvas.clipPath(getPathAToLeft()); 211 | canvas.drawBitmap(puppetView.getCurrentBitmap(), -offset, 0, paint); 212 | canvas.restore(); 213 | canvas.save(); 214 | canvas.clipPath(getPathB()); 215 | canvas.drawBitmap(puppetView.getNextBitmap(), 0, 0, paint); 216 | canvas.restore(); 217 | drawShadow((int) (vWidth - offset), canvas); 218 | } else { 219 | float leftOffset = vWidth - currentX; 220 | canvas.save(); 221 | canvas.clipPath(getPathAToRight()); 222 | canvas.drawBitmap(puppetView.getPreviousBitmap(), -leftOffset, 0, paint); 223 | canvas.restore(); 224 | drawShadow((int) currentX, canvas); 225 | } 226 | } 227 | 228 | private void drawShadow(int left, Canvas canvas) { 229 | GradientDrawable drawable = puppetView.getAnimHelper().getCoverGradientDrawable(); 230 | drawable.setBounds(left, 0, left + shadowWidth, vHeight); 231 | drawable.draw(canvas); 232 | } 233 | 234 | private Path getPathAToLeft() { 235 | pathA.reset(); 236 | float x = vWidth - (downX - currentX); 237 | x = Math.min(vWidth, x); 238 | pathA.lineTo(x, 0); 239 | pathA.lineTo(x, vHeight); 240 | pathA.lineTo(0, vHeight); 241 | pathA.close(); 242 | return pathA; 243 | } 244 | 245 | private Path getPathB() { 246 | pathB.reset(); 247 | float x = vWidth - (downX - currentX); 248 | x = Math.min(vWidth, x); 249 | pathB.moveTo(x, 0); 250 | pathB.lineTo(vWidth, 0); 251 | pathB.lineTo(vWidth, vHeight); 252 | pathB.lineTo(x, vHeight); 253 | pathB.close(); 254 | return pathB; 255 | } 256 | 257 | private Path getPathAToRight() { 258 | pathA.reset(); 259 | pathA.lineTo(currentX, 0); 260 | pathA.lineTo(currentX, vHeight); 261 | pathA.lineTo(0, vHeight); 262 | pathA.close(); 263 | return pathA; 264 | } 265 | 266 | 267 | private boolean checkDownArea(int downArea) { 268 | return downArea != DOWN_AREA_MENU && downArea != DOWN_AREA_NONE; 269 | } 270 | 271 | private boolean checkAnimCondition(int slideDirection) { 272 | if (slideDirection == AnimHelper.SLID_DIRECTION_LEFT && (null != puppetView.getNextBitmap() && null != puppetView.getCurrentBitmap())) { 273 | return true; 274 | } else if (slideDirection == AnimHelper.SLID_DIRECTION_RIGHT && null != puppetView.getPreviousBitmap()) { 275 | return true; 276 | } 277 | return false; 278 | } 279 | 280 | @Override 281 | public boolean animInEffect() { 282 | return isTouching || coverAnimationRunning; 283 | } 284 | 285 | @Override 286 | public void onViewSizeChanged(int vWidth, int vHeight) { 287 | this.vWidth = vWidth; 288 | this.vHeight = vHeight; 289 | menuBounds.left = vWidth / 3F; 290 | menuBounds.top = vHeight / 3F; 291 | menuBounds.right = vWidth * 2 / 3F; 292 | menuBounds.bottom = vHeight * 2 / 3F; 293 | } 294 | 295 | @Override 296 | public void onViewAttachedToWindow() { 297 | 298 | } 299 | 300 | @Override 301 | public void onViewDetachedFromWindow() { 302 | puppetView.removeCallbacks(scrollRunnable); 303 | } 304 | 305 | private void invalidate() { 306 | puppetView.postInvalidate(); 307 | } 308 | 309 | @Override 310 | public void onScroll() { 311 | if (scroller.computeScrollOffset()) { 312 | 313 | int x = scroller.getCurrX(); 314 | int y = scroller.getCurrY(); 315 | if (x == scroller.getFinalX() && y == scroller.getFinalY()) { 316 | scroller.forceFinished(true); 317 | //补一点时间,避免动画太快结束,提供两次动画触发间隔 318 | DLog.log("coverAnimationRunning coverAnimationRunning=%s 结束,延时开启 状态重置", coverAnimationRunning); 319 | puppetView.post(scrollRunnable); 320 | } else { 321 | currentX = x; 322 | invalidate(); 323 | } 324 | } 325 | 326 | } 327 | 328 | 329 | protected class ScrollRunnable implements Runnable { 330 | @Override 331 | public void run() { 332 | puppetView.reset(); 333 | coverAnimationRunning = false; 334 | if (!isCancelFlip) { 335 | if (coverSlideDirection == AnimHelper.SLID_DIRECTION_LEFT) { 336 | puppetView.onExpectNext(); 337 | } else if (coverSlideDirection == AnimHelper.SLID_DIRECTION_RIGHT) { 338 | puppetView.onExpectPrevious(); 339 | } 340 | } 341 | invalidate(); 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/anim/IAnimationEffecter.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.anim; 2 | 3 | import android.graphics.Canvas; 4 | import android.view.MotionEvent; 5 | 6 | /** 7 | * create by zhusw on 2020-08-24 14:05 8 | */ 9 | public interface IAnimationEffecter { 10 | 11 | void onScroll(); 12 | 13 | void handlerEvent(MotionEvent event); 14 | 15 | void draw(Canvas canvas); 16 | 17 | boolean animInEffect(); 18 | 19 | void onViewSizeChanged(int vWidth, int vHeight); 20 | 21 | void onViewAttachedToWindow(); 22 | 23 | void onViewDetachedFromWindow(); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/AnimParentView.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import com.juziml.read.business.read.anim.AnimHelper; 6 | 7 | /** 8 | * create by zhusw on 2020-08-03 11:28 9 | */ 10 | public interface AnimParentView { 11 | void onExpectNext(); 12 | 13 | void onExpectPrevious(); 14 | 15 | Bitmap getPreviousBitmap(); 16 | 17 | Bitmap getCurrentBitmap(); 18 | 19 | Bitmap getNextBitmap(); 20 | 21 | int getBackgroundColor(); 22 | 23 | AnimHelper getAnimHelper(); 24 | 25 | void onClickMenuArea(); 26 | 27 | /** 28 | * 只在非卷曲模式下调用 29 | */ 30 | void onClickNextArea(); 31 | 32 | /** 33 | * 只在非卷曲模式下调用 34 | */ 35 | void onClickPreviousArea(); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/BookLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ValueAnimator; 6 | import android.content.Context; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.view.animation.LinearInterpolator; 10 | 11 | import androidx.annotation.IntDef; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import java.lang.annotation.ElementType; 15 | import java.lang.annotation.Retention; 16 | import java.lang.annotation.RetentionPolicy; 17 | import java.lang.annotation.Target; 18 | import java.util.List; 19 | 20 | 21 | /** 22 | * create by zhusw on 2020-03-29 17:11 23 | */ 24 | public class BookLayoutManager extends RecyclerView.LayoutManager { 25 | private int bookFlipMode = BookFlipMode.MODE_CURL; 26 | 27 | private Context context; 28 | /** 29 | * 一次完整的聚焦滑动所需要的移动距离 30 | */ 31 | private float onceCompleteScrollLength = -1; 32 | 33 | /** 34 | * 屏幕可见第一个view的position 35 | */ 36 | private int firstPos; 37 | 38 | /** 39 | * 屏幕可见的最后一个view的position 40 | */ 41 | private int lastPos; 42 | 43 | /** 44 | * 水平方向累计偏移量 45 | */ 46 | private long horizontalOffset; 47 | 48 | private int childWidth = 0; 49 | private ValueAnimator selectAnimator; 50 | 51 | private boolean autoLeftScroll = false; 52 | 53 | private OnStopScroller onStopScroller; 54 | private OnForceLayoutCompleted onForceLayoutCompleted; 55 | 56 | public BookLayoutManager(Context context) { 57 | this.context = context; 58 | } 59 | 60 | public void setonStopScroller(OnStopScroller onStopScroller) { 61 | this.onStopScroller = onStopScroller; 62 | } 63 | 64 | @Override 65 | public RecyclerView.LayoutParams generateDefaultLayoutParams() { 66 | return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 67 | ViewGroup.LayoutParams.WRAP_CONTENT); 68 | } 69 | 70 | public void setBookFlipMode(@BookFlipMode int bookFlipMode) { 71 | this.bookFlipMode = bookFlipMode; 72 | requestLayout(); 73 | } 74 | 75 | public int getBookFlipMode() { 76 | return bookFlipMode; 77 | } 78 | 79 | public void setAutoLeftScroll(boolean autoLeftScroll) { 80 | this.autoLeftScroll = autoLeftScroll; 81 | } 82 | 83 | public void setOnForceLayoutCompleted(OnForceLayoutCompleted onForceLayoutCompleted) { 84 | this.onForceLayoutCompleted = onForceLayoutCompleted; 85 | } 86 | 87 | //确认允许水平滚动 88 | @Override 89 | public boolean canScrollHorizontally() { 90 | return true; 91 | } 92 | 93 | 94 | @Override 95 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 96 | // 返回用于布局的view数量,!= adapter 的 元素个数 97 | if (state.getItemCount() == 0) { 98 | removeAndRecycleAllViews(recycler); 99 | return; 100 | } 101 | // 分离全部已有的view 放入临时缓存 mAttachedScrap 集合中 102 | detachAndScrapAttachedViews(recycler); 103 | //确定布局,类似于 viewGroup的 onLayout 104 | layout(recycler, 0); 105 | } 106 | 107 | 108 | private int layout(RecyclerView.Recycler recycler, int dx) { 109 | int resultDelta = horizontalLayout(recycler, dx); 110 | recycleChildren(recycler); 111 | return resultDelta; 112 | } 113 | 114 | /** 115 | * 最大偏移量 116 | * 117 | * @return 118 | */ 119 | private float getMaxOffset() { 120 | if (childWidth == 0 || getItemCount() == 0) return 0; 121 | return (childWidth) * (getItemCount() - 1); 122 | } 123 | 124 | @Override 125 | public void onScrollStateChanged(int state) { 126 | switch (state) { 127 | case RecyclerView.SCROLL_STATE_DRAGGING://滑动手势开始 128 | cancelAnimator(); 129 | break; 130 | 131 | case RecyclerView.SCROLL_STATE_IDLE://停止滑动 132 | final int selectPosition = findShouldSelectPosition(); 133 | smoothScrollToPosition(selectPosition); 134 | if (null != onStopScroller) { 135 | onStopScroller.onStop(autoLeftScroll, selectPosition); 136 | } 137 | break; 138 | default: 139 | break; 140 | } 141 | 142 | } 143 | 144 | /** 145 | * 强制滚动到指定位置 146 | * 147 | * @param position 148 | */ 149 | public void forceScrollToPosition(int position) { 150 | if (position > -1 && position < getItemCount()) { 151 | scrollToPosition(position, false); 152 | } 153 | } 154 | 155 | /** 156 | * 平滑滚动到某个位置 157 | * 158 | * @param position 目标Item索引 159 | */ 160 | public void smoothScrollToPosition(int position) { 161 | if (position > -1 && position < getItemCount()) { 162 | scrollToPosition(position, true); 163 | } 164 | } 165 | 166 | public int findShouldSelectPosition() { 167 | if (onceCompleteScrollLength == -1 || firstPos == -1) { 168 | return -1; 169 | } 170 | int position; 171 | if (autoLeftScroll) { 172 | position = (int) (Math.abs(horizontalOffset) / childWidth); 173 | // int remainder = (int) (Math.abs(horizontalOffset) % childWidth); 174 | // 超过临界距离 选中下一页,108 为 1/10的 1080 屏幕 175 | //固定临界值,避免屏幕越大需要滑动的距离越远 176 | // if (remainder >= ReadRecyclerView.MOVE_LEFT_MIN) { 177 | if (position + 1 <= getItemCount() - 1) { 178 | return position + 1; 179 | } 180 | // } 181 | } else { 182 | position = (int) (Math.abs(horizontalOffset) / childWidth); 183 | } 184 | 185 | return position; 186 | } 187 | 188 | private float getScrollToPositionOffset(int position) { 189 | return position * childWidth - Math.abs(horizontalOffset); 190 | } 191 | 192 | private void scrollToPosition(final int position, boolean withAnim) { 193 | cancelAnimator(); 194 | final float distance = getScrollToPositionOffset(position); 195 | if (distance == 0) return; 196 | if (withAnim) { 197 | 198 | long minDuration = 100; 199 | long maxDuration = 300; 200 | long duration; 201 | 202 | float distanceFraction = (Math.abs(distance) / childWidth); 203 | 204 | if (distance <= childWidth) { 205 | duration = (long) (minDuration + (maxDuration - minDuration) * distanceFraction); 206 | } else { 207 | duration = (long) (maxDuration * distanceFraction); 208 | } 209 | selectAnimator = ValueAnimator.ofFloat(0.0f, distance); 210 | selectAnimator.setDuration(duration); 211 | selectAnimator.setInterpolator(new LinearInterpolator()); 212 | final float startedOffset = horizontalOffset; 213 | selectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 214 | @Override 215 | public void onAnimationUpdate(ValueAnimator animation) { 216 | float value = (float) animation.getAnimatedValue(); 217 | horizontalOffset = (long) (startedOffset + value); 218 | requestLayout(); 219 | if (value == distance) {//主动给一个滚动回调,因为不会处罚onScrollstop 220 | if (null != onStopScroller) { 221 | onStopScroller.onStop(distance > 0, position); 222 | } 223 | } 224 | } 225 | }); 226 | selectAnimator.addListener(new AnimatorListenerAdapter() { 227 | @Override 228 | public void onAnimationEnd(Animator animation) { 229 | super.onAnimationEnd(animation); 230 | } 231 | }); 232 | selectAnimator.start(); 233 | 234 | } else { 235 | horizontalOffset += (long) distance; 236 | requestLayout(); 237 | if (null != onForceLayoutCompleted) { 238 | onForceLayoutCompleted.onLayoutCompleted(position); 239 | } 240 | } 241 | 242 | 243 | } 244 | 245 | public void cancelAnimator() { 246 | if (selectAnimator != null && (selectAnimator.isStarted() || selectAnimator.isRunning())) { 247 | selectAnimator.cancel(); 248 | } 249 | } 250 | 251 | public boolean animIsRunning() { 252 | return selectAnimator != null && (selectAnimator.isStarted() || selectAnimator.isRunning()); 253 | } 254 | 255 | private int horizontalLayout(RecyclerView.Recycler recycler, int dx) { 256 | //-----------------------1 边界检测--------------- 257 | //已达左边界 258 | if (dx < 0) { 259 | if (horizontalOffset < 0) { 260 | dx = 0; 261 | horizontalOffset = dx; 262 | } 263 | } 264 | //确认右边界 265 | if (dx > 0) { 266 | if (horizontalOffset >= getMaxOffset()) { 267 | horizontalOffset = (long) getMaxOffset(); 268 | dx = 0; 269 | } 270 | } 271 | // 分离全部的view,加入到临时缓存,这里调用,因为在滑动过程中,可显view 可能发生改变 272 | detachAndScrapAttachedViews(recycler); 273 | 274 | //-----------------------2 计算用于 view 确定位置的参数--------------- 275 | 276 | float layoutX = 0; 277 | float fraction = 0; 278 | 279 | View tempView = null; 280 | 281 | int tempPosition = -1; 282 | 283 | if (onceCompleteScrollLength == -1) { 284 | // 因为firstVisiPos在下面可能被改变,所以用tempPosition暂存一下 285 | tempPosition = firstPos; 286 | tempView = recycler.getViewForPosition(tempPosition); 287 | measureChildWithMargins(tempView, 0, 0); 288 | //以第一个子view宽度为计算标准,这样就不支持 itemType 了。全部item要保持宽度一样,margin参数一样 289 | childWidth = getDecoratedMeasurementHorizontal(tempView); 290 | } 291 | // 修正第一个可见view firstVisiPos 已经滑动了多少个完整的onceCompleteScrollLength就代表滑动了多少个item 292 | 293 | /** 294 | * 第一个子view的偏移量 295 | */ 296 | float firstChildCompleteScrollLength = getWidth() / 2F + childWidth / 2F; 297 | 298 | if (horizontalOffset >= firstChildCompleteScrollLength) { 299 | layoutX = 0; 300 | onceCompleteScrollLength = childWidth; 301 | //计算 滚动到了 哪个view的区域 302 | firstPos = (int) Math.floor(Math.abs(horizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength) + 1; 303 | fraction = (Math.abs(horizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f); 304 | } else { 305 | firstPos = 0; 306 | layoutX = getMinOffset(); 307 | //记录单个view 需要滚动的距离 308 | onceCompleteScrollLength = firstChildCompleteScrollLength; 309 | fraction = (Math.abs(horizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f); 310 | } 311 | // 临时将fastVisiPos赋值为getItemCount() - 1,放心,下面遍历时会判断view是否已溢出屏幕,并及时修正该值并结束布局 312 | //注意这里 是adapter 的 个数,而不是 state 保存的view 显示数量 313 | lastPos = getItemCount() - 1; 314 | //这里似乎多此一举了 ,可以把 分母直接替换为 normalViewOffset 在上面的逻辑中计算出来 315 | float normalViewOffset = onceCompleteScrollLength * fraction; 316 | boolean isNormalViewOffsetSetted = false; 317 | 318 | //-----------------------3 确认view位置--------------- 319 | for (int itemIndex = firstPos; itemIndex <= lastPos; itemIndex++) { 320 | 321 | View itemView; 322 | 323 | // 如果初始化数据时已经取了一个临时view 324 | if (itemIndex == tempPosition && null != tempView) { 325 | itemView = tempView; 326 | } else { 327 | itemView = recycler.getViewForPosition(itemIndex); 328 | } 329 | //计算新view 位置 330 | int focusPosition = (int) (Math.abs(horizontalOffset) / (childWidth)); 331 | 332 | if (itemIndex <= focusPosition) { 333 | addView(itemView); 334 | } else { 335 | addView(itemView, 0); 336 | } 337 | //测量新view 338 | measureChildWithMargins(itemView, 0, 0); 339 | if (!isNormalViewOffsetSetted) { 340 | layoutX -= normalViewOffset; 341 | isNormalViewOffsetSetted = true; 342 | } 343 | //计算view layout 坐标 344 | int left, top, right, bottom; 345 | left = (int) layoutX; 346 | top = 0; 347 | right = left + getDecoratedMeasurementHorizontal(itemView); 348 | bottom = top + getDecoratedMeasurementVertical(itemView); 349 | layoutDecoratedWithMargins(itemView, left, top, right, bottom); 350 | //更新下一个view X 轴坐标 351 | layoutX += childWidth; 352 | //修正溢出屏幕的view个数 353 | if (layoutX > getWidth() - getPaddingRight()) { 354 | lastPos = itemIndex; 355 | break; 356 | } 357 | }//end for 358 | return dx; 359 | } 360 | 361 | @Override 362 | public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 363 | 364 | if (dx == 0 || getChildCount() == 0) { 365 | 366 | return 0; 367 | } 368 | horizontalOffset += dx; 369 | dx = layout(recycler, dx); 370 | 371 | return dx; 372 | } 373 | 374 | /** 375 | * 回收需回收的item 376 | */ 377 | private void recycleChildren(RecyclerView.Recycler recycler) { 378 | List scrapList = recycler.getScrapList(); 379 | for (int i = 0; i < scrapList.size(); i++) { 380 | RecyclerView.ViewHolder holder = scrapList.get(i); 381 | removeAndRecycleView(holder.itemView, recycler); 382 | } 383 | } 384 | 385 | /** 386 | * 获取某个childView在竖直方向所占的空间,将margin考虑进去 387 | * 包含了指示器 388 | * 389 | * @param view 390 | * @return 391 | */ 392 | public int getDecoratedMeasurementVertical(View view) { 393 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) 394 | view.getLayoutParams(); 395 | return getDecoratedMeasuredHeight(view) + params.topMargin 396 | + params.bottomMargin; 397 | } 398 | 399 | /** 400 | * 获取某个childView在水平方向所占的空间,将margin考虑进去 401 | * 402 | * @param view 403 | * @return 404 | */ 405 | public int getDecoratedMeasurementHorizontal(View view) { 406 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) 407 | view.getLayoutParams(); 408 | return getDecoratedMeasuredWidth(view) + params.leftMargin 409 | + params.rightMargin; 410 | } 411 | 412 | /** 413 | * 获取最小的偏移量 414 | * 415 | * @return 416 | */ 417 | private float getMinOffset() { 418 | if (childWidth == 0) return 0; 419 | return (getWidth() - childWidth) / 2; 420 | } 421 | 422 | protected void onRecyclerViewSizeChange() { 423 | onceCompleteScrollLength = -1; 424 | } 425 | 426 | @Retention(RetentionPolicy.SOURCE) 427 | @Target(ElementType.PARAMETER) 428 | @IntDef({ 429 | BookFlipMode.MODE_NORMAL, 430 | BookFlipMode.MODE_COVER, 431 | BookFlipMode.MODE_CURL, 432 | 433 | }) 434 | public @interface BookFlipMode { 435 | int MODE_NORMAL = 1; 436 | int MODE_COVER = 2; 437 | int MODE_CURL = 3; 438 | 439 | } 440 | 441 | public interface OnStopScroller { 442 | void onStop(boolean autoLeftScroll, int curPos); 443 | } 444 | 445 | public interface OnForceLayoutCompleted { 446 | void onLayoutCompleted(final int curPos); 447 | } 448 | } 449 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/BookRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | 10 | import androidx.annotation.Nullable; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | import java.lang.ref.WeakReference; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | 17 | 18 | /** 19 | * 关闭了抛投效果 20 | * create by zhusw on 2020-03-30 11:51 21 | */ 22 | public class BookRecyclerView extends RecyclerView implements RVInnerItemFunction, RVOuterFunction { 23 | 24 | private final BookLayoutManager layoutManager; 25 | 26 | private boolean allowInterceptTouchEvent = true; 27 | 28 | private int currentPosition = 0; 29 | private WeakReference eventProxyWeakReference; 30 | private AnimParentView animParentView; 31 | private BookView.OnPositionChangedListener onPositionChangedListener; 32 | 33 | private Bitmap previousBitmap = null; 34 | private Bitmap currentBitmap = null; 35 | private Bitmap nextBitmap = null; 36 | 37 | public BookRecyclerView(Context context) { 38 | this(context, null); 39 | } 40 | 41 | public BookRecyclerView(Context context, @Nullable AttributeSet attrs) { 42 | this(context, attrs, 0); 43 | } 44 | 45 | public BookRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { 46 | super(context, attrs, defStyle); 47 | layoutManager = new BookLayoutManager(context); 48 | setLayoutManager(layoutManager); 49 | layoutManager.setOnForceLayoutCompleted(new ItemOnForceLayoutCompleted()); 50 | layoutManager.setonStopScroller(new ItemOnScrollStop()); 51 | } 52 | 53 | @Override 54 | protected void onAttachedToWindow() { 55 | super.onAttachedToWindow(); 56 | animParentView = (AnimParentView) getParent(); 57 | } 58 | 59 | @Override 60 | protected void onDetachedFromWindow() { 61 | super.onDetachedFromWindow(); 62 | eventProxyWeakReference.clear(); 63 | clearBitmapCache(); 64 | } 65 | 66 | protected void bindReadCurlAnimProxy(EventProxy ic) { 67 | if (null != eventProxyWeakReference) { 68 | eventProxyWeakReference.clear(); 69 | } 70 | eventProxyWeakReference = new WeakReference<>(ic); 71 | } 72 | 73 | 74 | protected void setOnPositionChangedListener(BookView.OnPositionChangedListener onPositionChangedListener) { 75 | this.onPositionChangedListener = onPositionChangedListener; 76 | } 77 | 78 | @Override 79 | public boolean fling(int velocityX, int velocityY) { 80 | return false; 81 | } 82 | 83 | @Override 84 | public void scrollToPosition(int position) { 85 | layoutManager.forceScrollToPosition(position); 86 | } 87 | 88 | @Override 89 | public void smoothScrollToPosition(int position) { 90 | layoutManager.smoothScrollToPosition(position); 91 | } 92 | 93 | @Override 94 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 95 | super.onSizeChanged(w, h, oldw, oldh); 96 | layoutManager.onRecyclerViewSizeChange(); 97 | } 98 | 99 | 100 | private final List moveSampling = new LinkedList<>(); 101 | private final int MAX_COUNT = 5; 102 | 103 | @Override 104 | public boolean isScrollContainer() { 105 | if (allowInterceptTouchEvent) { 106 | return super.isScrollContainer(); 107 | } else { 108 | return false; 109 | } 110 | 111 | } 112 | 113 | private float downX = 0F; 114 | 115 | @SuppressLint("ClickableViewAccessibility") 116 | @Override 117 | public boolean onTouchEvent(MotionEvent e) { 118 | if (!allowInterceptTouchEvent) return false;//[偶现 动画期间 产生了item滑动,这里最后杀手锏再屏蔽下] 119 | 120 | switch (e.getAction()) { 121 | case MotionEvent.ACTION_DOWN: 122 | moveSampling.clear(); 123 | downX = e.getRawX(); 124 | break; 125 | case MotionEvent.ACTION_MOVE: 126 | float mx = e.getRawX(); 127 | 128 | if (moveSampling.size() == 0 || mx != moveSampling.get(moveSampling.size() - 1)) { 129 | moveSampling.add(mx); 130 | } 131 | if (moveSampling.size() > MAX_COUNT) { 132 | moveSampling.remove(0); 133 | } 134 | 135 | break; 136 | case MotionEvent.ACTION_UP: 137 | case MotionEvent.ACTION_CANCEL: 138 | if (moveSampling.size() > 0) { 139 | float lastMoveX = moveSampling.get(moveSampling.size() - 1); 140 | float firstMoveX = moveSampling.get(0); 141 | float finallyMoveX = lastMoveX - firstMoveX; 142 | if (lastMoveX - downX < 0) {//左滑 143 | layoutManager.setAutoLeftScroll(finallyMoveX < 10); 144 | } else { 145 | layoutManager.setAutoLeftScroll(finallyMoveX < 0); 146 | } 147 | moveSampling.clear(); 148 | } else { 149 | layoutManager.setAutoLeftScroll(false); 150 | } 151 | 152 | break; 153 | default: 154 | break; 155 | } 156 | return super.onTouchEvent(e); 157 | } 158 | 159 | @Override 160 | public boolean onInterceptTouchEvent(MotionEvent e) { 161 | //交由父类处理滑动,flip = BookFlipMode.MODE_NORMAL, 162 | if (allowInterceptTouchEvent) { 163 | return super.onInterceptTouchEvent(e); 164 | } 165 | //交由子View自行处理,flip = BookFlipMode.MODE_COVER| BookFlipMode.MODE_CURL 166 | return false; 167 | } 168 | 169 | @Override 170 | public void onExpectNext(boolean smooth) { 171 | Adapter adapter = getAdapter(); 172 | final int dataCount = adapter.getItemCount(); 173 | final int nextPos = currentPosition + 1; 174 | 175 | if (nextPos < dataCount) { 176 | if (smooth) { 177 | smoothScrollToPosition(nextPos); 178 | } else { 179 | scrollToPosition(nextPos); 180 | } 181 | } 182 | } 183 | 184 | @Override 185 | public void onExpectPrevious(boolean smooth) { 186 | if (currentPosition - 1 >= 0) { 187 | if (smooth) { 188 | smoothScrollToPosition(currentPosition - 1); 189 | } else { 190 | scrollToPosition(currentPosition - 1); 191 | } 192 | } 193 | 194 | } 195 | 196 | protected void setFlipMode(int flipMode) { 197 | layoutManager.setBookFlipMode(flipMode); 198 | if (flipMode == BookLayoutManager.BookFlipMode.MODE_CURL || flipMode == BookLayoutManager.BookFlipMode.MODE_COVER) { 199 | allowInterceptTouchEvent = false; 200 | } else { 201 | allowInterceptTouchEvent = true; 202 | } 203 | layoutManager.requestLayout(); 204 | } 205 | 206 | @Override 207 | public int getFlipMode() { 208 | return layoutManager.getBookFlipMode(); 209 | } 210 | 211 | @Override 212 | public void onItemViewTouchEvent(MotionEvent event) { 213 | if (null != eventProxyWeakReference && null != eventProxyWeakReference.get()) { 214 | eventProxyWeakReference.get().onItemViewTouchEvent(event); 215 | } 216 | } 217 | 218 | @Override 219 | public boolean animRunning() { 220 | if (null != eventProxyWeakReference && null != eventProxyWeakReference.get()) { 221 | eventProxyWeakReference.get().animRunning(); 222 | } 223 | return false; 224 | } 225 | 226 | @Override 227 | public void onClickMenu() { 228 | animParentView.onClickMenuArea(); 229 | } 230 | 231 | private class ItemOnScrollStop implements BookLayoutManager.OnStopScroller { 232 | @Override 233 | public void onStop(boolean autoLeftScroll, int curPos) { 234 | boolean arriveNext = currentPosition < curPos; 235 | currentPosition = curPos; 236 | if (null != onPositionChangedListener) { 237 | onPositionChangedListener.onChanged(arriveNext, curPos); 238 | } 239 | } 240 | 241 | } 242 | 243 | private class ItemOnForceLayoutCompleted implements BookLayoutManager.OnForceLayoutCompleted { 244 | 245 | @Override 246 | public void onLayoutCompleted(final int curPos) { 247 | boolean arriveNext = currentPosition < curPos; 248 | currentPosition = curPos; 249 | if (null != onPositionChangedListener) { 250 | onPositionChangedListener.onChanged(arriveNext, curPos); 251 | } 252 | } 253 | } 254 | 255 | @Override 256 | public Bitmap getPreviousBitmap() { 257 | int prePos = currentPosition - 1; 258 | Bitmap pb = null; 259 | if (prePos >= 0) { 260 | pb = printViewToBitmap(prePos, 0); 261 | } 262 | return pb; 263 | } 264 | 265 | @Override 266 | public Bitmap getCurrentBitmap() { 267 | return printViewToBitmap(currentPosition, 1); 268 | } 269 | 270 | @Override 271 | public Bitmap getNextBitmap() { 272 | final int dataCount = getAdapter().getItemCount(); 273 | int nextPos = currentPosition + 1; 274 | Bitmap nb = null; 275 | if (nextPos < dataCount) { 276 | nb = printViewToBitmap(nextPos, 2); 277 | } 278 | return nb; 279 | } 280 | 281 | /** 282 | * 将view渲染结果 打印到一个bitmap上 283 | * 284 | * @param type 0 前一页,1 当前页,2 后一页 285 | * @return 286 | */ 287 | private Bitmap printViewToBitmap(int pos, int type) { 288 | View view = layoutManager.findViewByPosition(pos); 289 | if (null != view) { 290 | if (view instanceof PaperLayout) { 291 | PaperLayout pageView = (PaperLayout) view; 292 | Bitmap bitmapTarget = obtainBitmap(pageView.getWidth(), pageView.getHeight(), type); 293 | pageView.drawViewScreenShotToBitmap(bitmapTarget); 294 | return bitmapTarget; 295 | } else { 296 | throw new IllegalArgumentException("item 根View必须使用 PaperLayout"); 297 | } 298 | } 299 | return null; 300 | } 301 | 302 | private Bitmap obtainBitmap(int w, int h, int type) { 303 | Bitmap cache = null; 304 | if (type == 0) { 305 | previousBitmap = previousBitmap != null ? previousBitmap : createNewBitmap(w, h); 306 | cache = previousBitmap; 307 | } else if (type == 1) { 308 | currentBitmap = currentBitmap != null ? currentBitmap : createNewBitmap(w, h); 309 | cache = currentBitmap; 310 | } else if (type == 2) { 311 | nextBitmap = nextBitmap != null ? nextBitmap : createNewBitmap(w, h); 312 | cache = nextBitmap; 313 | } 314 | return cache; 315 | } 316 | 317 | private Bitmap createNewBitmap(int w, int h) { 318 | return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_4444); 319 | } 320 | 321 | private void clearBitmapCache() { 322 | if (null != previousBitmap) { 323 | previousBitmap.recycle(); 324 | previousBitmap = null; 325 | } 326 | if (null != currentBitmap) { 327 | currentBitmap.recycle(); 328 | currentBitmap = null; 329 | } 330 | if (null != nextBitmap) { 331 | nextBitmap.recycle(); 332 | nextBitmap = null; 333 | } 334 | } 335 | 336 | } 337 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/BookView.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Color; 6 | import android.util.AttributeSet; 7 | import android.view.Gravity; 8 | import android.view.View; 9 | import android.widget.FrameLayout; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import com.juziml.read.business.read.anim.AnimHelper; 16 | 17 | /** 18 | * create by zhusw on 2020-08-14 17:33 19 | */ 20 | public class BookView extends FrameLayout implements AnimParentView { 21 | 22 | private BookRecyclerView bookRecyclerView; 23 | private PuppetView puppetView; 24 | private final AnimHelper animHelper; 25 | 26 | private OnClickMenuListener onClickMenuListener; 27 | 28 | private int itemViewBackgroundColor = Color.WHITE; 29 | private Runnable dataPendIntentTask; 30 | 31 | public BookView(@NonNull Context context) { 32 | this(context, null); 33 | } 34 | 35 | public BookView(@NonNull Context context, @Nullable AttributeSet attrs) { 36 | this(context, attrs, 0); 37 | } 38 | 39 | public BookView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 40 | super(context, attrs, defStyleAttr); 41 | animHelper = new AnimHelper(); 42 | init(); 43 | } 44 | 45 | private void init() { 46 | removeAllViews(); 47 | bookRecyclerView = new BookRecyclerView(getContext()); 48 | puppetView = new PuppetView(getContext()); 49 | puppetView.setAnimMode(bookRecyclerView.getFlipMode()); 50 | bookRecyclerView.bindReadCurlAnimProxy(puppetView); 51 | LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 52 | params.gravity = Gravity.CENTER; 53 | addView(bookRecyclerView, params); 54 | 55 | LayoutParams params2 = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 56 | params2.gravity = Gravity.CENTER; 57 | addView(puppetView, params2); 58 | } 59 | 60 | @Override 61 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 62 | super.onSizeChanged(w, h, oldw, oldh); 63 | 64 | bookRecyclerView.bindReadCurlAnimProxy(puppetView); 65 | 66 | } 67 | 68 | public void setOnPositionChangedListener(OnPositionChangedListener onPositionChangedListener) { 69 | bookRecyclerView.setOnPositionChangedListener(onPositionChangedListener); 70 | 71 | } 72 | 73 | public void setOnClickMenuListener(OnClickMenuListener onClickMenuListener) { 74 | this.onClickMenuListener = onClickMenuListener; 75 | } 76 | 77 | public void scrollToPosition(int position) { 78 | bookRecyclerView.scrollToPosition(position); 79 | } 80 | 81 | public void smoothScrollToPosition(int position) { 82 | bookRecyclerView.smoothScrollToPosition(position); 83 | } 84 | 85 | public void setAdapter(RecyclerView.Adapter adapter) { 86 | bookRecyclerView.setAdapter(adapter); 87 | } 88 | 89 | public void setItemViewBackgroundColor(int itemViewBackgroundColor) { 90 | this.itemViewBackgroundColor = itemViewBackgroundColor; 91 | } 92 | 93 | @Override 94 | protected void onDetachedFromWindow() { 95 | super.onDetachedFromWindow(); 96 | if (null != dataPendIntentTask) { 97 | bookRecyclerView.removeCallbacks(dataPendIntentTask); 98 | } 99 | } 100 | 101 | @Override 102 | public void onExpectNext() { 103 | bookRecyclerView.onExpectNext(false); 104 | if (null != dataPendIntentTask) { 105 | bookRecyclerView.post(dataPendIntentTask); 106 | dataPendIntentTask = null; 107 | } 108 | } 109 | 110 | @Override 111 | public void onExpectPrevious() { 112 | bookRecyclerView.onExpectPrevious(false); 113 | if (null != dataPendIntentTask) { 114 | bookRecyclerView.post(dataPendIntentTask); 115 | dataPendIntentTask = null; 116 | } 117 | } 118 | 119 | @Override 120 | public Bitmap getPreviousBitmap() { 121 | return bookRecyclerView.getPreviousBitmap(); 122 | } 123 | 124 | @Override 125 | public Bitmap getCurrentBitmap() { 126 | return bookRecyclerView.getCurrentBitmap(); 127 | } 128 | 129 | @Override 130 | public Bitmap getNextBitmap() { 131 | return bookRecyclerView.getNextBitmap(); 132 | } 133 | 134 | @Override 135 | public int getBackgroundColor() { 136 | return itemViewBackgroundColor; 137 | } 138 | 139 | 140 | public void setFlipMode(@BookLayoutManager.BookFlipMode int flipMode) { 141 | if (flipMode == BookLayoutManager.BookFlipMode.MODE_CURL 142 | || flipMode == BookLayoutManager.BookFlipMode.MODE_COVER) { 143 | puppetView.setVisibility(View.VISIBLE); 144 | } else { 145 | puppetView.setVisibility(View.INVISIBLE);//不可以设置为gone,避免animView 无法获取尺寸 146 | } 147 | bookRecyclerView.setFlipMode(flipMode); 148 | puppetView.setAnimMode(flipMode); 149 | 150 | } 151 | 152 | @Override 153 | public AnimHelper getAnimHelper() { 154 | return animHelper; 155 | } 156 | 157 | @Override 158 | public void onClickMenuArea() { 159 | if (null != onClickMenuListener) { 160 | onClickMenuListener.onClickMenu(); 161 | } 162 | } 163 | 164 | /** 165 | * 只在非卷曲模式下调用 166 | */ 167 | @Override 168 | public void onClickNextArea() { 169 | bookRecyclerView.onExpectNext(true); 170 | } 171 | 172 | /** 173 | * 只在非卷曲模式下调用 174 | */ 175 | @Override 176 | public void onClickPreviousArea() { 177 | bookRecyclerView.onExpectPrevious(true); 178 | } 179 | 180 | public boolean checkAllowChangeData() { 181 | return !puppetView.animRunningOrTouching() 182 | || bookRecyclerView.getFlipMode() == BookLayoutManager.BookFlipMode.MODE_NORMAL; 183 | } 184 | 185 | /** 186 | * @param dataPendIntentTask 187 | */ 188 | public void setDataPendIntentTask(Runnable dataPendIntentTask) { 189 | this.dataPendIntentTask = dataPendIntentTask; 190 | } 191 | 192 | public interface OnPositionChangedListener { 193 | void onChanged(boolean arriveNext, int curPosition); 194 | } 195 | 196 | public interface OnClickMenuListener { 197 | void onClickMenu(); 198 | } 199 | 200 | 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/EventProxy.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.view.MotionEvent; 4 | 5 | /** 6 | * create by zhusw on 2020-08-14 18:37 7 | */ 8 | public interface EventProxy { 9 | boolean onItemViewTouchEvent(MotionEvent event); 10 | 11 | boolean animRunning(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/FPoint.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | /** 4 | * create by zhusw on 2020-07-30 17:05 5 | */ 6 | public class FPoint { 7 | public float x, y; 8 | 9 | public FPoint() { 10 | } 11 | 12 | public FPoint(float x, float y) { 13 | this.x = x; 14 | this.y = y; 15 | } 16 | 17 | public void setXY(float x, float y) { 18 | this.x = x; 19 | this.y = y; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/PaperLayout.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.RectF; 8 | import android.util.AttributeSet; 9 | import android.view.MotionEvent; 10 | import android.widget.LinearLayout; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.annotation.Nullable; 14 | 15 | /** 16 | * 作为根布局使用,最好再填充一个ViewGroup并设置match_parent 17 | * 必须开启硬件加速,否则掉帧 18 | * create by zhusw on 2020-07-28 16:00 19 | */ 20 | public class PaperLayout extends LinearLayout { 21 | private final Canvas viewScreenShotCanvas; 22 | 23 | private BookRecyclerView bookRecyclerView; 24 | private final RectF menuBounds; 25 | 26 | public PaperLayout(@NonNull Context context) { 27 | this(context, null); 28 | } 29 | 30 | public PaperLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 31 | this(context, attrs, 0); 32 | } 33 | 34 | public PaperLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 35 | super(context, attrs, defStyleAttr); 36 | viewScreenShotCanvas = new Canvas(); 37 | menuBounds = new RectF(); 38 | } 39 | 40 | @Override 41 | protected void onAttachedToWindow() { 42 | super.onAttachedToWindow(); 43 | bookRecyclerView = (BookRecyclerView) getParent(); 44 | 45 | } 46 | 47 | @Override 48 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { 49 | super.onSizeChanged(w, h, oldw, oldh); 50 | if (bookRecyclerView.getFlipMode() == BookLayoutManager.BookFlipMode.MODE_CURL 51 | || bookRecyclerView.getFlipMode() == BookLayoutManager.BookFlipMode.MODE_COVER) { 52 | requestDisallowInterceptTouchEvent(true); 53 | } else { 54 | requestDisallowInterceptTouchEvent(false); 55 | } 56 | menuBounds.left = getWidth() / 3F; 57 | menuBounds.top = getHeight() / 3F; 58 | menuBounds.right = getWidth() * 2 / 3F; 59 | menuBounds.bottom = getHeight() * 2 / 3F; 60 | } 61 | 62 | private int offset = -2; 63 | 64 | /** 65 | * 这个会被调用多次,最终宽度为实际测量宽度-2px 66 | * 这样在 layoutManager 进行布局时 才可以同时保持3个item被显示 67 | * 68 | * @param widthMeasureSpec 69 | * @param heightMeasureSpec 70 | */ 71 | @Override 72 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 73 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 74 | int height = measureSize(5, heightMeasureSpec); 75 | int width = measureSize(5, widthMeasureSpec); 76 | setMeasuredDimension(width + offset, height); 77 | } 78 | 79 | 80 | private int measureSize(int defaultSize, int measureSpec) { 81 | int result = defaultSize; 82 | int specMode = MeasureSpec.getMode(measureSpec); 83 | int specSize = MeasureSpec.getSize(measureSpec); 84 | 85 | if (specMode == MeasureSpec.EXACTLY) { 86 | result = specSize; 87 | } else if (specMode == MeasureSpec.AT_MOST) { 88 | result = Math.min(result, specSize); 89 | } 90 | return result; 91 | } 92 | 93 | public Bitmap drawViewScreenShotToBitmap(Bitmap bitmap) { 94 | viewScreenShotCanvas.setBitmap(bitmap); 95 | draw(viewScreenShotCanvas); 96 | return bitmap; 97 | } 98 | 99 | @Override 100 | public boolean isScrollContainer() { 101 | return false; 102 | } 103 | 104 | 105 | private float interceptDownX; 106 | 107 | @Override 108 | public boolean onInterceptTouchEvent(MotionEvent ev) { 109 | //动画执行期间 子view 也不可获取事件 110 | if (bookRecyclerView.animRunning()) return true; 111 | if (ev.getAction() == MotionEvent.ACTION_DOWN) { 112 | interceptDownX = ev.getRawX(); 113 | } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { 114 | float currentX = ev.getRawX(); 115 | float distance = Math.abs(currentX - interceptDownX); 116 | if (distance > 1F) { 117 | return true; 118 | } 119 | } 120 | return super.onInterceptTouchEvent(ev); 121 | } 122 | 123 | private float receiveDownX = -1; 124 | private float downX = -1; 125 | private float downY = -1; 126 | 127 | @SuppressLint("ClickableViewAccessibility") 128 | @Override 129 | public boolean onTouchEvent(MotionEvent ev) { 130 | if (ev.getAction() == MotionEvent.ACTION_DOWN) { 131 | receiveDownX = ev.getRawX(); 132 | downX = ev.getRawX(); 133 | downY = ev.getRawY(); 134 | } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { 135 | if (receiveDownX == -1) { 136 | ev.setAction(MotionEvent.ACTION_DOWN); 137 | receiveDownX = ev.getRawX(); 138 | } 139 | } else if (ev.getAction() == MotionEvent.ACTION_UP 140 | || ev.getAction() == MotionEvent.ACTION_CANCEL) { 141 | receiveDownX = -1; 142 | } 143 | if (bookRecyclerView.getFlipMode() == BookLayoutManager.BookFlipMode.MODE_NORMAL) { 144 | if (ev.getAction() == MotionEvent.ACTION_UP) { 145 | float upX = ev.getRawX(); 146 | float upY = ev.getRawY(); 147 | if (downX > menuBounds.left 148 | && downY > menuBounds.top 149 | && downX < menuBounds.right 150 | && downY < menuBounds.bottom 151 | && upX > menuBounds.left 152 | && upY > menuBounds.top 153 | && upX < menuBounds.right 154 | && upY < menuBounds.bottom) { 155 | bookRecyclerView.onClickMenu(); 156 | } else if (upX >= getWidth() / 2F) { 157 | bookRecyclerView.onExpectNext(true); 158 | } else if (upX < getWidth() / 2F) { 159 | bookRecyclerView.onExpectPrevious(true); 160 | } 161 | } 162 | } else { 163 | bookRecyclerView.onItemViewTouchEvent(ev); 164 | } 165 | return true; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/PuppetView.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.util.AttributeSet; 7 | import android.view.MotionEvent; 8 | import android.view.View; 9 | import android.view.ViewParent; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | 14 | import com.juziml.read.business.read.anim.AnimHelper; 15 | import com.juziml.read.business.read.anim.CoverAnimationEffecter; 16 | import com.juziml.read.business.read.anim.IAnimationEffecter; 17 | import com.juziml.read.business.read.anim.SimulationAnimationEffecter; 18 | 19 | 20 | /** 21 | * 此View的作用就像幕后一样,负责接受事件并传递到动画,Effecter 22 | * create by zhusw on 2020-08-14 17:37 23 | */ 24 | public class PuppetView extends View implements EventProxy, AnimParentView { 25 | 26 | IAnimationEffecter animationEffecter; 27 | AnimParentView parentView; 28 | private Bitmap previousViewBitmap; 29 | private Bitmap currentViewBitmap; 30 | private Bitmap nextViewBitmap; 31 | boolean performDrawCurlTexture = false; 32 | private int vWidth, vHeight; 33 | 34 | public PuppetView(@NonNull Context context) { 35 | this(context, null); 36 | } 37 | 38 | public PuppetView(@NonNull Context context, @Nullable AttributeSet attrs) { 39 | this(context, attrs, 0); 40 | } 41 | 42 | public PuppetView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 43 | super(context, attrs, defStyleAttr); 44 | /* 45 | view 可以单独关,但是不能单独打开硬件加速 46 | 关闭硬件加速 卡到爆炸 47 | 开启硬件加速,诱发 OpenGLRenderer: Path too large to be rendered into a texture 48 | setLayerType(LAYER_TYPE_SOFTWARE,null); 49 | */ 50 | } 51 | 52 | public boolean animRunningOrTouching() { 53 | boolean animRunningOrTouching = false; 54 | if (null != animationEffecter) { 55 | animRunningOrTouching = animRunning(); 56 | } 57 | return animRunningOrTouching; 58 | } 59 | 60 | @Override 61 | protected void onAttachedToWindow() { 62 | super.onAttachedToWindow(); 63 | ViewParent viewParent = getParent(); 64 | parentView = (AnimParentView) viewParent; 65 | if (null != animationEffecter) { 66 | animationEffecter.onViewAttachedToWindow(); 67 | } 68 | } 69 | 70 | 71 | public void setAnimMode(int animMode) { 72 | //重置某些属性 与变量 73 | animationEffecter = null; 74 | if (animMode == BookLayoutManager.BookFlipMode.MODE_COVER) { 75 | animationEffecter = new CoverAnimationEffecter(this); 76 | } else if (animMode == BookLayoutManager.BookFlipMode.MODE_CURL) { 77 | animationEffecter = new SimulationAnimationEffecter(this); 78 | } 79 | if (null != animationEffecter) { 80 | animationEffecter.onViewSizeChanged(vWidth, vHeight); 81 | } 82 | } 83 | 84 | @Override 85 | public void draw(Canvas canvas) { 86 | if (performDrawCurlTexture && null != animationEffecter) { 87 | animationEffecter.draw(canvas); 88 | } else { 89 | super.draw(canvas); 90 | } 91 | } 92 | 93 | @Override 94 | protected void onDetachedFromWindow() { 95 | super.onDetachedFromWindow(); 96 | if (null != animationEffecter) { 97 | animationEffecter.onViewDetachedFromWindow(); 98 | } 99 | } 100 | 101 | /** 102 | * 这个会被调用多次,最终宽度为实际测量宽度-2px 103 | * 这样在 layoutmanager 进行布局时 才可以同时保持3个item被显示 104 | * 105 | * @param widthMeasureSpec 106 | * @param heightMeasureSpec 107 | */ 108 | @Override 109 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 110 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 111 | int height = measureSize(5, heightMeasureSpec); 112 | int width = measureSize(5, widthMeasureSpec) - 2; 113 | setMeasuredDimension(width - 2, height); 114 | vWidth = width; 115 | vHeight = height; 116 | if (null != animationEffecter) { 117 | animationEffecter.onViewSizeChanged(vWidth, vHeight); 118 | } 119 | } 120 | 121 | 122 | private int measureSize(int defaultSize, int measureSpec) { 123 | int result = defaultSize; 124 | int specMode = MeasureSpec.getMode(measureSpec); 125 | int specSize = MeasureSpec.getSize(measureSpec); 126 | if (specMode == MeasureSpec.EXACTLY) { 127 | result = specSize; 128 | } else if (specMode == MeasureSpec.AT_MOST) { 129 | result = Math.min(result, specSize); 130 | } 131 | return result; 132 | } 133 | 134 | 135 | public void buildBitmap(int slideDirection) { 136 | performDrawCurlTexture = false; 137 | if (slideDirection == AnimHelper.SLID_DIRECTION_LEFT) { 138 | currentViewBitmap = parentView.getCurrentBitmap(); 139 | nextViewBitmap = parentView.getNextBitmap(); 140 | } else if (slideDirection == AnimHelper.SLID_DIRECTION_RIGHT) { 141 | previousViewBitmap = parentView.getPreviousBitmap(); 142 | } 143 | performDrawCurlTexture = true; 144 | } 145 | 146 | 147 | @Override 148 | public boolean onItemViewTouchEvent(MotionEvent event) { 149 | if (null != animationEffecter) { 150 | animationEffecter.handlerEvent(event); 151 | } 152 | return true; 153 | } 154 | 155 | @Override 156 | public boolean animRunning() { 157 | if (null != animationEffecter) { 158 | return animationEffecter.animInEffect(); 159 | } 160 | return false; 161 | } 162 | 163 | @Override 164 | public void computeScroll() { 165 | if (null != animationEffecter) { 166 | animationEffecter.onScroll(); 167 | } 168 | } 169 | 170 | @Override 171 | public void onExpectNext() { 172 | parentView.onExpectNext(); 173 | } 174 | 175 | @Override 176 | public void onExpectPrevious() { 177 | parentView.onExpectPrevious(); 178 | } 179 | 180 | @Override 181 | public Bitmap getPreviousBitmap() { 182 | return previousViewBitmap; 183 | } 184 | 185 | @Override 186 | public Bitmap getCurrentBitmap() { 187 | return currentViewBitmap; 188 | } 189 | 190 | @Override 191 | public Bitmap getNextBitmap() { 192 | return nextViewBitmap; 193 | } 194 | 195 | @Override 196 | public int getBackgroundColor() { 197 | return parentView.getBackgroundColor(); 198 | } 199 | 200 | @Override 201 | public AnimHelper getAnimHelper() { 202 | return parentView.getAnimHelper(); 203 | } 204 | 205 | @Override 206 | public void onClickMenuArea() { 207 | parentView.onClickMenuArea(); 208 | } 209 | 210 | @Override 211 | public void onClickNextArea() { 212 | parentView.onClickNextArea(); 213 | } 214 | 215 | @Override 216 | public void onClickPreviousArea() { 217 | parentView.onClickPreviousArea(); 218 | } 219 | 220 | public void reset() { 221 | previousViewBitmap = null; 222 | nextViewBitmap = null; 223 | currentViewBitmap = null; 224 | performDrawCurlTexture = false; 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/RVInnerItemFunction.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.view.MotionEvent; 4 | 5 | /** 6 | * -recyclerview 内部功能接口 7 | * create by zhusw on 2020-08-17 10:07 8 | */ 9 | public interface RVInnerItemFunction { 10 | void onItemViewTouchEvent(MotionEvent event); 11 | 12 | boolean animRunning(); 13 | 14 | void onClickMenu(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/business/read/view/RVOuterFunction.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.business.read.view; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | /** 6 | * create by zhusw on 2020-08-17 10:15 7 | */ 8 | public interface RVOuterFunction { 9 | void onExpectNext(boolean smooth); 10 | 11 | void onExpectPrevious(boolean smooth); 12 | 13 | Bitmap getPreviousBitmap(); 14 | 15 | Bitmap getCurrentBitmap(); 16 | 17 | Bitmap getNextBitmap(); 18 | 19 | int getFlipMode(); 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/core/App.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.core; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.bumptech.glide.Glide; 8 | 9 | /** 10 | * Created by victor on 2020/9/10. 11 | * Email : victorhhl@163.com 12 | * Description : 13 | */ 14 | public class App extends ArchApplication { 15 | 16 | public static App instance; 17 | 18 | @Override 19 | protected void attachBaseContext(Context base) { 20 | super.attachBaseContext(base); 21 | instance = this; 22 | } 23 | 24 | /** 25 | * 获取Application实例 26 | */ 27 | @NonNull 28 | public static App get() { 29 | return instance; 30 | } 31 | 32 | @Override 33 | public void onTrimMemory(int level) { 34 | super.onTrimMemory(level); 35 | Glide.get(this).trimMemory(level); 36 | } 37 | 38 | @Override 39 | public void onLowMemory() { 40 | super.onLowMemory(); 41 | Glide.get(this).onLowMemory(); 42 | System.gc(); 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/core/ArchApplication.kt: -------------------------------------------------------------------------------- 1 | package com.juziml.read.core 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | import android.os.Bundle 6 | 7 | /** 8 | * create by zhusw on 6/7/21 14:16 9 | */ 10 | open class ArchApplication : Application() { 11 | init { 12 | @Suppress("LeakingThis")//里面什么都没做,所以抑制警告 13 | (ArchConfig.preInitApplication(this)) 14 | } 15 | 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | //ArchConfig 需要晚于Utils 初始化 20 | ArchConfig.initApplicationOnCreate() 21 | 22 | } 23 | 24 | override fun startActivity(intent: Intent) { 25 | startActivity(intent, null) 26 | } 27 | 28 | override fun startActivity(intent: Intent, options: Bundle?) { 29 | //默认增加new task标识 30 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 31 | super.startActivity(intent, options) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/core/ArchConfig.kt: -------------------------------------------------------------------------------- 1 | package com.juziml.read.core 2 | 3 | import android.app.Application 4 | import android.content.ContentProvider 5 | import android.content.pm.PackageInfo 6 | import android.content.pm.PackageManager 7 | import com.juziml.read.BuildConfig 8 | 9 | /** 10 | * 基础架构 配置类 11 | * create by zhusw on 5/24/21 17:20 12 | */ 13 | object ArchConfig { 14 | 15 | private var _application: Application? = null 16 | 17 | @JvmStatic 18 | val app: Application 19 | get() = _application!! 20 | 21 | /** 22 | * 预初始化,Application构造调用,这样可以保证任何情况都不为null([ContentProvider]) 23 | */ 24 | @JvmStatic 25 | fun preInitApplication(app: Application) { 26 | _application = app 27 | //此处不允许有逻辑 28 | } 29 | 30 | @JvmStatic 31 | fun initApplicationOnCreate() { 32 | } 33 | 34 | /** 35 | * 记录:非const无法被编译优化掉(开启混淆后可被混淆规则优化掉) 36 | */ 37 | @JvmField 38 | val debug: Boolean = BuildConfig.DEBUG 39 | 40 | private fun getHostVersionName(): String { 41 | return try { 42 | val pm: PackageManager = app.packageManager 43 | val pi: PackageInfo = pm.getPackageInfo( 44 | app.packageName, 0 45 | ) 46 | pi.versionName ?: "unknown" 47 | } catch (e: PackageManager.NameNotFoundException) { 48 | e.printStackTrace() 49 | "unknown" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/utils/DLog.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.utils; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * create by zhusw on 2020-03-27 15:46 7 | */ 8 | public class DLog { 9 | 10 | public static void log(String rules, Object... args) { 11 | Log.i("DLog:", String.format(rules, args)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/juziml/read/utils/TraceUtils.java: -------------------------------------------------------------------------------- 1 | package com.juziml.read.utils; 2 | 3 | import android.util.Log; 4 | 5 | import com.juziml.read.BuildConfig; 6 | 7 | /** 8 | * create by zhusw on 2020-08-13 13:43 9 | */ 10 | public class TraceUtils { 11 | private static String msg = null; 12 | private static long time = 0; 13 | 14 | public static void startTrace(String format, Object... o) { 15 | if (BuildConfig.DEBUG) { 16 | msg = String.format(format, o); 17 | time = System.currentTimeMillis(); 18 | } 19 | } 20 | 21 | public static void endTrace() { 22 | if (BuildConfig.DEBUG) { 23 | String content = String.format("TAG:%s time:%s", msg, System.currentTimeMillis() - time); 24 | Log.i("TraceUtils", content); 25 | time = 0l; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_add_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_add_chat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_chat_gpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_chat_gpt.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_chat_sakura_flower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_chat_sakura_flower.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_close_pink_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_close_pink_128.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_empty_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_empty_new.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_head.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher_20.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_loading_cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_loading_cycle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_send.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_voice_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_voice_dark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_voice_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/ic_voice_light.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/icon_net_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable-xxhdpi/icon_net_error.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_app_window.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sp_chat_item_item_bg_bot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sp_chat_item_item_bg_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sp_chat_session_list_item_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/sp_item_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/test_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Western-parotia/BookViewProject/59f24a609314e25bae435238d39e88cef82fb40c/app/src/main/res/drawable/test_img.jpg -------------------------------------------------------------------------------- /app/src/main/res/layout/act_simple.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 17 | 18 |