From ea4a02351907d98ae7374afbe146ecca3e863e2c Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 16 Jul 2024 13:48:18 +0800 Subject: [PATCH] text,photo,exchange end --- app/build.gradle.kts | 11 +- app/src/main/AndroidManifest.xml | 1 + .../java/com/assimilate/alltrans/MyApp.kt | 137 ++--- .../alltrans/allservice/SusService.kt | 2 +- .../alltrans/common/InferenceInfoGraphic.java | 42 +- .../alltrans/common/LanguagesConstants.java | 45 +- .../common/PreferenceLanguageUtils.kt | 87 +++ .../alltrans/common/PreferenceUtils.java | 2 +- .../assimilate/alltrans/common/TextGraphic.kt | 238 +++++---- .../common/TextRecognitionProcessor.kt | 7 + .../assimilate/alltrans/common/Widget.java | 20 + .../alltrans/curview/GraphicOverlay.java | 505 +++++++++--------- .../alltrans/viewui/HistoryActivity.java | 2 +- .../alltrans/viewui/LanguageChangeActivity.kt | 82 ++- .../alltrans/viewui/MainActivity.kt | 51 +- .../alltrans/viewui/StillImageActivity.kt | 193 ++++--- .../alltrans/viewui/TextResultActivity.kt | 59 +- .../drawable-xxxhdpi/ic_still_bulibuli.webp | Bin 0 -> 1830 bytes .../drawable-xxxhdpi/ic_still_notbuli.webp | Bin 0 -> 2046 bytes .../res/drawable-xxxhdpi/ic_still_pic.webp | Bin 0 -> 2132 bytes .../res/drawable-xxxhdpi/ic_still_take.webp | Bin 0 -> 3338 bytes .../main/res/drawable/button_r20_black_bg.xml | 4 +- app/src/main/res/drawable/ic_back_white.xml | 19 + .../res/drawable/ic_down_choose_white.xml | 10 + .../res/layout/activity_language_change.xml | 7 +- app/src/main/res/layout/activity_main.xml | 1 + .../main/res/layout/activity_still_image.xml | 143 ++++- .../main/res/layout/activity_text_result.xml | 7 +- app/src/main/res/layout/layout_sus_global.xml | 2 +- app/src/main/res/raw/languages.json | 4 +- app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 8 +- 33 files changed, 1057 insertions(+), 637 deletions(-) create mode 100644 app/src/main/java/com/assimilate/alltrans/common/PreferenceLanguageUtils.kt create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_still_bulibuli.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_still_notbuli.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_still_pic.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_still_take.webp create mode 100644 app/src/main/res/drawable/ic_back_white.xml create mode 100644 app/src/main/res/drawable/ic_down_choose_white.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 91db46c..9008dda 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,11 +56,20 @@ dependencies { // To recognize Korean script implementation("com.google.mlkit:text-recognition-korean:16.0.0") - // CameraX + // CameraX core library + + implementation(libs.androidx.camera.core) + + // CameraX Camera2 extensions implementation(libs.androidx.camera.camera2) + + // CameraX Lifecycle library implementation(libs.androidx.camera.lifecycle) + + // CameraX View class implementation(libs.androidx.camera.view) + // 文本识别 // To recognize Latin script // implementation(libs.play.services.mlkit.text.recognition) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d35de4..2479661 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ = Build.VERSION_CODES.P) { val processName = getUniqueProcessName() if (processName != null && processName != packageName) { @@ -81,76 +76,4 @@ class MyApp : Application() { object Config { const val openSusViewMode: String = "open_sus_view" } - - private fun initLanguage() { - // 拿到最近一次的翻译情况,分别设置最近一次的,并赋值setSourceLanguage|setTargetLanguage - - // 以下是默认情况 - - val languages: ArrayList = LanguagesConstants.getInstance().getList(applicationContext) - if (languages.isNotEmpty()) { - for (language in languages) { - if ("Afrikaans" == language.language) { - tl = language - break - } - } - for (language in languages) { - if ("English" == language.language) { - sl = language - break - } - } - } - } - - private fun getSourceLanguageCode(): String { - if (sl == null) { - return "en" - } - return sl!!.languageCode - } - - private fun getSourceLanguage(): String { - if (sl == null) { - return "English" - } - return sl!!.language - } - - private fun getSourceSpeechCode(): String { - if (sl == null) { - return "en-GB" - } - return sl!!.speechCode - } - - private fun getTargetLanguageCode(): String { - if (tl == null) { - return "en" - } - return tl!!.languageCode - } - - private fun getTargetLanguage(): String { - if (tl == null) { - return "English" - } - return tl!!.language - } - - private fun getTargetSpeechCode(): String { - if (tl == null) { - return "en-GB" - } - return tl!!.speechCode - } - - private fun setSourceLanguage(language: Language) { - sl = language - } - - private fun setTargetLanguage(language: Language) { - tl = language - } } diff --git a/app/src/main/java/com/assimilate/alltrans/allservice/SusService.kt b/app/src/main/java/com/assimilate/alltrans/allservice/SusService.kt index 3eb84f1..ada6964 100644 --- a/app/src/main/java/com/assimilate/alltrans/allservice/SusService.kt +++ b/app/src/main/java/com/assimilate/alltrans/allservice/SusService.kt @@ -266,7 +266,7 @@ class SusService : Service() { ) bitmap.copyPixelsFromBuffer(buffer) image.close() - bindingSubGlobal.preview.setImageBitmap(bitmap) + bindingSubGlobal.susPreview.setImageBitmap(bitmap) tryReloadAndDetectInImage(bitmap) } stopSelf() diff --git a/app/src/main/java/com/assimilate/alltrans/common/InferenceInfoGraphic.java b/app/src/main/java/com/assimilate/alltrans/common/InferenceInfoGraphic.java index 703162c..e097e68 100755 --- a/app/src/main/java/com/assimilate/alltrans/common/InferenceInfoGraphic.java +++ b/app/src/main/java/com/assimilate/alltrans/common/InferenceInfoGraphic.java @@ -52,26 +52,26 @@ public class InferenceInfoGraphic extends GraphicOverlay.Graphic { float x = TEXT_SIZE * 0.5f; float y = TEXT_SIZE * 1.5f; - canvas.drawText( - "InputImage size: " + overlay.getImageHeight() + "x" + overlay.getImageWidth(), - x, - y, - textPaint); - - if (!showLatencyInfo) { - return; - } - // Draw FPS (if valid) and inference latency - if (framesPerSecond != null) { - canvas.drawText( - "FPS: " + framesPerSecond + ", Frame latency: " + frameLatency + " ms", - x, - y + TEXT_SIZE, - textPaint); - } else { - canvas.drawText("Frame latency: " + frameLatency + " ms", x, y + TEXT_SIZE, textPaint); - } - canvas.drawText( - "Detector latency: " + detectorLatency + " ms", x, y + TEXT_SIZE * 2, textPaint); +// canvas.drawText( +// "InputImage size: " + overlay.getImageHeight() + "x" + overlay.getImageWidth(), +// x, +// y, +// textPaint); +// +// if (!showLatencyInfo) { +// return; +// } +// // Draw FPS (if valid) and inference latency +// if (framesPerSecond != null) { +// canvas.drawText( +// "FPS: " + framesPerSecond + ", Frame latency: " + frameLatency + " ms", +// x, +// y + TEXT_SIZE, +// textPaint); +// } else { +// canvas.drawText("Frame latency: " + frameLatency + " ms", x, y + TEXT_SIZE, textPaint); +// } +// canvas.drawText( +// "Detector latency: " + detectorLatency + " ms", x, y + TEXT_SIZE * 2, textPaint); } } diff --git a/app/src/main/java/com/assimilate/alltrans/common/LanguagesConstants.java b/app/src/main/java/com/assimilate/alltrans/common/LanguagesConstants.java index e77523c..458e699 100644 --- a/app/src/main/java/com/assimilate/alltrans/common/LanguagesConstants.java +++ b/app/src/main/java/com/assimilate/alltrans/common/LanguagesConstants.java @@ -22,6 +22,7 @@ import java.util.Objects; public class LanguagesConstants { private static boolean instanced = false; private volatile static LanguagesConstants languagesConstant; + private final Gson gson = new Gson(); private final ArrayList languages; @@ -30,11 +31,11 @@ public class LanguagesConstants { if (instanced) { throw new RuntimeException("Instance multiple LanguagesConstants."); } else { - languages = new ArrayList(); + languages = new ArrayList<>(); instanced = true; } - if(languagesConstant != null) { + if (languagesConstant != null) { throw new RuntimeException("looper error(LanguagesConstants instanced)."); } } @@ -58,6 +59,40 @@ public class LanguagesConstants { return languages; } + // 根据语言代码获取 Language 对象 + public Language getLanguageByLanguageCode(@NonNull String languageCode, @NonNull Context context) { + ArrayList languages = getList(context); + for (Language lang : languages) { + if (lang.getLanguageCode().equalsIgnoreCase(languageCode)) { + return lang; + } + } + return null; + } + + // 获取 languageCode + public String getLanguageCodeByLanguage(@NonNull String language, @NonNull Context context) { + Language lang = getLanguageObjectByLanguage(language, context); + return lang != null ? lang.getLanguageCode() : "en"; + } + + // 获取 speechCode + public String getSpeechCodeByLanguage(@NonNull String language, @NonNull Context context) { + Language lang = getLanguageObjectByLanguage(language, context); + return lang != null ? lang.getSpeechCode() : "en"; + } + + // 获取 Language 对象 + private Language getLanguageObjectByLanguage(@NonNull String language, @NonNull Context context) { + ArrayList languages = getList(context); + for (Language lang : languages) { + if (lang.getLanguage().equalsIgnoreCase(language)) { + return lang; + } + } + return null; + } + private void getLanguages(final Context context) { InputStream inputStream = null; InputStreamReader inputStreamReader = null; @@ -70,7 +105,7 @@ public class LanguagesConstants { // step2. 读取json文件信息 final StringBuilder builder = new StringBuilder(); - String line = null; + String line; while ((line = bufferedReader.readLine()) != null) { builder.append(line); } @@ -78,8 +113,8 @@ public class LanguagesConstants { // step3. 将json文本转换成Java对象 final String result = builder.toString(); if (!TextUtils.isEmpty(result)) { - Gson gson = new Gson(); - Type listType = new TypeToken>(){}.getType(); + Type listType = new TypeToken>() { + }.getType(); List temp = gson.fromJson(result, listType); if (null != temp && !temp.isEmpty()) { diff --git a/app/src/main/java/com/assimilate/alltrans/common/PreferenceLanguageUtils.kt b/app/src/main/java/com/assimilate/alltrans/common/PreferenceLanguageUtils.kt new file mode 100644 index 0000000..eff9973 --- /dev/null +++ b/app/src/main/java/com/assimilate/alltrans/common/PreferenceLanguageUtils.kt @@ -0,0 +1,87 @@ +package com.assimilate.alltrans.common + +import android.content.Context +import android.content.SharedPreferences +import com.assimilate.alltrans.MyApp +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +object PreferenceLanguageUtils { + + private const val SP_NAME = "sp_language" + private const val KEY_RECENT_LANGUAGES = "recent_languages" + private const val MAX_RECENT_LANGUAGES = 5 + private const val PREF_KEY_FIRST_TIME = "first_time" + + + @Volatile + private var sharedPreferences: SharedPreferences? = null + + private fun getSharedPreferences(): SharedPreferences { + return sharedPreferences ?: synchronized(this) { + val instance = MyApp.applicationContext() + .getSharedPreferences(SP_NAME, Context.MODE_PRIVATE) + sharedPreferences = instance + instance + } + } + + + fun putString(key: String, value: String) { + getSharedPreferences().edit().putString(key, value).apply() + } + + fun getString(key: String, defValue: String = "english"): String { + val value = getSharedPreferences().getString(key, defValue) ?: defValue + return value + } + + fun clear() { + getSharedPreferences().edit().clear().apply() + } + + fun clearExceptReferrerUrl() { + val sharedPreferences = getSharedPreferences() + val editor = sharedPreferences.edit() + sharedPreferences.all.keys.forEach { key -> + if (key != "refe_url") { + editor.remove(key) + } + } + editor.apply() + } + + fun addRecentLanguage(language: Language) { + val recentLanguages = getRecentLanguages().toMutableList() + recentLanguages.remove(language) // Remove if already exists + recentLanguages.add(0, language) // Add to the beginning + if (recentLanguages.size > MAX_RECENT_LANGUAGES) { + recentLanguages.removeAt(recentLanguages.size - 1) // Remove the oldest + } + saveRecentLanguages(recentLanguages) + } + + fun getRecentLanguages(): List { + val json = getSharedPreferences().getString(KEY_RECENT_LANGUAGES, null) + return if (json != null) { + val type = object : TypeToken>() {}.type + Gson().fromJson(json, type) + } else { + emptyList() + } + } + + private fun saveRecentLanguages(languages: List) { + val json = Gson().toJson(languages) + getSharedPreferences().edit().putString(KEY_RECENT_LANGUAGES, json).apply() + } + + // 检查是否是第一次进入应用 + fun isFirstTime(): Boolean { + return getSharedPreferences().getBoolean(PREF_KEY_FIRST_TIME, true) + } + // 设置已经不是第一次进入应用了 + fun setNotFirstTime() { + getSharedPreferences().edit().putBoolean(PREF_KEY_FIRST_TIME, false).apply() + } +} diff --git a/app/src/main/java/com/assimilate/alltrans/common/PreferenceUtils.java b/app/src/main/java/com/assimilate/alltrans/common/PreferenceUtils.java index 9e1c6e6..4af22b6 100755 --- a/app/src/main/java/com/assimilate/alltrans/common/PreferenceUtils.java +++ b/app/src/main/java/com/assimilate/alltrans/common/PreferenceUtils.java @@ -24,7 +24,7 @@ public class PreferenceUtils { public static boolean shouldGroupRecognizedTextInBlocks(Context context) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); String prefKey = context.getString(R.string.pref_key_group_recognized_text_in_blocks); - return sharedPreferences.getBoolean(prefKey, false); + return sharedPreferences.getBoolean(prefKey, true); } public static boolean showLanguageTag(Context context) { diff --git a/app/src/main/java/com/assimilate/alltrans/common/TextGraphic.kt b/app/src/main/java/com/assimilate/alltrans/common/TextGraphic.kt index df70440..8433f2b 100644 --- a/app/src/main/java/com/assimilate/alltrans/common/TextGraphic.kt +++ b/app/src/main/java/com/assimilate/alltrans/common/TextGraphic.kt @@ -1,23 +1,23 @@ package com.assimilate.alltrans.common + import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.RectF +import android.os.Handler +import android.os.Looper import android.text.TextPaint import android.util.Log +import com.assimilate.alltrans.MyApp import com.assimilate.alltrans.curview.GraphicOverlay - +import com.assimilate.alltrans.http.GoogleTranslator +import com.assimilate.alltrans.http.Translator import com.google.mlkit.vision.text.Text import java.util.Arrays import kotlin.math.max import kotlin.math.min -/** - * Graphic instance for rendering TextBlock position, size, and ID within an associated graphic - * overlay view. - */ -class TextGraphic -constructor( +class TextGraphic( overlay: GraphicOverlay?, private val text: Text, private val shouldGroupTextInBlocks: Boolean, @@ -28,14 +28,15 @@ constructor( private val rectPaint: Paint = Paint() private val textPaint: TextPaint private val labelPaint: Paint + private val handler = Handler(Looper.getMainLooper()) init { + prepareTranslation() rectPaint.color = MARKER_COLOR rectPaint.style = Paint.Style.STROKE rectPaint.strokeWidth = STROKE_WIDTH textPaint = TextPaint() textPaint.color = TEXT_COLOR - textPaint.textSize = TEXT_SIZE labelPaint = Paint() labelPaint.color = MARKER_COLOR labelPaint.style = Paint.Style.FILL @@ -43,25 +44,70 @@ constructor( postInvalidate() } - /** Draws the text block annotations for position, size, and raw value on the supplied canvas. */ + private var translatedTextBlocks: List = listOf() + + // Method to prepare translation before drawing + private fun prepareTranslation() { + Thread { + val textToTranslate = StringBuilder() + + // Collect all text to be translated and append delimiter + for (textBlock in text.textBlocks) { + val textWithDelimiter = textBlock.text + DELIMITER + textToTranslate.append(textWithDelimiter) + } + val lanSourceCode = LanguagesConstants.getInstance().getLanguageCodeByLanguage( + PreferenceLanguageUtils.getString("language_source"), + MyApp.applicationContext() + ) + val lanTargetCode = LanguagesConstants.getInstance().getLanguageCodeByLanguage( + PreferenceLanguageUtils.getString("language_target"), + MyApp.applicationContext() + ) + // Define translation parameters + val param = HashMap().apply { + put("sourceLanguage", lanSourceCode) + put("translationLanguage", lanTargetCode) + put("text", textToTranslate.toString()) + } + + val translator: Translator = + GoogleTranslator() + + // Perform translation + translator.translate(param, GoogleTranslator.GoogleTranslateCallback { translatedText -> + // Split translated text by delimiter + translatedTextBlocks = + translatedText.split(DELIMITER.toRegex()).dropLastWhile { it.isEmpty() } + + // Update UI thread + handler.post { + postInvalidate() // Notify to redraw + } + }) + }.start() + } + override fun draw(canvas: Canvas) { + Log.d(TAG, "Text is: " + text.text) - for (textBlock in text.textBlocks) { // Renders the text at the bottom of the box. - Log.d(TAG, "TextBlock text is: " + textBlock.text) - Log.d(TAG, "TextBlock recognizedLanguage is: " + textBlock.recognizedLanguage) - Log.d(TAG, "TextBlock boundingbox is: " + textBlock.boundingBox) - Log.d(TAG, "TextBlock cornerpoint is: " + Arrays.toString(textBlock.cornerPoints)) + for ((translatedIndex, textBlock) in text.textBlocks.withIndex()) { + val translatedBlockText = + if (translatedIndex < translatedTextBlocks.size) translatedTextBlocks[translatedIndex] else textBlock.text + val height1 = ((textBlock.boundingBox?.bottom?.toFloat() + ?: 30f) - (textBlock.boundingBox?.top?.toFloat() + ?: 0f)) / textBlock.lines.size if (shouldGroupTextInBlocks) { drawText( getFormattedText( - textBlock.text, + translatedBlockText, textBlock.recognizedLanguage, confidence = null ), RectF(textBlock.boundingBox), - TEXT_SIZE * textBlock.lines.size + 2 * STROKE_WIDTH, + height1 - 3 * STROKE_WIDTH, canvas ) } else { @@ -71,12 +117,18 @@ constructor( Log.d(TAG, "Line cornerpoint is: " + Arrays.toString(line.cornerPoints)) Log.d(TAG, "Line confidence is: " + line.confidence) Log.d(TAG, "Line angle is: " + line.angle) - // Draws the bounding box around the TextBlock. + // Draw the bounding box around the TextBlock. val rect = RectF(line.boundingBox) drawText( - getFormattedText(line.text, line.recognizedLanguage, line.confidence), + getFormattedText( + translatedBlockText, + line.recognizedLanguage, + line.confidence + ), rect, - TEXT_SIZE + 2 * STROKE_WIDTH, + ((line.boundingBox?.bottom?.toFloat() + ?: 20f) - (line.boundingBox?.top?.toFloat() + ?: 0f)) - 2 * STROKE_WIDTH, canvas ) for (element in line.elements) { @@ -103,79 +155,6 @@ constructor( } } } - - - val traString = - "aaaaaaaaaaa(`_`))bbbbbbbbbbb((`_`)ccccc(`_`)dhsihs(`_`)dhksskjh(`_`)dhskjfhsdkjfj(`_`)" - val delimiter = "(`_`)" - val parts = traString.split(delimiter) - - for (part in parts) { - Log.d(TAG, "TextBlock aaaaa is: $part") - - } - - // 遍历每个TextBlock并处理分割后的部分 - for ((index, textBlock) in text.textBlocks.withIndex()) { // 使用withIndex()获取索引 - Log.d(TAG, "TextBlock text is: " + textBlock.text) - Log.d(TAG, "TextBlock recognizedLanguage is: " + textBlock.recognizedLanguage) - Log.d(TAG, "TextBlock boundingbox is: " + textBlock.boundingBox) - Log.d(TAG, "TextBlock cornerpoint is: " + Arrays.toString(textBlock.cornerPoints)) - - if (shouldGroupTextInBlocks) { - // 获取当前索引对应的part - val part = if (index < parts.size) parts[index] else "" - - drawText( - getFormattedText( - part, - textBlock.recognizedLanguage, - confidence = null - ), - RectF(textBlock.boundingBox), - TEXT_SIZE * textBlock.lines.size + 2 * STROKE_WIDTH, - canvas - ) - } else { - for (line in textBlock.lines) { - Log.d(TAG, "Line text is: " + line.text) - Log.d(TAG, "Line boundingbox is: " + line.boundingBox) - Log.d(TAG, "Line cornerpoint is: " + Arrays.toString(line.cornerPoints)) - Log.d(TAG, "Line confidence is: " + line.confidence) - Log.d(TAG, "Line angle is: " + line.angle) - // Draws the bounding box around the TextBlock. - val rect = RectF(line.boundingBox) - drawText( - getFormattedText(line.text, line.recognizedLanguage, line.confidence), - rect, - TEXT_SIZE + 2 * STROKE_WIDTH, - canvas - ) - for (element in line.elements) { - Log.d(TAG, "Element text is: " + element.text) - Log.d(TAG, "Element boundingbox is: " + element.boundingBox) - Log.d( - TAG, - "Element cornerpoint is: " + Arrays.toString(element.cornerPoints) - ) - Log.d(TAG, "Element language is: " + element.recognizedLanguage) - Log.d(TAG, "Element confidence is: " + element.confidence) - Log.d(TAG, "Element angle is: " + element.angle) - for (symbol in element.symbols) { - Log.d(TAG, "Symbol text is: " + symbol.text) - Log.d(TAG, "Symbol boundingbox is: " + symbol.boundingBox) - Log.d( - TAG, - "Symbol cornerpoint is: " + Arrays.toString(symbol.cornerPoints) - ) - Log.d(TAG, "Symbol confidence is: " + symbol.confidence) - Log.d(TAG, "Symbol angle is: " + symbol.angle) - } - } - } - } - } - } private fun getFormattedText(text: String, languageTag: String, confidence: Float?): String { @@ -189,35 +168,78 @@ constructor( else res } - private fun drawText(text: String, rect: RectF, textHeight: Float, canvas: Canvas) { - // If the image is flipped, the left will be translated to right, and the right to left. + private fun drawText(text: String, rect: RectF, textSize: Float, canvas: Canvas) { val x0 = translateX(rect.left) val x1 = translateX(rect.right) rect.left = min(x0, x1) rect.right = max(x0, x1) rect.top = translateY(rect.top) rect.bottom = translateY(rect.bottom) - canvas.drawRect(rect, rectPaint) - val textWidth = textPaint.measureText(text) + + // Set initial text size + textPaint.textSize = textSize + + // Break the text into multiple lines if necessary + var lines = wrapText(text.trim(), rect.width()) + + // Calculate total height of the text + val totalTextHeight = textPaint.fontMetrics.descent - textPaint.fontMetrics.ascent + val totalTextHeightWithSpacing = totalTextHeight * lines.size + + // Adjust the text size if the total height is greater than the rectangle height + if (totalTextHeightWithSpacing > rect.height()) { + textPaint.textSize *= rect.height() / totalTextHeightWithSpacing + lines = wrapText(text.trim(), rect.width()) + } else if (totalTextHeightWithSpacing < rect.height()) { + // If the total text height is less than the rectangle height, increase text size + textPaint.textSize *= rect.height() / totalTextHeightWithSpacing + lines = wrapText(text.trim(), rect.width()) + } + + // Calculate new total height with adjusted text size + val finalTextHeight = textPaint.fontMetrics.descent - textPaint.fontMetrics.ascent + val finalTotalTextHeightWithSpacing = finalTextHeight * lines.size + + // Calculate starting Y coordinate to center the text vertically + var textY = + rect.top + ((rect.height() - finalTotalTextHeightWithSpacing) / 2) - textPaint.fontMetrics.ascent + + // Draw the background rectangle canvas.drawRect( rect.left - STROKE_WIDTH, - rect.top - textHeight, - rect.left + textWidth + 2 * STROKE_WIDTH, - rect.top, + rect.top - STROKE_WIDTH, + rect.right + STROKE_WIDTH, + rect.bottom + STROKE_WIDTH, labelPaint ) - // Renders the text at the bottom of the box. - canvas.drawText(text, rect.left, rect.top - STROKE_WIDTH, textPaint) + + // Draw each line of text + for (line in lines) { + canvas.drawText(line, rect.left, textY, textPaint) + textY += finalTextHeight + } + } + + private fun wrapText(text: String, maxWidth: Float): List { + val lines = mutableListOf() + var remainingText = text.trim() + + while (remainingText.isNotEmpty()) { + val breakPoint = textPaint.breakText(remainingText, true, maxWidth, null) + val line = remainingText.substring(0, breakPoint) + lines.add(line) + remainingText = remainingText.substring(breakPoint) + } + + return lines } companion object { + private const val DELIMITER = "`0_.._0`" private const val TAG = "TextGraphic" private const val TEXT_WITH_LANGUAGE_TAG_FORMAT = "%s:%s" - private const val TEXT_COLOR = Color.BLACK - private const val MARKER_COLOR = Color.GREEN - private const val TEXT_SIZE = 54.0f - private const val STROKE_WIDTH = 4.0f + private val TEXT_COLOR = Color.parseColor("#FF474747") + private val MARKER_COLOR = Color.parseColor("#FFD9D9D9") + private const val STROKE_WIDTH = 2.0f } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/assimilate/alltrans/common/TextRecognitionProcessor.kt b/app/src/main/java/com/assimilate/alltrans/common/TextRecognitionProcessor.kt index fe9151a..ac1e6f4 100644 --- a/app/src/main/java/com/assimilate/alltrans/common/TextRecognitionProcessor.kt +++ b/app/src/main/java/com/assimilate/alltrans/common/TextRecognitionProcessor.kt @@ -2,8 +2,12 @@ package com.assimilate.alltrans.common import android.content.Context +import android.text.TextUtils import android.util.Log +import com.assimilate.alltrans.MyApp import com.assimilate.alltrans.curview.GraphicOverlay +import com.assimilate.alltrans.http.GoogleTranslator +import com.assimilate.alltrans.http.Translator import com.google.android.gms.tasks.Task import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.text.Text @@ -31,6 +35,9 @@ class TextRecognitionProcessor( return textRecognizer.process(image) } + + + override fun onSuccess(text: Text, graphicOverlay: GraphicOverlay) { Log.d(TAG, "On-device Text detection successful") logExtrasForTesting(text) diff --git a/app/src/main/java/com/assimilate/alltrans/common/Widget.java b/app/src/main/java/com/assimilate/alltrans/common/Widget.java index 954e562..c84a47e 100644 --- a/app/src/main/java/com/assimilate/alltrans/common/Widget.java +++ b/app/src/main/java/com/assimilate/alltrans/common/Widget.java @@ -1,10 +1,16 @@ package com.assimilate.alltrans.common; import android.app.Activity; +import android.graphics.Color; +import android.view.Gravity; +import android.view.View; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import com.google.android.material.snackbar.Snackbar; + public class Widget { private static volatile Toast toast; @@ -15,4 +21,18 @@ public class Widget { toast = Toast.makeText(mActivity, msg, Toast.LENGTH_SHORT); toast.show(); } + + public static void makeSnackbar(@NonNull final Activity mActivity, @NonNull final String msg) { + View view = mActivity.findViewById(android.R.id.content); + Snackbar snackbar = Snackbar.make(view, msg, Snackbar.LENGTH_LONG); + View snackbarView = snackbar.getView(); + snackbarView.setBackgroundColor(Color.parseColor("#66000000")); + + TextView textView = snackbarView.findViewById(com.google.android.material.R.id.snackbar_text); + textView.setTextColor(Color.WHITE); + textView.setTextSize(16f); + textView.setGravity(Gravity.CENTER); + + snackbar.show(); + } } diff --git a/app/src/main/java/com/assimilate/alltrans/curview/GraphicOverlay.java b/app/src/main/java/com/assimilate/alltrans/curview/GraphicOverlay.java index 2b49a0e..53ae5c1 100755 --- a/app/src/main/java/com/assimilate/alltrans/curview/GraphicOverlay.java +++ b/app/src/main/java/com/assimilate/alltrans/curview/GraphicOverlay.java @@ -9,6 +9,7 @@ import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import com.google.common.base.Preconditions; @@ -37,266 +38,294 @@ import java.util.List; * */ public class GraphicOverlay extends View { - private final Object lock = new Object(); - private final List graphics = new ArrayList<>(); - // Matrix for transforming from image coordinates to overlay view coordinates. - private final Matrix transformationMatrix = new Matrix(); + private final Object lock = new Object(); + private final List graphics = new ArrayList<>(); + // Matrix for transforming from image coordinates to overlay view coordinates. + private final Matrix transformationMatrix = new Matrix(); - private int imageWidth; - private int imageHeight; - // The factor of overlay View size to image size. Anything in the image coordinates need to be - // scaled by this amount to fit with the area of overlay View. - private float scaleFactor = 1.0f; - // The number of horizontal pixels needed to be cropped on each side to fit the image with the - // area of overlay View after scaling. - private float postScaleWidthOffset; - // The number of vertical pixels needed to be cropped on each side to fit the image with the - // area of overlay View after scaling. - private float postScaleHeightOffset; - private boolean isImageFlipped; - private boolean needUpdateTransformation = true; + private int imageWidth; + private int imageHeight; + // The factor of overlay View size to image size. Anything in the image coordinates need to be + // scaled by this amount to fit with the area of overlay View. + private float scaleFactor = 1.0f; + // The number of horizontal pixels needed to be cropped on each side to fit the image with the + // area of overlay View after scaling. + private float postScaleWidthOffset; + // The number of vertical pixels needed to be cropped on each side to fit the image with the + // area of overlay View after scaling. + private float postScaleHeightOffset; + private boolean isImageFlipped; + private boolean needUpdateTransformation = true; - /** - * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass - * this and implement the {@link Graphic#draw(Canvas)} method to define the graphics element. Add - * instances to the overlay using {@link GraphicOverlay#add(Graphic)}. - */ - public abstract static class Graphic { - private GraphicOverlay overlay; + /** + * Base class for a custom graphics object to be rendered within the graphic overlay. Subclass + * this and implement the {@link Graphic#draw(Canvas)} method to define the graphics element. Add + * instances to the overlay using {@link GraphicOverlay#add(Graphic)}. + */ + public abstract static class Graphic { + private GraphicOverlay overlay; - public Graphic(GraphicOverlay overlay) { - this.overlay = overlay; + public Graphic(GraphicOverlay overlay) { + this.overlay = overlay; + } + + /** + * Draw the graphic on the supplied canvas. Drawing should use the following methods to convert + * to view coordinates for the graphics that are drawn: + * + *
    + *
  1. {@link Graphic#scale(float)} adjusts the size of the supplied value from the image + * scale to the view scale. + *
  2. {@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the + * coordinate from the image's coordinate system to the view coordinate system. + *
+ * + * @param canvas drawing canvas + */ + public abstract void draw(Canvas canvas); + + protected void drawRect( + Canvas canvas, float left, float top, float right, float bottom, Paint paint) { + canvas.drawRect(left, top, right, bottom, paint); + } + + protected void drawText(Canvas canvas, String text, float x, float y, Paint paint) { + canvas.drawText(text, x, y, paint); + } + + /** + * Adjusts the supplied value from the image scale to the view scale. + */ + public float scale(float imagePixel) { + return imagePixel * overlay.scaleFactor; + } + + /** + * Returns the application context of the app. + */ + public Context getApplicationContext() { + return overlay.getContext().getApplicationContext(); + } + + public boolean isImageFlipped() { + return overlay.isImageFlipped; + } + + /** + * Adjusts the x coordinate from the image's coordinate system to the view coordinate system. + */ + public float translateX(float x) { + if (overlay.isImageFlipped) { + return overlay.getWidth() - (scale(x) - overlay.postScaleWidthOffset); + } else { + return scale(x) - overlay.postScaleWidthOffset; + } + } + + /** + * Adjusts the y coordinate from the image's coordinate system to the view coordinate system. + */ + public float translateY(float y) { + return scale(y) - overlay.postScaleHeightOffset; + } + + /** + * Returns a {@link Matrix} for transforming from image coordinates to overlay view coordinates. + */ + public Matrix getTransformationMatrix() { + return overlay.transformationMatrix; + } + + public void postInvalidate() { + overlay.postInvalidate(); + } + + /** + * Given the {@code zInImagePixel}, update the color for the passed in {@code paint}. The color will be + * more red if the {@code zInImagePixel} is smaller, or more blue ish vice versa. This is + * useful to visualize the z value of landmarks via color for features like Pose and Face Mesh. + * + * @param paint the paint to update color with + * @param canvas the canvas used to draw with paint + * @param visualizeZ if true, paint color will be changed. + * @param rescaleZForVisualization if true, re-scale the z value with zMin and zMax to make + * color more distinguishable + * @param zInImagePixel the z value used to update the paint color + * @param zMin min value of all z values going to be passed in + * @param zMax max value of all z values going to be passed in + */ + public void updatePaintColorByZValue( + Paint paint, + Canvas canvas, + boolean visualizeZ, + boolean rescaleZForVisualization, + float zInImagePixel, + float zMin, + float zMax) { + if (!visualizeZ) { + return; + } + + // When visualizeZ is true, sets up the paint to different colors based on z values. + // Gets the range of z value. + float zLowerBoundInScreenPixel; + float zUpperBoundInScreenPixel; + + if (rescaleZForVisualization) { + zLowerBoundInScreenPixel = min(-0.001f, scale(zMin)); + zUpperBoundInScreenPixel = max(0.001f, scale(zMax)); + } else { + // By default, assume the range of z value in screen pixel is [-canvasWidth, canvasWidth]. + float defaultRangeFactor = 1f; + zLowerBoundInScreenPixel = -defaultRangeFactor * canvas.getWidth(); + zUpperBoundInScreenPixel = defaultRangeFactor * canvas.getWidth(); + } + + float zInScreenPixel = scale(zInImagePixel); + + if (zInScreenPixel < 0) { + // Sets up the paint to be red if the item is in front of the z origin. + // Maps values within [zLowerBoundInScreenPixel, 0) to [255, 0) and use it to control the + // color. The larger the value is, the more red it will be. + int v = (int) (zInScreenPixel / zLowerBoundInScreenPixel * 255); + v = Ints.constrainToRange(v, 0, 255); + paint.setARGB(255, 255, 255 - v, 255 - v); + } else { + // Sets up the paint to be blue if the item is behind the z origin. + // Maps values within [0, zUpperBoundInScreenPixel] to [0, 255] and use it to control the + // color. The larger the value is, the more blue it will be. + int v = (int) (zInScreenPixel / zUpperBoundInScreenPixel * 255); + v = Ints.constrainToRange(v, 0, 255); + paint.setARGB(255, 255 - v, 255 - v, 255); + } + } + } + + public GraphicOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + addOnLayoutChangeListener( + (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + needUpdateTransformation = true); } /** - * Draw the graphic on the supplied canvas. Drawing should use the following methods to convert - * to view coordinates for the graphics that are drawn: + * Removes all graphics from the overlay. + */ + public void clear() { + synchronized (lock) { + graphics.clear(); + } + postInvalidate(); + } + + /** + * Adds a graphic to the overlay. + */ + public void add(Graphic graphic) { + synchronized (lock) { + graphics.add(graphic); + } + } + + /** + * Removes a graphic from the overlay. + */ + public void remove(Graphic graphic) { + synchronized (lock) { + graphics.remove(graphic); + } + postInvalidate(); + } + + /** + * Sets the source information of the image being processed by detectors, including size and + * whether it is flipped, which informs how to transform image coordinates later. * - *
    - *
  1. {@link Graphic#scale(float)} adjusts the size of the supplied value from the image - * scale to the view scale. - *
  2. {@link Graphic#translateX(float)} and {@link Graphic#translateY(float)} adjust the - * coordinate from the image's coordinate system to the view coordinate system. - *
- * - * @param canvas drawing canvas + * @param imageWidth the width of the image sent to ML Kit detectors + * @param imageHeight the height of the image sent to ML Kit detectors + * @param isFlipped whether the image is flipped. Should set it to true when the image is from the + * front camera. */ - public abstract void draw(Canvas canvas); - - protected void drawRect( - Canvas canvas, float left, float top, float right, float bottom, Paint paint) { - canvas.drawRect(left, top, right, bottom, paint); + public void setImageSourceInfo(int imageWidth, int imageHeight, boolean isFlipped) { + Preconditions.checkState(imageWidth > 0, "image width must be positive"); + Preconditions.checkState(imageHeight > 0, "image height must be positive"); + synchronized (lock) { + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.isImageFlipped = isFlipped; + needUpdateTransformation = true; + } + postInvalidate(); } - protected void drawText(Canvas canvas, String text, float x, float y, Paint paint) { - canvas.drawText(text, x, y, paint); + public int getImageWidth() { + return imageWidth; } - /** Adjusts the supplied value from the image scale to the view scale. */ - public float scale(float imagePixel) { - return imagePixel * overlay.scaleFactor; + public int getImageHeight() { + return imageHeight; } - /** Returns the application context of the app. */ - public Context getApplicationContext() { - return overlay.getContext().getApplicationContext(); - } + private void updateTransformationIfNeeded() { + if (!needUpdateTransformation || imageWidth <= 0 || imageHeight <= 0) { + return; + } + float viewAspectRatio = (float) getWidth() / getHeight(); + float imageAspectRatio = (float) imageWidth / imageHeight; + postScaleWidthOffset = 0; + postScaleHeightOffset = 0; + if (viewAspectRatio > imageAspectRatio) { + // The image needs to be vertically cropped to be displayed in this view. + scaleFactor = (float) getWidth() / imageWidth; + postScaleHeightOffset = ((float) getWidth() / imageAspectRatio - getHeight()) / 2; + } else { + // The image needs to be horizontally cropped to be displayed in this view. + scaleFactor = (float) getHeight() / imageHeight; + postScaleWidthOffset = ((float) getHeight() * imageAspectRatio - getWidth()) / 2; + } - public boolean isImageFlipped() { - return overlay.isImageFlipped; + transformationMatrix.reset(); + transformationMatrix.setScale(scaleFactor, scaleFactor); + transformationMatrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset); + + if (isImageFlipped) { + transformationMatrix.postScale(-1f, 1f, getWidth() / 2f, getHeight() / 2f); + } + + needUpdateTransformation = false; } /** - * Adjusts the x coordinate from the image's coordinate system to the view coordinate system. + * Draws the overlay with its associated graphic objects. */ - public float translateX(float x) { - if (overlay.isImageFlipped) { - return overlay.getWidth() - (scale(x) - overlay.postScaleWidthOffset); - } else { - return scale(x) - overlay.postScaleWidthOffset; - } + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + synchronized (lock) { + updateTransformationIfNeeded(); + + for (Graphic graphic : graphics) { + graphic.draw(canvas); + } + } } - /** - * Adjusts the y coordinate from the image's coordinate system to the view coordinate system. - */ - public float translateY(float y) { - return scale(y) - overlay.postScaleHeightOffset; + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + + setVisibility(View.INVISIBLE); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + + setVisibility(View.VISIBLE); + return true; + } + return super.onTouchEvent(event); } - /** - * Returns a {@link Matrix} for transforming from image coordinates to overlay view coordinates. - */ - public Matrix getTransformationMatrix() { - return overlay.transformationMatrix; - } - - public void postInvalidate() { - overlay.postInvalidate(); - } - - /** - * Given the {@code zInImagePixel}, update the color for the passed in {@code paint}. The color will be - * more red if the {@code zInImagePixel} is smaller, or more blue ish vice versa. This is - * useful to visualize the z value of landmarks via color for features like Pose and Face Mesh. - * - * @param paint the paint to update color with - * @param canvas the canvas used to draw with paint - * @param visualizeZ if true, paint color will be changed. - * @param rescaleZForVisualization if true, re-scale the z value with zMin and zMax to make - * color more distinguishable - * @param zInImagePixel the z value used to update the paint color - * @param zMin min value of all z values going to be passed in - * @param zMax max value of all z values going to be passed in - */ - public void updatePaintColorByZValue( - Paint paint, - Canvas canvas, - boolean visualizeZ, - boolean rescaleZForVisualization, - float zInImagePixel, - float zMin, - float zMax) { - if (!visualizeZ) { - return; - } - - // When visualizeZ is true, sets up the paint to different colors based on z values. - // Gets the range of z value. - float zLowerBoundInScreenPixel; - float zUpperBoundInScreenPixel; - - if (rescaleZForVisualization) { - zLowerBoundInScreenPixel = min(-0.001f, scale(zMin)); - zUpperBoundInScreenPixel = max(0.001f, scale(zMax)); - } else { - // By default, assume the range of z value in screen pixel is [-canvasWidth, canvasWidth]. - float defaultRangeFactor = 1f; - zLowerBoundInScreenPixel = -defaultRangeFactor * canvas.getWidth(); - zUpperBoundInScreenPixel = defaultRangeFactor * canvas.getWidth(); - } - - float zInScreenPixel = scale(zInImagePixel); - - if (zInScreenPixel < 0) { - // Sets up the paint to be red if the item is in front of the z origin. - // Maps values within [zLowerBoundInScreenPixel, 0) to [255, 0) and use it to control the - // color. The larger the value is, the more red it will be. - int v = (int) (zInScreenPixel / zLowerBoundInScreenPixel * 255); - v = Ints.constrainToRange(v, 0, 255); - paint.setARGB(255, 255, 255 - v, 255 - v); - } else { - // Sets up the paint to be blue if the item is behind the z origin. - // Maps values within [0, zUpperBoundInScreenPixel] to [0, 255] and use it to control the - // color. The larger the value is, the more blue it will be. - int v = (int) (zInScreenPixel / zUpperBoundInScreenPixel * 255); - v = Ints.constrainToRange(v, 0, 255); - paint.setARGB(255, 255 - v, 255 - v, 255); - } - } - } - - public GraphicOverlay(Context context, AttributeSet attrs) { - super(context, attrs); - addOnLayoutChangeListener( - (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> - needUpdateTransformation = true); - } - - /** Removes all graphics from the overlay. */ - public void clear() { - synchronized (lock) { - graphics.clear(); - } - postInvalidate(); - } - - /** Adds a graphic to the overlay. */ - public void add(Graphic graphic) { - synchronized (lock) { - graphics.add(graphic); - } - } - - /** Removes a graphic from the overlay. */ - public void remove(Graphic graphic) { - synchronized (lock) { - graphics.remove(graphic); - } - postInvalidate(); - } - - /** - * Sets the source information of the image being processed by detectors, including size and - * whether it is flipped, which informs how to transform image coordinates later. - * - * @param imageWidth the width of the image sent to ML Kit detectors - * @param imageHeight the height of the image sent to ML Kit detectors - * @param isFlipped whether the image is flipped. Should set it to true when the image is from the - * front camera. - */ - public void setImageSourceInfo(int imageWidth, int imageHeight, boolean isFlipped) { - Preconditions.checkState(imageWidth > 0, "image width must be positive"); - Preconditions.checkState(imageHeight > 0, "image height must be positive"); - synchronized (lock) { - this.imageWidth = imageWidth; - this.imageHeight = imageHeight; - this.isImageFlipped = isFlipped; - needUpdateTransformation = true; - } - postInvalidate(); - } - - public int getImageWidth() { - return imageWidth; - } - - public int getImageHeight() { - return imageHeight; - } - - private void updateTransformationIfNeeded() { - if (!needUpdateTransformation || imageWidth <= 0 || imageHeight <= 0) { - return; - } - float viewAspectRatio = (float) getWidth() / getHeight(); - float imageAspectRatio = (float) imageWidth / imageHeight; - postScaleWidthOffset = 0; - postScaleHeightOffset = 0; - if (viewAspectRatio > imageAspectRatio) { - // The image needs to be vertically cropped to be displayed in this view. - scaleFactor = (float) getWidth() / imageWidth; - postScaleHeightOffset = ((float) getWidth() / imageAspectRatio - getHeight()) / 2; - } else { - // The image needs to be horizontally cropped to be displayed in this view. - scaleFactor = (float) getHeight() / imageHeight; - postScaleWidthOffset = ((float) getHeight() * imageAspectRatio - getWidth()) / 2; - } - - transformationMatrix.reset(); - transformationMatrix.setScale(scaleFactor, scaleFactor); - transformationMatrix.postTranslate(-postScaleWidthOffset, -postScaleHeightOffset); - - if (isImageFlipped) { - transformationMatrix.postScale(-1f, 1f, getWidth() / 2f, getHeight() / 2f); - } - - needUpdateTransformation = false; - } - - /** Draws the overlay with its associated graphic objects. */ - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - synchronized (lock) { - updateTransformationIfNeeded(); - - for (Graphic graphic : graphics) { - graphic.draw(canvas); - } - } - } - } diff --git a/app/src/main/java/com/assimilate/alltrans/viewui/HistoryActivity.java b/app/src/main/java/com/assimilate/alltrans/viewui/HistoryActivity.java index 13c3a92..52d2358 100644 --- a/app/src/main/java/com/assimilate/alltrans/viewui/HistoryActivity.java +++ b/app/src/main/java/com/assimilate/alltrans/viewui/HistoryActivity.java @@ -122,7 +122,7 @@ public class HistoryActivity extends AppCompatActivity { @Override public void onClick(View v) { if (ids.isEmpty()) { - Widget.makeToast(HistoryActivity.this, "Noting to remove."); + Widget.makeToast(HistoryActivity.this, "Noting to remove."); return; } diff --git a/app/src/main/java/com/assimilate/alltrans/viewui/LanguageChangeActivity.kt b/app/src/main/java/com/assimilate/alltrans/viewui/LanguageChangeActivity.kt index 8f77b68..e497cac 100644 --- a/app/src/main/java/com/assimilate/alltrans/viewui/LanguageChangeActivity.kt +++ b/app/src/main/java/com/assimilate/alltrans/viewui/LanguageChangeActivity.kt @@ -7,64 +7,102 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.recyclerview.widget.LinearLayoutManager -import com.assimilate.alltrans.MyApp import com.assimilate.alltrans.R import com.assimilate.alltrans.adapters.LanguageAdapter import com.assimilate.alltrans.common.Language import com.assimilate.alltrans.common.LanguagesConstants +import com.assimilate.alltrans.common.PreferenceLanguageUtils import com.assimilate.alltrans.databinding.ActivityLanguageChangeBinding class LanguageChangeActivity : AppCompatActivity() { private lateinit var binding: ActivityLanguageChangeBinding private var lastTranslateLanguage = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityLanguageChangeBinding.inflate(layoutInflater) enableEdgeToEdge() setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } + initView() initList() initClick() } + private fun initView() { + binding.tvChangeSource.text = PreferenceLanguageUtils.getString("language_source") + binding.tvChangeTarget.text = PreferenceLanguageUtils.getString("language_target") + updateRecentLanguages() + } + + private fun updateRecentLanguages() { + val recentLanguages = PreferenceLanguageUtils.getRecentLanguages() + if (recentLanguages.isNotEmpty()) { + val recentAdapter = LanguageAdapter(this, ArrayList(recentLanguages)) { _, language -> + Log.d("LanguageChange", language.language) + if (lastTranslateLanguage) { + PreferenceLanguageUtils.putString("language_target", language.language) + onBackPressed() + } else { + PreferenceLanguageUtils.putString("language_source", language.language) + } + PreferenceLanguageUtils.addRecentLanguage(language) + binding.tvChangeSource.text = PreferenceLanguageUtils.getString("language_source") + binding.tvChangeTarget.text = PreferenceLanguageUtils.getString("language_target") + updateRecentLanguages() + } + binding.listLanCommon5.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + binding.listLanCommon5.adapter = recentAdapter + } + } + private fun initClick() { binding.tvChangeSource.setOnClickListener { lastTranslateLanguage = false } binding.tvChangeTarget.setOnClickListener { lastTranslateLanguage = true + } + binding.tvExchange.setOnClickListener { + binding.tvExchange.setOnClickListener { + val currentSourceLanguage = PreferenceLanguageUtils.getString("language_source") + val currentTargetLanguage = PreferenceLanguageUtils.getString("language_target") + // 交换源语言和目标语言 + PreferenceLanguageUtils.putString("language_source", currentTargetLanguage) + PreferenceLanguageUtils.putString("language_target", currentSourceLanguage) + // 更新界面显示 + binding.tvChangeSource.text = currentTargetLanguage + binding.tvChangeTarget.text = currentSourceLanguage + onBackPressed() + } } } private fun initList() { - val languages: ArrayList = LanguagesConstants.getInstance().getList( - this - ) + val languages: ArrayList = LanguagesConstants.getInstance().getList(this) if (languages.isNotEmpty()) { - val adapter = - LanguageAdapter( - this, languages - ) { _, language -> - - Log.d("fsdafsdfd", language.language) - if (lastTranslateLanguage) { - MyApp.setTargetLanguage(language) - onBackPressed() - } else { - MyApp.setSourceLanguage(language) - } - binding.tvChangeSource.text = MyApp.getSourceLanguage() - binding.tvChangeTarget.text = MyApp.getTargetLanguage() + val adapter = LanguageAdapter(this, languages) { _, language -> + Log.d("LanguageChange", language.language) + if (lastTranslateLanguage) { + PreferenceLanguageUtils.putString("language_target", language.language) + onBackPressed() + } else { + PreferenceLanguageUtils.putString("language_source", language.language) } - val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) - binding.listLanguages.setLayoutManager(layoutManager) - binding.listLanguages.setAdapter(adapter) + PreferenceLanguageUtils.addRecentLanguage(language) + binding.tvChangeSource.text = PreferenceLanguageUtils.getString("language_source") + binding.tvChangeTarget.text = PreferenceLanguageUtils.getString("language_target") + updateRecentLanguages() + } + binding.listLanguages.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + binding.listLanguages.adapter = adapter } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/assimilate/alltrans/viewui/MainActivity.kt b/app/src/main/java/com/assimilate/alltrans/viewui/MainActivity.kt index b95fd5d..3f42926 100644 --- a/app/src/main/java/com/assimilate/alltrans/viewui/MainActivity.kt +++ b/app/src/main/java/com/assimilate/alltrans/viewui/MainActivity.kt @@ -13,6 +13,7 @@ import android.speech.RecognizerIntent import android.text.Editable import android.text.TextUtils import android.text.TextWatcher +import android.util.Log import android.view.View import android.widget.EditText import androidx.activity.enableEdgeToEdge @@ -25,6 +26,8 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.assimilate.alltrans.MyApp import com.assimilate.alltrans.R import com.assimilate.alltrans.allservice.SusService +import com.assimilate.alltrans.common.LanguagesConstants +import com.assimilate.alltrans.common.PreferenceLanguageUtils import com.assimilate.alltrans.common.Widget import com.assimilate.alltrans.databinding.ActivityMainBinding @@ -101,8 +104,8 @@ class MainActivity : AppCompatActivity() { private fun initView() { - binding.chSourceLanguage.text = MyApp.getSourceLanguage() - binding.chTargetLanguage.text = MyApp.getTargetLanguage() + binding.chSourceLanguage.text = PreferenceLanguageUtils.getString("language_source") + binding.chTargetLanguage.text = PreferenceLanguageUtils.getString("language_target") } private fun initSet() { @@ -187,6 +190,25 @@ class MainActivity : AppCompatActivity() { } } + binding.ivMainExChange.setOnClickListener { + // 读取当前的源语言和目标语言 + val currentSourceLanguage = PreferenceLanguageUtils.getString("language_source") + val currentTargetLanguage = PreferenceLanguageUtils.getString("language_target") + + // 交换源语言和目标语言 + PreferenceLanguageUtils.putString("language_source", currentTargetLanguage) + PreferenceLanguageUtils.putString("language_target", currentSourceLanguage) + + // 更新界面显示 + binding.chSourceLanguage.text = currentTargetLanguage + binding.chTargetLanguage.text = currentSourceLanguage + + // 打印日志,验证交换后的语言设置 + Log.d("fdhash_su", PreferenceLanguageUtils.getString("language_source")) + Log.d("fdhash_ta", PreferenceLanguageUtils.getString("language_target")) + } + + } private fun toTextTransResult() { @@ -200,6 +222,7 @@ class MainActivity : AppCompatActivity() { binding.etText.text = null } + // 语音转文本 private fun voiceToText() { val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) @@ -212,18 +235,22 @@ class MainActivity : AppCompatActivity() { 5000 ) // 设置5秒的可能完全静默时间 - speechIntent.putExtra( - "android.speech.extra.LANGUAGE_MODEL", - MyApp.getSourceLanguage() - ) + +// speechIntent.putExtra( +// "android.speech.extra.LANGUAGE_MODEL", +// MyApp.getSourceLanguage() +// ) + val languageCode = LanguagesConstants.getInstance() + .getLanguageCodeByLanguage(PreferenceLanguageUtils.getString("language_source"), this) speechIntent.putExtra( "android.speech.extra.LANGUAGE", - MyApp.getSourceLanguageCode() - ) - speechIntent.putExtra( - "android.speech.extra.LANGUAGE_PREFERENCE", - MyApp.getSourceLanguage() + languageCode + ) +// speechIntent.putExtra( +// "android.speech.extra.LANGUAGE_PREFERENCE", +// +// ) try { launcher?.launch(speechIntent) } catch (ea: ActivityNotFoundException) { @@ -250,7 +277,6 @@ class MainActivity : AppCompatActivity() { } } - override fun onResume() { super.onResume() initView() @@ -264,5 +290,4 @@ class MainActivity : AppCompatActivity() { } } - } \ No newline at end of file diff --git a/app/src/main/java/com/assimilate/alltrans/viewui/StillImageActivity.kt b/app/src/main/java/com/assimilate/alltrans/viewui/StillImageActivity.kt index 834be83..0b4aa45 100644 --- a/app/src/main/java/com/assimilate/alltrans/viewui/StillImageActivity.kt +++ b/app/src/main/java/com/assimilate/alltrans/viewui/StillImageActivity.kt @@ -13,34 +13,43 @@ import android.os.Bundle import android.provider.MediaStore import android.util.Log import android.util.Pair -import android.view.MenuItem import android.view.View import android.view.ViewTreeObserver import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter import android.widget.ImageView -import android.widget.PopupMenu import android.widget.Spinner +import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.assimilate.alltrans.R import com.assimilate.alltrans.common.BitmapUtils import com.assimilate.alltrans.common.TextRecognitionProcessor import com.assimilate.alltrans.common.VisionImageProcessor +import com.assimilate.alltrans.common.Widget import com.assimilate.alltrans.curview.GraphicOverlay import com.google.android.gms.common.annotation.KeepName - +import com.google.common.util.concurrent.ListenableFuture import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions import com.google.mlkit.vision.text.devanagari.DevanagariTextRecognizerOptions import com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions import com.google.mlkit.vision.text.latin.TextRecognizerOptions +import java.io.File import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Locale -/** Activity demonstrating different image detector features with a still image from camera. */ +/** 演示使用相机拍摄静态图像进行不同图像检测功能的活动。 */ @KeepName class StillImageActivity : AppCompatActivity() { private var preview: ImageView? = null @@ -49,17 +58,16 @@ class StillImageActivity : AppCompatActivity() { private var selectedSize: String? = SIZE_SCREEN private var isLandScape = false private var imageUri: Uri? = null - - // Max width (portrait mode) private var imageMaxWidth = 0 - - // Max height (portrait mode) private var imageMaxHeight = 0 private var imageProcessor: VisionImageProcessor? = null + private lateinit var imageCapture: ImageCapture + private lateinit var outputDirectory: File + private lateinit var cameraProviderFuture: ListenableFuture + private var isFlashOn = false private val REQUEST_CAMERA_PERMISSION = 100 - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_still_image) @@ -74,31 +82,13 @@ class StillImageActivity : AppCompatActivity() { REQUEST_CAMERA_PERMISSION ) } + Widget.makeSnackbar(this, "Photographing text for translation") - - findViewById(R.id.select_image_button).setOnClickListener { view: View -> - // Menu for selecting either: a) take new photo b) select from existing - val popup = PopupMenu(this@StillImageActivity, view) - popup.setOnMenuItemClickListener { menuItem: MenuItem -> - val itemId = menuItem.itemId - if (itemId == R.id.select_images_from_local) { - startChooseImageIntentForResult() - return@setOnMenuItemClickListener true - } else if (itemId == R.id.take_photo_using_camera) { - startCameraIntentForResult() - return@setOnMenuItemClickListener true - } - false - } - val inflater = popup.menuInflater - inflater.inflate(R.menu.camera_button_menu, popup.menu) - popup.show() - } preview = findViewById(R.id.preview) graphicOverlay = findViewById(R.id.graphic_overlay) populateFeatureSelector() - populateSizeSelector() + isLandScape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE if (savedInstanceState != null) { imageUri = savedInstanceState.getParcelable(KEY_IMAGE_URI) @@ -121,12 +111,107 @@ class StillImageActivity : AppCompatActivity() { } ) - val settingsButton = findViewById(R.id.settings_button) - settingsButton.setOnClickListener { -// val intent = Intent(applicationContext, SettingsActivity::class.java) -// intent.putExtra(SettingsActivity.EXTRA_LAUNCH_SOURCE, LaunchSource.STILL_IMAGE) -// startActivity(intent) + // 初始化相机 + startCamera() + outputDirectory = getOutputDirectory() + initClick() + } + + private fun initClick() { + findViewById(R.id.iv_still_pic).setOnClickListener { startChooseImageIntentForResult() } + findViewById(R.id.iv_still_take).setOnClickListener { takePhoto() } + findViewById(R.id.iv_still_back).setOnClickListener { onBackPressed() } + findViewById(R.id.iv_still_buling).setOnClickListener { + toggleFlash() + updateFlashButtonUI() } + findViewById(R.id.still_source_language).setOnClickListener { } + findViewById(R.id.still_target_language).setOnClickListener { } + findViewById(R.id.still_exChange).setOnClickListener { } + + } + + private fun toggleFlash() { + if (isFlashOn) { + imageCapture.flashMode = ImageCapture.FLASH_MODE_OFF + } else { + imageCapture.flashMode = ImageCapture.FLASH_MODE_ON + } + isFlashOn = !isFlashOn + } + + private fun updateFlashButtonUI() { + if (isFlashOn) { + findViewById(R.id.iv_still_buling).setImageResource(R.drawable.ic_still_bulibuli) + } else { + findViewById(R.id.iv_still_buling).setImageResource(R.drawable.ic_still_notbuli) + } + } + + + private fun startCamera() { + val previewView = findViewById(R.id.photo_preview) + cameraProviderFuture = ProcessCameraProvider.getInstance(this) + + cameraProviderFuture.addListener(Runnable { + val cameraProvider = cameraProviderFuture.get() + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + imageCapture = ImageCapture.Builder() + .setFlashMode(ImageCapture.FLASH_MODE_OFF) // 默认关闭闪光灯 + .build() + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) + } catch (exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + }, ContextCompat.getMainExecutor(this)) + } + + private fun takePhoto() { + + val imageCapture = imageCapture + + val photoFile = File( + outputDirectory, + SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".jpg" + ) + + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(this), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Log.e(TAG, "Photo capture failed: ${exc.message}", exc) + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + val savedUri = Uri.fromFile(photoFile) + imageUri = savedUri + val msg = "Photo capture succeeded: $savedUri" + Log.d(TAG, msg) + tryReloadAndDetectInImage() + } + } + ) + } + + private fun getOutputDirectory(): File { + val mediaDir = externalMediaDirs.firstOrNull()?.let { + File(it, resources.getString(R.string.app_name)).apply { mkdirs() } + } + return if (mediaDir != null && mediaDir.exists()) + mediaDir else filesDir } @@ -169,7 +254,12 @@ class StillImageActivity : AppCompatActivity() { public override fun onDestroy() { super.onDestroy() + imageProcessor?.run { this.stop() } + // 释放相机资源 + if (::cameraProviderFuture.isInitialized) { + cameraProviderFuture.get().unbindAll() + } } private fun populateFeatureSelector() { @@ -207,37 +297,6 @@ class StillImageActivity : AppCompatActivity() { } } - private fun populateSizeSelector() { - val sizeSpinner = findViewById(R.id.size_selector) - val options: MutableList = ArrayList() - options.add(SIZE_SCREEN) - options.add(SIZE_1024_768) - options.add(SIZE_640_480) - options.add(SIZE_ORIGINAL) - // Creating adapter for featureSpinner - val dataAdapter = ArrayAdapter(this, R.layout.spinner_style, options) - // Drop down layout style - list view with radio button - dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - // attaching data adapter to spinner - sizeSpinner.adapter = dataAdapter - sizeSpinner.onItemSelectedListener = - object : OnItemSelectedListener { - override fun onItemSelected( - parentView: AdapterView<*>, - selectedItemView: View?, - pos: Int, - id: Long - ) { - if (pos >= 0) { - selectedSize = parentView.getItemAtPosition(pos).toString() - tryReloadAndDetectInImage() - } - } - - override fun onNothingSelected(arg0: AdapterView<*>?) {} - } - } - public override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelable(KEY_IMAGE_URI, imageUri) @@ -247,6 +306,7 @@ class StillImageActivity : AppCompatActivity() { } private fun startCameraIntentForResult() { + // Ensure permission is still granted before starting camera intent if (ContextCompat.checkSelfPermission( this, @@ -299,12 +359,10 @@ class StillImageActivity : AppCompatActivity() { if (imageUri == null) { return } - if (SIZE_SCREEN == selectedSize && imageMaxWidth == 0) { // UI layout has not finished yet, will reload once it's ready. return } - val imageBitmap = BitmapUtils.getBitmapFromContentUri(contentResolver, imageUri) ?: return // Clear the overlay first @@ -443,5 +501,6 @@ class StillImageActivity : AppCompatActivity() { private const val KEY_SELECTED_SIZE = "com.google.mlkit.vision.demo.KEY_SELECTED_SIZE" private const val REQUEST_IMAGE_CAPTURE = 1001 private const val REQUEST_CHOOSE_IMAGE = 1002 + private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" } } diff --git a/app/src/main/java/com/assimilate/alltrans/viewui/TextResultActivity.kt b/app/src/main/java/com/assimilate/alltrans/viewui/TextResultActivity.kt index 27a3496..b861274 100644 --- a/app/src/main/java/com/assimilate/alltrans/viewui/TextResultActivity.kt +++ b/app/src/main/java/com/assimilate/alltrans/viewui/TextResultActivity.kt @@ -7,13 +7,16 @@ import android.content.Intent import android.os.Bundle import android.speech.tts.TextToSpeech import android.text.TextUtils +import android.util.Log import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.assimilate.alltrans.MyApp import com.assimilate.alltrans.R +import com.assimilate.alltrans.common.LanguagesConstants import com.assimilate.alltrans.common.Logger +import com.assimilate.alltrans.common.PreferenceLanguageUtils import com.assimilate.alltrans.common.Widget import com.assimilate.alltrans.databinding.ActivityTextResultBinding import com.assimilate.alltrans.http.GoogleTranslator @@ -67,8 +70,20 @@ class TextResultActivity : AppCompatActivity() { } binding.ivTrCopy.setOnClickListener { copyToClipboard() } binding.ivSourceClear.setOnClickListener { onBackPressed() } - binding.ivSourceTts.setOnClickListener { readText(binding.tvTrSource.text.toString()) } - binding.ivTargetTts.setOnClickListener { readText(binding.tvTrTarget.text.toString()) } + binding.ivSourceTts.setOnClickListener { + readText( + binding.tvTrSource.text.toString(), + PreferenceLanguageUtils.getString("language_source"), + this + ) + } + binding.ivTargetTts.setOnClickListener { + readText( + binding.tvTrTarget.text.toString(), + PreferenceLanguageUtils.getString("language_target"), + this + ) + } binding.ivTrTargetShare.setOnClickListener { shareText(binding.tvTrTarget.text.toString()) } binding.ivTrCollect.setOnClickListener { addCollect() } @@ -81,9 +96,19 @@ class TextResultActivity : AppCompatActivity() { } translating = true + + val lanSourceCode = LanguagesConstants.getInstance().getLanguageCodeByLanguage( + PreferenceLanguageUtils.getString("language_source"), + MyApp.applicationContext() + ) + val lanTargetCode = LanguagesConstants.getInstance().getLanguageCodeByLanguage( + PreferenceLanguageUtils.getString("language_target"), + MyApp.applicationContext() + ) + val param = HashMap().apply { - put("sourceLanguage", MyApp.getSourceLanguageCode()) - put("translationLanguage", MyApp.getTargetLanguageCode()) + put("sourceLanguage", lanSourceCode) + put("translationLanguage", lanTargetCode) put("text", text) } @@ -104,9 +129,9 @@ class TextResultActivity : AppCompatActivity() { private fun addHistory(transResult: String) { val dbTranslation = DbTranslation(this) val translations = Translations( - MyApp.getSourceLanguage(), + PreferenceLanguageUtils.getString("language_source"), binding.tvTrSource.text.toString(), - MyApp.getTargetLanguage(), + PreferenceLanguageUtils.getString("language_target"), transResult ) @@ -131,17 +156,29 @@ class TextResultActivity : AppCompatActivity() { } } - private fun readText(text: String) { + // readText 方法,整合获取语言代码和朗读文本功能 + private fun readText(text: String, targetLanguage: String, context: Context) { val speech: String = text.trim() if (!TextUtils.isEmpty(speech)) { - if (TextToSpeech.LANG_NOT_SUPPORTED != tts.isLanguageAvailable(Locale.getDefault()) - ) { - tts.speak(speech, 0, null, null) + // 获取目标语言的语言代码 + val languageCode = + LanguagesConstants.getInstance().getLanguageCodeByLanguage(targetLanguage, context) + Log.d("LanguageCode", "Language Code for $targetLanguage: $languageCode") + + // 创建语言 Locale 对象 + val locale = Locale(languageCode) + + // 判断语言是否支持 + if (TextToSpeech.LANG_NOT_SUPPORTED != tts.isLanguageAvailable(locale)) { + tts.language = locale + tts.speak(speech, TextToSpeech.QUEUE_FLUSH, null, null) + } else { + Widget.makeToast(this, getString(R.string.tr_tts_error)) } } - } + // 复制到粘贴板 private fun copyToClipboard() { val tip = "Copied to clipboard!" diff --git a/app/src/main/res/drawable-xxxhdpi/ic_still_bulibuli.webp b/app/src/main/res/drawable-xxxhdpi/ic_still_bulibuli.webp new file mode 100644 index 0000000000000000000000000000000000000000..7ad4412ccfbc9342f14a0eeb7b840bc274d297a0 GIT binary patch literal 1830 zcmaKp3p~?n7{}kGh2@e<9U*o~#X&8n59>CW#G=Xu}fdB5-T{NDfjzWDGv>agx^u{*+F0d*@>54F!TYn%U;j7SwtW&7IQlS$i7D*TK-i3Ezv zR;VGHE5p1Fuv4;E=ODRed9UZKH16FM&!oP$HfwhqbPJ~5%nmtWsBh#jzZREi^v_<^ zIDY(AM`BkT?+vHrYa5c*V3jVneI#SD{i(`_- zZWM8IN%vG+7)!vO9+;q6VOOG$l%Qt(gMylL+l8zh%<0V5hA6N|blISW;#)QqU&vZn z_wlvlT0%!`fM09aL|e-dy6~Ea9bK59GRY)&&=paxlkK%ZjfoyN#`L|aI#2gi;GS%` z&+5vlC<)mryHXsm>h|XAYjSoU8zS(Wrx5K?uH{)i$C~X-4>NN65;R6b`fWU4wm3hO zBy@n~T1LS-v~gUqdCzI5tBmk|dzL$WW>YJz6ju5ybESZ00@n(!wK1-1VmKnoj&dIl zYYOy#sw}nLD~`>%M8EZckoB?#!%%h|xIZ^z7|*k5HvL|3O-7`T$@CfkX^-r}*unXo zXOhd~`9Fs1o)gWixYEV9r$@sp8}7uWXi9T{ousE)D}J?Vx%<#g$qA=ZfxHHXw>#Fs z3KHQsV^w+@kO<3}umM$JN)@B>wm#E5w9`j^AcS&bMdI(@bV-h4p!`l z=}oujH?D|vWY@7f*Av7SHikHewM;VnTy!;OckQF?SYKAmmnkO?DKx_0&2gA_@*TaG zD}RVxy{-(QO^9EZd;4Ho8p=r#bltWFw-REFl|tI| zR-9^FuBw?+T<-&AiuzL$Opi$wJhZe*<#F|r4yr#b9@k|GB_rN9qX&NTJ8zLW!F^--&UtW%f#MrLR-(a`+O; PaQ&pzJI#(e5To=Lkqr0v literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_still_notbuli.webp b/app/src/main/res/drawable-xxxhdpi/ic_still_notbuli.webp new file mode 100644 index 0000000000000000000000000000000000000000..a4dd01b310305adb0d5d6ee4908f60c1ccafeadd GIT binary patch literal 2046 zcmaKsc{r5o8^<5}2sv5CArhCe6+)4HUq%Ll2H6>$5E=X4IXEO+N)0iZMwa8CXkrDcKH0iD*k_x(;I{Jf+QP|ONBt2Lp=#VEa3ZVuvz> z*c)OQ-y`gKguQ}882USqMk_ENjIkHP7^maL+_1NWW*Fq601HsS6c{kh53L~!A^@OH z1%M^{Th0>)Kz%X*-;aOGNfiUYn*;!9;9Kt7HwpF#@%ZvORwy$2_yDk33ILBI0Q`dh z9J}_V28I98+i9pI3iTBP4POudFyJ&W2RDEhP=H7oy3`Bc5_H2?VC(=W;#;M^b9-DI zc=FA@`A6Q@`vWbwMDw2{XBYk?1-^c1iELouKbaD&#O-0#PJOKdz$u`$y#u@_NE4)? zFlXgH<01j#ZpGoDo-rRRQ7AU^gRYcW$o!=;3~_MQLsJ4i`&jqC{T`X!8>aWi)_JEW ztB|3Lj@p~}Fqirn`dDS&5&eJgeyC7E;L@Iyv-n#~y$e2jsgmR?$*>o8SrdXm{nHz9 z_WnLCp8MF(D!m@v>NEK4s{3GLq`-BcTdwC#7%~D<+f1m_%%Md5+`+Rwu$4c%^V9Iu#r(51l zy}^t>wDOz_QOx1;=6H_a^i>1+^>Q2Hc_q7-K^dVV%9gU8GQ*E7x-A5L7%Yhl94y(6 z&Q)L|jS^@&2W(Y$6F%! z76xbbbYKHp>p852rF?%`#&VEf;Uls-UcJQbaIz!WeHyZqH(YTW)hUgULB_&>H7WIeus_sLP@Cw+|TYs z%Pdea62eiY{YF1l6EZziS&HjVmQK9D+A9-W zvOVu~BEKRWCB_zIM-v}trq|pk*0D3#Bv5RNln!dDZUO!R4V%W1q|PYO5to`->io(w zLY0jBc}Nm2z-}N|1Y7>jh79Vn6P+CN-mjxz%i!E`-3t|ZDTnA>yt&;l37SB17 zXwbVZ!@l`mOUamu1H5C}^P90z2yd$whbo%9_MHz{&-PLi;BAIjnJD@IbL59#FTL`v zD^)`hw8Y%+QO@LOQz<=~V!7g5lry>7VQ&e{Z&G{F8ZjC*mlUhdHLKIh<1)01(Q=;- zkgh+F!R|m78uxxNDX%FKlf&X2_Iqrzn%ZJ~M%Dv0;ELf&NBoxB4p)3)xKL8{G_q)} zZfN!q@?O+&G5dL+SmlUr3%|5KI9en{Bx@tHnW%r6Ne@S6b5a+1J`h_AxKyk2L>XW) zA9QAHC~aLjkH@Gm>P12*;d2_C>ng@|lee%8S5c5js`sfgS&N&l>W*^}w zDy@{OXW#8ZJ5|hF%Pc*CUBb^ry|*g2T>fc??$pD+!{jwB&i#wQx&{Ct_sX#J-uYXq zl;V%OD=+EE^s4-n@HHOQQ}M%zVvFY|AKa($5!c4vkK9@N__;)X6uxfJ7sDxnov2rA z4BP8D%tuOH_DA3r3;o@-WdkN2VkH6``?~z68l;4hYh#@14i0v=XN7-3@{eDiZZKHK z;xd>09cTkDG%SkZk53=d+Y$J2QWl1@MJ2IOkoge`o~zv%2`Af(V@)5roUNsnjyF8L zWv@zz7w0r1lFGuX)*bIo&XK#@GD>F6(|eXQMlXMq3!l59?bhrn@wev0*xbcTp!4K6 zFO5(8+4EK5SYPjOcUllJR-u*6vVX%F%q_`@a+){}V+FhDts@ibgz?>Xfog*EN7uDS fy)NpEK3Qp`G>E;j@?0H#rg>+8>VS@};sE~uclAlV literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_still_pic.webp b/app/src/main/res/drawable-xxxhdpi/ic_still_pic.webp new file mode 100644 index 0000000000000000000000000000000000000000..383c39f9e7d57c034ad3f3f50fe6d266d69dc897 GIT binary patch literal 2132 zcmaKs3pAAL8pmHzD%B`T+hszi-NmKcs@YSG+a81%m$Ab(Vu~2Xee8^cAwozEN0Ym# znNpe<9Xq+-bB{D~J;NBbF)T(o-_$y*)>-?U=l$O2`MuBcU*G$@@3+=xi#9d2+zNoB z3ChO7#?Way06-g>w671^$jGKi3|a!QCXttbO1^$UL|ZeIvJ=)>S#lJJLF22rdj$G_ zqrdf#YPtWLw&(cYD*k_R+hxx{4@j~AO*JC4In5A5Mj6zP*74Z`KC;&h_^?MFS1%Md~!2Y4{IkkrXz+wP++4(*9{Z9PdiSA!tCjt3QUS0q!76Y&o z3xG@)0Na22T7%sG=uH_aDMEb_pn(V1fG1D}Xy6AtfF?vIppU8p^r0_o>9Pd?E!v-l zr17~AeHEJ5(epL=+8ca^2H2yoaYEB(_~p)V{Zo^>E?G*SJ16}oO=6aE9TM_YX&O_yl9527z>SYRXwgJPKi*nb-SA?~-v z8}RFiP9tnK`)oq|57xw~igR~rBY36j{Gk7aQFcS`t|5DWNhqZ(>9!Ti-EOp;T2U35 zE83BdX6YnGuS|wy)fd!vax@aIs*kw`ymMMRZ){xfB!04>y`mwl)7jZ4r0|A>9NyRMbJ#NcMrq*pqSTdYQDw52)=;^qWdSTz#mQL?vKrVfcWW5}YBd85TfC0X$#^7R-T|M@_B6+rO9&FI(2%R=$MwhsqB*{F$ z?4e3EtXxzB7v6;6ti#_+u9XSfRf-RW32!^JTKx6-^%pLNd6p4NdLx6V_0TOPuHfxA z8t_a%sXyS04kWTxBVB5?{iVdszSTb3sz1HZc}(hyHp;=g#RyZNqUAZMqLF5!3I(+e zBWlK7O1$C6s$e#QlBj(Eg$>^t&rNT-WQq-sr(9}6L*Zs=n$vr}jAWr6wkWeTgA#SP zSEAl|Z0YHyJj?V<@ArJl-d*7)wDj^t=^YgaHM`-T2q*WI>ppVo7?zETK_%DkwdmG( zj%5DYl-himzO`J_*GuDYCC!zYGoRxx;iscTXjc(3M|QrnEP~ReCgq zv>VZp;cNc3Lt1QP$j5Q{;Sl$r{AZ0QVUy@DevpTCX3dhxczt3@VkJk22JpLUp>FC*pLwUDzk z2hESKcu5_NV;4|V)6MS|XQPwDmIacm%!i}cWu>|3!KS;%vigj6UkN#H#yX;(+rINm zw`p)}=D;fPbYQ-5}+(ORFTE$|T^y94K4_w<$=8!&x zJ8Z_Vy8m%QZ)9kQ&`EXC?9|Lx%Cgd{MlyZ8enYLP# zw;vNFBRUZ19Ru&5OAlH;AF8eB;AH5ir8;FPR_Rkj2}KmR1{r{c)d`*vVCg0;=eJPZ6I7JZ6^M Ipx^$#0rzBxN&o-= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_still_take.webp b/app/src/main/res/drawable-xxxhdpi/ic_still_take.webp new file mode 100644 index 0000000000000000000000000000000000000000..3f9d5b19277bf1bc9e5a90532d7991c37b895fe8 GIT binary patch literal 3338 zcmaixc|6oz7r=j(%966nE+mNw*(O`Yo*`r#Tg{B!5W`rrW^Ab>r3sah5n~%!LQ+P? zRzyNcC1gkv#unb6`aEyXAJ6+f=l;&Q-+RvaoOAE**qWP|FmnNbqcP0d!CK3S0{{SJ zP&t`)K667u>kcOH2rzZ*SXM9;5EzQJHHAqzA)KZ5d<2-l^-GZGkf7i6j~2{gy!o9w zp!AO!|KG{(;TeJkQ@(>Liv^p5cj5<`-}^Vq?=b2&>+EoNXizAaW4*&zJ6l7LJwTTC z{u`tI#^@mIPJRZMqZ5F^?eyA_o!9O4475K3?rfln0U>}bU<$x?-VdHZIZp%ttz`gU z$^7F(5dfeb4*&<={_)A)1AzTW08rQa$M@%-1R=4=U*%Xq%k1R^06&TWfC~Wte0>1G z;qog6n*VV(DKI1k-s?QLya5d02}l9vKp=nyARwuNLsbDZzzM_oIRW4p=Q9|$X~De! z(e|I_^ZThkd-Vx5O5ZB*n0TUc1MBm@dI7XC{4xs=Q4{da(%=8x31}yZICt67Su1*U zTf9}U|10_>G?~F(WVCcwFo2As%3HW+DZ}RAek@rL6MX-z^6wD8;PJ$%VrcM;+t9%z zZWtTR&2iAH*|mrC*BJi2gtcPx2&$RB+A9HL`&9!pt)I6ui~VYES&32edmINfc1nY8 zwd6qZ;Hc#5S2cSaC2>&k1||Z8{5#ZN;6N^62^GpsokzC%YAjOdneL5<%w{G`iU)g@ zplWpd1ZUM!Q8Pw2_Xj5Q`^8zxwMw$p&(T+Nh3_w)1=i=%JpIUQ{a$HW`w5AtSqlt+R}5?e-dYG^haV_ z^ec*EoUvpZgb)2u<5bY*bdhQ1M&lDT_Q*V_z?HJBxqK{b3aOKE}#NRm;332DBsTA|gRLGdb8Y?PB3*I-sb|Z!;92?RWpKLEG zZwo)X+gU~`>z<8-vNQBt`b_-fQq9bgg3Qqyr%q{b%y_)E@C4dSmdB<0IMk@f zfp|_XLHvmPVfW|=zRp7aTJc2JTB_w9IldWt*$ZI7DOL33r;PI zEEx9S4rip11*Wij)U535HNp;XjmrY0sk#^G!*2V;c!;D1?Ir{5bhc*$@*&|Qjuck` zC8m-qDvlDwQdZtR?+x@ScWHWgi5o*2u3|e|TA^R?GVufL)>)hN>snnC{0yNPWvXm? zbyPsbZv6Nnu?pumAgr4n#2Qxp);d6^c1$wi#n3)(Wab7%Ajx1THDhT#nnzQ?tD&X7 z*ZgFl9lX90++(xF2j=KsC=R|K+YgZ54Gi7KG;Sr)!&VT!eLNl{^1%6gJ1i@K9wy-_ zLvb^Z>roDksbi0DdpP97W@(#4KYe8gN9y*;I#HK*CMLWwsj>E=!eDr@1$T_jjc3d6 z>8eJO-?+WwjKyYnH6`kR5|s$62ZL-}pQ-soBQ(oC0-vVDBavOg9Ri5dkC?aK#I(f) zX^9)jPFX(e2D0`ksl4ytE@Bdg_BV!JJc&V;js+tr=^&_Pw4Bi#=h=QH+aQvoC5qY} z&3|V}M5@Pza8oeh`ZKp&*;FI^PZXRfG^q=7Zfcw45e=M|K_v7YqA0t!MASDBA9~R< z1ogJWI8TcA1ciSLFi#etzd4!sI(z)*dl54;{PD@fN~^DXrSG@yGVWHgWknmx3mS{b z2((C0retCD_gf?6&Qq(@uc&A&+*NQt!T7o;QVA_N@SKQ-yFb46^tNYsjD78!g}WBp zv>Tf!>a=0yAe5ZAz5V)Xua5iAQ{SpSZl>Vjs}t1~Qtszf6+_MZb8wdyI2)+2FOM_M zj;(w(%(FQl=x-NtbqJF7!iMkaP)(W)2NhLG4cNbzHUVwgSPx8j{;An!Ve9@kZaGQv)7lb92Eh(!r@T+T z`E$~C->rhT+D0ng5L(K@&gM>^Ow(E%(&H?RptGS_bXgSBQp{c;WS)5Qypv)`kv0`FsmK# zfyXU;J!!7VDyO!jad)(!%;pOmf{{Mds!jc@8w!q#d$sUYCyT< zgmezS$MG~EM}3w@#=SDdN1VG4l+EK}zY#Boz4Z4zJ!JT9GrPDU_U^utqkOv0k4Ci7 zt1B%WAX*6pVjCt&1d3JFZW``ihY-HyU{AmyKTa3GcDi7 zG1I*=O<`A|H!4pqZ@^zJh*|vZ&~Do4r%=PwQ=FSahtE!jJ~f^3SKFP8qi%F~2hJws zrS^IBBs+hyhI3(FIE=-=^mrYRhW@TRoXsp;eD~D!IsW1=aEzRvN)tEaSlM&m{uCX% zYm`x^OZQz&XHGxPSnw_rK<3I&B{8d8#?ygb}5 zFP@%8DS8#`zVFJd&uRu!G$O5aiRE^Uu8Bdjnb|ViZHXpnNyyiGslHgJwnnHtO6rA% zhQsw$VZ1AQe>>cw)JE|NAzSXW4#K(Ui`e&wOppzW7;=qOm;My6fB_}Uw0cT%Dw@c{Rup+tPf}5`gTo8qDv&TtpwQEO6 zwa4@h3e`f>Gq)MH*McW+FJ*hlL}#RUcxP5tiv6K>hV`)kI+(MhQ#6{e=E3VhD^g&ZA?V8Z~=6$D8v$wBB1Wy{ViL%D$S< z;knVL+3=b6s%|dr#{78&N<{oHXU74Q+PQ}>rblWBqI+zrZK}$u?={(7a2r`UV*$?@ z)cCYg<@|Oj!zzFNGX`fkNOOC>naqFX!f_$;jJfgtYJ`$k*Q1;I-Ng@|W1CA#*9E^C TI}Qus?8}F`*Dly9=mP%%ZIz}V literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/button_r20_black_bg.xml b/app/src/main/res/drawable/button_r20_black_bg.xml index 44a9fea..e51716e 100644 --- a/app/src/main/res/drawable/button_r20_black_bg.xml +++ b/app/src/main/res/drawable/button_r20_black_bg.xml @@ -1,6 +1,6 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back_white.xml b/app/src/main/res/drawable/ic_back_white.xml new file mode 100644 index 0000000..efe8e48 --- /dev/null +++ b/app/src/main/res/drawable/ic_back_white.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_down_choose_white.xml b/app/src/main/res/drawable/ic_down_choose_white.xml new file mode 100644 index 0000000..ae39bf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_down_choose_white.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/activity_language_change.xml b/app/src/main/res/layout/activity_language_change.xml index c0f9663..a50a52c 100644 --- a/app/src/main/res/layout/activity_language_change.xml +++ b/app/src/main/res/layout/activity_language_change.xml @@ -31,15 +31,17 @@ android:background="@drawable/button_r20_white_bg" android:drawablePadding="25dp" android:gravity="center" + android:paddingStart="16dp" android:paddingEnd="12dp" - android:text="@string/text_source_language" + android:textColor="@color/main_text_ff1f1724" android:textSize="16sp" android:textStyle="bold" app:drawableEndCompat="@drawable/ic_down_choose" /> + + @@ -26,20 +36,48 @@ app:layout_constraintTop_toTopOf="@id/preview" /> -