diff --git a/CHANGELOG.md b/CHANGELOG.md index 39dc9b51..fa91c799 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Logs +## 3.1.1 +- New : Add `Position` and `PhotoEditor.addText(..., position)` overloads for initial text placement +- New : Sample app now lets users tap the image to place text before entering it +- Test : Add targeted placement coverage in `GraphicManagerTest` and update sample app test flow for tap-to-place text +- Docs : Update `README.md` with developer API usage and end-user sample app instructions, including adding multiple text labels +- Fixed : Mirror upstream issue #589 in this fork by removing the "always centered" text insertion limitation + ## 0.1.1 - Change : `app:src="@drawable/got_s"` to `app:photo_src="@drawable/got_s"` in `PhotoEditorView` @@ -106,4 +113,4 @@ new TextStyleBuilder() - Fixed : Clearing redo stack after brush drawing - Fixed : Using eraser size when in erasing mode - Change : Bumped Kotlin to 2.0.0, AGP to 8.5.1, Gradle to 8.9, target SDK to 34 -- Removed : Non-functional `OnMultiTouchListener` interface \ No newline at end of file +- Removed : Non-functional `OnMultiTouchListener` interface diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 636059e4..da219167 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,9 @@ 2. Fork the project. 3. Create a branch with name PE-[#Issue No.] Ex : PE-146 4. Make required changes and commit to that branch. -5. Generate pull request. Mention all the required description regarding changes you made. +5. Before opening a pull request, run the broader local verification gates used by CI: + - `./gradlew check` + - `./gradlew build` +6. Generate pull request. Mention all the required description regarding changes you made. Happy coding.:-) diff --git a/README.md b/README.md index bc69221e..6b82cd9c 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,20 @@ It will take default fonts provided in the builder. If we want different fonts f `mPhotoEditor.addText(mTypeface,inputText, colorCode);` +If you want the text to start at a specific location instead of the default centered position, use the overload with a `Position`: + +`mPhotoEditor.addText(inputText, colorCode, new Position(80, 160));` + +After insertion, text can still be dragged, rotated, and scaled on the canvas. + +In the sample app, the end-user flow is now: + +1. Tap `Text`. +2. Tap the image where the label should appear. +3. Enter the text in the dialog. + +Repeat the same flow to add multiple text labels in different positions on the image. + In order to edit the text we need the view, which we will receive in our PhotoEditor callback. This callback will trigger when we **Long Press** the added text ```java diff --git a/app/src/androidTest/java/com/burhanrashid52/photoediting/EditImageActivityTest.kt b/app/src/androidTest/java/com/burhanrashid52/photoediting/EditImageActivityTest.kt index eb4a3749..be5c783f 100644 --- a/app/src/androidTest/java/com/burhanrashid52/photoediting/EditImageActivityTest.kt +++ b/app/src/androidTest/java/com/burhanrashid52/photoediting/EditImageActivityTest.kt @@ -256,6 +256,7 @@ class EditImageActivityTest { // Add a text to the image. Espresso.onView(ViewMatchers.withText(R.string.label_text)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.photoEditorView)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.add_text_edit_text)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.add_text_edit_text)) .perform(ViewActions.typeText("Test Text")) @@ -304,6 +305,7 @@ class EditImageActivityTest { // Open the emoji menu (delay to give time to load lower menu) Thread.sleep(2000) Espresso.onView(ViewMatchers.withText(R.string.label_text)).perform(ViewActions.click()) + Espresso.onView(ViewMatchers.withId(R.id.photoEditorView)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.add_text_edit_text)).perform(ViewActions.click()) // Type the text (delay to allow keyboard to load) @@ -413,4 +415,4 @@ class EditImageActivityTest { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/burhanrashid52/photoediting/EditImageActivity.kt b/app/src/main/java/com/burhanrashid52/photoediting/EditImageActivity.kt index bae63f99..d3f091dc 100644 --- a/app/src/main/java/com/burhanrashid52/photoediting/EditImageActivity.kt +++ b/app/src/main/java/com/burhanrashid52/photoediting/EditImageActivity.kt @@ -41,6 +41,7 @@ import ja.burhanrashid52.photoeditor.OnPhotoEditorListener import ja.burhanrashid52.photoeditor.PhotoEditor import ja.burhanrashid52.photoeditor.PhotoEditorView import ja.burhanrashid52.photoeditor.PhotoFilter +import ja.burhanrashid52.photoeditor.Position import ja.burhanrashid52.photoeditor.SaveFileResult import ja.burhanrashid52.photoeditor.SaveSettings import ja.burhanrashid52.photoeditor.TextStyleBuilder @@ -68,11 +69,13 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis private lateinit var mRvFilters: RecyclerView private lateinit var mImgUndo: View private lateinit var mImgRedo: View + private lateinit var mTextPlacementOverlay: View private val mEditingToolsAdapter = EditingToolsAdapter(this) private val mFilterViewAdapter = FilterViewAdapter(this) private lateinit var mRootView: ConstraintLayout private val mConstraintSet = ConstraintSet() private var mIsFilterVisible = false + private var mIsTextPlacementPending = false @VisibleForTesting var mSaveImageUri: Uri? = null @@ -157,6 +160,16 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis private fun initViews() { mPhotoEditorView = findViewById(R.id.photoEditorView) + mTextPlacementOverlay = findViewById(R.id.viewTextPlacementOverlay) + mTextPlacementOverlay.setOnTouchListener { _, event -> + if (mIsTextPlacementPending && event.action == MotionEvent.ACTION_UP) { + clearPendingTextPlacement() + showTextEditorDialog(Position(event.x.toInt(), event.y.toInt())) + true + } else { + false + } + } mTxtCurrentTool = findViewById(R.id.txtCurrentTool) mRvTools = findViewById(R.id.rvConstraintTools) mRvFilters = findViewById(R.id.rvFilterView) @@ -187,6 +200,7 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis } override fun onEditTextChangeListener(rootView: View, text: String, colorCode: Int) { + clearPendingTextPlacement() val textEditorDialogFragment = TextEditorDialogFragment.show(this, text.toString(), colorCode) textEditorDialogFragment.setOnTextEditorListener(object : @@ -383,11 +397,13 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis } override fun onEmojiClick(emojiUnicode: String) { + clearPendingTextPlacement() mPhotoEditor.addEmoji(emojiUnicode) mTxtCurrentTool.setText(R.string.label_emoji) } override fun onStickerClick(bitmap: Bitmap) { + clearPendingTextPlacement() mPhotoEditor.addImage(bitmap) mTxtCurrentTool.setText(R.string.label_sticker) } @@ -414,6 +430,9 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis } override fun onToolSelected(toolType: ToolType) { + if (toolType != ToolType.TEXT) { + clearPendingTextPlacement() + } when (toolType) { ToolType.SHAPE -> { mPhotoEditor.setBrushDrawingMode(true) @@ -424,16 +443,10 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis } ToolType.TEXT -> { - val textEditorDialogFragment = TextEditorDialogFragment.show(this) - textEditorDialogFragment.setOnTextEditorListener(object : - TextEditorDialogFragment.TextEditorListener { - override fun onDone(inputText: String, colorCode: Int) { - val styleBuilder = TextStyleBuilder() - styleBuilder.withTextColor(colorCode) - mPhotoEditor.addText(inputText, styleBuilder) - mTxtCurrentTool.setText(R.string.label_text) - } - }) + mIsTextPlacementPending = true + mTextPlacementOverlay.visibility = View.VISIBLE + mTxtCurrentTool.setText(R.string.label_text) + showSnackbar(getString(R.string.msg_tap_image_for_text)) } ToolType.ERASER -> { @@ -491,7 +504,10 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis } override fun onBackPressed() { - if (mIsFilterVisible) { + if (mIsTextPlacementPending) { + clearPendingTextPlacement() + mTxtCurrentTool.setText(R.string.app_name) + } else if (mIsFilterVisible) { showFilter(false) mTxtCurrentTool.setText(R.string.app_name) } else if (!mPhotoEditor.isCacheEmpty) { @@ -501,6 +517,24 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis } } + private fun showTextEditorDialog(position: Position) { + val textEditorDialogFragment = TextEditorDialogFragment.show(this) + textEditorDialogFragment.setOnTextEditorListener(object : + TextEditorDialogFragment.TextEditorListener { + override fun onDone(inputText: String, colorCode: Int) { + val styleBuilder = TextStyleBuilder() + styleBuilder.withTextColor(colorCode) + mPhotoEditor.addText(inputText, styleBuilder, position) + mTxtCurrentTool.setText(R.string.label_text) + } + }) + } + + private fun clearPendingTextPlacement() { + mIsTextPlacementPending = false + mTextPlacementOverlay.visibility = View.GONE + } + companion object { private const val TAG = "EditImageActivity" @@ -511,4 +545,4 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis const val ACTION_NEXTGEN_EDIT = "action_nextgen_edit" const val PINCH_TEXT_SCALABLE_INTENT_KEY = "PINCH_TEXT_SCALABLE" } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/activity_edit_image.xml b/app/src/main/res/layout/activity_edit_image.xml index 7c257a41..34196cbb 100644 --- a/app/src/main/res/layout/activity_edit_image.xml +++ b/app/src/main/res/layout/activity_edit_image.xml @@ -26,6 +26,20 @@ app:layout_constraintTop_toTopOf="parent" app:photo_src="@drawable/blank_image" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8179f321..6da597de 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -13,6 +13,7 @@ Texte Filtres Ajuster + Touchez l\'image pour placer votre texte Souhaitez-vous quitter sans enregistrer l\'image ? PhotoEditor Veuillez enregistrer l\'image pour la partager diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8ad32a1f..55798095 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -13,6 +13,7 @@ Tekst Filtr Dostosuj + Dotknij obrazu, aby umieścić tekst Czy na pewno chcesz wyjść bez zapisywania? PhotoEditor Zapisz obraz, aby udostępnić diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 52b0bf3e..b1f85eeb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ Text Filter Adjust + Tap the image to place your text Are you want to exit without saving image ? PhotoEditor Please save image to share diff --git a/photoeditor/build.gradle b/photoeditor/build.gradle index 37dc37bf..f0f73310 100644 --- a/photoeditor/build.gradle +++ b/photoeditor/build.gradle @@ -61,7 +61,7 @@ dependencies { ext { PUBLISH_GROUP_ID = 'com.burhanrashid52' - PUBLISH_VERSION = '3.1.0' + PUBLISH_VERSION = '3.1.1' PUBLISH_ARTIFACT_ID = 'photoeditor' } diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/GraphicManager.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/GraphicManager.kt index 06ccb82d..4c304bf9 100644 --- a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/GraphicManager.kt +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/GraphicManager.kt @@ -19,12 +19,19 @@ internal class GraphicManager( val redoStackCount get() = mViewState.redoViewsCount - fun addView(graphic: Graphic) { + fun addView(graphic: Graphic, position: Position? = null) { val view = graphic.rootView val params = RelativeLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) - params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE) + if (position == null) { + params.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE) + } else { + params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE) + params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE) + params.leftMargin = position.x + params.topMargin = position.y + } mPhotoEditorView.addView(view, params) mViewState.addAddedView(view) @@ -99,4 +106,4 @@ internal class GraphicManager( return redoStackCount > 0 } -} \ No newline at end of file +} diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditor.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditor.kt index 6e7dd627..c922e812 100644 --- a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditor.kt +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditor.kt @@ -36,6 +36,22 @@ interface PhotoEditor { @SuppressLint("ClickableViewAccessibility") fun addText(text: String, colorCodeTextView: Int) + /** + * This adds the text on the [PhotoEditorView] at the provided initial [position]. + * by default [TextView.setText] will be 18sp + * The default interface implementation preserves backward binary compatibility by + * delegating to the centered overload, so custom implementations should override this + * method to honor [position]. + * + * @param text text to display + * @param colorCodeTextView text color to be displayed + * @param position initial position in pixels from the top-left of the editor + */ + @SuppressLint("ClickableViewAccessibility") + fun addText(text: String, colorCodeTextView: Int, position: Position) { + addText(text, colorCodeTextView) + } + /** * This add the text on the [PhotoEditorView] with provided parameters * by default [TextView.setText] will be 18sp @@ -47,6 +63,23 @@ interface PhotoEditor { @SuppressLint("ClickableViewAccessibility") fun addText(textTypeface: Typeface?, text: String, colorCodeTextView: Int) + /** + * This adds the text on the [PhotoEditorView] at the provided initial [position]. + * by default [TextView.setText] will be 18sp + * The default interface implementation preserves backward binary compatibility by + * delegating to the centered overload, so custom implementations should override this + * method to honor [position]. + * + * @param textTypeface typeface for custom font in the text + * @param text text to display + * @param colorCodeTextView text color to be displayed + * @param position initial position in pixels from the top-left of the editor + */ + @SuppressLint("ClickableViewAccessibility") + fun addText(textTypeface: Typeface?, text: String, colorCodeTextView: Int, position: Position) { + addText(textTypeface, text, colorCodeTextView) + } + /** * This add the text on the [PhotoEditorView] with provided parameters * by default [TextView.setText] will be 18sp @@ -57,6 +90,22 @@ interface PhotoEditor { @SuppressLint("ClickableViewAccessibility") fun addText(text: String, styleBuilder: TextStyleBuilder?) + /** + * This adds the text on the [PhotoEditorView] at the provided initial [position]. + * by default [TextView.setText] will be 18sp + * The default interface implementation preserves backward binary compatibility by + * delegating to the centered overload, so custom implementations should override this + * method to honor [position]. + * + * @param text text to display + * @param styleBuilder text style builder with your style + * @param position initial position in pixels from the top-left of the editor + */ + @SuppressLint("ClickableViewAccessibility") + fun addText(text: String, styleBuilder: TextStyleBuilder?, position: Position) { + addText(text, styleBuilder) + } + /** * This will update text and color on provided view * @@ -383,4 +432,4 @@ interface PhotoEditor { * through the use of a ShapeBuilder. */ fun setShape(shapeBuilder: ShapeBuilder) // endregion -} \ No newline at end of file +} diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditorImpl.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditorImpl.kt index ab0233ce..d8796a73 100644 --- a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditorImpl.kt +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/PhotoEditorImpl.kt @@ -59,16 +59,46 @@ internal class PhotoEditorImpl @SuppressLint("ClickableViewAccessibility") const addText(null, text, colorCodeTextView) } + override fun addText(text: String, colorCodeTextView: Int, position: Position) { + addText(null, text, colorCodeTextView, position) + } + override fun addText(textTypeface: Typeface?, text: String, colorCodeTextView: Int) { val styleBuilder = TextStyleBuilder() styleBuilder.withTextColor(colorCodeTextView) if (textTypeface != null) { styleBuilder.withTextFont(textTypeface) } - addText(text, styleBuilder) + addTextInternal(text, styleBuilder) + } + + override fun addText( + textTypeface: Typeface?, + text: String, + colorCodeTextView: Int, + position: Position + ) { + val styleBuilder = TextStyleBuilder() + styleBuilder.withTextColor(colorCodeTextView) + if (textTypeface != null) { + styleBuilder.withTextFont(textTypeface) + } + addTextInternal(text, styleBuilder, position) } override fun addText(text: String, styleBuilder: TextStyleBuilder?) { + addTextInternal(text, styleBuilder) + } + + override fun addText(text: String, styleBuilder: TextStyleBuilder?, position: Position) { + addTextInternal(text, styleBuilder, position) + } + + private fun addTextInternal( + text: String, + styleBuilder: TextStyleBuilder?, + position: Position? = null + ) { drawingView.enableDrawing(false) val multiTouchListener = getMultiTouchListener(isTextPinchScalable) val textGraphic = Text( @@ -79,7 +109,7 @@ internal class PhotoEditorImpl @SuppressLint("ClickableViewAccessibility") const mGraphicManager ) textGraphic.buildView(text, styleBuilder) - addToEditor(textGraphic) + addToEditor(textGraphic, position) } override fun editText(view: View, inputText: String, colorCode: Int) { @@ -125,9 +155,9 @@ internal class PhotoEditorImpl @SuppressLint("ClickableViewAccessibility") const addToEditor(emoji) } - private fun addToEditor(graphic: Graphic) { + private fun addToEditor(graphic: Graphic, position: Position? = null) { clearHelperBox() - mGraphicManager.addView(graphic) + mGraphicManager.addView(graphic, position) // Change the in-focus view viewState.currentSelectedView = graphic.rootView } @@ -293,4 +323,4 @@ internal class PhotoEditorImpl @SuppressLint("ClickableViewAccessibility") const } photoEditorView.setClipSourceImage(builder.clipSourceImage) } -} \ No newline at end of file +} diff --git a/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/Position.kt b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/Position.kt new file mode 100644 index 00000000..2f2a8a1a --- /dev/null +++ b/photoeditor/src/main/java/ja/burhanrashid52/photoeditor/Position.kt @@ -0,0 +1,13 @@ +package ja.burhanrashid52.photoeditor + +import androidx.annotation.Px + +/** + * Initial position for an overlay inside the [PhotoEditorView]. + * + * Coordinates are expressed in pixels from the top-left corner of the editor. + */ +data class Position( + @Px val x: Int, + @Px val y: Int +) diff --git a/photoeditor/src/test/java/ja/burhanrashid52/photoeditor/GraphicManagerTest.kt b/photoeditor/src/test/java/ja/burhanrashid52/photoeditor/GraphicManagerTest.kt index 087dc68a..e2a49f6c 100644 --- a/photoeditor/src/test/java/ja/burhanrashid52/photoeditor/GraphicManagerTest.kt +++ b/photoeditor/src/test/java/ja/burhanrashid52/photoeditor/GraphicManagerTest.kt @@ -1,10 +1,12 @@ package ja.burhanrashid52.photoeditor import android.content.Context +import android.widget.RelativeLayout import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import androidx.test.core.app.ApplicationProvider import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertNotNull import org.junit.Test @@ -39,4 +41,41 @@ class GraphicManagerTest { assertEquals(4, photoEditorView.childCount.toLong()) assertNotNull(photoEditorView.findViewById(childId)) } -} \ No newline at end of file + + @Test + fun testGraphicManagerAddViewsUsesCenterByDefault() { + val photoEditorView = PhotoEditorView(mContext) + val graphicManager = GraphicManager(photoEditorView, PhotoEditorViewState()) + val graphic = newTextGraphic(graphicManager) + + graphicManager.addView(graphic) + + val params = graphic.rootView.layoutParams as RelativeLayout.LayoutParams + assertEquals(RelativeLayout.TRUE, params.getRule(RelativeLayout.CENTER_IN_PARENT)) + } + + @Test + fun testGraphicManagerAddViewsUsesExplicitPositionWhenProvided() { + val photoEditorView = PhotoEditorView(mContext) + val graphicManager = GraphicManager(photoEditorView, PhotoEditorViewState()) + val graphic = newTextGraphic(graphicManager) + val position = Position(x = 42, y = 84) + + graphicManager.addView(graphic, position) + + val params = graphic.rootView.layoutParams as RelativeLayout.LayoutParams + assertEquals(42, params.leftMargin) + assertEquals(84, params.topMargin) + assertEquals(RelativeLayout.TRUE, params.getRule(RelativeLayout.ALIGN_PARENT_TOP)) + assertFalse(params.getRule(RelativeLayout.CENTER_IN_PARENT) == RelativeLayout.TRUE) + } + + private fun newTextGraphic(graphicManager: GraphicManager): Graphic { + return object : Graphic( + context = mContext, + layoutId = R.layout.view_photo_editor_text, + viewType = ViewType.TEXT, + graphicManager = graphicManager + ) {} + } +}