├── README.md ├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable │ │ │ │ ├── chm_j.jpeg │ │ │ │ ├── chm_p.jpeg │ │ │ │ ├── tz_j.jpeg │ │ │ │ ├── wt_h.jpeg │ │ │ │ ├── wt_p.jpeg │ │ │ │ ├── ytz_b.jpeg │ │ │ │ ├── ytz_p.jpeg │ │ │ │ ├── zj_j.jpeg │ │ │ │ ├── zy_p.jpeg │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ ├── view_album_style_two.xml │ │ │ │ └── view_album_style_one.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── myapplication │ │ │ │ ├── view │ │ │ │ ├── PageFlipListener.kt │ │ │ │ ├── direction │ │ │ │ │ ├── LTDrawAction.kt │ │ │ │ │ ├── RTDrawAction.kt │ │ │ │ │ ├── LBDrawAction.kt │ │ │ │ │ ├── RBDrawAction.kt │ │ │ │ │ ├── DirectDrawAction.kt │ │ │ │ │ ├── BaseDirectDrawAction.kt │ │ │ │ │ ├── RightBaseDirectDrawAction.kt │ │ │ │ │ └── LeftBaseDirectDrawAction.kt │ │ │ │ ├── DeviceUtil.kt │ │ │ │ ├── DefineView.kt │ │ │ │ ├── DoubleFlipView.kt │ │ │ │ └── DoubleRealFlipView.kt │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── myapplication │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── example │ │ └── myapplication │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── gradlew.bat └── gradlew /README.md: -------------------------------------------------------------------------------- 1 | # DoubleFlipView 2 | 3 | 双仿真页面 4 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle 3 | /.idea -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/drawable/chm_j.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/chm_j.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/chm_p.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/chm_p.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/tz_j.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/tz_j.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/wt_h.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/wt_h.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/wt_p.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/wt_p.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ytz_b.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/ytz_b.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ytz_p.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/ytz_p.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/zj_j.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/zj_j.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/zy_p.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/drawable/zy_p.jpeg -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mCyp/DoubleFlipView/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | com.example.myapplication.wh 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/PageFlipListener.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view 2 | 3 | interface PageFlipListener { 4 | fun onNextPage() 5 | fun onPrePage() 6 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 15 11:05:13 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/LTDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import com.qidian.fonttest.view.TOP_SIDE 4 | 5 | class LTDrawAction: LeftBaseDirectDrawAction() { 6 | override fun flipSide(): Int { 7 | return TOP_SIDE 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/RTDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import com.qidian.fonttest.view.TOP_SIDE 4 | 5 | class RTDrawAction: RightBaseDirectDrawAction() { 6 | override fun flipSide(): Int { 7 | return TOP_SIDE 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/LBDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import com.qidian.fonttest.view.BOTTOM_SIDE 4 | 5 | class LBDrawAction: LeftBaseDirectDrawAction() { 6 | override fun flipSide(): Int { 7 | return BOTTOM_SIDE 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/RBDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import com.qidian.fonttest.view.BOTTOM_SIDE 4 | 5 | class RBDrawAction: RightBaseDirectDrawAction() { 6 | override fun flipSide(): Int { 7 | return BOTTOM_SIDE 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | /.idea/compiler.xml 18 | /.idea/gradle.xml 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/myapplication/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/DeviceUtil.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view 2 | 3 | import android.content.Context 4 | 5 | object DeviceUtil { 6 | fun dip2px(context: Context, dp: Float):Int { 7 | val scale = context.resources.displayMetrics.density 8 | return (dp * scale + 0.5f).toInt() 9 | } 10 | 11 | fun getScreenWidth(context: Context): Int { 12 | return context.resources.displayMetrics.widthPixels 13 | } 14 | 15 | fun getScreenHeight(context: Context): Int { 16 | return context.resources.displayMetrics.heightPixels 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.myapplication", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_album_style_two.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/DefineView.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.util.AttributeSet 8 | import android.view.View 9 | 10 | class DefineView @JvmOverloads constructor( 11 | context: Context, 12 | attrs: AttributeSet? = null, 13 | defStyleInt: Int = 0 14 | ) : View(context, attrs, defStyleInt) { 15 | 16 | var mPaint: Paint = Paint() 17 | 18 | init { 19 | mPaint.isAntiAlias = true 20 | mPaint.isDither = true 21 | 22 | } 23 | 24 | 25 | override fun onDraw(canvas: Canvas?) { 26 | super.onDraw(canvas) 27 | 28 | mPaint.color = Color.RED 29 | canvas?.drawRect(100f, 100f, 400f, 500f, mPaint) 30 | 31 | canvas?.save() 32 | mPaint.color = Color.BLUE 33 | canvas?.rotate(60f, 100f, 500f) 34 | canvas?.drawRect(100f, 100f, 400f, 500f, mPaint) 35 | canvas?.restore() 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } 4 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } 5 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } 6 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' } 7 | maven { url "https://www.jitpack.io" } 8 | gradlePluginPortal() 9 | google() 10 | mavenCentral() 11 | } 12 | } 13 | dependencyResolutionManagement { 14 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 15 | repositories { 16 | maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } 17 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter' } 18 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/google' } 19 | maven { url 'https://maven.aliyun.com/nexus/content/repositories/gradle-plugin' } 20 | maven { url "https://www.jitpack.io" } 21 | google() 22 | mavenCentral() 23 | } 24 | } 25 | rootProject.name = "My Application" 26 | include ':app' 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/DirectDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view 2 | 3 | import android.graphics.* 4 | 5 | interface DirectDrawAction { 6 | fun drawNoFlipSide(canvas: Canvas, reUsePath: Path, radius: Int) 7 | fun drawFlipPageContent(canvas: Canvas, reUsePath: Path, flipPath: Path, r: Int) 8 | fun drawBookMiddleArea(canvas: Canvas, reUsePath: Path, r: Int, mPaint: Paint) 9 | fun drawFlipPageBottomPageContent(canvas: Canvas, reUsePath: Path, flipPath: Path, mDegree: Double, mBezierStart1: PointF, mBezierStart2: PointF, mPaint: Paint, mTouchDis: Float, per: Float, minDis: Float) 10 | fun drawTwoSideShadow(canvas: Canvas, reUsePath: Path, flipPath: Path, mDegree: Double, mCurCornerPoint: PointF, mBezierControl1: PointF, mBezierStart1: PointF, mBezierControl2: PointF, mBezierStart2: PointF, mOriginalCorner: PointF, mPaint: Paint) 11 | fun drawBackContentAndShadow(canvas: Canvas, reUsePath: Path, flipPath: Path, mBezierVertex1: PointF, mBezierVertex2: PointF, mBezierEnd2: PointF, mBezierEnd1: PointF, mCurCornerPoint: PointF, mDegree: Double, mMatrix: Matrix, mPaint: Paint, mBezierStart1: PointF, mBezierStart2: PointF, mTouchDis: Float) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_album_style_one.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 17 | 18 | 19 | 27 | 28 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 32 8 | 9 | defaultConfig { 10 | applicationId "com.example.myapplication" 11 | minSdk 21 12 | targetSdk 32 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dependencies { 35 | 36 | implementation 'androidx.core:core-ktx:1.7.0' 37 | implementation 'androidx.appcompat:appcompat:1.3.0' 38 | implementation 'com.google.android.material:material:1.4.0' 39 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 40 | testImplementation 'junit:junit:4.13.2' 41 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 42 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 43 | 44 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' 45 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' 46 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01' 47 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01' 48 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 49 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 50 | implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0' 51 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/DoubleFlipView.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.GestureDetector 6 | import android.view.MotionEvent 7 | import android.view.ViewConfiguration 8 | import android.widget.FrameLayout 9 | import androidx.core.view.GestureDetectorCompat 10 | import com.qidian.fonttest.view.* 11 | import kotlin.math.abs 12 | 13 | private const val STATUS_NONE = 0 14 | private const val STATUS_DOWN = 1 15 | private const val STATUS_MOVE = 2 16 | // 滑动处理 17 | // 滑动优化处理 18 | class DoubleFlipView @JvmOverloads constructor( 19 | context: Context, 20 | attrs: AttributeSet? = null, 21 | defStyleInt: Int = 0 22 | ) : FrameLayout(context, attrs, defStyleInt), GestureDetector.OnGestureListener { 23 | 24 | // 确定翻页的手势 25 | // 1. 根据手指触碰的位置进行翻页,实际是围绕手指的点进行旋转 26 | // 2. 到达翻页顶点触发的行为,bezierc2 和 beziers2 往一处汇集 27 | // 3. 折成什么角度由位置决定的 28 | // 4. 滑动的时候其实有一个最小的触发动作 29 | 30 | // 处理滑动 31 | private var status: Int = STATUS_NONE 32 | private val mGestureDetector: GestureDetectorCompat 33 | val mDoubleRealFlipView: DoubleRealFlipView 34 | private var isTap: Boolean = false 35 | 36 | 37 | init { 38 | mGestureDetector = GestureDetectorCompat(context, this) 39 | // 添加View进去 40 | mDoubleRealFlipView = DoubleRealFlipView(context) 41 | val lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) 42 | addView(mDoubleRealFlipView, lp) 43 | } 44 | 45 | override fun onTouchEvent(event: MotionEvent?): Boolean { 46 | val result = mGestureDetector.onTouchEvent(event) 47 | event?.let { e-> 48 | when(e.action) { 49 | MotionEvent.ACTION_UP -> { 50 | // 翻页或者取消翻页 51 | status = STATUS_NONE 52 | if(!isTap) { 53 | if(!mDoubleRealFlipView.release(e.x, e.y)) { 54 | mDoubleRealFlipView.resetScrollTag() 55 | } 56 | mDoubleRealFlipView.invalidate() 57 | } 58 | isTap = false 59 | } 60 | MotionEvent.ACTION_CANCEL -> { 61 | // 翻页 62 | status = STATUS_NONE 63 | if(!isTap) { 64 | mDoubleRealFlipView.resetScrollTag() 65 | mDoubleRealFlipView.invalidate() 66 | } 67 | isTap = false 68 | } 69 | } 70 | } 71 | return result 72 | } 73 | 74 | override fun onDown(e: MotionEvent): Boolean { 75 | // 计算位置 76 | status = STATUS_DOWN 77 | mDoubleRealFlipView.prepareOnDown(e.x, e.y) 78 | return true 79 | } 80 | 81 | override fun onShowPress(e: MotionEvent?) { 82 | 83 | } 84 | 85 | override fun onSingleTapUp(e: MotionEvent?): Boolean { 86 | e?.let { 87 | mDoubleRealFlipView.onTap(it.x) 88 | isTap = true 89 | } 90 | return true 91 | } 92 | 93 | fun reset() { 94 | mDoubleRealFlipView.resetScrollTag() 95 | } 96 | 97 | override fun onScroll( 98 | e1: MotionEvent, 99 | e2: MotionEvent, 100 | distanceX: Float, 101 | distanceY: Float 102 | ): Boolean { 103 | // 往左上是正 104 | if(status == STATUS_DOWN || status == STATUS_MOVE) { 105 | if(distanceX != 0f && status == STATUS_DOWN && abs(distanceX) > ViewConfiguration.get(context).scaledTouchSlop) { 106 | if(distanceX > 0 && distanceY > 0) { 107 | mDoubleRealFlipView.prePareDirection(DIRECT_TL) 108 | } else if (distanceX > 0 && distanceY <= 0) { 109 | mDoubleRealFlipView.prePareDirection(DIRECT_BL) 110 | } else if (distanceX < 0 && distanceY > 0) { 111 | mDoubleRealFlipView.prePareDirection(DIRECT_TR) 112 | } else { 113 | mDoubleRealFlipView.prePareDirection(DIRECT_BR) 114 | } 115 | status = STATUS_MOVE 116 | } 117 | if(status == STATUS_MOVE) { 118 | mDoubleRealFlipView.setTouchPoint(e2.x, e2.y) 119 | mDoubleRealFlipView.invalidate() 120 | } 121 | } 122 | return true 123 | } 124 | 125 | fun setPageFlipListener(listener: PageFlipListener) { 126 | mDoubleRealFlipView.pageFlipListener = listener 127 | } 128 | 129 | override fun onLongPress(e: MotionEvent?) { 130 | 131 | } 132 | 133 | override fun onFling( 134 | e1: MotionEvent?, 135 | e2: MotionEvent?, 136 | velocityX: Float, 137 | velocityY: Float 138 | ): Boolean { 139 | // 翻页 140 | return true 141 | } 142 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/BaseDirectDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import android.content.Context 4 | import android.graphics.* 5 | import android.os.Build 6 | import com.example.myapplication.view.DeviceUtil 7 | import com.example.myapplication.view.DirectDrawAction 8 | import com.qidian.fonttest.view.OUT_LEN 9 | import com.qidian.fonttest.view.TOP_SIDE 10 | import kotlin.math.* 11 | 12 | 13 | abstract class BaseDirectDrawAction: DirectDrawAction { 14 | 15 | val shadowColors = intArrayOf(-0x4f99999a, 0x666666) 16 | val shadowReverseColors = intArrayOf(0x666666, -0x4f99999a) 17 | 18 | var mLeftBottomBitmap: Bitmap? = null 19 | var mLeftMiddleBitmap: Bitmap? = null 20 | var mLeftTopBitmap: Bitmap? = null 21 | var mRightTopBitmap: Bitmap? = null 22 | var mRightMiddleBitmap: Bitmap? = null 23 | var mRightBottomBitmap: Bitmap? = null 24 | lateinit var mLeftPageLTPoint: PointF 25 | lateinit var mLeftPageRBPoint: PointF 26 | lateinit var mRightPageLTPoint: PointF 27 | lateinit var mRightPageRBPoint: PointF 28 | 29 | var context: Context? = null 30 | var bgColor: Int = 0 31 | 32 | 33 | abstract fun flipSide(): Int 34 | 35 | abstract fun flipPage(): Int 36 | 37 | override fun drawTwoSideShadow( 38 | canvas: Canvas, 39 | reUsePath: Path, 40 | flipPath: Path, 41 | mDegree: Double, 42 | mCurCornerPoint: PointF, 43 | mBezierControl1: PointF, 44 | mBezierStart1: PointF, 45 | mBezierControl2: PointF, 46 | mBezierStart2: PointF, 47 | mOriginalCorner: PointF, 48 | mPaint: Paint 49 | ) { 50 | // 绘制翻转的时候,页脚的阴影 51 | val outPoint = PointF() 52 | // 计算旋转的角度 53 | var offsetDegree = Math.toDegrees( 54 | atan2( 55 | (mBezierControl1.y - mCurCornerPoint.y).toDouble(), 56 | (mCurCornerPoint.x - mBezierControl1.x).toDouble() 57 | ) 58 | ) 59 | // 计算页面偏移角度,页面不同,造成旋转的偏角不同 60 | if(flipPage() == 0) { 61 | if(flipSide() == TOP_SIDE) { 62 | offsetDegree = abs(offsetDegree) 63 | } 64 | } else { 65 | offsetDegree = if(flipSide() == TOP_SIDE) { 66 | 180 - abs(offsetDegree) 67 | } else { 68 | 180 - offsetDegree 69 | } 70 | } 71 | // 阴影的顶点还有偏移 45 度 72 | val rad = Math.toRadians(offsetDegree - 45f) 73 | // 求阴影的顶点 74 | if (flipPage() == 0) { 75 | outPoint.x = mCurCornerPoint.x + OUT_LEN * sqrt(2f) * cos(rad).toFloat() 76 | } else { 77 | outPoint.x = mCurCornerPoint.x - OUT_LEN * sqrt(2f) * cos(rad).toFloat() 78 | } 79 | if (flipSide() == TOP_SIDE) { 80 | outPoint.y = mCurCornerPoint.y + OUT_LEN * sqrt(2f) * sin(rad).toFloat() 81 | } else { 82 | outPoint.y = mCurCornerPoint.y - OUT_LEN * sqrt(2f) * sin(rad).toFloat() 83 | } 84 | // 纵轴 - 左右方向的阴影需要旋转的角度 85 | if(flipPage() == 0) { 86 | if(flipSide() == TOP_SIDE) { 87 | offsetDegree -= 90 88 | } else { 89 | offsetDegree = 90 - offsetDegree 90 | } 91 | } else { 92 | if(flipSide() == TOP_SIDE) { 93 | offsetDegree = 90 - offsetDegree 94 | } else { 95 | offsetDegree -= 90 96 | } 97 | } 98 | canvas.save() 99 | // 不同页面翻转的角度不一致 100 | reUsePath.reset() 101 | reUsePath.moveTo(outPoint.x, outPoint.y) 102 | reUsePath.lineTo(mCurCornerPoint.x, mCurCornerPoint.y) 103 | reUsePath.lineTo(mBezierControl1.x, mBezierControl1.y) 104 | reUsePath.lineTo(mBezierStart1.x, mBezierStart1.y) 105 | reUsePath.close() 106 | try { 107 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 108 | canvas.clipOutPath(flipPath) 109 | } else { 110 | canvas.clipPath(flipPath, Region.Op.DIFFERENCE) 111 | } 112 | canvas.clipPath(reUsePath) 113 | } catch (e: Exception) { 114 | // Logger.exception(e); 115 | } 116 | canvas.rotate(offsetDegree.toFloat(), outPoint.x, outPoint.y) 117 | val colors = shadowReverseColors 118 | val rightFirst = if(flipPage() == 0) { 119 | (outPoint.x - OUT_LEN) 120 | } else { 121 | (outPoint.x + OUT_LEN) 122 | } 123 | val bottomFirst = if(flipSide() == TOP_SIDE) { 124 | outPoint.y - abs(mOriginalCorner.x - mBezierControl1.x) 125 | } else { 126 | outPoint.y + abs(mOriginalCorner.x - mBezierControl1.x) 127 | } 128 | mPaint.shader = getGradient(outPoint.x, mBezierControl1.y, rightFirst, mBezierControl1.y, colors) 129 | canvas.drawRect(outPoint.x, outPoint.y, rightFirst, bottomFirst ,mPaint) 130 | canvas.restore() 131 | // 绘制纵轴上下方向的阴影 132 | canvas.save() 133 | reUsePath.reset() 134 | reUsePath.moveTo(outPoint.x, outPoint.y) 135 | reUsePath.lineTo(mCurCornerPoint.x, mCurCornerPoint.y) 136 | reUsePath.lineTo(mBezierControl2.x, mBezierControl2.y) 137 | reUsePath.lineTo(mBezierStart2.x, mBezierStart2.y) 138 | reUsePath.close() 139 | try { 140 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 141 | canvas.clipOutPath(flipPath) 142 | } else { 143 | canvas.clipPath(flipPath, Region.Op.DIFFERENCE) 144 | } 145 | canvas.clipPath(reUsePath) 146 | } catch (e: Exception) { 147 | // Logger.exception(e); 148 | } 149 | canvas.rotate(offsetDegree.toFloat(), outPoint.x, outPoint.y) 150 | val secondColors = shadowReverseColors 151 | val secondBottom: Float = if(flipSide() == TOP_SIDE) { 152 | outPoint.y - OUT_LEN 153 | } else { 154 | outPoint.y + OUT_LEN 155 | } 156 | val secondRight = if(flipPage() == 0) { 157 | outPoint.x - abs(mOriginalCorner.y - mBezierControl2.y) 158 | } else { 159 | outPoint.x + abs(mOriginalCorner.y - mBezierControl2.y) 160 | } 161 | mPaint.shader = getGradient(outPoint.x, outPoint.y, outPoint.x, secondBottom, secondColors) 162 | canvas.drawRect(outPoint.x, outPoint.y, secondRight, secondBottom ,mPaint) 163 | canvas.restore() 164 | } 165 | 166 | @Suppress("SameParameterValue") 167 | fun getGradient(l: Float, t: Float, r: Float, b: Float, colors: IntArray): LinearGradient { 168 | return LinearGradient(l, t, r, b, colors, floatArrayOf(0f, 1.0f), Shader.TileMode.CLAMP) 169 | } 170 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication 2 | 3 | import android.graphics.* 4 | import android.os.Bundle 5 | import android.util.DisplayMetrics 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.widget.ImageView 9 | import android.widget.TextView 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.lifecycle.lifecycleScope 12 | import com.example.myapplication.view.DeviceUtil 13 | import com.example.myapplication.view.DoubleFlipView 14 | import com.example.myapplication.view.PageFlipListener 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.launch 17 | import kotlinx.coroutines.withContext 18 | 19 | 20 | private const val TAG = "MainActivity" 21 | class MainActivity : AppCompatActivity(), PageFlipListener { 22 | 23 | private var bimapArray = Array(6, {null}) 24 | 25 | private var flipView: DoubleFlipView? = null 26 | 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | supportActionBar?.hide() 31 | setContentView(R.layout.activity_main) 32 | 33 | flipView = findViewById(R.id.flipView) 34 | flipView?.setPageFlipListener(this) 35 | lifecycleScope.launch(Dispatchers.IO) { 36 | var firstBitmap = createBitmapFirst(0) 37 | firstBitmap = adjustBitmap(firstBitmap) 38 | bimapArray[0] = firstBitmap 39 | var twoBitmap = createBitmapTwo(1) 40 | twoBitmap = adjustBitmap(twoBitmap) 41 | bimapArray[1] = twoBitmap 42 | var threeBitmap = createBitmapFirst(2) 43 | threeBitmap = adjustBitmap(threeBitmap) 44 | bimapArray[2] = threeBitmap 45 | var fourBitmap = createBitmapTwo(3) 46 | fourBitmap = adjustBitmap(fourBitmap) 47 | bimapArray[3] = fourBitmap 48 | var fiveBitmap = createBitmapFirst(4) 49 | fiveBitmap = adjustBitmap(fiveBitmap) 50 | bimapArray[4] = fiveBitmap 51 | var sixBitmap = createBitmapTwo(5) 52 | sixBitmap = adjustBitmap(sixBitmap) 53 | bimapArray[5] = sixBitmap 54 | 55 | withContext(Dispatchers.Main) { 56 | updateFlip() 57 | flipView?.mDoubleRealFlipView?.invalidate() 58 | } 59 | } 60 | } 61 | 62 | private fun updateFlip() { 63 | flipView?.mDoubleRealFlipView?.mLeftBottomBitmap = bimapArray[0] 64 | flipView?.mDoubleRealFlipView?.mLeftMiddleBitmap = bimapArray[1] 65 | flipView?.mDoubleRealFlipView?.mLeftTopBitmap = bimapArray[2] 66 | flipView?.mDoubleRealFlipView?.mRightTopBitmap = bimapArray[3] 67 | flipView?.mDoubleRealFlipView?.mRightMiddleBitmap = bimapArray[4] 68 | flipView?.mDoubleRealFlipView?.mRightBottomBitmap = bimapArray[5] 69 | } 70 | 71 | private fun adjustBitmap(bitmap: Bitmap): Bitmap { 72 | val curH = bitmap.height 73 | val curW = bitmap.width 74 | if(curH == 0 || curW == 0) { 75 | return bitmap 76 | } 77 | val dm: DisplayMetrics = resources.getDisplayMetrics() 78 | val height = dm.heightPixels 79 | val scaleHeight = height.toFloat() / curH 80 | val matrix = Matrix() 81 | matrix.postScale(scaleHeight, scaleHeight) 82 | return Bitmap.createBitmap(bitmap, 0, 0, curW, curH, matrix, true) 83 | } 84 | 85 | override fun onNextPage() { 86 | val cacheOne = bimapArray[0] 87 | val cacheTwo = bimapArray[1] 88 | for(i in 0..3){ 89 | bimapArray[i] = bimapArray[i + 2] 90 | } 91 | bimapArray[4] = cacheOne 92 | bimapArray[5] = cacheTwo 93 | updateFlip() 94 | flipView?.reset() 95 | flipView?.invalidate() 96 | } 97 | 98 | override fun onPrePage() { 99 | val cacheOne = bimapArray[4] 100 | val cacheTwo = bimapArray[5] 101 | for(i in 5 downTo 2){ 102 | bimapArray[i] = bimapArray[i - 2] 103 | } 104 | bimapArray[0] = cacheOne 105 | bimapArray[1] = cacheTwo 106 | updateFlip() 107 | flipView?.reset() 108 | flipView?.invalidate() 109 | } 110 | 111 | private fun createBitmapFirst(index: Int): Bitmap { 112 | val root: View = LayoutInflater.from(this).inflate(R.layout.view_album_style_one, null, false) 113 | val tvTitle = root.findViewById(R.id.tvTitle) 114 | val ivTop = root.findViewById(R.id.ivTop) 115 | val ivBottom = root.findViewById(R.id.ivBottom) 116 | when(index) { 117 | 0 -> { 118 | tvTitle.text = "来鼋头渚吧!" 119 | ivTop.setImageResource(R.drawable.ytz_b) 120 | ivBottom.setImageResource(R.drawable.ytz_p) 121 | } 122 | 2 -> { 123 | tvTitle.text = "元宵城隍庙~" 124 | ivTop.setImageResource(R.drawable.chm_j) 125 | ivBottom.setImageResource(R.drawable.chm_p) 126 | } 127 | 4 -> { 128 | tvTitle.text = "外滩建筑" 129 | ivTop.setImageResource(R.drawable.wt_p) 130 | ivBottom.setImageResource(R.drawable.wt_h) 131 | } 132 | } 133 | 134 | val margin = DeviceUtil.dip2px(this, 20f) 135 | val targetWidth = DeviceUtil.getScreenWidth(this) / 2 - margin 136 | val targetHeight = DeviceUtil.getScreenHeight(this) - margin 137 | val measureWidth = View.MeasureSpec.makeMeasureSpec(targetWidth, View.MeasureSpec.EXACTLY) 138 | val measureHeight = View.MeasureSpec.makeMeasureSpec(targetHeight, View.MeasureSpec.EXACTLY) 139 | root.measure(measureWidth, measureHeight) 140 | root.layout(0, 0, root.measuredWidth, root.measuredHeight) 141 | val bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) 142 | val canvas = Canvas(bitmap) 143 | canvas.drawColor(Color.WHITE) 144 | root.draw(canvas) 145 | return bitmap 146 | } 147 | 148 | private fun createBitmapTwo(index: Int): Bitmap { 149 | val root: View = LayoutInflater.from(this).inflate(R.layout.view_album_style_two, null, false) 150 | val tvTitle = root.findViewById(R.id.tvTitle) 151 | val ivTop = root.findViewById(R.id.ivTop) 152 | when(index) { 153 | 1 -> { 154 | tvTitle.text = "记录张园" 155 | ivTop.setImageResource(R.drawable.zy_p) 156 | } 157 | 3 -> { 158 | tvTitle.text = "唐镇随手拍" 159 | ivTop.setImageResource(R.drawable.tz_j) 160 | } 161 | 5 -> { 162 | tvTitle.text = "张江微电子四号楼" 163 | ivTop.setImageResource(R.drawable.zj_j) 164 | } 165 | } 166 | val margin = DeviceUtil.dip2px(this, 20f) 167 | val targetWidth = DeviceUtil.getScreenWidth(this) / 2 - margin 168 | val targetHeight = DeviceUtil.getScreenHeight(this) - margin 169 | val measureWidth = View.MeasureSpec.makeMeasureSpec(targetWidth, View.MeasureSpec.EXACTLY) 170 | val measureHeight = View.MeasureSpec.makeMeasureSpec(targetHeight, View.MeasureSpec.EXACTLY) 171 | root.measure(measureWidth, measureHeight) 172 | root.layout(0, 0, root.measuredWidth, root.measuredHeight) 173 | val bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888) 174 | val canvas = Canvas(bitmap) 175 | canvas.drawColor(Color.WHITE) 176 | root.draw(canvas) 177 | return bitmap 178 | } 179 | 180 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/RightBaseDirectDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import android.graphics.* 4 | import android.os.Build 5 | import com.example.myapplication.view.DeviceUtil 6 | import com.qidian.fonttest.view.TOP_SIDE 7 | import kotlin.math.* 8 | 9 | abstract class RightBaseDirectDrawAction: BaseDirectDrawAction() { 10 | 11 | override fun flipPage(): Int { 12 | return 1 13 | } 14 | 15 | override fun drawNoFlipSide( 16 | canvas: Canvas, 17 | reUsePath: Path, 18 | radius: Int 19 | ) { 20 | canvas.save() 21 | reUsePath.reset() 22 | reUsePath.moveTo(mLeftPageRBPoint.x - radius, mLeftPageLTPoint.y) 23 | reUsePath.arcTo( mLeftPageRBPoint.x - 2 * radius, mLeftPageLTPoint.y, mLeftPageRBPoint.x, mLeftPageLTPoint.y + 2 * radius, -90f, 90f, false) 24 | reUsePath.lineTo(mLeftPageRBPoint.x, mLeftPageRBPoint.y - radius) 25 | reUsePath.arcTo(mLeftPageRBPoint.x - 2 * radius, mLeftPageRBPoint.y - 2 * radius,mLeftPageRBPoint.x, mLeftPageRBPoint.y, 0f, 90f, false) 26 | reUsePath.lineTo(mLeftPageLTPoint.x, mLeftPageRBPoint.y) 27 | reUsePath.lineTo(mLeftPageLTPoint.x, mLeftPageLTPoint.y) 28 | reUsePath.close() 29 | canvas.clipPath(reUsePath) 30 | mLeftTopBitmap?.let { b-> 31 | if(!b.isRecycled) { 32 | canvas.drawBitmap(b, mLeftPageLTPoint.x, mLeftPageLTPoint.y, null) 33 | } 34 | } 35 | canvas.restore() 36 | } 37 | 38 | override fun drawFlipPageContent( 39 | canvas: Canvas, 40 | reUsePath: Path, 41 | flipPath: Path, 42 | r: Int 43 | ) { 44 | canvas.save() 45 | reUsePath.reset() 46 | reUsePath.moveTo(mRightPageLTPoint.x + r, mRightPageLTPoint.y) 47 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageLTPoint.y, mRightPageLTPoint.x + 2 * r, mRightPageLTPoint.y + 2 * r, -90f, -90f, false) 48 | reUsePath.lineTo(mRightPageLTPoint.x, mRightPageRBPoint.y - r) 49 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageRBPoint.y - 2 * r,mRightPageLTPoint.x + 2 * r, mRightPageRBPoint.y, -180f, -90f, false) 50 | reUsePath.lineTo(mRightPageRBPoint.x, mRightPageRBPoint.y) 51 | reUsePath.lineTo(mRightPageRBPoint.x, mRightPageLTPoint.y) 52 | reUsePath.close() 53 | canvas.clipPath(reUsePath) 54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 55 | canvas.clipOutPath(flipPath) 56 | } else { 57 | canvas.clipPath(flipPath, Region.Op.DIFFERENCE) 58 | } 59 | 60 | mRightTopBitmap?.let { 61 | canvas.drawBitmap(it, mRightPageLTPoint.x, mRightPageLTPoint.y, null) 62 | } 63 | 64 | canvas.restore() 65 | } 66 | 67 | override fun drawBookMiddleArea( 68 | canvas: Canvas, 69 | reUsePath: Path, 70 | r: Int, 71 | mPaint: Paint 72 | ) { 73 | reUsePath.reset() 74 | reUsePath.moveTo(mLeftPageRBPoint.x - r, mLeftPageLTPoint.y) 75 | reUsePath.arcTo( mLeftPageRBPoint.x - 2 * r, mLeftPageLTPoint.y, mLeftPageRBPoint.x, mLeftPageLTPoint.y + 2 * r, -90f, 90f, false) 76 | reUsePath.lineTo(mLeftPageRBPoint.x, mLeftPageRBPoint.y - r) 77 | reUsePath.arcTo(mLeftPageRBPoint.x - 2 * r, mLeftPageRBPoint.y - 2 * r,mLeftPageRBPoint.x, mLeftPageRBPoint.y, 0f, 90f, false) 78 | reUsePath.close() 79 | mPaint.shader = getGradient(mLeftPageRBPoint.x - r, 0f, mLeftPageRBPoint.x, 0f, shadowReverseColors) 80 | canvas.drawPath(reUsePath, mPaint) 81 | 82 | reUsePath.reset() 83 | reUsePath.moveTo(mRightPageLTPoint.x + r, mRightPageLTPoint.y) 84 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageLTPoint.y, mRightPageLTPoint.x + 2 * r, mRightPageLTPoint.y + 2 * r, -90f, -90f, false) 85 | reUsePath.lineTo(mRightPageLTPoint.x, mRightPageRBPoint.y - r) 86 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageRBPoint.y - 2 * r,mRightPageLTPoint.x + 2 * r, mRightPageRBPoint.y, -180f, -90f, false) 87 | reUsePath.close() 88 | mPaint.shader = getGradient(mRightPageLTPoint.x, 0f, mRightPageLTPoint.x + r, 0f, shadowColors) 89 | canvas.drawPath(reUsePath, mPaint) 90 | } 91 | 92 | override fun drawFlipPageBottomPageContent( 93 | canvas: Canvas, 94 | reUsePath: Path, 95 | flipPath: Path, 96 | mDegree: Double, 97 | mBezierStart1: PointF, 98 | mBezierStart2: PointF, 99 | mPaint: Paint, 100 | mTouchDis: Float, 101 | per: Float, 102 | minDis: Float 103 | ) { 104 | canvas.save() 105 | // 绘制内容部分 106 | reUsePath.reset() 107 | reUsePath.addRect(mRightPageLTPoint.x, mRightPageLTPoint.y, mRightPageRBPoint.x, mRightPageRBPoint.y, Path.Direction.CW) 108 | canvas.clipPath(reUsePath) 109 | canvas.clipPath(flipPath) 110 | mRightBottomBitmap?.let { 111 | canvas.drawBitmap(it, mRightPageLTPoint.x, mRightPageLTPoint.y, null) 112 | } 113 | // 绘制阴影 114 | val rotateDegree = if(flipSide() == TOP_SIDE) { 115 | -(90 - mDegree.toFloat()) 116 | } else { 117 | (90 - mDegree.toFloat()) 118 | } 119 | canvas.clipPath(flipPath) 120 | canvas.rotate(rotateDegree, mBezierStart1.x, mBezierStart1.y) 121 | var left = 0f 122 | var right = 0f 123 | var top = 0f 124 | var bottom = 0f 125 | val rectHeight = hypot((mBezierStart1.x - mBezierStart2.x).toDouble(), (mBezierStart1.y - mBezierStart2.y).toDouble()) 126 | left = mBezierStart1.x 127 | 128 | val minDistance = if(context != null) DeviceUtil.dip2px(context!!, 20f) else 30 129 | right = left + (minDis + max((mTouchDis / 4 - minDis) * per * 0.2f, minDistance.toFloat())) 130 | if(flipSide() == TOP_SIDE) { 131 | top = mBezierStart1.y 132 | bottom = mBezierStart1.y + rectHeight.toFloat() 133 | } else { 134 | bottom = mBezierStart1.y 135 | top = mBezierStart1.y - rectHeight.toFloat() 136 | } 137 | mPaint.shader = getGradient(left, top, right, top, shadowColors) 138 | canvas.drawRect(left, top, right, bottom, mPaint) 139 | canvas.restore() 140 | } 141 | 142 | override fun drawBackContentAndShadow( 143 | canvas: Canvas, 144 | reUsePath: Path, 145 | flipPath: Path, 146 | mBezierVertex1: PointF, 147 | mBezierVertex2: PointF, 148 | mBezierEnd2: PointF, 149 | mBezierEnd1: PointF, 150 | mCurCornerPoint: PointF, 151 | mDegree: Double, 152 | mMatrix: Matrix, 153 | mPaint: Paint, 154 | mBezierStart1: PointF, 155 | mBezierStart2: PointF, 156 | mTouchDis: Float 157 | ) { 158 | // 旋转 + 平移 159 | // 1. 限制绘制翻开白区域 160 | reUsePath.reset() 161 | reUsePath.moveTo(mBezierVertex1.x, mBezierVertex1.y) 162 | reUsePath.lineTo(mBezierVertex2.x, mBezierVertex2.y) 163 | reUsePath.lineTo(mBezierEnd2.x, mBezierEnd2.y) 164 | reUsePath.lineTo(mCurCornerPoint.x, mCurCornerPoint.y) 165 | reUsePath.lineTo(mBezierEnd1.x, mBezierEnd1.y) 166 | reUsePath.close() 167 | // 这个offset根据页面调整 168 | canvas.save() 169 | 170 | canvas.clipPath(flipPath) 171 | canvas.clipPath(reUsePath) 172 | //mPaint.colorFilter = mColorMatrixFilter 173 | canvas.drawColor(bgColor) 174 | // 2. 绘制在白色区域 175 | // 以中间的两个点为圆心,旋转 176 | var pivotX: Float 177 | val pivotY: Float 178 | var de = 180f - 2 * mDegree 179 | // 计算旋转 180 | if(flipSide() == TOP_SIDE) { 181 | pivotX = mRightPageLTPoint.x 182 | pivotY = mRightPageLTPoint.y 183 | de = -(de) 184 | } else { 185 | pivotX = mRightPageLTPoint.x 186 | pivotY = mRightPageRBPoint.y 187 | } 188 | mMatrix.setRotate(de.toFloat(), pivotX, pivotY) 189 | 190 | val originArr = floatArrayOf(0f, 0f) 191 | val mapArr = floatArrayOf(0f, 0f) 192 | if(flipSide() == TOP_SIDE) { 193 | originArr[0] = mLeftPageLTPoint.x 194 | originArr[1] = mLeftPageLTPoint.y 195 | } else { 196 | originArr[0] = mLeftPageLTPoint.x 197 | originArr[1] = mLeftPageRBPoint.y 198 | } 199 | mMatrix.mapPoints(mapArr, originArr) 200 | mMatrix.postTranslate(mCurCornerPoint.x - mapArr[0], mCurCornerPoint.y - mapArr[1]) 201 | 202 | mRightMiddleBitmap?.let { 203 | canvas.drawBitmap(it, mMatrix, null) 204 | } 205 | mPaint.colorFilter = null 206 | 207 | // 3. 设置阴影 208 | val rectHeight = hypot((mBezierStart1.x - mBezierStart2.x).toDouble(), (mBezierStart1.y - mBezierStart2.y).toDouble()) 209 | val left: Float 210 | val right: Float 211 | val bottom: Float 212 | val top: Float 213 | val rotateDegree: Float 214 | left = mBezierStart1.x - 1 215 | right = mBezierVertex1.x + 1 216 | if(flipSide() == TOP_SIDE) { 217 | top = mBezierStart1.y 218 | bottom = top + rectHeight.toFloat() 219 | rotateDegree = -(90 - mDegree.toFloat()) 220 | } else { 221 | bottom = mBezierStart1.y 222 | top = bottom - rectHeight.toFloat() 223 | rotateDegree = (90 - mDegree.toFloat()) 224 | } 225 | mPaint.shader = getGradient(left, mBezierStart1.y, right, mBezierStart1.y, shadowReverseColors) 226 | canvas.rotate(rotateDegree, mBezierStart1.x, mBezierStart1.y) 227 | //Log.d("jj", "left: $left, right: $right") 228 | canvas.drawRect(left, top, right, bottom, mPaint) 229 | canvas.restore() 230 | } 231 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/direction/LeftBaseDirectDrawAction.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.view.direction 2 | 3 | import android.graphics.* 4 | import android.os.Build 5 | import com.example.myapplication.view.DeviceUtil 6 | import com.qidian.fonttest.view.TOP_SIDE 7 | import kotlin.math.* 8 | 9 | abstract class LeftBaseDirectDrawAction: BaseDirectDrawAction() { 10 | 11 | override fun flipPage(): Int { 12 | return 0 13 | } 14 | 15 | override fun drawNoFlipSide( 16 | canvas: Canvas, 17 | reUsePath: Path, 18 | radius: Int 19 | ) { 20 | canvas.save() 21 | reUsePath.reset() 22 | reUsePath.moveTo(mRightPageLTPoint.x + radius, mRightPageLTPoint.y) 23 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageLTPoint.y, mRightPageLTPoint.x + 2 * radius, mRightPageLTPoint.y + 2 * radius, -90f, -90f, false) 24 | reUsePath.lineTo(mRightPageLTPoint.x, mRightPageRBPoint.y - radius) 25 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageRBPoint.y - 2 * radius,mRightPageLTPoint.x + 2 * radius, mRightPageRBPoint.y, -180f, -90f, false) 26 | reUsePath.lineTo(mRightPageRBPoint.x, mRightPageRBPoint.y) 27 | reUsePath.lineTo(mRightPageRBPoint.x, mRightPageLTPoint.y) 28 | reUsePath.close() 29 | canvas.clipPath(reUsePath) 30 | mRightTopBitmap?.let { b-> 31 | if(!b.isRecycled) { 32 | canvas.drawBitmap(b, mRightPageLTPoint.x, mRightPageLTPoint.y, null) 33 | } 34 | } 35 | canvas.restore() 36 | } 37 | 38 | override fun drawFlipPageContent( 39 | canvas: Canvas, 40 | reUsePath: Path, 41 | flipPath: Path, 42 | r: Int 43 | ) { 44 | canvas.save() 45 | reUsePath.reset() 46 | reUsePath.moveTo(mLeftPageRBPoint.x - r, mLeftPageLTPoint.y) 47 | reUsePath.arcTo( mLeftPageRBPoint.x - 2 * r, mLeftPageLTPoint.y, mLeftPageRBPoint.x, mLeftPageLTPoint.y + 2 * r, -90f, 90f, false) 48 | reUsePath.lineTo(mLeftPageRBPoint.x, mLeftPageRBPoint.y - r) 49 | reUsePath.arcTo(mLeftPageRBPoint.x - 2 * r, mLeftPageRBPoint.y - 2 * r,mLeftPageRBPoint.x, mLeftPageRBPoint.y, 0f, 90f, false) 50 | reUsePath.lineTo(mLeftPageLTPoint.x, mLeftPageRBPoint.y) 51 | reUsePath.lineTo(mLeftPageLTPoint.x, mLeftPageLTPoint.y) 52 | reUsePath.close() 53 | canvas.clipPath(reUsePath) 54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 55 | canvas.clipOutPath(flipPath) 56 | } else { 57 | canvas.clipPath(flipPath, Region.Op.DIFFERENCE) 58 | } 59 | mLeftTopBitmap?.let { 60 | canvas.drawBitmap(it, mLeftPageLTPoint.x, mLeftPageLTPoint.y, null) 61 | } 62 | canvas.restore() 63 | } 64 | 65 | override fun drawBookMiddleArea( 66 | canvas: Canvas, 67 | reUsePath: Path, 68 | r: Int, 69 | mPaint: Paint 70 | ) { 71 | reUsePath.reset() 72 | reUsePath.moveTo(mLeftPageRBPoint.x - r, mLeftPageLTPoint.y) 73 | reUsePath.arcTo( mLeftPageRBPoint.x - 2 * r, mLeftPageLTPoint.y, mLeftPageRBPoint.x, mLeftPageLTPoint.y + 2 * r, -90f, 90f, false) 74 | reUsePath.lineTo(mLeftPageRBPoint.x, mLeftPageRBPoint.y - r) 75 | reUsePath.arcTo(mLeftPageRBPoint.x - 2 * r, mLeftPageRBPoint.y - 2 * r,mLeftPageRBPoint.x, mLeftPageRBPoint.y, 0f, 90f, false) 76 | reUsePath.close() 77 | mPaint.shader = getGradient(mLeftPageRBPoint.x - r, 0f, mLeftPageRBPoint.x, 0f, shadowReverseColors) 78 | canvas.drawPath(reUsePath, mPaint) 79 | 80 | reUsePath.reset() 81 | reUsePath.moveTo(mRightPageLTPoint.x + r, mRightPageLTPoint.y) 82 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageLTPoint.y, mRightPageLTPoint.x + 2 * r, mRightPageLTPoint.y + 2 * r, -90f, -90f, false) 83 | reUsePath.lineTo(mRightPageLTPoint.x, mRightPageRBPoint.y - r) 84 | reUsePath.arcTo(mRightPageLTPoint.x, mRightPageRBPoint.y - 2 * r,mRightPageLTPoint.x + 2 * r, mRightPageRBPoint.y, -180f, -90f, false) 85 | reUsePath.close() 86 | mPaint.shader = getGradient(mRightPageLTPoint.x, 0f, mRightPageLTPoint.x + r, 0f, shadowColors) 87 | canvas.drawPath(reUsePath, mPaint) 88 | } 89 | 90 | override fun drawFlipPageBottomPageContent( 91 | canvas: Canvas, 92 | reUsePath: Path, 93 | flipPath: Path, 94 | mDegree: Double, 95 | mBezierStart1: PointF, 96 | mBezierStart2: PointF, 97 | mPaint: Paint, 98 | mTouchDis: Float, 99 | per: Float, 100 | minDis: Float 101 | ) { 102 | canvas.save() 103 | reUsePath.reset() 104 | reUsePath.addRect(mLeftPageLTPoint.x, mLeftPageLTPoint.y, mLeftPageRBPoint.x, mLeftPageRBPoint.y, Path.Direction.CW) 105 | canvas.clipPath(reUsePath) 106 | canvas.clipPath(flipPath) 107 | mLeftBottomBitmap?.let { 108 | canvas.drawBitmap(it, mLeftPageLTPoint.x, mLeftPageLTPoint.y, null) 109 | } 110 | // 绘制阴影 111 | val rotateDegree = if(flipSide() == TOP_SIDE) { 112 | (90 - mDegree.toFloat()) 113 | } else { 114 | -(90 - mDegree.toFloat()) 115 | } 116 | canvas.clipPath(flipPath) 117 | canvas.rotate(rotateDegree, mBezierStart1.x, mBezierStart1.y) 118 | val left: Float 119 | var top = 0f 120 | var bottom = 0f 121 | val rectHeight = hypot((mBezierStart1.x - mBezierStart2.x).toDouble(), (mBezierStart1.y - mBezierStart2.y).toDouble()) 122 | val right: Float = mBezierStart1.x 123 | 124 | val minDistance = if(context != null) DeviceUtil.dip2px(context!!, 20f) else 30 125 | left = right - (minDis + max((mTouchDis / 4 - minDis) * per * 0.2f, minDistance.toFloat())) 126 | if(flipSide() == TOP_SIDE) { 127 | top = mBezierStart1.y 128 | bottom = mBezierStart1.y + rectHeight.toFloat() 129 | } else { 130 | bottom = mBezierStart1.y 131 | top = mBezierStart1.y - rectHeight.toFloat() 132 | } 133 | mPaint.shader = getGradient(left, top, right, top, shadowReverseColors) 134 | canvas.drawRect(left, top, right, bottom, mPaint) 135 | canvas.restore() 136 | } 137 | 138 | 139 | override fun drawBackContentAndShadow( 140 | canvas: Canvas, 141 | reUsePath: Path, 142 | flipPath: Path, 143 | mBezierVertex1: PointF, 144 | mBezierVertex2: PointF, 145 | mBezierEnd2: PointF, 146 | mBezierEnd1: PointF, 147 | mCurCornerPoint: PointF, 148 | mDegree: Double, 149 | mMatrix: Matrix, 150 | mPaint: Paint, 151 | mBezierStart1: PointF, 152 | mBezierStart2: PointF, 153 | mTouchDis: Float 154 | ) { 155 | // 旋转 + 平移 156 | // 1. 限制绘制翻开白区域 157 | reUsePath.reset() 158 | reUsePath.moveTo(mBezierVertex1.x, mBezierVertex1.y) 159 | reUsePath.lineTo(mBezierVertex2.x, mBezierVertex2.y) 160 | reUsePath.lineTo(mBezierEnd2.x, mBezierEnd2.y) 161 | reUsePath.lineTo(mCurCornerPoint.x, mCurCornerPoint.y) 162 | reUsePath.lineTo(mBezierEnd1.x, mBezierEnd1.y) 163 | // flip 0 特有 164 | reUsePath.offset(-mRightPageLTPoint.x, 0f) 165 | reUsePath.close() 166 | // 这个offset根据页面调整 167 | canvas.save() 168 | // flip 0 特有 169 | canvas.translate(mRightPageLTPoint.x, mRightPageLTPoint.y) 170 | flipPath.offset(-mRightPageLTPoint.x, 0f) 171 | 172 | canvas.clipPath(flipPath) 173 | canvas.clipPath(reUsePath) 174 | //mPaint.colorFilter = mColorMatrixFilter 175 | canvas.drawColor(bgColor) 176 | // 2. 绘制在白色区域 177 | // 以中间的两个点为圆心,旋转 178 | var pivotX: Float 179 | val pivotY: Float 180 | var de = 180f - 2 * mDegree 181 | // 计算旋转 182 | if(flipSide() == TOP_SIDE) { 183 | pivotX = mRightPageLTPoint.x 184 | pivotY = mRightPageLTPoint.y 185 | pivotX -= mRightPageLTPoint.x 186 | } else { 187 | pivotX = mRightPageLTPoint.x 188 | pivotY = mRightPageRBPoint.y 189 | //mMatrix 190 | de = -de 191 | pivotX -= mRightPageLTPoint.x 192 | } 193 | mMatrix.setRotate(de.toFloat(), pivotX, pivotY) 194 | 195 | val originArr = floatArrayOf(0f, 0f) 196 | val mapArr = floatArrayOf(0f, 0f) 197 | if(flipSide() == TOP_SIDE) { 198 | originArr[0] = mRightPageRBPoint.x - mRightPageLTPoint.x 199 | originArr[1] = mRightPageLTPoint.y 200 | } else { 201 | originArr[0] = mRightPageRBPoint.x - mRightPageLTPoint.x 202 | originArr[1] = mRightPageRBPoint.y 203 | } 204 | mMatrix.mapPoints(mapArr, originArr) 205 | mMatrix.postTranslate(mCurCornerPoint.x - mRightPageLTPoint.x - mapArr[0], mCurCornerPoint.y - mapArr[1]) 206 | mLeftMiddleBitmap?.let { 207 | canvas.drawBitmap(it, mMatrix, null) 208 | } 209 | mPaint.colorFilter = null 210 | 211 | // 3. 绘制背部的阴影 212 | canvas.translate(-mRightPageLTPoint.x, -mRightPageLTPoint.y) 213 | val rectHeight = hypot((mBezierStart1.x - mBezierStart2.x).toDouble(), (mBezierStart1.y - mBezierStart2.y).toDouble()) 214 | val left: Float 215 | val right: Float 216 | val bottom: Float 217 | val top: Float 218 | val rotateDegree: Float 219 | // 绘制在BezierControl 220 | left = mBezierVertex1.x - 1 221 | right = mBezierStart1.x + 1 222 | if(flipSide() == TOP_SIDE) { 223 | top = mBezierStart1.y 224 | bottom = top + rectHeight.toFloat() 225 | rotateDegree = (90 - mDegree.toFloat()) 226 | } else { 227 | bottom = mBezierStart1.y 228 | top = bottom - rectHeight.toFloat() 229 | rotateDegree = -(90 - mDegree.toFloat()) 230 | } 231 | mPaint.shader = getGradient(left, mBezierStart1.y, right, mBezierStart1.y, shadowColors) 232 | canvas.rotate(rotateDegree, mBezierStart1.x, mBezierStart1.y) 233 | canvas.drawRect(left, top, right, bottom, mPaint) 234 | canvas.restore() 235 | } 236 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/myapplication/view/DoubleRealFlipView.kt: -------------------------------------------------------------------------------- 1 | package com.qidian.fonttest.view 2 | 3 | import android.animation.Animator 4 | import android.animation.Animator.AnimatorListener 5 | import android.animation.TypeEvaluator 6 | import android.animation.ValueAnimator 7 | import android.content.Context 8 | import android.graphics.* 9 | import android.util.AttributeSet 10 | import android.util.Log 11 | import android.view.View 12 | import com.example.myapplication.view.DeviceUtil 13 | import com.example.myapplication.view.PageFlipListener 14 | import com.example.myapplication.view.direction.* 15 | import kotlin.math.* 16 | 17 | const val TOP_SIDE = 1 18 | const val BOTTOM_SIDE = 2 19 | const val OUT_LEN = 40f 20 | 21 | const val DIRECT_TL = 1 22 | const val DIRECT_TR = 2 23 | const val DIRECT_BL = 3 24 | const val DIRECT_BR = 4 25 | 26 | @Suppress("DEPRECATION") 27 | class DoubleRealFlipView @JvmOverloads constructor( 28 | context: Context, 29 | attrs: AttributeSet? = null, 30 | defStyleInt: Int = 0 31 | ) : View(context, attrs, defStyleInt) { 32 | // fixme 需要处理的问题 33 | // 1. 取消的时候是否需要动画 34 | // 背景色需要调整 35 | // 横屏的时候采用 36 | 37 | // 需要同时准备6页数据 可不可以优化一点? 38 | var mLeftBottomBitmap: Bitmap? = null 39 | set(value) { 40 | directDrawAction.mLeftBottomBitmap = value 41 | field = value 42 | } 43 | var mLeftMiddleBitmap: Bitmap? = null 44 | set(value) { 45 | directDrawAction.mLeftMiddleBitmap = value 46 | field = value 47 | } 48 | var mLeftTopBitmap: Bitmap? = null 49 | set(value) { 50 | directDrawAction.mLeftTopBitmap = value 51 | field = value 52 | } 53 | var mRightTopBitmap: Bitmap? = null 54 | set(value) { 55 | directDrawAction.mRightTopBitmap = value 56 | field = value 57 | } 58 | var mRightMiddleBitmap: Bitmap? = null 59 | set(value) { 60 | directDrawAction.mRightMiddleBitmap = value 61 | field = value 62 | } 63 | var mRightBottomBitmap: Bitmap? = null 64 | set(value) { 65 | directDrawAction.mRightBottomBitmap = value 66 | field = value 67 | } 68 | 69 | private val flipPath = Path() 70 | private val reUsePath = Path() 71 | private val mPaint = Paint() 72 | 73 | private val mStartPoint: PointF = PointF(-1.0f, -1.0f) 74 | // 初页脚 75 | private val mOriginalCorner = PointF() 76 | // 滑动过程中的页脚 77 | private val mCurCornerPoint: PointF = PointF(-1.0f, -1.0f) 78 | 79 | // 触摸点和页脚之间的中点 80 | private val mMiddlePoint: PointF = PointF() 81 | private val mBezierStart1 = PointF() 82 | private val mBezierControl1 = PointF() 83 | private val mBezierVertex1 = PointF() 84 | private var mBezierEnd1 = PointF() 85 | private val mBezierStart2 = PointF() 86 | private val mBezierControl2 = PointF() 87 | private val mBezierVertex2 = PointF() 88 | private var mBezierEnd2 = PointF() 89 | private val mLeftPageLTPoint = PointF() 90 | private val mLeftPageRBPoint = PointF() 91 | private val mRightPageLTPoint = PointF() 92 | private val mRightPageRBPoint = PointF() 93 | private var curWidth = 0 94 | private var curHeight = 0 95 | // 每页的宽高 96 | private var pageWidth = 0 97 | private var pageHeight = 0 98 | 99 | // 0-代表左边一页 1-右边页 100 | private var flipPage = 0 101 | private var flipSide = 0 102 | private var mDegree: Double = 0.0 103 | private var mTouchDis: Float = 0.0f 104 | private var per: Float = 1.0f 105 | 106 | var isStopScroll: Boolean = true 107 | var bgColor: Int = 0 108 | 109 | private var mColorMatrixFilter: ColorMatrixColorFilter 110 | private val mMatrix: Matrix 111 | private val r = DeviceUtil.dip2px(context, 13f) 112 | 113 | private lateinit var directDrawAction: BaseDirectDrawAction 114 | // 动画相关 115 | var pageFlipListener: PageFlipListener? = null 116 | private var curAnimator: ValueAnimator? = null 117 | 118 | init { 119 | val array = floatArrayOf(0.55f, 0f, 0f, 0f, 80.0f, 0f, 0.55f, 0f, 0f, 80.0f, 0f, 0f, 0.55f, 0f, 80.0f, 0f, 0f, 0f, 0.2f, 0f) 120 | val cm = ColorMatrix() 121 | cm.set(array) 122 | mColorMatrixFilter = ColorMatrixColorFilter(cm) 123 | mMatrix = Matrix() 124 | initDirectAction(DIRECT_TL) 125 | } 126 | 127 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 128 | super.onSizeChanged(w, h, oldw, oldh) 129 | 130 | if (w == 0 || h == 0) { 131 | return 132 | } 133 | 134 | curWidth = w 135 | curHeight = h 136 | pageWidth = w / 2 137 | pageHeight = h 138 | mLeftPageLTPoint.x = 0f 139 | mLeftPageLTPoint.y = 0f 140 | calculatePagePoint() 141 | } 142 | 143 | 144 | private fun calculatePagePoint() { 145 | mLeftPageRBPoint.x = mLeftPageLTPoint.x + pageWidth 146 | mLeftPageRBPoint.y = mLeftPageRBPoint.y + pageHeight 147 | mRightPageLTPoint.x = mLeftPageLTPoint.x + pageWidth 148 | mRightPageLTPoint.y = mLeftPageLTPoint.y 149 | mRightPageRBPoint.x = mRightPageLTPoint.x + pageWidth 150 | mRightPageRBPoint.y = mLeftPageRBPoint.y 151 | directDrawAction.mLeftPageLTPoint = mLeftPageLTPoint 152 | directDrawAction.mLeftPageRBPoint = mLeftPageRBPoint 153 | directDrawAction.mRightPageLTPoint = mRightPageLTPoint 154 | directDrawAction.mRightPageRBPoint = mRightPageRBPoint 155 | directDrawAction.context = context 156 | } 157 | 158 | private fun isOnOriginPoint(): Boolean { 159 | return (mCurCornerPoint.x == mOriginalCorner.x) 160 | } 161 | 162 | override fun onDraw(canvas: Canvas) { 163 | super.onDraw(canvas) 164 | 165 | if (isStopScroll || isOnOriginPoint()) { 166 | resetPoint() 167 | Log.d("wangjie", "resetPoint") 168 | } else { 169 | calculatePoint() 170 | } 171 | 172 | // 1. 绘制非翻页的一边 173 | drawNoFlipSide(canvas) 174 | // 2. 绘制翻页的一边 175 | drawFlipPageContent(canvas) 176 | // 3. 绘制中间区域和阴影 177 | drawBookMiddleArea(canvas) 178 | // 4. 绘制翻页下面一页露出的区域和阴影 179 | drawFlipPageBottomPageContent(canvas) 180 | // 5. 绘制翻页的两侧阴影 181 | drawTwoSideShadow(canvas) 182 | // 6. 绘制翻页的背部内容和阴影 183 | drawBackContentAndShadow(canvas) 184 | 185 | /*val paint = Paint() 186 | paint.setColor(Color.BLACK) 187 | canvas.drawLine(mBezierControl1.x, mBezierControl1.y, mBezierControl2.x, mBezierControl2.y, paint) 188 | paint.strokeWidth = 10f 189 | canvas.drawPoint(mBezierStart1.x, mBezierStart1.y, paint) 190 | canvas.drawPoint(mBezierStart2.x, mBezierStart2.y, paint)*/ 191 | } 192 | 193 | private fun drawBackContentAndShadow(canvas: Canvas){ 194 | // 旋转 + 平移 195 | // 1. 限制绘制翻开白区域 196 | directDrawAction.drawBackContentAndShadow(canvas, reUsePath, flipPath, mBezierVertex1, mBezierVertex2, mBezierEnd2, mBezierEnd1, mCurCornerPoint, mDegree, mMatrix, mPaint, mBezierStart1, mBezierStart2, mTouchDis) 197 | } 198 | 199 | private fun drawTwoSideShadow(canvas: Canvas) { 200 | directDrawAction.drawTwoSideShadow(canvas, reUsePath, flipPath, mDegree, mCurCornerPoint, mBezierControl1, mBezierStart1, mBezierControl2, mBezierStart2, mOriginalCorner, mPaint) 201 | } 202 | 203 | private fun drawFlipPageBottomPageContent(canvas: Canvas) { 204 | directDrawAction.drawFlipPageBottomPageContent(canvas, reUsePath, flipPath, mDegree, mBezierStart1, mBezierStart2, mPaint, mTouchDis, per, abs(mBezierStart1.x - mBezierControl1.x)) 205 | } 206 | 207 | private fun drawFlipPageContent(canvas: Canvas) { 208 | directDrawAction.drawFlipPageContent(canvas, reUsePath, flipPath, r) 209 | } 210 | 211 | private fun drawBookMiddleArea(canvas: Canvas){ 212 | directDrawAction.drawBookMiddleArea(canvas, reUsePath, r, mPaint) 213 | } 214 | 215 | private fun drawNoFlipSide(canvas: Canvas) { 216 | directDrawAction.drawNoFlipSide(canvas, reUsePath, r) 217 | } 218 | 219 | private fun calculatePointInNormal() { 220 | mMiddlePoint.x = (mCurCornerPoint.x + mOriginalCorner.x) / 2 221 | mMiddlePoint.y = (mCurCornerPoint.y + mOriginalCorner.y) / 2 222 | mBezierControl1.x = 223 | mMiddlePoint.x - (mOriginalCorner.y - mMiddlePoint.y) * (mOriginalCorner.y - mMiddlePoint.y) / (mOriginalCorner.x - mMiddlePoint.x) 224 | if(flipPage == 0) { 225 | mBezierControl1.x = min(mBezierControl1.x, mLeftPageRBPoint.x) 226 | } else { 227 | mBezierControl1.x = max(mBezierControl1.x, mRightPageLTPoint.x) 228 | } 229 | mBezierControl1.y = mOriginalCorner.y 230 | mBezierControl2.x = mOriginalCorner.x 231 | mBezierControl2.y = 232 | mMiddlePoint.y - (mOriginalCorner.x - mMiddlePoint.x) * (mOriginalCorner.x - mMiddlePoint.x) / (mOriginalCorner.y - mMiddlePoint.y) 233 | mBezierStart1.x = mBezierControl1.x - (mOriginalCorner.x - mBezierControl1.x) / 2 234 | if(flipPage == 0) { 235 | mBezierStart1.x = min(mBezierStart1.x, mLeftPageRBPoint.x) 236 | } else { 237 | mBezierStart1.x = max(mBezierStart1.x, mRightPageLTPoint.x) 238 | } 239 | 240 | mBezierStart1.y = mOriginalCorner.y 241 | // 和 Start1 等比例 242 | mBezierStart2.x = mOriginalCorner.x 243 | mBezierStart2.y = 244 | mBezierControl2.y - abs((mBezierControl1.x - mBezierStart1.x) / (mOriginalCorner.x - mBezierControl1.x)) * (mOriginalCorner.y - mBezierControl2.y) 245 | mBezierEnd1 = getCross(mCurCornerPoint, mBezierControl1, mBezierStart1, mBezierStart2) 246 | mBezierEnd2 = getCross(mCurCornerPoint, mBezierControl2, mBezierStart1, mBezierStart2) 247 | //Log.d("jj", "mBezierStart1:$mBezierStart1, mBezierControl1: $mBezierControl1, mBezierEnd1: $mBezierEnd1, mCurCornerPoint: $mCurCornerPoint") 248 | mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 249 | mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 250 | mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 251 | mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4 252 | 253 | 254 | mDegree = Math.toDegrees(atan2((abs(mOriginalCorner.y - mBezierControl2.y)).toDouble(), (abs(mOriginalCorner.x - mBezierControl1.x)).toDouble())) 255 | mTouchDis = hypot((mCurCornerPoint.x - mOriginalCorner.x).toDouble(), (mCurCornerPoint.y - mOriginalCorner.y).toDouble()).toFloat() 256 | } 257 | 258 | private fun calculatePoint() { 259 | calculatePointInNormal() 260 | /*Log.d("jj", "mBezierStart1:$mBezierStart1, mBezierControl1: $mBezierControl1, mBezierEnd1: $mBezierEnd1, mCurCornerPoint: $mCurCornerPoint, mBezierVertex1: $mBezierVertex1") 261 | Log.d("jj", "mBezierStart2:$mBezierStart2, mBezierControl2: $mBezierControl2, mBezierEnd2: $mBezierEnd2, mOriginalCorner: $mOriginalCorner, mBezierVertex2: $mBezierVertex2")*/ 262 | flipPath.reset() 263 | flipPath.moveTo(mBezierStart1.x, mBezierStart1.y) 264 | flipPath.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, mBezierEnd1.y) 265 | flipPath.lineTo(mCurCornerPoint.x, mCurCornerPoint.y) 266 | flipPath.lineTo(mBezierEnd2.x, mBezierEnd2.y) 267 | flipPath.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y) 268 | flipPath.lineTo(mOriginalCorner.x, mOriginalCorner.y) 269 | flipPath.close() 270 | } 271 | 272 | private fun resetPoint() { 273 | mCurCornerPoint.x = 0f 274 | mCurCornerPoint.y = 0f 275 | flipPage = 0 276 | mBezierControl1.x = 0f 277 | mBezierControl1.y = 0f 278 | mBezierControl2.x = 0f 279 | mBezierControl2.y = 0f 280 | mBezierStart1.x = 0f 281 | mBezierStart1.y = mOriginalCorner.y 282 | mBezierStart2.x = mOriginalCorner.x 283 | mBezierStart2.y = mBezierControl2.y - (mOriginalCorner.y - mBezierControl2.y) / 2 284 | mBezierEnd1.x = mOriginalCorner.x 285 | mBezierEnd1.y = mOriginalCorner.y 286 | mBezierEnd2.x = mOriginalCorner.x 287 | mBezierEnd2.y = mOriginalCorner.y 288 | mBezierVertex1.x = mOriginalCorner.x 289 | mBezierVertex1.y = mOriginalCorner.y 290 | mBezierVertex2.x = mOriginalCorner.x 291 | mBezierVertex2.y = mOriginalCorner.y 292 | //Log.d("jj", "reset mBezierStart1:$mBezierStart1, mBezierControl1: $mBezierControl1, mBezierEnd1: $mBezierEnd1, mCurCornerPoint: $mCurCornerPoint") 293 | flipPath.reset() 294 | } 295 | 296 | /** 297 | * 求线P1P2和线P3P4的焦点 298 | */ 299 | private fun getCross(P1: PointF, P2: PointF, P3: PointF, P4: PointF): PointF { 300 | val crossPoint = PointF() 301 | // 二元函数通式: y=ax+b 302 | val a1 = (P2.y - P1.y) / (P2.x - P1.x) 303 | val b1 = (P1.x * P2.y - P2.x * P1.y) / (P1.x - P2.x) 304 | val a2 = (P4.y - P3.y) / (P4.x - P3.x) 305 | val b2 = (P3.x * P4.y - P4.x * P3.y) / (P3.x - P4.x) 306 | crossPoint.x = (b2 - b1) / (a1 - a2) 307 | crossPoint.y = a1 * crossPoint.x + b1 308 | return crossPoint 309 | } 310 | 311 | /** 312 | * 手指落下的位置 313 | */ 314 | fun prepareOnDown(x: Float, y: Float) { 315 | if(x < mLeftPageLTPoint.x || x > mRightPageRBPoint.x || y < mLeftPageLTPoint.y || y > mLeftPageRBPoint.y) { 316 | return 317 | } 318 | 319 | mStartPoint.x = x 320 | mStartPoint.y = y 321 | } 322 | 323 | fun prePareDirection(direct: Int) { 324 | // 确定方向 325 | when(direct) { 326 | DIRECT_TL -> { 327 | flipPage = 1 328 | flipSide = BOTTOM_SIDE 329 | initDirectAction(DIRECT_TL) 330 | } 331 | DIRECT_TR -> { 332 | flipPage = 0 333 | flipSide = BOTTOM_SIDE 334 | initDirectAction(DIRECT_TR) 335 | } 336 | DIRECT_BL -> { 337 | flipPage = 1 338 | flipSide = TOP_SIDE 339 | initDirectAction(DIRECT_BL) 340 | } 341 | DIRECT_BR -> { 342 | flipPage = 0 343 | flipSide = TOP_SIDE 344 | initDirectAction(DIRECT_BR) 345 | } 346 | } 347 | initCornerPoint() 348 | } 349 | 350 | private fun initDirectAction(direct: Int) { 351 | when(direct) { 352 | DIRECT_TL -> { 353 | directDrawAction = RBDrawAction() 354 | } 355 | DIRECT_TR -> { 356 | directDrawAction = LBDrawAction() 357 | } 358 | DIRECT_BL -> { 359 | directDrawAction = RTDrawAction() 360 | } 361 | DIRECT_BR -> { 362 | directDrawAction = LTDrawAction() 363 | } 364 | } 365 | directDrawAction.mLeftTopBitmap = mLeftTopBitmap 366 | directDrawAction.mLeftMiddleBitmap = mLeftMiddleBitmap 367 | directDrawAction.mLeftBottomBitmap = mLeftBottomBitmap 368 | directDrawAction.mRightTopBitmap = mRightTopBitmap 369 | directDrawAction.mRightMiddleBitmap = mRightMiddleBitmap 370 | directDrawAction.mRightBottomBitmap = mRightBottomBitmap 371 | directDrawAction.mLeftPageLTPoint = mLeftPageLTPoint 372 | directDrawAction.mLeftPageRBPoint = mLeftPageRBPoint 373 | directDrawAction.mRightPageLTPoint = mRightPageLTPoint 374 | directDrawAction.mRightPageRBPoint = mRightPageRBPoint 375 | directDrawAction.context = context 376 | } 377 | 378 | private fun covertTouchPointToCurCornerPoint(x: Float, y: Float) { 379 | if(x == mOriginalCorner.x) { 380 | return 381 | } 382 | 383 | var targetTouchY = y 384 | // 1. 方向是否发生变更 385 | var offsetY = y - mStartPoint.y 386 | if(offsetY > 0) { 387 | if(flipSide == BOTTOM_SIDE) { 388 | flipSide = TOP_SIDE 389 | if(flipPage == 0) { 390 | initDirectAction(DIRECT_BR) 391 | } else { 392 | initDirectAction(DIRECT_BL) 393 | } 394 | initCornerPoint() 395 | } 396 | } else if(offsetY < 0) { 397 | if(flipSide == TOP_SIDE) { 398 | flipSide = BOTTOM_SIDE 399 | if(flipPage == 0) { 400 | initDirectAction(DIRECT_TR) 401 | } else { 402 | initDirectAction(DIRECT_TL) 403 | } 404 | initCornerPoint() 405 | } 406 | } else { 407 | // 如果 offsetY == 0 408 | offsetY = if(flipSide == TOP_SIDE){ 409 | 1f 410 | } else { 411 | -1f 412 | } 413 | } 414 | // 最大极限情况下求坐标 415 | val len = hypot(abs(mStartPoint.y - mOriginalCorner.y), pageWidth.toFloat()) 416 | val maxYDis = sqrt(len * len - (x - mRightPageLTPoint.x) * (x - mRightPageLTPoint.x)) 417 | var maxY = maxYDis 418 | if(flipSide == BOTTOM_SIDE) { 419 | maxY = mRightPageRBPoint.y - maxY 420 | } 421 | // 中间点坐标 422 | val midX: Float = if(flipPage == 0){ 423 | (mLeftPageLTPoint.x + x) / 2 424 | } else { 425 | (mRightPageRBPoint.x + x) / 2 426 | } 427 | val midY = (maxY + mStartPoint.y) / 2 428 | // 开始点 429 | val startX: Float 430 | val startY: Float 431 | if(flipSide == TOP_SIDE) { 432 | startX = mLeftPageRBPoint.x 433 | startY = mLeftPageLTPoint.y 434 | } else { 435 | startX = mLeftPageRBPoint.x 436 | startY = mLeftPageRBPoint.y 437 | } 438 | // 计算页脚能够翻转的最大角度 439 | val maxDegree = Math.toDegrees(atan2(abs(midX - startX).toDouble(), abs(midY - startY).toDouble())) * 2 440 | offsetY = abs(offsetY) 441 | if(offsetY >= abs(maxY - mStartPoint.y)) { 442 | offsetY = abs(maxY - mStartPoint.y) 443 | } 444 | per = 1 - offsetY / abs(maxY - mStartPoint.y) 445 | val perDe = (offsetY / abs(maxY - mStartPoint.y)) * maxDegree 446 | if(flipSide == BOTTOM_SIDE) { 447 | if(y < maxY) { 448 | targetTouchY = maxY 449 | } 450 | } else { 451 | if(y > maxY) { 452 | targetTouchY = maxY 453 | } 454 | } 455 | // 转动的半径 456 | val r = abs(mStartPoint.y - mOriginalCorner.y) 457 | val rad = Math.toRadians(perDe) 458 | // 计算边角的坐标 459 | if(flipPage == 0) { 460 | mCurCornerPoint.x = (x + r * sin(rad)).toFloat() 461 | } else { 462 | mCurCornerPoint.x = (x - r * sin(rad)).toFloat() 463 | } 464 | if(flipSide == TOP_SIDE) { 465 | mCurCornerPoint.y = (targetTouchY - r * cos(rad)).toFloat() 466 | } else { 467 | mCurCornerPoint.y = (targetTouchY + r * cos(rad)).toFloat() 468 | } 469 | } 470 | 471 | // 设置的页脚的顶点 472 | fun setTouchPoint(x: Float, y: Float) { 473 | val targetX = min(max(mLeftPageLTPoint.x, x), mRightPageRBPoint.x) 474 | val targetY = min(max(mLeftPageLTPoint.y, y), mRightPageRBPoint.y) 475 | covertTouchPointToCurCornerPoint(targetX, targetY) 476 | } 477 | 478 | 479 | fun resetScrollTag() { 480 | isStopScroll = true 481 | } 482 | 483 | private fun initCornerPoint() { 484 | if(flipPage == 0) { 485 | if(flipSide == TOP_SIDE) { 486 | mOriginalCorner.x = mLeftPageLTPoint.x 487 | mOriginalCorner.y = mLeftPageLTPoint.y 488 | } else { 489 | mOriginalCorner.x = mLeftPageLTPoint.x 490 | mOriginalCorner.y = mLeftPageRBPoint.y 491 | } 492 | } else { 493 | if(flipSide == TOP_SIDE) { 494 | mOriginalCorner.x = mRightPageRBPoint.x 495 | mOriginalCorner.y = mRightPageLTPoint.y 496 | } else { 497 | mOriginalCorner.x = mRightPageRBPoint.x 498 | mOriginalCorner.y = mRightPageRBPoint.y 499 | } 500 | } 501 | isStopScroll = false 502 | } 503 | 504 | fun cancelAnimation(){ 505 | curAnimator?.apply { 506 | if(isRunning) { 507 | cancel() 508 | } 509 | } 510 | } 511 | 512 | fun onTap(x: Float) { 513 | val tapAreaWidth = pageWidth / 3 514 | when(x) { 515 | in mLeftPageLTPoint.x .. (mLeftPageLTPoint.x + tapAreaWidth) -> { 516 | animPrePage() 517 | } 518 | in (mRightPageRBPoint.x - tapAreaWidth) .. mRightPageRBPoint.x -> { 519 | animNextPage() 520 | } 521 | } 522 | } 523 | 524 | private fun animPrePage(isFullAnim: Boolean = true, touchX: Float = 0f, touchY: Float = 0f) { 525 | cancelAnimation() 526 | 527 | var startTouchY = (mLeftPageLTPoint.y + mRightPageRBPoint.y) / 2 528 | var startTouchX = mLeftPageLTPoint.x + 50 529 | var endTouchY = (mLeftPageLTPoint.y + mRightPageRBPoint.y) / 2 530 | val endTouchX = mRightPageRBPoint.x - 20 531 | if(isFullAnim) { 532 | prepareOnDown(mLeftPageLTPoint.x, endTouchY) 533 | endTouchY += 6 534 | } else { 535 | startTouchX = touchX 536 | startTouchY = touchY 537 | endTouchY = mStartPoint.y 538 | } 539 | prePareDirection(DIRECT_TR) 540 | val animator = ValueAnimator.ofObject(PointTypeEvaluator(), PointF(startTouchX, startTouchY), PointF(endTouchX, endTouchY)) 541 | animator.addUpdateListener { 542 | val point = it.animatedValue as PointF 543 | // 更新坐标 544 | setTouchPoint(point.x, point.y) 545 | invalidate() 546 | } 547 | animator.addListener(object : AnimatorListener{ 548 | override fun onAnimationStart(animation: Animator?) { 549 | } 550 | 551 | override fun onAnimationEnd(animation: Animator?) { 552 | pageFlipListener?.onPrePage() 553 | } 554 | 555 | override fun onAnimationCancel(animation: Animator?) { 556 | } 557 | 558 | override fun onAnimationRepeat(animation: Animator?) { 559 | } 560 | }) 561 | animator.duration = 200 562 | animator.start() 563 | curAnimator = animator 564 | } 565 | 566 | private fun animNextPage(isFullAnim: Boolean = true, touchX: Float = 0f, touchY: Float = 0f) { 567 | cancelAnimation() 568 | var starTouchY = (mLeftPageLTPoint.y + mRightPageRBPoint.y) / 2 569 | var startTouchX = mRightPageRBPoint.x - 50 570 | var endTouchY = (mLeftPageLTPoint.y + mRightPageRBPoint.y) / 2 571 | val endTouchX = mLeftPageLTPoint.x + 20 572 | if(isFullAnim) { 573 | prepareOnDown(mRightPageRBPoint.x, endTouchY) 574 | endTouchY += 6 575 | } else { 576 | startTouchX = touchX 577 | starTouchY = touchY 578 | endTouchY = mStartPoint.y 579 | } 580 | prePareDirection(DIRECT_TL) 581 | val animator = ValueAnimator.ofObject(PointTypeEvaluator(), PointF(startTouchX, starTouchY), PointF(endTouchX , endTouchY)) 582 | animator.addUpdateListener { 583 | val point = it.animatedValue as PointF 584 | // 更新坐标 585 | setTouchPoint(point.x, point.y) 586 | invalidate() 587 | } 588 | animator.addListener(object : AnimatorListener{ 589 | override fun onAnimationStart(animation: Animator?) { 590 | } 591 | 592 | override fun onAnimationEnd(animation: Animator?) { 593 | pageFlipListener?.onNextPage() 594 | } 595 | 596 | override fun onAnimationCancel(animation: Animator?) { 597 | } 598 | 599 | override fun onAnimationRepeat(animation: Animator?) { 600 | } 601 | }) 602 | animator.duration = 200 603 | animator.start() 604 | curAnimator = animator 605 | } 606 | 607 | fun release(x: Float, y: Float): Boolean { 608 | when(x) { 609 | in mLeftPageLTPoint.x .. (mLeftPageLTPoint.x + 100) -> { 610 | if(flipPage == 1) { 611 | pageFlipListener?.onNextPage() 612 | return true 613 | } 614 | } 615 | in mLeftPageLTPoint.x + 100 .. mLeftPageRBPoint.x -> { 616 | if(flipPage == 1) { 617 | animNextPage(false, x, y) 618 | return true 619 | } 620 | } 621 | in mRightPageLTPoint.x .. (mRightPageRBPoint.x - 100) -> { 622 | if(flipPage == 0) { 623 | animPrePage(false, x, y) 624 | return true 625 | } 626 | } 627 | in (mRightPageLTPoint.x - 100) .. mRightPageRBPoint.x -> { 628 | if(flipPage == 0) { 629 | pageFlipListener?.onPrePage() 630 | return true 631 | } 632 | } 633 | } 634 | return false 635 | } 636 | 637 | class PointTypeEvaluator: TypeEvaluator { 638 | private val targetPoint = PointF() 639 | 640 | override fun evaluate(fraction: Float, startValue: PointF, endValue: PointF): PointF { 641 | val xOffset = endValue.x - startValue.x 642 | val yOffset = endValue.y - startValue.y 643 | targetPoint.x = startValue.x + xOffset * fraction 644 | targetPoint.y = startValue.y + yOffset * fraction 645 | return targetPoint 646 | } 647 | } 648 | } --------------------------------------------------------------------------------