├── 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 | ![](gif/readme.gif) 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 | --------------------------------------------------------------------------------