├── 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 | }
--------------------------------------------------------------------------------