├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── attr.xml
│ │ │ ├── dimens.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── drawable-xxhdpi
│ │ │ ├── icon.png
│ │ │ ├── ic_add.png
│ │ │ ├── ic_face.png
│ │ │ ├── ic_voice.png
│ │ │ ├── ic_keyboard.png
│ │ │ └── ic_voice_dark.png
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── drawable
│ │ │ ├── bg_trans_oval.xml
│ │ │ ├── bg_trans_oval_white.xml
│ │ │ ├── bg_white_round.xml
│ │ │ ├── bg_white_round_on_focus.xml
│ │ │ ├── bg_window_drawable.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── layout
│ │ │ ├── activity_send_voice.xml
│ │ │ └── view_voice.xml
│ │ ├── java
│ │ └── com
│ │ │ └── yocn
│ │ │ └── af
│ │ │ ├── MApplication.java
│ │ │ ├── view
│ │ │ ├── activity
│ │ │ │ ├── WeChatSendVoiceActivity.java
│ │ │ │ └── BaseActivity.java
│ │ │ └── widget
│ │ │ │ ├── WeChatVoiceTextView.java
│ │ │ │ ├── WeChatParentViewGroup.java
│ │ │ │ ├── WeChatVoiceBottomArc.java
│ │ │ │ ├── WeChatVoiceView.java
│ │ │ │ └── WeChatVoiceBubble.java
│ │ │ └── util
│ │ │ ├── LogUtil.java
│ │ │ └── DisplayUtil.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gif
└── readme.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── README.md
├── .gitignore
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /.idea
3 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/gif/readme.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/gif/readme.gif
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | AndroidFeature
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/drawable-xxhdpi/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/drawable-xxhdpi/ic_add.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_face.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/drawable-xxhdpi/ic_face.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_voice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/drawable-xxhdpi/ic_voice.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_keyboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/drawable-xxhdpi/ic_keyboard.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_voice_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/drawable-xxhdpi/ic_voice_dark.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yocn/WeChatSendVoice/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeChatSendVoice
2 |
3 | 详细文章可以查看 https://www.jianshu.com/p/5b9cbcba68f2
4 |
5 | 模拟微信发送语音view
6 |
7 | 仿微信发送语音 | 自定义view | 事件分发
8 |
9 | 
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_trans_oval.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_trans_oval_white.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches/build_file_checksums.ser
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea
9 | .DS_Store
10 | /build
11 | /captures
12 | .externalNativeBuild
13 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_white_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attr.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_white_round_on_focus.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 180dp
5 | 170dp
6 | 140dp
7 | 70dp
8 | 18dp
9 | 20dp
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_window_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/MApplication.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af;
2 |
3 | import android.app.Application;
4 |
5 | public class MApplication extends Application {
6 |
7 | private static MApplication mApplication;
8 |
9 | @Override
10 | public void onCreate() {
11 | super.onCreate();
12 | mApplication = this;
13 | }
14 |
15 | public static MApplication getApp() {
16 | return mApplication;
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/activity/WeChatSendVoiceActivity.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.activity;
2 |
3 | import android.os.Bundle;
4 | import android.widget.LinearLayout;
5 |
6 | import com.yocn.af.R;
7 |
8 | public class WeChatSendVoiceActivity extends BaseActivity {
9 |
10 | private LinearLayout optionLl;
11 |
12 | protected void useWindowParams() {
13 | }
14 |
15 | @Override
16 | protected void onCreate(Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | setContentView(R.layout.activity_send_voice);
19 | optionLl = findViewById(R.id.ll_option);
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/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=-Xmx1536m
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 |
15 |
--------------------------------------------------------------------------------
/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
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 | #FFFFFF
7 | #000000
8 | #aa000000
9 | #7a000000
10 | #00000000
11 |
12 | #EDEDED
13 |
14 | #8FBC8F
15 | #FAEBD7
16 | #F08080
17 | #7FFFD4
18 | #F0FFFF
19 | #F5F5DC
20 | #6495ED
21 | #ADFF2F
22 | #FF69B4
23 |
24 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 28
5 | defaultConfig {
6 | applicationId "com.yocn.af"
7 | minSdkVersion 25
8 | targetSdkVersion 28
9 | versionCode 1
10 | versionName "1.0"
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
16 | }
17 | }
18 | compileOptions {
19 | targetCompatibility 1.8
20 | sourceCompatibility 1.8
21 | }
22 | }
23 |
24 | dependencies {
25 | implementation fileTree(dir: 'libs', include: ['*.jar'])
26 | implementation 'com.github.bumptech.glide:glide:4.11.0'
27 | implementation 'jp.wasabeef:glide-transformations:4.1.0'
28 | implementation 'androidx.appcompat:appcompat:1.2.0'
29 | implementation 'androidx.recyclerview:recyclerview:1.2.0'
30 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/util/LogUtil.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.util;
2 |
3 | import android.os.SystemClock;
4 | import android.util.Log;
5 |
6 | /**
7 | * @Author yocn
8 | * @Date 2019/8/2 11:05 AM
9 | * @ClassName LogUtil
10 | */
11 | public class LogUtil {
12 | private static final String TAG = LogUtil.class.getSimpleName();
13 |
14 | public static void d(String msg) {
15 | Log.d(TAG, msg);
16 | }
17 |
18 | private static long lastTS = 0;
19 |
20 | public static void logWithInterval(String msg) {
21 | if (SystemClock.elapsedRealtime() - lastTS > 1000) {
22 | lastTS = SystemClock.elapsedRealtime();
23 | Log.d(TAG, msg);
24 | }
25 | }
26 |
27 | public static void v(String msg) {
28 | Log.d(TAG, msg);
29 | }
30 |
31 | public static void v(String... msg) {
32 | StringBuilder stringBuilder = new StringBuilder();
33 | for (String s : msg) {
34 | stringBuilder.append(s);
35 | }
36 | Log.d(TAG, stringBuilder.toString());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
20 |
21 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/activity/BaseActivity.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.activity;
2 |
3 | import android.graphics.Color;
4 | import android.os.Bundle;
5 | import android.view.View;
6 | import android.view.Window;
7 | import android.view.WindowManager;
8 |
9 | import androidx.appcompat.app.AppCompatActivity;
10 |
11 | /**
12 | * @Author yocn
13 | * @Date 2019/9/23 4:12 PM
14 | * @ClassName BaseActivity
15 | */
16 | public class BaseActivity extends AppCompatActivity {
17 | @Override
18 | protected void onCreate(Bundle savedInstanceState) {
19 | useWindowParams();
20 | super.onCreate(savedInstanceState);
21 | }
22 |
23 | protected void useWindowParams() {
24 | getWindow().requestFeature(Window.FEATURE_NO_TITLE);
25 | Window window = getWindow();
26 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
27 | | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
28 | window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
29 | // | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
30 | | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
31 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
32 | window.setStatusBarColor(Color.TRANSPARENT);
33 | window.setNavigationBarColor(Color.TRANSPARENT);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/widget/WeChatVoiceTextView.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.widget;
2 |
3 | import android.content.Context;
4 | import android.os.Vibrator;
5 | import android.util.AttributeSet;
6 | import android.view.MotionEvent;
7 |
8 | import androidx.annotation.NonNull;
9 | import androidx.annotation.Nullable;
10 | import androidx.appcompat.widget.AppCompatTextView;
11 |
12 | public class WeChatVoiceTextView extends AppCompatTextView {
13 | private String TAG = "WeChatVoiceTextView";
14 | private WeChatParentViewGroup.OnVoiceViewStatusListener onVoiceViewStatusListener;
15 |
16 | public WeChatVoiceTextView(@NonNull Context context) {
17 | super(context);
18 | }
19 |
20 | public WeChatVoiceTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
21 | super(context, attrs);
22 | }
23 |
24 | public WeChatVoiceTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
25 | super(context, attrs, defStyleAttr);
26 | }
27 |
28 | public void setListener(WeChatParentViewGroup.OnVoiceViewStatusListener onVoiceViewStatusListener) {
29 | this.onVoiceViewStatusListener = onVoiceViewStatusListener;
30 | }
31 |
32 | @Override
33 | public boolean dispatchTouchEvent(MotionEvent ev) {
34 | boolean result = super.dispatchTouchEvent(ev);
35 | // LogUtil.d(TAG + "::dispatchTouchEvent:::" + result + " " + ViewUtil.printEvent(ev));
36 | if (ev.getAction() == MotionEvent.ACTION_DOWN) {
37 | vibrator(getContext());
38 | onVoiceViewStatusListener.showVoiceView();
39 | setText("松开 结束");
40 | }
41 | if (ev.getAction() == MotionEvent.ACTION_CANCEL || ev.getAction() == MotionEvent.ACTION_UP) {
42 | setText("按住 说话");
43 | }
44 | return true;
45 | }
46 |
47 | @Override
48 | public boolean onTouchEvent(MotionEvent event) {
49 | boolean result = super.onTouchEvent(event);
50 | // LogUtil.d(TAG + "::onTouchEvent::" + result + " " + ViewUtil.printEvent(event));
51 | return true;
52 | }
53 |
54 | private void vibrator(Context context) {
55 | Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
56 | long[] patter = {0, 100};
57 | vibrator.vibrate(patter, -1);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_send_voice.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
21 |
22 |
35 |
36 |
43 |
44 |
51 |
52 |
53 |
59 |
60 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/util/DisplayUtil.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.util;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.view.View;
6 | import android.view.Window;
7 | import android.view.WindowManager;
8 |
9 | /**
10 | * @Author yocn
11 | * @Date 2019/8/13 4:41 PM
12 | * @ClassName DisplayUtil
13 | */
14 | public class DisplayUtil {
15 | /**
16 | * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
17 | */
18 | public static int dip2px(Context context, float dpValue) {
19 | final float scale = context.getResources().getDisplayMetrics().density;
20 | return (int) (dpValue * scale + 0.5f);
21 | }
22 |
23 | /**
24 | * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
25 | */
26 | public static int px2dip(Context context, float pxValue) {
27 | final float scale = context.getResources().getDisplayMetrics().density;
28 | return (int) (pxValue / scale + 0.5f);
29 | }
30 |
31 | public static int[] getHW(Context context) {
32 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
33 | int screenWidth = wm.getDefaultDisplay().getWidth();
34 | int screenHeight = wm.getDefaultDisplay().getHeight();
35 | return new int[]{screenWidth, screenHeight};
36 | }
37 |
38 | public static void setStatusBarColor(Activity activity, int colorId) {
39 | Window window = activity.getWindow();
40 | window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
41 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
42 | window.setStatusBarColor(activity.getResources().getColor(colorId));
43 | }
44 |
45 | public static void setAndroidNativeLightStatusBar(Activity activity, boolean dark) {
46 | View decor = activity.getWindow().getDecorView();
47 | if (dark) {
48 | decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
49 | } else {
50 | decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
51 | }
52 | }
53 |
54 | public static String getColor(int percent) {
55 | String prefix = "#";
56 | String rawColor = "EDEDED";
57 | String s = get(percent);
58 | return prefix + s + rawColor;
59 | }
60 |
61 | public static String get(int num) {
62 | String hexStr = "";
63 | float temp = 255 * num * 1.0f / 100f;
64 | int alpha = Math.round(temp);
65 | hexStr = Integer.toHexString(alpha);
66 | if (hexStr.length() < 2) {
67 | hexStr = "0" + hexStr;
68 | }
69 | return hexStr.toUpperCase();
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/widget/WeChatParentViewGroup.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.widget;
2 |
3 | import android.content.Context;
4 | import android.graphics.Rect;
5 | import android.util.AttributeSet;
6 | import android.view.MotionEvent;
7 | import android.view.View;
8 | import android.widget.RelativeLayout;
9 |
10 | import com.yocn.af.R;
11 | import com.yocn.af.util.LogUtil;
12 |
13 | /**
14 | * https://blog.csdn.net/xyz_lmn/article/details/12517911
15 | *
16 | * public boolean dispatchTouchEvent(MotionEvent ev) {
17 | * boolean consume = false;//事件是否被消费
18 | * if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
19 | * consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
20 | * }else{
21 | * consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
22 | * }
23 | * return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
24 | * }
25 | */
26 | public class WeChatParentViewGroup extends RelativeLayout {
27 | private String TAG = "WeChatParentViewGroup";
28 | private WeChatVoiceTextView voiceTv;
29 | private WeChatVoiceView voiceView;
30 | private Rect voiceRect;
31 |
32 | public interface OnVoiceViewStatusListener {
33 | public void showVoiceView();
34 | }
35 |
36 | public WeChatParentViewGroup(Context context) {
37 | super(context);
38 | init();
39 | }
40 |
41 | public WeChatParentViewGroup(Context context, AttributeSet attrs) {
42 | super(context, attrs);
43 | init();
44 | }
45 |
46 | public WeChatParentViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
47 | super(context, attrs, defStyleAttr);
48 | init();
49 | }
50 |
51 | private void init() {
52 | }
53 |
54 | OnVoiceViewStatusListener onVoiceViewStatusListener = new OnVoiceViewStatusListener() {
55 | @Override
56 | public void showVoiceView() {
57 | voiceView.setVisibility(View.VISIBLE);
58 | voiceView.doStart();
59 | }
60 | };
61 |
62 | @Override
63 | public boolean dispatchTouchEvent(MotionEvent ev) {
64 | boolean result = super.dispatchTouchEvent(ev);
65 | if (ev.getAction() == MotionEvent.ACTION_UP) {
66 | voiceView.setVisibility(View.GONE);
67 | voiceView.doDefault();
68 | }
69 | // LogUtil.d(TAG + "::dispatchTouchEvent::" + result + " " + ViewUtil.printEvent(ev));
70 | return result;
71 | }
72 |
73 | boolean hasIntercepted = false;
74 |
75 | @Override
76 | public boolean onInterceptTouchEvent(MotionEvent ev) {
77 | if (voiceRect == null) {
78 | voiceTv = findViewById(R.id.tv_voice);
79 | voiceView = findViewById(R.id.voice_view);
80 | final int[] locations = new int[2];
81 | voiceTv.getLocationOnScreen(locations);
82 | voiceTv.setListener(onVoiceViewStatusListener);
83 | voiceRect = new Rect(locations[0], locations[1], locations[0] + voiceTv.getWidth(), locations[1] + voiceTv.getHeight());
84 | LogUtil.d(TAG + "::voiceRect::" + voiceRect.toShortString());
85 | }
86 | float x = ev.getX();
87 | float y = ev.getY();
88 | boolean result;
89 | if (hasIntercepted) {
90 | result = false;
91 | hasIntercepted = false;
92 | } else {
93 | if (pointInRect(x, y, voiceRect)) {
94 | result = false;
95 | } else {
96 | hasIntercepted = true;
97 | result = true;
98 | }
99 | }
100 |
101 | // LogUtil.d(TAG + "::onInterceptTouchEvent::" + result + " " + ViewUtil.printEvent(ev) + " " + x + "/" + y + ":" + voiceRect.toShortString());
102 | return result;
103 | }
104 |
105 | @Override
106 | public boolean onTouchEvent(MotionEvent event) {
107 | boolean result = voiceView.onTouchEvent(event);
108 | // LogUtil.d(TAG + "::onTouchEvent::" + result + " ");
109 | return result;
110 | }
111 |
112 | public static boolean pointInRect(float x, float y, Rect rect) {
113 | return x > rect.left && x < rect.right && y > rect.top && y < rect.bottom;
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_voice.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
23 |
24 |
34 |
35 |
43 |
44 |
56 |
57 |
70 |
71 |
84 |
85 |
98 |
99 |
107 |
108 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/widget/WeChatVoiceBottomArc.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.widget;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Canvas;
6 | import android.graphics.LinearGradient;
7 | import android.graphics.Paint;
8 | import android.graphics.Path;
9 | import android.graphics.Point;
10 | import android.graphics.Rect;
11 | import android.graphics.RectF;
12 | import android.graphics.Region;
13 | import android.graphics.Shader;
14 | import android.util.AttributeSet;
15 | import android.view.View;
16 |
17 | import com.yocn.af.R;
18 | import com.yocn.af.util.DisplayUtil;
19 | import com.yocn.af.util.LogUtil;
20 |
21 | import androidx.annotation.Nullable;
22 |
23 | public class WeChatVoiceBottomArc extends View {
24 | private final int HEIGHT_MARGIN = 20;
25 | private Paint paint;
26 | private String type;
27 | private int height;
28 | private int screenWidth;
29 | private int[] screenWH;
30 | private Path path;
31 |
32 | public WeChatVoiceBottomArc(Context context) {
33 | this(context, null);
34 | }
35 |
36 | public WeChatVoiceBottomArc(Context context, @Nullable AttributeSet attrs) {
37 | this(context, attrs, 0);
38 | }
39 |
40 | public WeChatVoiceBottomArc(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
41 | super(context, attrs, defStyleAttr);
42 | TypedArray style = context.obtainStyledAttributes(attrs, R.styleable.WeChatVoiceBottomArc);
43 | try {
44 | type = style.getString(R.styleable.WeChatVoiceBottomArc_type);
45 | } finally {
46 | style.recycle();
47 | }
48 | }
49 |
50 | private boolean isLightMode() {
51 | return "light".equals(type);
52 | }
53 |
54 | @Override
55 | protected void onAttachedToWindow() {
56 | super.onAttachedToWindow();
57 | init();
58 | }
59 |
60 | private void init() {
61 | boolean isLight = isLightMode();
62 | height = getContext().getResources().getDimensionPixelSize(isLight ? R.dimen.arc_height_light : R.dimen.arc_height_dark);
63 | screenWH = DisplayUtil.getHW(getContext());
64 | screenWidth = screenWH[0];
65 | path = new Path();
66 | if (isLight) {
67 | initLight();
68 | } else {
69 | initDark();
70 | }
71 | }
72 |
73 | private void initLight() {
74 | paint = new Paint();
75 | paint.setAntiAlias(true);
76 | paint.setColor(0xFFCCC7CC);
77 | paint.setStyle(Paint.Style.FILL);
78 | LinearGradient linearGradient = new LinearGradient(screenWidth / 2, 0, screenWidth / 2, height,
79 | 0xFF999999, 0xFFe6e6e6, Shader.TileMode.CLAMP);
80 | paint.setShader(linearGradient);
81 | }
82 |
83 | private void initDark() {
84 | paint = new Paint();
85 | paint.setAntiAlias(true);
86 | paint.setColor(0xFF4c4c4c);
87 | paint.setStyle(Paint.Style.FILL);
88 | }
89 |
90 | @Override
91 | protected void onDraw(Canvas canvas) {
92 | super.onDraw(canvas);
93 | path.moveTo(0, height / 2);
94 | path.cubicTo(screenWidth / 4, 0, screenWidth * 3 / 4, 0, screenWidth, height / 2);
95 | path.lineTo(screenWidth, height);
96 | path.lineTo(0, height);
97 | path.lineTo(0, height / 2);
98 | path.close();
99 | canvas.drawPath(path, paint);
100 | }
101 |
102 | public boolean isOnRect(float x, float y) {
103 | float viewY = getY();
104 | return isInTriangle(new Point(screenWidth / 2, 0), new Point(0, height), new Point(screenWidth, height), new Point((int) x, (int) (y - viewY)));
105 | }
106 |
107 | public boolean isInTriangle(Point A, Point B, Point C, Point P) {
108 | double ABC = triAngleArea(A, B, C);
109 | double ABp = triAngleArea(A, B, P);
110 | double ACp = triAngleArea(A, C, P);
111 | double BCp = triAngleArea(B, C, P);
112 | if ((int) ABC == (int) (ABp + ACp + BCp)) {// 若面积之和等于原三角形面积,证明点在三角形内,这里做了一个约等于小数点之后没有算(25714.25390625、25714.255859375)
113 | return true;
114 | } else {
115 | return false;
116 | }
117 | }
118 |
119 | private double triAngleArea(Point A, Point B, Point C) {// 由三个点计算这三个点组成三角形面积
120 | double result = Math.abs((A.x * B.y + B.x * C.y
121 | + C.x * A.y - B.x * A.y - C.x
122 | * B.y - A.x * C.y) / 2.0D);
123 | return result;
124 | }
125 |
126 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/widget/WeChatVoiceView.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.widget;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.AnimatorSet;
6 | import android.animation.ObjectAnimator;
7 | import android.content.Context;
8 | import android.os.Handler;
9 | import android.os.Looper;
10 | import android.util.AttributeSet;
11 | import android.view.LayoutInflater;
12 | import android.view.MotionEvent;
13 | import android.view.View;
14 | import android.widget.FrameLayout;
15 | import android.widget.ImageView;
16 | import android.widget.TextView;
17 |
18 | import com.yocn.af.R;
19 | import com.yocn.af.util.DisplayUtil;
20 |
21 | import androidx.annotation.NonNull;
22 | import androidx.annotation.Nullable;
23 |
24 | public class WeChatVoiceView extends FrameLayout {
25 | private String TAG = "WeChatVoiceView";
26 | private WeChatVoiceBottomArc weChatVoiceBottomArcLight;
27 | private WeChatVoiceBottomArc weChatVoiceBottomArcDark;
28 | private final int ANIM_DURATION = 300;
29 | private final int ANIM_DURATION_TEXT = 500;
30 | private final int ANIM_DURATION_TEXT_BIGGER = 100;
31 | private ImageView voiceIv;
32 | private int bottomArcTransY;
33 | private TextView voiceTv;
34 | private TextView cancelTv;
35 | private TextView translateTv;
36 | private ObjectAnimator darkAlphaAnim;
37 | private ObjectAnimator lightAlphaAnim;
38 | boolean currentArcLight = true;
39 | boolean lightAniming = false;
40 | boolean darkAniming = false;
41 | private AnimatorSet bottomArcSet;
42 | private AnimatorSet textAnimSet;
43 | private int[] screenWH;
44 | private AnimatorSet cancelTvScaleBiggerAnim;
45 | private AnimatorSet translateTvScaleBiggerAnim;
46 | private AnimatorSet cancelTvScaleSmallAnim;
47 | private AnimatorSet translateTvScaleSmallAnim;
48 | private TextView cancelHintTv;
49 | private TextView translateHintTv;
50 | private WeChatVoiceBubble weChatVoiceBubble;
51 |
52 | public WeChatVoiceView(@NonNull Context context) {
53 | this(context, null);
54 | }
55 |
56 | public WeChatVoiceView(@NonNull Context context, @Nullable AttributeSet attrs) {
57 | this(context, attrs, 0);
58 | }
59 |
60 | public WeChatVoiceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
61 | super(context, attrs, defStyleAttr);
62 | init();
63 | }
64 |
65 | private void init() {
66 | LayoutInflater.from(getContext()).inflate(R.layout.view_voice, this);
67 | initView();
68 | initAnimation();
69 | }
70 |
71 | private void initView() {
72 | voiceIv = findViewById(R.id.iv_voice);
73 | weChatVoiceBottomArcLight = findViewById(R.id.bottom_arc_light);
74 | weChatVoiceBottomArcDark = findViewById(R.id.bottom_arc_dark);
75 | weChatVoiceBubble = findViewById(R.id.bubble);
76 | cancelTv = findViewById(R.id.tv_cancel);
77 | translateTv = findViewById(R.id.tv_trans);
78 | bottomArcTransY = getResources().getDimensionPixelOffset(R.dimen.arc_height_light);
79 | voiceTv = findViewById(R.id.voice);
80 | cancelHintTv = findViewById(R.id.tv_cancel_text);
81 | translateHintTv = findViewById(R.id.tv_trans_text);
82 | screenWH = DisplayUtil.getHW(getContext());
83 | }
84 |
85 | private void initAnimation() {
86 | ObjectAnimator bottomArcTransYAnim = ObjectAnimator.ofFloat(weChatVoiceBottomArcLight, "translationY", bottomArcTransY, 0);
87 | ObjectAnimator bottomArcAlphaAnim = ObjectAnimator.ofFloat(weChatVoiceBottomArcLight, "alpha", 0f, 1f);
88 | ObjectAnimator voiceTvTransYAnim = ObjectAnimator.ofFloat(voiceTv, "translationY", 300, 0);
89 | ObjectAnimator voiceTvAlphaAnim = ObjectAnimator.ofFloat(voiceTv, "alpha", 0f, 1f);
90 | weChatVoiceBottomArcLight.setVisibility(View.VISIBLE);
91 | bottomArcSet = new AnimatorSet();
92 | bottomArcSet.playTogether(bottomArcTransYAnim, bottomArcAlphaAnim, voiceTvTransYAnim, voiceTvAlphaAnim);
93 | bottomArcSet.setDuration(ANIM_DURATION);
94 | bottomArcSet.addListener(new AnimatorListenerAdapter() {
95 | @Override
96 | public void onAnimationEnd(Animator animation, boolean isReverse) {
97 | weChatVoiceBottomArcDark.setVisibility(View.VISIBLE);
98 | }
99 | });
100 | ObjectAnimator cancelTvTransYAnim = ObjectAnimator.ofFloat(cancelTv, "translationY", 100, 0);
101 | ObjectAnimator cancelTvAlphaAnim = ObjectAnimator.ofFloat(cancelTv, "alpha", 0f, 1f);
102 | ObjectAnimator translateTvTransYAnim = ObjectAnimator.ofFloat(translateTv, "translationY", 100, 0);
103 | ObjectAnimator translateTvAlphaAnim = ObjectAnimator.ofFloat(translateTv, "alpha", 0f, 1f);
104 | textAnimSet = new AnimatorSet();
105 | textAnimSet.playTogether(cancelTvTransYAnim, cancelTvAlphaAnim, translateTvTransYAnim, translateTvAlphaAnim);
106 | textAnimSet.setDuration(ANIM_DURATION_TEXT);
107 |
108 | darkAlphaAnim = ObjectAnimator.ofFloat(weChatVoiceBottomArcLight, "alpha", 1f, 0f);
109 | darkAlphaAnim.setDuration(100);
110 | darkAlphaAnim.addListener(new AnimatorListenerAdapter() {
111 | @Override
112 | public void onAnimationEnd(Animator animation, boolean isReverse) {
113 | darkAniming = false;
114 | currentArcLight = false;
115 | voiceIv.setImageResource(R.drawable.ic_voice_dark);
116 | }
117 | });
118 |
119 | lightAlphaAnim = ObjectAnimator.ofFloat(weChatVoiceBottomArcLight, "alpha", 0f, 1f);
120 | lightAlphaAnim.setDuration(100);
121 | lightAlphaAnim.addListener(new AnimatorListenerAdapter() {
122 | @Override
123 | public void onAnimationEnd(Animator animation, boolean isReverse) {
124 | lightAniming = false;
125 | currentArcLight = true;
126 | voiceIv.setImageResource(R.drawable.ic_voice);
127 | }
128 | });
129 |
130 | float src = 1f, tar = 1.2f;
131 | ObjectAnimator cancelTvScaleXBiggerAnim = ObjectAnimator.ofFloat(cancelTv, "scaleX", src, tar);
132 | ObjectAnimator cancelTvScaleYBiggerAnim = ObjectAnimator.ofFloat(cancelTv, "scaleY", src, tar);
133 | ObjectAnimator cancelTvScaleXSmallAnim = ObjectAnimator.ofFloat(cancelTv, "scaleX", tar, src);
134 | ObjectAnimator cancelTvScaleYSmallAnim = ObjectAnimator.ofFloat(cancelTv, "scaleY", tar, src);
135 |
136 | cancelTvScaleBiggerAnim = new AnimatorSet();
137 | cancelTvScaleBiggerAnim.playTogether(cancelTvScaleXBiggerAnim, cancelTvScaleYBiggerAnim);
138 | cancelTvScaleBiggerAnim.setDuration(ANIM_DURATION_TEXT_BIGGER);
139 | cancelTvScaleBiggerAnim.addListener(new AnimatorListenerAdapter() {
140 | @Override
141 | public void onAnimationEnd(Animator animation, boolean isReverse) {
142 | cancelTvAnimationing = false;
143 | cancelTvBig = true;
144 | }
145 | });
146 | cancelTvScaleSmallAnim = new AnimatorSet();
147 | cancelTvScaleSmallAnim.playTogether(cancelTvScaleXSmallAnim, cancelTvScaleYSmallAnim);
148 | cancelTvScaleSmallAnim.setDuration(ANIM_DURATION_TEXT_BIGGER);
149 | cancelTvScaleSmallAnim.addListener(new AnimatorListenerAdapter() {
150 | @Override
151 | public void onAnimationEnd(Animator animation, boolean isReverse) {
152 | cancelTvAnimationing = false;
153 | cancelTvBig = false;
154 | }
155 | });
156 |
157 | ObjectAnimator translateTvScaleXBiggerAnim = ObjectAnimator.ofFloat(translateTv, "scaleX", src, tar);
158 | ObjectAnimator translateTvScaleYBiggerAnim = ObjectAnimator.ofFloat(translateTv, "scaleY", src, tar);
159 | ObjectAnimator translateTvScaleXSmallAnim = ObjectAnimator.ofFloat(translateTv, "scaleX", tar, src);
160 | ObjectAnimator translateTvScaleYSmallAnim = ObjectAnimator.ofFloat(translateTv, "scaleY", tar, src);
161 |
162 | translateTvScaleBiggerAnim = new AnimatorSet();
163 | translateTvScaleBiggerAnim.playTogether(translateTvScaleXBiggerAnim, translateTvScaleYBiggerAnim);
164 | translateTvScaleBiggerAnim.setDuration(ANIM_DURATION_TEXT_BIGGER);
165 | translateTvScaleBiggerAnim.addListener(new AnimatorListenerAdapter() {
166 | @Override
167 | public void onAnimationEnd(Animator animation, boolean isReverse) {
168 | translateTvAnimationing = false;
169 | translateTvBig = true;
170 | }
171 | });
172 | translateTvScaleSmallAnim = new AnimatorSet();
173 | translateTvScaleSmallAnim.playTogether(translateTvScaleXSmallAnim, translateTvScaleYSmallAnim);
174 | translateTvScaleSmallAnim.setDuration(ANIM_DURATION_TEXT_BIGGER);
175 | translateTvScaleSmallAnim.addListener(new AnimatorListenerAdapter() {
176 | @Override
177 | public void onAnimationEnd(Animator animation, boolean isReverse) {
178 | translateTvAnimationing = false;
179 | translateTvBig = false;
180 | }
181 | });
182 | }
183 |
184 |
185 | private void startAnim() {
186 | bottomArcSet.start();
187 | textAnimSet.start();
188 | }
189 |
190 | @Override
191 | protected void onAttachedToWindow() {
192 | super.onAttachedToWindow();
193 | }
194 |
195 | public void doStart() {
196 | new Handler(Looper.getMainLooper()).post(this::startAnim);
197 | }
198 |
199 | public void doDefault() {
200 | weChatVoiceBottomArcLight.setTranslationY(bottomArcTransY);
201 | weChatVoiceBottomArcDark.setVisibility(View.GONE);
202 | }
203 |
204 | @Override
205 | public boolean dispatchTouchEvent(MotionEvent ev) {
206 | return super.dispatchTouchEvent(ev);
207 | }
208 |
209 | @Override
210 | public boolean onInterceptTouchEvent(MotionEvent ev) {
211 | return true;
212 | }
213 |
214 | @Override
215 | public boolean onTouchEvent(MotionEvent ev) {
216 | float x = ev.getX();
217 | float y = ev.getY();
218 | if (weChatVoiceBottomArcLight.isOnRect(x, y)) {
219 | tryChangeToLight();
220 | tryChangeCancelTextToSmall();
221 | tryChangeTranslateTextToSmall();
222 | voiceTv.setVisibility(View.VISIBLE);
223 | weChatVoiceBubble.setShowType(WeChatVoiceBubble.SHOW_TYPE.TYPE_CENTER);
224 | } else {
225 | // 不在区域里,看在屏幕左边右边
226 | tryChangeToDark();
227 | if (x < screenWH[0] / 2) {
228 | tryChangeCancelTextToBigger();
229 | tryChangeTranslateTextToSmall();
230 | weChatVoiceBubble.setShowType(WeChatVoiceBubble.SHOW_TYPE.TYPE_CANCEL);
231 | } else {
232 | tryChangeTranslateTextToBigger();
233 | tryChangeCancelTextToSmall();
234 | weChatVoiceBubble.setShowType(WeChatVoiceBubble.SHOW_TYPE.TYPE_TRANSLATE);
235 | }
236 | voiceTv.setVisibility(View.GONE);
237 | }
238 | return true;
239 | }
240 |
241 | private void setTextBigger(TextView text) {
242 | text.setTextColor(getResources().getColor(R.color.black));
243 | text.setBackgroundResource(R.drawable.bg_trans_oval_white);
244 | }
245 |
246 | private void setTextSmall(TextView text) {
247 | text.setTextColor(getResources().getColor(R.color.write));
248 | text.setBackgroundResource(R.drawable.bg_trans_oval);
249 | }
250 |
251 | boolean cancelTvBig = false;
252 | boolean translateTvBig = false;
253 | boolean cancelTvAnimationing = false;
254 | boolean translateTvAnimationing = false;
255 |
256 | private void tryChangeCancelTextToBigger() {
257 | if (cancelTvBig || cancelTvAnimationing) {
258 | return;
259 | }
260 | cancelTvAnimationing = true;
261 | setTextBigger(cancelTv);
262 | cancelHintTv.setVisibility(View.VISIBLE);
263 | cancelTvScaleBiggerAnim.start();
264 | }
265 |
266 | private void tryChangeCancelTextToSmall() {
267 | if (!cancelTvBig || cancelTvAnimationing) {
268 | return;
269 | }
270 | cancelHintTv.setVisibility(View.GONE);
271 | cancelTvAnimationing = true;
272 | setTextSmall(cancelTv);
273 | cancelTvScaleSmallAnim.start();
274 | }
275 |
276 | private void tryChangeTranslateTextToBigger() {
277 | if (translateTvBig || translateTvAnimationing) {
278 | return;
279 | }
280 | translateHintTv.setVisibility(View.VISIBLE);
281 | translateTvAnimationing = true;
282 | setTextBigger(translateTv);
283 | translateTvScaleBiggerAnim.start();
284 | }
285 |
286 | private void tryChangeTranslateTextToSmall() {
287 | if (!translateTvBig || translateTvAnimationing) {
288 | return;
289 | }
290 | translateHintTv.setVisibility(View.GONE);
291 | translateTvAnimationing = true;
292 | setTextSmall(translateTv);
293 | translateTvScaleSmallAnim.start();
294 | }
295 |
296 | private void tryChangeToDark() {
297 | if (!currentArcLight || darkAniming) {
298 | return;
299 | }
300 | darkAniming = true;
301 | darkAlphaAnim.start();
302 | }
303 |
304 | private void tryChangeToLight() {
305 | if (currentArcLight || lightAniming) {
306 | return;
307 | }
308 | lightAniming = true;
309 | lightAlphaAnim.start();
310 | }
311 |
312 | }
313 |
--------------------------------------------------------------------------------
/app/src/main/java/com/yocn/af/view/widget/WeChatVoiceBubble.java:
--------------------------------------------------------------------------------
1 | package com.yocn.af.view.widget;
2 |
3 | import android.content.Context;
4 | import android.graphics.Canvas;
5 | import android.graphics.Color;
6 | import android.graphics.Paint;
7 | import android.graphics.Path;
8 | import android.graphics.PointF;
9 | import android.graphics.RectF;
10 | import android.text.Layout;
11 | import android.text.StaticLayout;
12 | import android.text.TextPaint;
13 | import android.text.TextUtils;
14 | import android.util.AttributeSet;
15 | import android.view.View;
16 |
17 | import com.yocn.af.R;
18 |
19 | import java.lang.annotation.Retention;
20 | import java.lang.annotation.RetentionPolicy;
21 | import java.util.Arrays;
22 |
23 | import androidx.annotation.IntDef;
24 | import androidx.annotation.Nullable;
25 |
26 | public class WeChatVoiceBubble extends View {
27 | // cancel、trans状态下的音波长度
28 | private final int NUM_CANCEL_VOICE = 10;
29 | // recording状态下的音波长度
30 | private final int NUM_RECORD_VOICE = 24;
31 | // 音波最短的高度
32 | private final int MIN_VOICE_HEIGHT = 10;
33 | private final int MAX_VOICE_HEIGHT = 24;
34 | // 无声音下音波循环波纹最短的长度
35 | private final int MIN_VOICE_SIMULATE_LENGTH = 10;
36 | // 音波线宽度
37 | private final int VOICE_LINE_WIDTH = 4;
38 | // 音波线之间间隔的宽度
39 | private final int VOICE_DIVIDER_WIDTH = 4;
40 |
41 | private Paint redPaint;
42 | private Paint greenPaint;
43 | private Paint writePaint;
44 | private Paint currPaint;
45 | private RectF translateRectF;
46 | private RectF cancelRectF;
47 | private RectF centerRectF;
48 | private RectF currRectF;
49 | private RectF targetRectF;
50 | private final PointF[] translateTrianglePoints = new PointF[3];
51 | private final PointF[] cancelTrianglePoints = new PointF[3];
52 | private final PointF[] centerTrianglePoints = new PointF[3];
53 | private final PointF[] currTrianglePoints = new PointF[3];
54 | private PointF[] targetTrianglePoints = new PointF[3];
55 | private float triangleHeight;
56 | private Path trianglePath;
57 | private final float triangleLine = getResources().getDimensionPixelOffset(R.dimen.height_triangle_line);
58 | private final int topDivider = getResources().getDimensionPixelOffset(R.dimen.height_top_divider);
59 | private float deltaLeftX = 0, deltaRightX = 0, deltaTopY = 0, deltaTriangleX = 0, deltaVoiceX = 0, deltaVoiceY = 0;
60 | private final int[] cancelVoiceData = new int[NUM_CANCEL_VOICE];
61 | private final int[] centerVoiceData = new int[NUM_RECORD_VOICE];
62 | private int cancelCurrIndex = NUM_CANCEL_VOICE + MIN_VOICE_SIMULATE_LENGTH, centerCurrIndex = NUM_RECORD_VOICE;
63 | private float cancelLineViewWidth, centerLineViewWidth, translateLineViewWidth;
64 | private RectF translateVoiceRectF;
65 | private RectF cancelVoiceRectF;
66 | private RectF centerVoiceRectF;
67 | private RectF currVoiceRectF;
68 | private RectF targetVoiceRectF;
69 | private boolean recording = true;
70 | private boolean translating = false;
71 | private int controlSpeed = 0;
72 | private TextPaint textPaint;
73 | private StaticLayout myStaticLayout;
74 | private String preMessage = "";
75 |
76 | @Retention(RetentionPolicy.SOURCE)
77 | @IntDef({
78 | SHOW_TYPE.TYPE_CENTER,
79 | SHOW_TYPE.TYPE_CANCEL,
80 | SHOW_TYPE.TYPE_TRANSLATE
81 | })
82 | public @interface SHOW_TYPE {
83 | int TYPE_CENTER = 101;
84 | int TYPE_CANCEL = 102;
85 | int TYPE_TRANSLATE = 103;
86 | }
87 |
88 | public WeChatVoiceBubble(Context context) {
89 | this(context, null);
90 | }
91 |
92 | public WeChatVoiceBubble(Context context, @Nullable AttributeSet attrs) {
93 | this(context, attrs, 0);
94 | }
95 |
96 | public WeChatVoiceBubble(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
97 | super(context, attrs, defStyleAttr);
98 | init();
99 | }
100 |
101 | @Override
102 | public void onWindowFocusChanged(boolean hasWindowFocus) {
103 | super.onWindowFocusChanged(hasWindowFocus);
104 | }
105 |
106 | private void init() {
107 | textPaint = new TextPaint();
108 | textPaint.setColor(Color.WHITE);
109 | textPaint.setStyle(Paint.Style.FILL);
110 | textPaint.setTextSize(50);
111 | greenPaint = new Paint();
112 | greenPaint.setAntiAlias(true);
113 | greenPaint.setColor(0xFF00cb32);
114 | greenPaint.setStyle(Paint.Style.FILL);
115 | redPaint = new Paint();
116 | redPaint.setAntiAlias(true);
117 | redPaint.setColor(0xFFcb3a35);
118 | redPaint.setStyle(Paint.Style.FILL);
119 | writePaint = new Paint();
120 | writePaint.setAntiAlias(true);
121 | writePaint.setColor(0xFFffffff);
122 | writePaint.setStyle(Paint.Style.FILL);
123 | writePaint.setStrokeWidth(VOICE_LINE_WIDTH);
124 | currPaint = greenPaint;
125 | triangleHeight = (float) Math.sqrt(Math.pow(triangleLine, 2) - Math.pow(triangleLine / 2, 2));
126 | for (int i = 0; i < NUM_CANCEL_VOICE; i++) {
127 | cancelVoiceData[i] = MIN_VOICE_HEIGHT;
128 | }
129 | for (int i = 0; i < NUM_RECORD_VOICE; i++) {
130 | centerVoiceData[i] = MIN_VOICE_HEIGHT;
131 | }
132 | cancelLineViewWidth = translateLineViewWidth = NUM_CANCEL_VOICE * VOICE_LINE_WIDTH + (NUM_CANCEL_VOICE - 1) * VOICE_DIVIDER_WIDTH;
133 | centerLineViewWidth = NUM_RECORD_VOICE * VOICE_LINE_WIDTH + (NUM_RECORD_VOICE - 1) * VOICE_DIVIDER_WIDTH;
134 | }
135 |
136 | @Override
137 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
138 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
139 | int height = getResources().getDimensionPixelSize(R.dimen.height_round_rect);
140 | float width = getMeasuredWidth();
141 | if (translateRectF == null) {
142 | translateRectF = new RectF(0, 0, width, height - triangleHeight);
143 | cancelRectF = new RectF(0, topDivider, height - triangleHeight, height - triangleHeight);
144 | centerRectF = new RectF(width / 2 - (height - triangleHeight), topDivider, width / 2 + (height - triangleHeight), height - triangleHeight);
145 |
146 | translateTrianglePoints[0] = new PointF(width - cancelRectF.width() / 2 - triangleLine / 2, height - triangleHeight - 1);
147 | translateTrianglePoints[1] = new PointF(width - cancelRectF.width() / 2, height);
148 | translateTrianglePoints[2] = new PointF(width - cancelRectF.width() / 2 + triangleLine / 2, height - triangleHeight - 1);
149 |
150 | cancelTrianglePoints[0] = new PointF(cancelRectF.width() / 2 - triangleLine / 2, height - triangleHeight - 1);
151 | cancelTrianglePoints[1] = new PointF(cancelRectF.width() / 2, height);
152 | cancelTrianglePoints[2] = new PointF(cancelRectF.width() / 2 + triangleLine / 2, height - triangleHeight - 1);
153 |
154 | centerTrianglePoints[0] = new PointF(width / 2 - triangleLine / 2, height - triangleHeight - 1);
155 | centerTrianglePoints[1] = new PointF(width / 2, height);
156 | centerTrianglePoints[2] = new PointF(width / 2 + triangleLine / 2, height - triangleHeight - 1);
157 |
158 | int translateLineViewRightMargin = 50;
159 | translateVoiceRectF = new RectF(
160 | width - translateLineViewRightMargin - translateLineViewWidth,
161 | height - triangleHeight - translateLineViewRightMargin - MAX_VOICE_HEIGHT,
162 | width - translateLineViewRightMargin,
163 | height - triangleHeight - translateLineViewRightMargin);
164 |
165 | float VOICE_LINE_VIEW_HEIGHT = MAX_VOICE_HEIGHT;
166 | cancelVoiceRectF = new RectF(cancelRectF.left + cancelRectF.width() / 2 - cancelLineViewWidth / 2,
167 | cancelRectF.top + cancelRectF.height() / 2 - VOICE_LINE_VIEW_HEIGHT / 2,
168 | cancelRectF.left + cancelRectF.width() / 2 + cancelLineViewWidth / 2,
169 | cancelRectF.top + cancelRectF.height() / 2 + VOICE_LINE_VIEW_HEIGHT / 2);
170 |
171 | centerVoiceRectF = new RectF(centerRectF.left + centerRectF.width() / 2 - centerLineViewWidth / 2,
172 | centerRectF.top + centerRectF.height() / 2 - VOICE_LINE_VIEW_HEIGHT / 2,
173 | centerRectF.left + centerRectF.width() / 2 + centerLineViewWidth / 2,
174 | centerRectF.top + centerRectF.height() / 2 + VOICE_LINE_VIEW_HEIGHT / 2);
175 |
176 | currVoiceRectF = new RectF(centerVoiceRectF);
177 | trianglePath = new Path();
178 | currRectF = new RectF(centerRectF);
179 | currTrianglePoints[0] = new PointF(centerTrianglePoints[0].x, centerTrianglePoints[0].y);
180 | currTrianglePoints[1] = new PointF(centerTrianglePoints[1].x, centerTrianglePoints[1].y);
181 | currTrianglePoints[2] = new PointF(centerTrianglePoints[2].x, centerTrianglePoints[2].y);
182 | }
183 | }
184 |
185 | @Override
186 | protected void onDraw(Canvas canvas) {
187 | super.onDraw(canvas);
188 | // 圆角矩形
189 | refreshRectRectF();
190 | canvas.drawRoundRect(currRectF, 50, 50, currPaint);
191 | // 三角形
192 | refreshTriangleRectF();
193 | trianglePath.reset();
194 | trianglePath.setFillType(Path.FillType.EVEN_ODD);
195 | trianglePath.moveTo(currTrianglePoints[0].x, currTrianglePoints[0].y);
196 | trianglePath.lineTo(currTrianglePoints[1].x, currTrianglePoints[1].y);
197 | trianglePath.lineTo(currTrianglePoints[2].x, currTrianglePoints[2].y);
198 | trianglePath.close();
199 | canvas.drawPath(trianglePath, currPaint);
200 |
201 | startSimulateVoice();
202 | refreshVoiceRectF();
203 | float centerLineY = currVoiceRectF.top + currVoiceRectF.height() / 2;
204 | float lineStartX = currVoiceRectF.left;
205 | int[] currData = getVoiceLineData();
206 | // voiceView
207 | for (int i = 0; i < currData.length; i++) {
208 | canvas.drawLine(lineStartX + getLineStartX(i), centerLineY - currData[i] * 1f / 2,
209 | lineStartX + getLineStartX(i), centerLineY + currData[i] * 1f / 2, writePaint);
210 | }
211 |
212 | paintText(canvas);
213 | }
214 |
215 | private void paintText(Canvas canvas) {
216 | if (!isSameRectRectF(translateRectF)) {
217 | return;
218 | }
219 | canvas.translate(50, 30);
220 | int width = canvas.getWidth() - 100;
221 | String message = "你好,是一个汉语词语,拼音是nǐ hǎo,是汉语中打招呼的敬语常用词语";
222 | getStaticLayout(message, width).draw(canvas);
223 | }
224 |
225 | private StaticLayout getStaticLayout(String message, int width) {
226 | if (!TextUtils.equals(message, preMessage)) {
227 | preMessage = message;
228 | myStaticLayout = new StaticLayout(message, textPaint, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
229 | }
230 | return myStaticLayout;
231 | }
232 |
233 | private int getLineStartX(int index) {
234 | int x = index * VOICE_LINE_WIDTH;
235 | if (x > 0) {
236 | x += index * VOICE_DIVIDER_WIDTH;
237 | }
238 | return x;
239 | }
240 |
241 | private int[] getVoiceLineData() {
242 | return recording ? centerVoiceData : cancelVoiceData;
243 | }
244 |
245 | private void refreshRectRectF() {
246 | if (!isSameRectRectF(targetRectF)) {
247 | currRectF.top += deltaTopY;
248 | currRectF.left += deltaLeftX;
249 | currRectF.right += deltaRightX;
250 | invalidate();
251 | }
252 | }
253 |
254 | private void refreshTriangleRectF() {
255 | if (!isSameTriangleRectF()) {
256 | currTrianglePoints[0].x += deltaTriangleX;
257 | currTrianglePoints[1].x += deltaTriangleX;
258 | currTrianglePoints[2].x += deltaTriangleX;
259 | invalidate();
260 | }
261 | }
262 |
263 | private void refreshVoiceRectF() {
264 | if (!isSameVoiceRectF()) {
265 | currVoiceRectF.left += deltaVoiceX;
266 | currVoiceRectF.top += deltaVoiceY;
267 | invalidate();
268 | }
269 | }
270 |
271 | private boolean isSameRectRectF(RectF tarRectF) {
272 | if (tarRectF == null) {
273 | return true;
274 | }
275 | return Math.abs((currRectF.right - currRectF.left) - (tarRectF.right - tarRectF.left)) < 10;
276 | }
277 |
278 | private boolean isSameTriangleRectF() {
279 | if (targetTrianglePoints == null || targetTrianglePoints[0] == null) {
280 | return true;
281 | }
282 | return Math.abs(targetTrianglePoints[0].x - currTrianglePoints[0].x) < 10;
283 | }
284 |
285 | private boolean isSameVoiceRectF() {
286 | if (targetVoiceRectF == null) {
287 | return true;
288 | }
289 | return Math.abs(currVoiceRectF.left - targetVoiceRectF.left) < 10;
290 | }
291 |
292 | public void setShowType(@SHOW_TYPE int type) {
293 | switch (type) {
294 | case SHOW_TYPE.TYPE_CENTER:
295 | targetRectF = centerRectF;
296 | targetTrianglePoints = centerTrianglePoints;
297 | currPaint = greenPaint;
298 | targetVoiceRectF = centerVoiceRectF;
299 | recording = true;
300 | translating = false;
301 | break;
302 | case SHOW_TYPE.TYPE_CANCEL:
303 | targetRectF = cancelRectF;
304 | targetTrianglePoints = cancelTrianglePoints;
305 | currPaint = redPaint;
306 | targetVoiceRectF = cancelVoiceRectF;
307 | recording = false;
308 | translating = false;
309 | break;
310 | case SHOW_TYPE.TYPE_TRANSLATE:
311 | targetRectF = translateRectF;
312 | targetTrianglePoints = translateTrianglePoints;
313 | currPaint = greenPaint;
314 | targetVoiceRectF = translateVoiceRectF;
315 | recording = false;
316 | translating = true;
317 | break;
318 | default:
319 | }
320 | int num = 10;
321 | deltaTopY = (targetRectF.top - currRectF.top) / num;
322 | deltaLeftX = (targetRectF.left - currRectF.left) / num;
323 | deltaRightX = (targetRectF.right - currRectF.right) / num;
324 | deltaTriangleX = (targetTrianglePoints[0].x - currTrianglePoints[0].x) / num;
325 | deltaVoiceX = (targetVoiceRectF.left - currVoiceRectF.left) / num;
326 | deltaVoiceY = (targetVoiceRectF.top - currVoiceRectF.top) / num;
327 | invalidate();
328 | }
329 |
330 | private void startSimulateVoice() {
331 | int VOICE_SPEED = 6;
332 | if (controlSpeed++ < VOICE_SPEED) {
333 | invalidate();
334 | return;
335 | }
336 | controlSpeed = 0;
337 | if (cancelCurrIndex <= 0) {
338 | cancelCurrIndex = NUM_CANCEL_VOICE + MIN_VOICE_SIMULATE_LENGTH;
339 | }
340 | if (centerCurrIndex <= 0) {
341 | centerCurrIndex = NUM_RECORD_VOICE + MIN_VOICE_SIMULATE_LENGTH;
342 | }
343 | if (recording) {
344 | centerCurrIndex--;
345 | Arrays.fill(centerVoiceData, MIN_VOICE_HEIGHT);
346 | for (int i = centerCurrIndex - (MIN_VOICE_SIMULATE_LENGTH / 2); i < centerCurrIndex + (MIN_VOICE_SIMULATE_LENGTH / 2); i++) {
347 | if (i > 0 && i < centerVoiceData.length) {
348 | // radio范围[0,1],离centerCurrIndex越近越靠近1
349 | float radio = 1f - 1f * Math.abs(i - centerCurrIndex) / (1f * MIN_VOICE_SIMULATE_LENGTH / 2);
350 | centerVoiceData[i] = (int) (MIN_VOICE_HEIGHT + radio * (MAX_VOICE_HEIGHT - MIN_VOICE_HEIGHT));
351 | }
352 | }
353 | } else {
354 | cancelCurrIndex--;
355 | Arrays.fill(cancelVoiceData, MIN_VOICE_HEIGHT);
356 | for (int i = cancelCurrIndex - (MIN_VOICE_SIMULATE_LENGTH / 2); i < cancelCurrIndex + (MIN_VOICE_SIMULATE_LENGTH / 2); i++) {
357 | if (i > 0 && i < cancelVoiceData.length) {
358 | // radio范围[0,1],离centerCurrIndex越近越靠近1
359 | float radio = 1f - 1f * Math.abs(i - cancelCurrIndex) / (1f * MIN_VOICE_SIMULATE_LENGTH / 2);
360 | cancelVoiceData[i] = (int) (MIN_VOICE_HEIGHT + radio * (MAX_VOICE_HEIGHT - MIN_VOICE_HEIGHT));
361 | }
362 | }
363 | }
364 | invalidate();
365 | }
366 | }
367 |
--------------------------------------------------------------------------------