From f6c1fa2f5ce399df907df7e3cfa60184c4e6d236 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Wed, 11 Mar 2026 13:40:09 +0100 Subject: [PATCH] apply code format part 1 --- .../java/com/b44t/messenger/TestUtils.java | 53 +- .../uibenchmarks/EnterChatsBenchmark.java | 80 +- .../uitests/offline/ForwardingTest.java | 27 +- .../uitests/offline/SharingTest.java | 136 +- .../uitests/online/OnboardingTest.java | 14 +- src/foss/AndroidManifest.xml | 18 +- src/gplay/AndroidManifest.xml | 57 +- .../java/com/b44t/messenger/DcAccounts.java | 124 +- .../com/b44t/messenger/DcBackupProvider.java | 46 +- .../java/com/b44t/messenger/DcChatlist.java | 89 +- src/main/java/com/b44t/messenger/DcEvent.java | 41 +- .../com/b44t/messenger/DcEventChannel.java | 11 +- .../com/b44t/messenger/DcEventEmitter.java | 35 +- .../com/b44t/messenger/DcJsonrpcInstance.java | 29 +- src/main/java/com/b44t/messenger/DcLot.java | 47 +- .../b44t/messenger/DcMediaGalleryElement.java | 10 +- .../java/com/b44t/messenger/DcProvider.java | 38 +- .../java/com/b44t/messenger/FFITransport.java | 2 +- .../StickyHeaderGridAdapter.java | 1111 ++++--- .../StickyHeaderGridLayoutManager.java | 2576 +++++++++-------- .../securesms/imageeditor/CanvasMatrix.java | 27 +- .../imageeditor/ColorableRenderer.java | 4 +- .../securesms/imageeditor/DrawingSession.java | 14 +- .../securesms/imageeditor/EditSession.java | 19 +- .../imageeditor/ElementDragEditSession.java | 17 +- .../imageeditor/ElementEditSession.java | 14 +- .../imageeditor/ElementScaleEditSession.java | 31 +- .../securesms/imageeditor/HiddenEditText.java | 52 +- .../ImageEditorMediaConstraints.java | 33 +- .../imageeditor/ImageEditorView.java | 267 +- .../securesms/imageeditor/Renderer.java | 9 +- .../imageeditor/RendererContext.java | 68 +- .../imageeditor/ThumbDragEditSession.java | 34 +- .../imageeditor/model/AlphaAnimation.java | 27 +- .../imageeditor/model/AnimationMatrix.java | 52 +- .../securesms/imageeditor/model/Bisect.java | 156 +- .../imageeditor/model/CropThumbRenderer.java | 48 +- .../imageeditor/model/EditorElement.java | 144 +- .../model/EditorElementHierarchy.java | 154 +- .../imageeditor/model/EditorFlags.java | 19 +- .../imageeditor/model/EditorModel.java | 327 ++- .../imageeditor/model/ElementStack.java | 54 +- .../imageeditor/model/InBoundsMemory.java | 9 +- .../imageeditor/model/ParcelUtils.java | 10 +- .../imageeditor/model/ThumbRenderer.java | 54 +- .../imageeditor/model/UndoRedoStacks.java | 43 +- .../AutomaticControlPointBezierLine.java | 53 +- .../renderers/BezierDrawingRenderer.java | 79 +- .../renderers/CropAreaRenderer.java | 58 +- .../renderers/InvalidateableRenderer.java | 7 +- .../renderers/InverseFillRenderer.java | 26 +- .../renderers/MultiLineTextRenderer.java | 115 +- .../renderers/OvalGuideRenderer.java | 41 +- src/main/res/anim/animation_toggle_in.xml | 17 +- src/main/res/anim/animation_toggle_out.xml | 17 +- src/main/res/anim/fade_scale_in.xml | 17 +- src/main/res/anim/fade_scale_out.xml | 17 +- src/main/res/anim/slide_from_right.xml | 8 +- src/main/res/anim/slide_to_right.xml | 8 +- .../bottom_pause_to_play_animation.xml | 15 +- .../bottom_play_to_pause_animation.xml | 15 +- src/main/res/animator/rotate_90_animation.xml | 6 +- .../animator/rotate_minus_90_animation.xml | 6 +- .../upper_pause_to_play_animation.xml | 15 +- .../upper_play_to_pause_animation.xml | 15 +- src/main/res/color/text_color_dark_theme.xml | 9 +- .../color/text_color_secondary_dark_theme.xml | 9 +- ...ssage_bubble_background_received_alone.xml | 10 +- .../message_bubble_background_sent_alone.xml | 10 +- ...bble_background_sent_alone_with_border.xml | 10 +- .../res/drawable-night/button_background.xml | 7 +- .../button_secondary_background.xml | 7 +- src/main/res/drawable-night/jumpto_btn_bg.xml | 4 +- .../archived_indicator_background.xml | 10 +- .../drawable/attachment_selector_shadow.xml | 11 +- src/main/res/drawable/badge_divider.xml | 3 +- .../res/drawable/baseline_bookmark_24.xml | 9 +- .../drawable/baseline_bookmark_border_24.xml | 13 +- .../drawable/baseline_bookmark_remove_24.xml | 9 +- src/main/res/drawable/baseline_call_24.xml | 6 +- .../res/drawable/baseline_call_end_24.xml | 13 +- src/main/res/drawable/button_background.xml | 7 +- .../drawable/button_secondary_background.xml | 7 +- src/main/res/drawable/circle_alpha.xml | 4 +- src/main/res/drawable/circle_tintable.xml | 2 +- .../circle_touch_highlight_background.xml | 7 +- .../res/drawable/circle_universal_overlay.xml | 7 +- src/main/res/drawable/circle_white.xml | 7 +- .../drawable/compose_divider_background.xml | 4 +- src/main/res/drawable/contact_blocked_24.xml | 13 +- .../drawable/contact_list_divider_dark.xml | 8 +- .../drawable/contact_list_divider_light.xml | 10 +- .../res/drawable/contact_photo_background.xml | 5 +- .../conversation_attachment_close_circle.xml | 11 +- .../drawable/conversation_attachment_edit.xml | 14 +- .../drawable/conversation_item_background.xml | 9 +- .../conversation_item_background_animated.xml | 10 +- ...rsation_item_sent_indicator_text_shape.xml | 12 +- ...on_item_sent_indicator_text_shape_dark.xml | 12 +- .../conversation_item_update_background.xml | 3 +- .../conversation_list_divider_shape.xml | 3 +- .../conversation_list_divider_shape_dark.xml | 3 +- .../conversation_list_item_background.xml | 11 +- ...conversation_list_item_background_dark.xml | 11 +- .../delete_account_item_background.xml | 3 +- src/main/res/drawable/dismiss_background.xml | 4 +- .../res/drawable/floating_mini_bg_dark.xml | 5 +- .../res/drawable/floating_mini_bg_light.xml | 5 +- src/main/res/drawable/ic_advanced_24dp.xml | 5 +- src/main/res/drawable/ic_apps_24.xml | 9 +- .../res/drawable/ic_baseline_devices_24.xml | 6 +- src/main/res/drawable/ic_baseline_eye.xml | 13 +- .../res/drawable/ic_brightness_6_24dp.xml | 5 +- src/main/res/drawable/ic_chevron_up.xml | 12 +- .../res/drawable/ic_circle_status_online.xml | 3 +- .../drawable/ic_delivery_status_failed.xml | 6 +- src/main/res/drawable/ic_donate_24dp.xml | 9 +- src/main/res/drawable/ic_forum_24dp.xml | 5 +- .../res/drawable/ic_forward_white_24dp.xml | 9 +- src/main/res/drawable/ic_help_24dp.xml | 5 +- .../ic_launcher_foreground_monochrome.xml | 22 +- src/main/res/drawable/ic_link_24.xml | 13 +- src/main/res/drawable/ic_lock.xml | 12 +- src/main/res/drawable/ic_map_white_24dp.xml | 6 +- .../res/drawable/ic_notifications_24dp.xml | 5 +- src/main/res/drawable/ic_outline_email.xml | 12 +- src/main/res/drawable/ic_person_large.xml | 6 +- .../res/drawable/ic_proxy_disabled_24.xml | 13 +- src/main/res/drawable/ic_proxy_enabled_24.xml | 13 +- src/main/res/drawable/ic_qr_code_24.xml | 9 +- .../res/drawable/ic_qr_code_scanner_24.xml | 9 +- src/main/res/drawable/ic_share_white_24dp.xml | 6 +- src/main/res/drawable/ic_sticker_24.xml | 9 +- src/main/res/drawable/ic_swap_vert_24dp.xml | 5 +- src/main/res/drawable/ic_timer_gray_18dp.xml | 6 +- src/main/res/drawable/ic_verified.xml | 8 +- .../res/drawable/ic_videocam_white_24dp.xml | 6 +- src/main/res/drawable/image_shade.xml | 5 +- src/main/res/drawable/jumpto_btn_bg.xml | 4 +- ...ssage_bubble_background_received_alone.xml | 10 +- .../message_bubble_background_sent_alone.xml | 10 +- ...bble_background_sent_alone_with_border.xml | 10 +- src/main/res/drawable/pause_icon.xml | 32 +- .../res/drawable/pause_to_play_animation.xml | 18 +- .../drawable/pinned_list_item_background.xml | 11 +- .../pinned_list_item_background_dark.xml | 11 +- src/main/res/drawable/play_icon.xml | 29 +- .../res/drawable/play_to_pause_animation.xml | 18 +- .../res/drawable/reaction_pill_background.xml | 3 +- .../reaction_pill_background_selected.xml | 3 +- .../recording_lock_background_dark.xml | 9 +- .../recording_lock_background_light.xml | 9 +- .../res/drawable/rounded_arrow_forward_24.xml | 9 +- .../res/drawable/search_toolbar_shadow.xml | 10 +- src/main/res/drawable/send_button_bg_dark.xml | 5 +- .../res/drawable/send_button_bg_light.xml | 5 +- .../drawable/sticker_missing_background.xml | 3 +- .../sticky_date_header_background_dark.xml | 5 +- .../sticky_date_header_background_light.xml | 5 +- .../drawable/touch_highlight_background.xml | 8 +- .../touch_highlight_background_strong.xml | 13 +- src/main/res/drawable/triangle_right.xml | 10 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 11 +- .../res/xml-v29/preferences_appearance.xml | 24 +- src/main/res/xml/file_provider_paths.xml | 10 +- src/main/res/xml/preferences.xml | 57 +- src/main/res/xml/preferences_advanced.xml | 57 +- src/main/res/xml/preferences_appearance.xml | 24 +- src/main/res/xml/preferences_chats.xml | 38 +- .../res/xml/preferences_notifications.xml | 127 +- src/main/res/xml/shortcuts.xml | 12 +- 171 files changed, 4115 insertions(+), 3849 deletions(-) diff --git a/src/androidTest/java/com/b44t/messenger/TestUtils.java b/src/androidTest/java/com/b44t/messenger/TestUtils.java index 1953ee179..b7bbf0407 100644 --- a/src/androidTest/java/com/b44t/messenger/TestUtils.java +++ b/src/androidTest/java/com/b44t/messenger/TestUtils.java @@ -11,7 +11,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.view.View; - import androidx.annotation.NonNull; import androidx.test.espresso.NoMatchingViewException; import androidx.test.espresso.UiController; @@ -19,7 +18,6 @@ import androidx.test.espresso.ViewAction; import androidx.test.espresso.ViewInteraction; import androidx.test.espresso.util.TreeIterables; import androidx.test.ext.junit.rules.ActivityScenarioRule; - import org.hamcrest.Matcher; import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; @@ -60,10 +58,12 @@ public class TestUtils { } @NonNull - public static ActivityScenarioRule getOfflineActivityRule(boolean useExistingChats) { + public static ActivityScenarioRule getOfflineActivityRule( + boolean useExistingChats) { Intent intent = - Intent.makeMainActivity( - new ComponentName(getInstrumentation().getTargetContext(), ConversationListActivity.class)); + Intent.makeMainActivity( + new ComponentName( + getInstrumentation().getTargetContext(), ConversationListActivity.class)); if (!useExistingChats) { createOfflineAccount(); } @@ -72,18 +72,22 @@ public class TestUtils { } @NonNull - public static ActivityScenarioRule getOnlineActivityRule(Class activityClass) { + public static ActivityScenarioRule getOnlineActivityRule( + Class activityClass) { Context context = getInstrumentation().getTargetContext(); AccountManager.getInstance().beginAccountCreation(context); prepare(); - return new ActivityScenarioRule<>(new Intent(getInstrumentation().getTargetContext(), activityClass)); + return new ActivityScenarioRule<>( + new Intent(getInstrumentation().getTargetContext(), activityClass)); } private static void prepare() { - Prefs.setBooleanPreference(getInstrumentation().getTargetContext(), Prefs.DOZE_ASKED_DIRECTLY, true); + Prefs.setBooleanPreference( + getInstrumentation().getTargetContext(), Prefs.DOZE_ASKED_DIRECTLY, true); if (!AccessibilityUtil.areAnimationsDisabled(getInstrumentation().getTargetContext())) { - throw new RuntimeException("To run the tests, disable animations at Developer options' " + - "-> 'Window/Transition/Animator animation scale' -> Set all 3 to 'off'"); + throw new RuntimeException( + "To run the tests, disable animations at Developer options' " + + "-> 'Window/Transition/Animator animation scale' -> Set all 3 to 'off'"); } } @@ -116,26 +120,22 @@ public class TestUtils { } throw new NoMatchingViewException.Builder() - .withRootView(view) - .withViewMatcher(matcher) - .build(); + .withRootView(view) + .withViewMatcher(matcher) + .build(); } }; } /** - * Perform action of implicitly waiting for a certain view. - * This differs from EspressoExtensions.searchFor in that, - * upon failure to locate an element, it will fetch a new root view - * in which to traverse searching for our @param match + * Perform action of implicitly waiting for a certain view. This differs from + * EspressoExtensions.searchFor in that, upon failure to locate an element, it will fetch a new + * root view in which to traverse searching for our @param match * * @param viewMatcher ViewMatcher used to find our view */ public static ViewInteraction waitForView( - Matcher viewMatcher, - int waitMillis, - int waitMillisPerTry - ) { + Matcher viewMatcher, int waitMillis, int waitMillisPerTry) { // Derive the max tries int maxTries = (int) (waitMillis / waitMillisPerTry); @@ -164,12 +164,11 @@ public class TestUtils { } /** - * Normally, you would do - * onView(withId(R.id.send_button)).perform(click()); - * to send the draft message. However, in order to change the send button to the attach button - * while there is no draft, the send button is made invisible and the attach button is made - * visible instead. This confuses the test framework.

- * + * Normally, you would do onView(withId(R.id.send_button)).perform(click()); to send the draft + * message. However, in order to change the send button to the attach button while there is no + * draft, the send button is made invisible and the attach button is made visible instead. This + * confuses the test framework.
+ *
* So, this is a workaround for pressing the send button. */ public static void pressSend() { diff --git a/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java b/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java index 645585b5a..e9bbb6a03 100644 --- a/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java +++ b/src/androidTest/java/com/b44t/messenger/uibenchmarks/EnterChatsBenchmark.java @@ -10,14 +10,11 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import android.util.Log; - import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.TestUtils; - import org.junit.After; import org.junit.Ignore; import org.junit.Rule; @@ -34,18 +31,19 @@ public class EnterChatsBenchmark { // ============================================================================================== // Set this to true if you already have at least 10 chats on your existing DeltaChat installation // and want to traverse through them instead of 10 newly created chats - private final static boolean USE_EXISTING_CHATS = false; + private static final boolean USE_EXISTING_CHATS = false; // ============================================================================================== - private final static int GO_THROUGH_ALL_CHATS_N_TIMES = 8; + private static final int GO_THROUGH_ALL_CHATS_N_TIMES = 8; // ============================================================================================== // PLEASE BACKUP YOUR ACCOUNT BEFORE RUNNING THIS! // ============================================================================================== - private final static String TAG = EnterChatsBenchmark.class.getSimpleName(); + private static final String TAG = EnterChatsBenchmark.class.getSimpleName(); @Rule - public ActivityScenarioRule activityRule = TestUtils.getOfflineActivityRule(USE_EXISTING_CHATS); + public ActivityScenarioRule activityRule = + TestUtils.getOfflineActivityRule(USE_EXISTING_CHATS); @Test public void createAndEnter10FilledChats() { @@ -55,7 +53,9 @@ public class EnterChatsBenchmark { for (int i = 0; i < GO_THROUGH_ALL_CHATS_N_TIMES; i++) { times[i] = "" + timeGoToNChats(10); // 10 group chats were created } - Log.i(TAG, "MEASURED RESULTS (Benchmark) - Going thorough all 10 chats: " + String.join(",", times)); + Log.i( + TAG, + "MEASURED RESULTS (Benchmark) - Going thorough all 10 chats: " + String.join(",", times)); } @Test @@ -66,13 +66,17 @@ public class EnterChatsBenchmark { for (int i = 0; i < GO_THROUGH_ALL_CHATS_N_TIMES; i++) { times[i] = "" + timeGoToNChats(1); } - Log.i(TAG, "MEASURED RESULTS (Benchmark) - Entering and leaving 1 empty chat: " + String.join(",", times)); + Log.i( + TAG, + "MEASURED RESULTS (Benchmark) - Entering and leaving 1 empty chat: " + + String.join(",", times)); } @Test public void enterFilledChat() { if (!USE_EXISTING_CHATS) { - createChatAndGoBack("Group #1", true, "Hello!", "Some links: https://testrun.org", "And a command: /help"); + createChatAndGoBack( + "Group #1", true, "Hello!", "Some links: https://testrun.org", "And a command: /help"); } String[] times = new String[50]; @@ -82,7 +86,18 @@ public class EnterChatsBenchmark { long end = System.currentTimeMillis(); long diff = end - start; pressBack(); - Log.i(TAG, "Measured (Benchmark) " + (i+1) + "/" + times.length + ": Entering 1 filled chat took " + diff + "ms " + "(going back took " + (System.currentTimeMillis() - end) + "ms)"); + Log.i( + TAG, + "Measured (Benchmark) " + + (i + 1) + + "/" + + times.length + + ": Entering 1 filled chat took " + + diff + + "ms " + + "(going back took " + + (System.currentTimeMillis() - end) + + "ms)"); times[i] = "" + diff; } @@ -91,16 +106,37 @@ public class EnterChatsBenchmark { private void create10Chats(boolean fillWithMsgs) { if (!USE_EXISTING_CHATS) { - createChatAndGoBack("Group #1", fillWithMsgs, "Hello!", "Some links: https://testrun.org", "And a command: /help"); - createChatAndGoBack("Group #2", fillWithMsgs, "example.org, alice@example.org", "aaaaaaa", "bbbbbb"); - createChatAndGoBack("Group #3", fillWithMsgs, repeat("Some string ", 600), repeat("Another string", 200), "Hi!!!"); + createChatAndGoBack( + "Group #1", + fillWithMsgs, + "Hello!", + "Some links: https://testrun.org", + "And a command: /help"); + createChatAndGoBack( + "Group #2", fillWithMsgs, "example.org, alice@example.org", "aaaaaaa", "bbbbbb"); + createChatAndGoBack( + "Group #3", + fillWithMsgs, + repeat("Some string ", 600), + repeat("Another string", 200), + "Hi!!!"); createChatAndGoBack("Group #4", fillWithMsgs, "xyzabc", "Hi!!!!", "Let's meet!"); - createChatAndGoBack("Group #5", fillWithMsgs, repeat("aaaa", 40), "bbbbbbbbbbbbbbbbbb", "ccccccccccccccc"); - createChatAndGoBack("Group #6", fillWithMsgs, "aaaaaaaaaaa", repeat("Hi! ", 1000), "bbbbbbbbbb"); - createChatAndGoBack("Group #7", fillWithMsgs, repeat("abcdefg ", 500), repeat("xxxxx", 100), "yrrrrrrrrrrrrr"); - createChatAndGoBack("Group #8", fillWithMsgs, "and a number: 037362/384756", "ccccc", "Nice!"); - createChatAndGoBack("Group #9", fillWithMsgs, "ddddddddddddddddd", "zuuuuuuuuuuuuuuuu", "ccccc"); - createChatAndGoBack("Group #10", fillWithMsgs, repeat("xxxxxxyyyyy", 100), repeat("String!!", 10), "abcd"); + createChatAndGoBack( + "Group #5", fillWithMsgs, repeat("aaaa", 40), "bbbbbbbbbbbbbbbbbb", "ccccccccccccccc"); + createChatAndGoBack( + "Group #6", fillWithMsgs, "aaaaaaaaaaa", repeat("Hi! ", 1000), "bbbbbbbbbb"); + createChatAndGoBack( + "Group #7", + fillWithMsgs, + repeat("abcdefg ", 500), + repeat("xxxxx", 100), + "yrrrrrrrrrrrrr"); + createChatAndGoBack( + "Group #8", fillWithMsgs, "and a number: 037362/384756", "ccccc", "Nice!"); + createChatAndGoBack( + "Group #9", fillWithMsgs, "ddddddddddddddddd", "zuuuuuuuuuuuuuuuu", "ccccc"); + createChatAndGoBack( + "Group #10", fillWithMsgs, repeat("xxxxxxyyyyy", 100), repeat("String!!", 10), "abcd"); } } @@ -130,10 +166,10 @@ public class EnterChatsBenchmark { onView(withContentDescription(R.string.group_create_button)).perform(click()); if (fillWithMsgs) { - for (String t: texts) { + for (String t : texts) { sendText(t); } - for (String t: texts) { + for (String t : texts) { sendText(t); } } diff --git a/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java b/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java index 5c6acacb1..aa2be24eb 100644 --- a/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java +++ b/src/androidTest/java/com/b44t/messenger/uitests/offline/ForwardingTest.java @@ -14,11 +14,10 @@ import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.DcContact; import com.b44t.messenger.DcContext; import com.b44t.messenger.TestUtils; - +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -29,8 +28,6 @@ import org.thoughtcrime.securesms.ConversationListActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.connect.DcHelper; -import java.util.concurrent.TimeUnit; - @RunWith(AndroidJUnit4.class) @LargeTest public class ForwardingTest { @@ -43,7 +40,8 @@ public class ForwardingTest { } @Rule - public final ActivityScenarioRule activityRule = TestUtils.getOfflineActivityRule(false); + public final ActivityScenarioRule activityRule = + TestUtils.getOfflineActivityRule(false); @Before public void createChats() { @@ -51,10 +49,13 @@ public class ForwardingTest { dcContext.createChatByContactId(DcContact.DC_CONTACT_ID_SELF); // Disable bcc_self so that DC doesn't try to send messages to the server. // If we didn't do this, messages would stay in DC_STATE_OUT_PENDING forever. - // The thing is, DC_STATE_OUT_PENDING show a rotating circle animation, and Espresso doesn't work + // The thing is, DC_STATE_OUT_PENDING show a rotating circle animation, and Espresso doesn't + // work // with animations, and the tests would hang and never finish. dcContext.setConfig("bcc_self", "0"); - activityRule.getScenario().onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat( "group")); + activityRule + .getScenario() + .onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat("group")); } @After @@ -68,7 +69,8 @@ public class ForwardingTest { // The group is at position 0, self chat is at position 1, device talk is at position 2 onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click())); onView(withId(R.id.title)).check(matches(withText(R.string.device_talk))); - onView(withId(android.R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); + onView(withId(android.R.id.list)) + .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); onView(withId(R.id.menu_context_forward)).perform(click()); // Send it to self chat (which is sorted to the top because we're forwarding) onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); @@ -77,11 +79,13 @@ public class ForwardingTest { pressBack(); - onView(withId(R.id.toolbar_title)).check(matches(withText(R.string.connectivity_not_connected))); + onView(withId(R.id.toolbar_title)) + .check(matches(withText(R.string.connectivity_not_connected))); // Self chat moved up because we sent a message there onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); onView(withId(R.id.title)).check(matches(withText(R.string.saved_messages))); - onView(withId(android.R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); + onView(withId(android.R.id.list)) + .perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick())); onView(withId(R.id.menu_context_forward)).perform(click()); // Send it to the group onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click())); @@ -89,6 +93,7 @@ public class ForwardingTest { onView(withId(R.id.title)).check(matches(withText("group"))); pressBack(); - onView(withId(R.id.toolbar_title)).check(matches(withText(R.string.connectivity_not_connected))); + onView(withId(R.id.toolbar_title)) + .check(matches(withText(R.string.connectivity_not_connected))); } } diff --git a/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java b/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java index 023940335..11416cd1c 100644 --- a/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java +++ b/src/androidTest/java/com/b44t/messenger/uitests/offline/SharingTest.java @@ -14,15 +14,13 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import android.content.ComponentName; import android.content.Intent; import android.net.Uri; - import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.DcContext; import com.b44t.messenger.TestUtils; - +import java.io.File; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -34,9 +32,6 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.ShareActivity; import org.thoughtcrime.securesms.connect.DcHelper; -import java.io.File; - - @RunWith(AndroidJUnit4.class) @LargeTest public class SharingTest { @@ -48,26 +43,34 @@ public class SharingTest { private static int createdSingleChatId; @Rule - public final ActivityScenarioRule activityRule = TestUtils.getOfflineActivityRule(false); + public final ActivityScenarioRule activityRule = + TestUtils.getOfflineActivityRule(false); @Before public void createGroup() { - activityRule.getScenario().onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat( "group")); + activityRule + .getScenario() + .onActivity(a -> createdGroupId = DcHelper.getContext(a).createGroupChat("group")); } @Before public void createSingleChat() { - activityRule.getScenario().onActivity(a -> { - int contactId = DcHelper.getContext(a).createContact("", "abc@example.org"); - createdSingleChatId = DcHelper.getContext(a).createChatByContactId(contactId); - }); + activityRule + .getScenario() + .onActivity( + a -> { + int contactId = DcHelper.getContext(a).createContact("", "abc@example.org"); + createdSingleChatId = DcHelper.getContext(a).createChatByContactId(contactId); + }); } @Test public void testNormalSharing() { Intent i = new Intent(Intent.ACTION_SEND); i.putExtra(Intent.EXTRA_TEXT, "Hello!"); - i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); + i.setComponent( + new ComponentName( + getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); activityRule.getScenario().onActivity(a -> a.startActivity(i)); onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); @@ -77,9 +80,9 @@ public class SharingTest { } /** - * Test direct sharing from a screenshot. - * Also, this is the regression test for https://github.com/deltachat/deltachat-android/issues/2040 - * where network changes during sharing lead to a bug + * Test direct sharing from a screenshot. Also, this is the regression test for + * https://github.com/deltachat/deltachat-android/issues/2040 where network changes during sharing + * lead to a bug */ @Test public void testShareFromScreenshot() { @@ -92,20 +95,25 @@ public class SharingTest { pngImage = file; } } - Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(pngImage)); + Uri uri = + Uri.parse( + "content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(pngImage)); DcHelper.sharedFiles.put(pngImage, "image/png"); Intent i = new Intent(Intent.ACTION_SEND); i.setType("image/png"); i.putExtra(Intent.EXTRA_SUBJECT, "Screenshot (Sep 27, 2021 00:00:00"); - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET + i.setFlags( + Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP | Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_GRANT_READ_URI_PERMISSION); i.putExtra(Intent.EXTRA_STREAM, uri); i.putExtra(ShareActivity.EXTRA_CHAT_ID, createdGroupId); - i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); + i.setComponent( + new ComponentName( + getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); activityRule.getScenario().onActivity(a -> a.startActivity(i)); TestUtils.waitForView(withId(R.id.send_button), 10000, 50); @@ -121,16 +129,32 @@ public class SharingTest { } /** - * Tests https://github.com/deltachat/interface/blob/master/user-testing/mailto-links.md#mailto-links: + * Tests + * https://github.com/deltachat/interface/blob/master/user-testing/mailto-links.md#mailto-links: * *
    - *
  • Just an email address - should open a chat with abc@example.org (and maybe ask whether a chat should be created if it does not exist already)
  • - *
  • email address with subject - should open a chat with abc@example.org and fill testing mailto uris; as we created the chat in the previous step, it should not ask Chat with … but directly open the chat
  • - *
  • email address with body - should open a chat with abc@example.org, draft this is a test
  • - *
  • email address with subject and body - should open a chat with abc@example.org, draft testing mailto uris <newline> this is a test
  • - *
  • HTML encoding - should open a chat with info@example.org
  • - *
  • more HTML encoding - should open a chat with simplebot@example.org, draft !web https://duckduckgo.com/lite?q=duck%20it
  • - *
  • no email, just subject&body - this should let you choose a chat and create a draft bla <newline> blub there
  • + *
  • Just an email address - should open a chat with + * abc@example.org (and maybe ask whether a chat should be created if it does + * not exist already) + *
  • email address with + * subject - should open a chat with abc@example.org and fill + * testing mailto uris; as we created the chat in the previous step, it should not + * ask Chat with … but directly open the chat + *
  • email address with body - + * should open a chat with abc@example.org, draft this is a test + *
  • email + * address with subject and body - should open a chat with abc@example.org, + * draft testing mailto uris <newline> this is a test + *
  • HTML encoding - should open a chat with + * info@example.org + *
  • more + * HTML encoding - should open a chat with simplebot@example.org, draft + * !web https://duckduckgo.com/lite?q=duck%20it + *
  • no email, just subject&body - this + * should let you choose a chat and create a draft bla <newline> + * blub there *
*/ @Test @@ -140,7 +164,8 @@ public class SharingTest { openLink("mailto:abc@example.org?subject=testing%20mailto%20uris"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("testing mailto uris"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("testing mailto uris"))); openLink("mailto:abc@example.org?body=this%20is%20a%20test"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); @@ -148,17 +173,22 @@ public class SharingTest { openLink("mailto:abc@example.org?subject=testing%20mailto%20uris&body=this%20is%20a%20test"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("testing mailto uris\nthis is a test"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("testing mailto uris\nthis is a test"))); openLink("mailto:%20abc@example.org"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - openLink("mailto:abc@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it"); + openLink( + "mailto:abc@example.org?body=!web%20https%3A%2F%2Fduckduckgo.com%2Flite%3Fq%3Dduck%2520it"); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("!web https://duckduckgo.com/lite?q=duck%20it"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("!web https://duckduckgo.com/lite?q=duck%20it"))); openLink("mailto:?subject=bla&body=blub"); - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); onView(withId(R.id.subtitle)).check(matches(withText("abc@example.org"))); onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("bla\nblub"))); } @@ -170,49 +200,61 @@ public class SharingTest { } /** + * + * *
    - *
  • Open Saved Messages chat (could be any other chat too)
  • - *
  • Go to another app and share some text to DC
  • - *
  • In DC select Saved Messages. Edit the shared text if you like. Don't hit the Send button.
  • - *
  • Leave DC
  • - *
  • Open DC again from the "Recent apps"
  • - *
  • Check that your draft is still there
  • + *
  • Open Saved Messages chat (could be any other chat too) + *
  • Go to another app and share some text to DC + *
  • In DC select Saved Messages. Edit the shared text if you like. Don't hit the + * Send button. + *
  • Leave DC + *
  • Open DC again from the "Recent apps" + *
  • Check that your draft is still there *
*/ @Test public void testOpenAgainFromRecents() { // Open a chat - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); // Share some text to DC Intent i = new Intent(Intent.ACTION_SEND); i.putExtra(Intent.EXTRA_TEXT, "Veeery important draft"); - i.setComponent(new ComponentName(getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); + i.setComponent( + new ComponentName( + getInstrumentation().getTargetContext().getApplicationContext(), ShareActivity.class)); activityRule.getScenario().onActivity(a -> a.startActivity(i)); // In DC, select the same chat you opened before - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); // Leave DC and go back to the previous activity pressBack(); // Here, we can't exactly replicate the "steps to reproduce". Previously, the other activity // stayed open in the background, but since it doesn't anymore, we need to open it again: - onView(withId(R.id.list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); + onView(withId(R.id.list)) + .perform( + RecyclerViewActions.actionOnItem(hasDescendant(withText("abc@example.org")), click())); // Check that the draft is still there // Util.sleep(2000); // Uncomment for debugging - onView(withHint(R.string.chat_input_placeholder)).check(matches(withText("Veeery important draft"))); + onView(withHint(R.string.chat_input_placeholder)) + .check(matches(withText("Veeery important draft"))); } /** * Regression test: * - * If you save your contacts's emails in the contacts app of the phone, there are buttons to call - * them and also to write an email to them. + *

If you save your contacts's emails in the contacts app of the phone, there are buttons to + * call them and also to write an email to them. * - * If you click the email button, Delta Chat opened but instead of opening a chat with that contact, - * the chat list was show and "share with" was displayed at the top + *

If you click the email button, Delta Chat opened but instead of opening a chat with that + * contact, the chat list was show and "share with" was displayed at the top */ @Test public void testOpenChatFromContacts() { diff --git a/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java b/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java index 880de597e..e5cc22677 100644 --- a/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java +++ b/src/androidTest/java/com/b44t/messenger/uitests/online/OnboardingTest.java @@ -1,6 +1,5 @@ package com.b44t.messenger.uitests.online; - import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.replaceText; @@ -11,13 +10,10 @@ import static androidx.test.espresso.matcher.ViewMatchers.withHint; import static androidx.test.espresso.matcher.ViewMatchers.withText; import android.text.TextUtils; - import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; - import com.b44t.messenger.TestUtils; - import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -30,14 +26,16 @@ import org.thoughtcrime.securesms.WelcomeActivity; @LargeTest public class OnboardingTest { @Rule - public ActivityScenarioRule activityRule = TestUtils.getOnlineActivityRule(WelcomeActivity.class); + public ActivityScenarioRule activityRule = + TestUtils.getOnlineActivityRule(WelcomeActivity.class); @Test public void testAccountCreation() { if (TextUtils.isEmpty(BuildConfig.TEST_ADDR) || TextUtils.isEmpty(BuildConfig.TEST_MAIL_PW)) { - throw new RuntimeException("You need to set TEST_ADDR and TEST_MAIL_PW; " + - "either in gradle.properties or via an environment variable. " + - "See README.md for more details."); + throw new RuntimeException( + "You need to set TEST_ADDR and TEST_MAIL_PW; " + + "either in gradle.properties or via an environment variable. " + + "See README.md for more details."); } onView(withText(R.string.scan_invitation_code)).check(matches(isClickable())); onView(withText(R.string.import_backup_title)).check(matches(isClickable())); diff --git a/src/foss/AndroidManifest.xml b/src/foss/AndroidManifest.xml index 498c668a4..03efd9a0c 100644 --- a/src/foss/AndroidManifest.xml +++ b/src/foss/AndroidManifest.xml @@ -1,14 +1,14 @@ - + - + - - - + + + diff --git a/src/gplay/AndroidManifest.xml b/src/gplay/AndroidManifest.xml index b0d786db4..a15be01a4 100644 --- a/src/gplay/AndroidManifest.xml +++ b/src/gplay/AndroidManifest.xml @@ -1,34 +1,39 @@ - + - - + + - - - + + + - - + + - - - - - + + + + + - - - + + + diff --git a/src/main/java/com/b44t/messenger/DcAccounts.java b/src/main/java/com/b44t/messenger/DcAccounts.java index 8a08984d3..d0bc07895 100644 --- a/src/main/java/com/b44t/messenger/DcAccounts.java +++ b/src/main/java/com/b44t/messenger/DcAccounts.java @@ -2,56 +2,82 @@ package com.b44t.messenger; public class DcAccounts { - public DcAccounts(String dir, DcEventChannel channel) { - accountsCPtr = createAccountsCPtr(dir, channel); - if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer"); + public DcAccounts(String dir, DcEventChannel channel) { + accountsCPtr = createAccountsCPtr(dir, channel); + if (accountsCPtr == 0) throw new RuntimeException("createAccountsCPtr() returned null pointer"); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unref(); + } + + public void unref() { + if (accountsCPtr != 0) { + unrefAccountsCPtr(); + accountsCPtr = 0; } + } - @Override - protected void finalize() throws Throwable { - super.finalize(); - unref(); - } - - public void unref() { - if (accountsCPtr != 0) { - unrefAccountsCPtr(); - accountsCPtr = 0; - } - } - - public DcEventEmitter getEventEmitter () { return new DcEventEmitter(getEventEmitterCPtr()); } - public DcJsonrpcInstance getJsonrpcInstance () { return new DcJsonrpcInstance(getJsonrpcInstanceCPtr()); } - public native void startIo (); - public native void stopIo (); - public native void maybeNetwork (); - public native void setPushDeviceToken (String token); - public native boolean backgroundFetch (int timeoutSeconds); - public native void stopBackgroundFetch (); - - public native int migrateAccount (String dbfile); - public native boolean removeAccount (int accountId); - public native int[] getAll (); - public DcContext getAccount (int accountId) { return new DcContext(getAccountCPtr(accountId)); } - public DcContext getSelectedAccount () { return new DcContext(getSelectedAccountCPtr()); } - public native boolean selectAccount (int accountId); - - // working with raw c-data - private long accountsCPtr; // CAVE: the name is referenced in the JNI - private native long createAccountsCPtr (String dir, DcEventChannel channel); - private native void unrefAccountsCPtr (); - private native long getEventEmitterCPtr (); - private native long getJsonrpcInstanceCPtr (); - private native long getAccountCPtr (int accountId); - private native long getSelectedAccountCPtr (); - - public boolean isAllChatmail() { - for (int accountId : getAll()) { - DcContext dcContext = getAccount(accountId); - if (!dcContext.isChatmail()) { - return false; - } - } - return true; + public DcEventEmitter getEventEmitter() { + return new DcEventEmitter(getEventEmitterCPtr()); + } + + public DcJsonrpcInstance getJsonrpcInstance() { + return new DcJsonrpcInstance(getJsonrpcInstanceCPtr()); + } + + public native void startIo(); + + public native void stopIo(); + + public native void maybeNetwork(); + + public native void setPushDeviceToken(String token); + + public native boolean backgroundFetch(int timeoutSeconds); + + public native void stopBackgroundFetch(); + + public native int migrateAccount(String dbfile); + + public native boolean removeAccount(int accountId); + + public native int[] getAll(); + + public DcContext getAccount(int accountId) { + return new DcContext(getAccountCPtr(accountId)); + } + + public DcContext getSelectedAccount() { + return new DcContext(getSelectedAccountCPtr()); + } + + public native boolean selectAccount(int accountId); + + // working with raw c-data + private long accountsCPtr; // CAVE: the name is referenced in the JNI + + private native long createAccountsCPtr(String dir, DcEventChannel channel); + + private native void unrefAccountsCPtr(); + + private native long getEventEmitterCPtr(); + + private native long getJsonrpcInstanceCPtr(); + + private native long getAccountCPtr(int accountId); + + private native long getSelectedAccountCPtr(); + + public boolean isAllChatmail() { + for (int accountId : getAll()) { + DcContext dcContext = getAccount(accountId); + if (!dcContext.isChatmail()) { + return false; + } } + return true; + } } diff --git a/src/main/java/com/b44t/messenger/DcBackupProvider.java b/src/main/java/com/b44t/messenger/DcBackupProvider.java index 459be4a36..2461357b3 100644 --- a/src/main/java/com/b44t/messenger/DcBackupProvider.java +++ b/src/main/java/com/b44t/messenger/DcBackupProvider.java @@ -2,31 +2,35 @@ package com.b44t.messenger; public class DcBackupProvider { - public DcBackupProvider(long backupProviderCPtr) { - this.backupProviderCPtr = backupProviderCPtr; + public DcBackupProvider(long backupProviderCPtr) { + this.backupProviderCPtr = backupProviderCPtr; + } + + public boolean isOk() { + return backupProviderCPtr != 0; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + unref(); + } + + public void unref() { + if (backupProviderCPtr != 0) { + unrefBackupProviderCPtr(); + backupProviderCPtr = 0; } + } - public boolean isOk() { - return backupProviderCPtr != 0; - } + public native String getQr(); - @Override protected void finalize() throws Throwable { - super.finalize(); - unref(); - } + public native String getQrSvg(); - public void unref() { - if (backupProviderCPtr != 0) { - unrefBackupProviderCPtr(); - backupProviderCPtr = 0; - } - } + public native void waitForReceiver(); - public native String getQr (); - public native String getQrSvg (); - public native void waitForReceiver (); + // working with raw c-data + private long backupProviderCPtr; // CAVE: the name is referenced in the JNI - // working with raw c-data - private long backupProviderCPtr; // CAVE: the name is referenced in the JNI - private native void unrefBackupProviderCPtr(); + private native void unrefBackupProviderCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcChatlist.java b/src/main/java/com/b44t/messenger/DcChatlist.java index 4a250d15d..7180758dd 100644 --- a/src/main/java/com/b44t/messenger/DcChatlist.java +++ b/src/main/java/com/b44t/messenger/DcChatlist.java @@ -2,45 +2,64 @@ package com.b44t.messenger; public class DcChatlist { - private int accountId; + private int accountId; - public DcChatlist(int accountId, long chatlistCPtr) { - this.accountId = accountId; - this.chatlistCPtr = chatlistCPtr; - } + public DcChatlist(int accountId, long chatlistCPtr) { + this.accountId = accountId; + this.chatlistCPtr = chatlistCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefChatlistCPtr(); - chatlistCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefChatlistCPtr(); + chatlistCPtr = 0; + } - public int getAccountId() { return accountId; } - public native int getCnt (); - public native int getChatId (int index); - public DcChat getChat (int index) { return new DcChat(accountId, getChatCPtr(index)); } - public native int getMsgId (int index); - public DcMsg getMsg (int index) { return new DcMsg(getMsgCPtr(index)); } - public DcLot getSummary(int index, DcChat chat) { return new DcLot(getSummaryCPtr(index, chat==null? 0 : chat.getChatCPtr())); } + public int getAccountId() { + return accountId; + } - public class Item { - public DcLot summary; - public int msgId; - public int chatId; - } + public native int getCnt(); - public Item getItem(int index) { - Item item = new Item(); - item.summary = getSummary(index, null); - item.msgId = getMsgId(index); - item.chatId = getChatId(index); - return item; - } + public native int getChatId(int index); - // working with raw c-data - private long chatlistCPtr; // CAVE: the name is referenced in the JNI - private native void unrefChatlistCPtr(); - private native long getChatCPtr (int index); - private native long getMsgCPtr (int index); - private native long getSummaryCPtr (int index, long chatCPtr); + public DcChat getChat(int index) { + return new DcChat(accountId, getChatCPtr(index)); + } + + public native int getMsgId(int index); + + public DcMsg getMsg(int index) { + return new DcMsg(getMsgCPtr(index)); + } + + public DcLot getSummary(int index, DcChat chat) { + return new DcLot(getSummaryCPtr(index, chat == null ? 0 : chat.getChatCPtr())); + } + + public class Item { + public DcLot summary; + public int msgId; + public int chatId; + } + + public Item getItem(int index) { + Item item = new Item(); + item.summary = getSummary(index, null); + item.msgId = getMsgId(index); + item.chatId = getChatId(index); + return item; + } + + // working with raw c-data + private long chatlistCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefChatlistCPtr(); + + private native long getChatCPtr(int index); + + private native long getMsgCPtr(int index); + + private native long getSummaryCPtr(int index, long chatCPtr); } diff --git a/src/main/java/com/b44t/messenger/DcEvent.java b/src/main/java/com/b44t/messenger/DcEvent.java index 1acfc2ee0..4180bd48c 100644 --- a/src/main/java/com/b44t/messenger/DcEvent.java +++ b/src/main/java/com/b44t/messenger/DcEvent.java @@ -2,24 +2,31 @@ package com.b44t.messenger; public class DcEvent { - public DcEvent(long eventCPtr) { - this.eventCPtr = eventCPtr; - } + public DcEvent(long eventCPtr) { + this.eventCPtr = eventCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefEventCPtr(); - eventCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefEventCPtr(); + eventCPtr = 0; + } - public native int getId (); - public native int getData1Int (); - public native int getData2Int (); - public native String getData2Str (); - public native byte[] getData2Blob(); - public native int getAccountId(); + public native int getId(); - // working with raw c-data - private long eventCPtr; // CAVE: the name is referenced in the JNI - private native void unrefEventCPtr(); + public native int getData1Int(); + + public native int getData2Int(); + + public native String getData2Str(); + + public native byte[] getData2Blob(); + + public native int getAccountId(); + + // working with raw c-data + private long eventCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefEventCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcEventChannel.java b/src/main/java/com/b44t/messenger/DcEventChannel.java index 2c3ece1dd..b5e9ee241 100644 --- a/src/main/java/com/b44t/messenger/DcEventChannel.java +++ b/src/main/java/com/b44t/messenger/DcEventChannel.java @@ -20,8 +20,11 @@ public class DcEventChannel { } // working with raw c-data - private long eventChannelCPtr; // CAVE: the name is referenced in the JNI - private native long createEventChannelCPtr (); - private native void unrefEventChannelCPtr (); - private native long getEventEmitterCPtr (); + private long eventChannelCPtr; // CAVE: the name is referenced in the JNI + + private native long createEventChannelCPtr(); + + private native void unrefEventChannelCPtr(); + + private native long getEventEmitterCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcEventEmitter.java b/src/main/java/com/b44t/messenger/DcEventEmitter.java index bd7c8ad54..b51715a7c 100644 --- a/src/main/java/com/b44t/messenger/DcEventEmitter.java +++ b/src/main/java/com/b44t/messenger/DcEventEmitter.java @@ -2,23 +2,26 @@ package com.b44t.messenger; public class DcEventEmitter { - public DcEventEmitter(long eventEmitterCPtr) { - this.eventEmitterCPtr = eventEmitterCPtr; - } + public DcEventEmitter(long eventEmitterCPtr) { + this.eventEmitterCPtr = eventEmitterCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefEventEmitterCPtr(); - eventEmitterCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefEventEmitterCPtr(); + eventEmitterCPtr = 0; + } - public DcEvent getNextEvent () { - long eventCPtr = getNextEventCPtr(); - return eventCPtr == 0 ? null : new DcEvent(eventCPtr); - } + public DcEvent getNextEvent() { + long eventCPtr = getNextEventCPtr(); + return eventCPtr == 0 ? null : new DcEvent(eventCPtr); + } - // working with raw c-data - private long eventEmitterCPtr; // CAVE: the name is referenced in the JNI - private native long getNextEventCPtr (); - private native void unrefEventEmitterCPtr(); + // working with raw c-data + private long eventEmitterCPtr; // CAVE: the name is referenced in the JNI + + private native long getNextEventCPtr(); + + private native void unrefEventEmitterCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java b/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java index 57b353b44..6eded9ced 100644 --- a/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java +++ b/src/main/java/com/b44t/messenger/DcJsonrpcInstance.java @@ -2,20 +2,23 @@ package com.b44t.messenger; public class DcJsonrpcInstance { - public DcJsonrpcInstance(long jsonrpcInstanceCPtr) { - this.jsonrpcInstanceCPtr = jsonrpcInstanceCPtr; - } + public DcJsonrpcInstance(long jsonrpcInstanceCPtr) { + this.jsonrpcInstanceCPtr = jsonrpcInstanceCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefJsonrpcInstanceCPtr(); - jsonrpcInstanceCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefJsonrpcInstanceCPtr(); + jsonrpcInstanceCPtr = 0; + } - public native void request(String request); - public native String getNextResponse(); + public native void request(String request); - // working with raw c-data - private long jsonrpcInstanceCPtr; // CAVE: the name is referenced in the JNI - private native void unrefJsonrpcInstanceCPtr(); + public native String getNextResponse(); + + // working with raw c-data + private long jsonrpcInstanceCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefJsonrpcInstanceCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcLot.java b/src/main/java/com/b44t/messenger/DcLot.java index 97ab2f013..2d036e130 100644 --- a/src/main/java/com/b44t/messenger/DcLot.java +++ b/src/main/java/com/b44t/messenger/DcLot.java @@ -2,28 +2,35 @@ package com.b44t.messenger; public class DcLot { - public final static int DC_TEXT1_DRAFT = 1; - public final static int DC_TEXT1_USERNAME = 2; - public final static int DC_TEXT1_SELF = 3; + public static final int DC_TEXT1_DRAFT = 1; + public static final int DC_TEXT1_USERNAME = 2; + public static final int DC_TEXT1_SELF = 3; - public DcLot(long lotCPtr) { - this.lotCPtr = lotCPtr; - } + public DcLot(long lotCPtr) { + this.lotCPtr = lotCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefLotCPtr(); - lotCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefLotCPtr(); + lotCPtr = 0; + } - public native String getText1 (); - public native int getText1Meaning(); - public native String getText2 (); - public native long getTimestamp (); - public native int getState (); - public native int getId (); + public native String getText1(); - // working with raw c-data - private long lotCPtr; // CAVE: the name is referenced in the JNI - private native void unrefLotCPtr(); + public native int getText1Meaning(); + + public native String getText2(); + + public native long getTimestamp(); + + public native int getState(); + + public native int getId(); + + // working with raw c-data + private long lotCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefLotCPtr(); } diff --git a/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java b/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java index 288ae7f82..bb33319f0 100644 --- a/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java +++ b/src/main/java/com/b44t/messenger/DcMediaGalleryElement.java @@ -1,14 +1,14 @@ package com.b44t.messenger; -/** - * Contains a list of media entries, their respective positions and ability to move through it. - */ +/** Contains a list of media entries, their respective positions and ability to move through it. */ public class DcMediaGalleryElement { final int[] mediaMsgs; int position; final DcContext context; - public DcMediaGalleryElement(int[] mediaMsgs, int position, DcContext context, boolean leftIsRecent) { + + public DcMediaGalleryElement( + int[] mediaMsgs, int position, DcContext context, boolean leftIsRecent) { this.mediaMsgs = mediaMsgs; this.position = position; this.context = context; @@ -33,7 +33,7 @@ public class DcMediaGalleryElement { } public void moveToPosition(int newPosition) { - if(newPosition < 0 || newPosition >= mediaMsgs.length) + if (newPosition < 0 || newPosition >= mediaMsgs.length) throw new IllegalArgumentException("can't move outside of known area."); position = newPosition; } diff --git a/src/main/java/com/b44t/messenger/DcProvider.java b/src/main/java/com/b44t/messenger/DcProvider.java index 09941cfe4..336526302 100644 --- a/src/main/java/com/b44t/messenger/DcProvider.java +++ b/src/main/java/com/b44t/messenger/DcProvider.java @@ -2,25 +2,29 @@ package com.b44t.messenger; public class DcProvider { - public final static int DC_PROVIDER_STATUS_OK = 1; - public final static int DC_PROVIDER_STATUS_PREPARATION = 2; - public final static int DC_PROVIDER_STATUS_BROKEN = 3; + public static final int DC_PROVIDER_STATUS_OK = 1; + public static final int DC_PROVIDER_STATUS_PREPARATION = 2; + public static final int DC_PROVIDER_STATUS_BROKEN = 3; - public DcProvider(long providerCPtr) { - this.providerCPtr = providerCPtr; - } + public DcProvider(long providerCPtr) { + this.providerCPtr = providerCPtr; + } - @Override protected void finalize() throws Throwable { - super.finalize(); - unrefProviderCPtr(); - providerCPtr = 0; - } + @Override + protected void finalize() throws Throwable { + super.finalize(); + unrefProviderCPtr(); + providerCPtr = 0; + } - public native int getStatus (); - public native String getBeforeLoginHint (); - public native String getOverviewPage (); + public native int getStatus(); - // working with raw c-data - private long providerCPtr; // CAVE: the name is referenced in the JNI - private native void unrefProviderCPtr(); + public native String getBeforeLoginHint(); + + public native String getOverviewPage(); + + // working with raw c-data + private long providerCPtr; // CAVE: the name is referenced in the JNI + + private native void unrefProviderCPtr(); } diff --git a/src/main/java/com/b44t/messenger/FFITransport.java b/src/main/java/com/b44t/messenger/FFITransport.java index 401048c52..7891a8809 100644 --- a/src/main/java/com/b44t/messenger/FFITransport.java +++ b/src/main/java/com/b44t/messenger/FFITransport.java @@ -19,4 +19,4 @@ public class FFITransport extends BaseTransport { protected String getResponse() { return dcJsonrpcInstance.getNextResponse(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java index fb409a73d..8121bbbc8 100644 --- a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java +++ b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridAdapter.java @@ -4,609 +4,590 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import java.security.InvalidParameterException; import java.util.ArrayList; -/** - * Created by Sergej Kravcenko on 4/24/2017. - * Copyright (c) 2017 Sergej Kravcenko - */ - +/** Created by Sergej Kravcenko on 4/24/2017. Copyright (c) 2017 Sergej Kravcenko */ @SuppressWarnings({"unused", "WeakerAccess"}) -public abstract class StickyHeaderGridAdapter extends RecyclerView.Adapter { - public static final String TAG = "StickyHeaderGridAdapter"; +public abstract class StickyHeaderGridAdapter + extends RecyclerView.Adapter { + public static final String TAG = "StickyHeaderGridAdapter"; - public static final int TYPE_HEADER = 0; - public static final int TYPE_ITEM = 1; + public static final int TYPE_HEADER = 0; + public static final int TYPE_ITEM = 1; - private ArrayList

mSections; - private int[] mSectionIndices; - private int mTotalItemNumber; + private ArrayList
mSections; + private int[] mSectionIndices; + private int mTotalItemNumber; - @SuppressWarnings("WeakerAccess") - public static class ViewHolder extends RecyclerView.ViewHolder { - public ViewHolder(View itemView) { - super(itemView); - } + @SuppressWarnings("WeakerAccess") + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(View itemView) { + super(itemView); + } - public boolean isHeader() { - return false; - } + public boolean isHeader() { + return false; + } + public int getSectionItemViewType() { + return StickyHeaderGridAdapter.externalViewType(getItemViewType()); + } + } - public int getSectionItemViewType() { - return StickyHeaderGridAdapter.externalViewType(getItemViewType()); - } - } + public static class ItemViewHolder extends ViewHolder { + public ItemViewHolder(View itemView) { + super(itemView); + } + } - public static class ItemViewHolder extends ViewHolder { - public ItemViewHolder(View itemView) { - super(itemView); - } - } + public static class HeaderViewHolder extends ViewHolder { + public HeaderViewHolder(View itemView) { + super(itemView); + } - public static class HeaderViewHolder extends ViewHolder { - public HeaderViewHolder(View itemView) { - super(itemView); - } - - @Override - public boolean isHeader() { - return true; - } - } - - private static class Section { - private int position; - private int itemNumber; - private int length; - } - - private void calculateSections() { - mSections = new ArrayList<>(); - - int total = 0; - int sectionCount = getSectionCount(); - for (int s = 0; s < sectionCount; s++) { - final Section section = new Section(); - section.position = total; - section.itemNumber = getSectionItemCount(s); - section.length = section.itemNumber + 1; - mSections.add(section); - - total += section.length; - } - mTotalItemNumber = total; - - total = 0; - mSectionIndices = new int[mTotalItemNumber]; - for (int s = 0; s < sectionCount; s++) { - final Section section = mSections.get(s); - for (int i = 0; i < section.length; i++) { - mSectionIndices[total + i] = s; - } - total += section.length; - } - } - - protected int getItemViewInternalType(int position) { - final int section = getAdapterPositionSection(position); - final Section sectionObject = mSections.get(section); - final int sectionPosition = position - sectionObject.position; - - return getItemViewInternalType(section, sectionPosition); - } - - private int getItemViewInternalType(int section, int position) { - return position == 0 ? TYPE_HEADER : TYPE_ITEM; - } - - static private int internalViewType(int type) { - return type & 0xFF; - } - - static private int externalViewType(int type) { - return type >> 8; - } - - @Override - final public int getItemCount() { - if (mSections == null) { - calculateSections(); - } - return mTotalItemNumber; - } - - @NonNull - @Override - final public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - final int internalType = internalViewType(viewType); - final int externalType = externalViewType(viewType); - - switch (internalType) { - case TYPE_HEADER: - return onCreateHeaderViewHolder(parent, externalType); - case TYPE_ITEM: - return onCreateItemViewHolder(parent, externalType); - default: - throw new InvalidParameterException("Invalid viewType: " + viewType); - } - } - - @Override - final public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - if (mSections == null) { - calculateSections(); - } - - final int section = mSectionIndices[position]; - final int internalType = internalViewType(holder.getItemViewType()); - final int externalType = externalViewType(holder.getItemViewType()); - - switch (internalType) { - case TYPE_HEADER: - onBindHeaderViewHolder((HeaderViewHolder)holder, section); - break; - case TYPE_ITEM: - final ItemViewHolder itemHolder = (ItemViewHolder)holder; - final int offset = getItemSectionOffset(section, position); - onBindItemViewHolder((ItemViewHolder)holder, section, offset); - break; - default: - throw new InvalidParameterException("invalid viewType: " + internalType); - } - } - - @Override - final public int getItemViewType(int position) { - final int section = getAdapterPositionSection(position); - final Section sectionObject = mSections.get(section); - final int sectionPosition = position - sectionObject.position; - final int internalType = getItemViewInternalType(section, sectionPosition); - int externalType = 0; - - switch (internalType) { - case TYPE_HEADER: - externalType = getSectionHeaderViewType(section); - break; - case TYPE_ITEM: - externalType = getSectionItemViewType(section, sectionPosition - 1); - break; - } - - return ((externalType & 0xFF) << 8) | (internalType & 0xFF); - } - - // Helpers - private int getItemSectionHeaderPosition(int position) { - return getSectionHeaderPosition(getAdapterPositionSection(position)); - } - - private int getAdapterPosition(int section, int offset) { - if (mSections == null) { - calculateSections(); - } - - if (section < 0) { - throw new IndexOutOfBoundsException("section " + section + " < 0"); - } - - if (section >= mSections.size()) { - throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); - } - - final Section sectionObject = mSections.get(section); - return sectionObject.position + offset; - } - - /** - * Given a section and an adapter position get the offset of an item - * inside section. - * - * @param section section to query - * @param position adapter position - * @return The item offset inside the section. - */ - public int getItemSectionOffset(int section, int position) { - if (mSections == null) { - calculateSections(); - } - - if (section < 0) { - throw new IndexOutOfBoundsException("section " + section + " < 0"); - } - - if (section >= mSections.size()) { - throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); - } - - final Section sectionObject = mSections.get(section); - final int localPosition = position - sectionObject.position; - if (localPosition >= sectionObject.length) { - throw new IndexOutOfBoundsException("localPosition: " + localPosition + " >=" + sectionObject.length); - } - - return localPosition - 1; - } - - /** - * Returns the section index having item or header with provided - * provider position. - * - * @param position adapter position - * @return The section containing provided adapter position. - */ - public int getAdapterPositionSection(int position) { - if (mSections == null) { - calculateSections(); - } - - if (getItemCount() == 0) { - return NO_POSITION; - } - - if (position < 0) { - throw new IndexOutOfBoundsException("position " + position + " < 0"); - } - - if (position >= getItemCount()) { - throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount()); - } - - return mSectionIndices[position]; - } - - /** - * Returns the adapter position for given section header. Use - * this only for {@link RecyclerView#scrollToPosition(int)} or similar functions. - * Never directly manipulate adapter items using this position. - * - * @param section section to query - * @return The adapter position. - */ - public int getSectionHeaderPosition(int section) { - return getAdapterPosition(section, 0); - } - - /** - * Returns the adapter position for given section and - * offset. Use this only for {@link RecyclerView#scrollToPosition(int)} - * or similar functions. Never directly manipulate adapter items using this position. - * - * @param section section to query - * @param position item position inside the section - * @return The adapter position. - */ - public int getSectionItemPosition(int section, int position) { - return getAdapterPosition(section, position + 1); - } - - // Overrides - /** - * Returns the total number of sections in the data set held by the adapter. - * - * @return The total number of section in this adapter. - */ - public int getSectionCount() { - return 0; - } - - /** - * Returns the number of items in the section. - * - * @param section section to query - * @return The total number of items in the section. - */ - public int getSectionItemCount(int section) { - return 0; - } - - /** - * Return the view type of the section header for the purposes - * of view recycling. - * - *

The default implementation of this method returns 0, making the assumption of - * a single view type for the headers. Unlike ListView adapters, types need not - * be contiguous. Consider using id resources to uniquely identify item view types. - * - * @param section section to query - * @return integer value identifying the type of the view needed to represent the header in - * section. Type codes need not be contiguous. - */ - public int getSectionHeaderViewType(int section) { - return 0; - } - - /** - * Return the view type of the item at position in section for - * the purposes of view recycling. - * - *

The default implementation of this method returns 0, making the assumption of - * a single view type for the adapter. Unlike ListView adapters, types need not - * be contiguous. Consider using id resources to uniquely identify item view types. - * - * @param section section to query - * @param offset section position to query - * @return integer value identifying the type of the view needed to represent the item at - * position in section. Type codes need not be - * contiguous. - */ - public int getSectionItemViewType(int section, int offset) { - return 0; - } - - /** - * Returns true if header in section is sticky. - * - * @param section section to query - * @return true if section header is sticky. - */ - public boolean isSectionHeaderSticky(int section) { + @Override + public boolean isHeader() { return true; - } + } + } - /** - * Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent - * a header. - *

- * This new HeaderViewHolder should be constructed with a new View that can represent the headers - * of the given type. You can either create a new View manually or inflate it from an XML - * layout file. - *

- * The new HeaderViewHolder will be used to display items of the adapter using - * {@link #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of - * the View to avoid unnecessary {@link View#findViewById(int)} calls. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to - * an adapter position. - * @param headerType The view type of the new View. - * - * @return A new ViewHolder that holds a View of the given view type. - * @see #getSectionHeaderViewType(int) - * @see #onBindHeaderViewHolder(HeaderViewHolder, int) - */ - public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType); + private static class Section { + private int position; + private int itemNumber; + private int length; + } - /** - * Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent - * an item. - *

- * This new ViewHolder should be constructed with a new View that can represent the items - * of the given type. You can either create a new View manually or inflate it from an XML - * layout file. - *

- * The new ViewHolder will be used to display items of the adapter using - * {@link #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of - * the View to avoid unnecessary {@link View#findViewById(int)} calls. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to - * an adapter position. - * @param itemType The view type of the new View. - * - * @return A new ViewHolder that holds a View of the given view type. - * @see #getSectionItemViewType(int, int) - * @see #onBindItemViewHolder(ItemViewHolder, int, int) - */ - public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType); + private void calculateSections() { + mSections = new ArrayList<>(); - /** - * Called by RecyclerView to display the data at the specified position. This method should - * update the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given - * position. - *

- * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method - * again if the position of the header changes in the data set unless the header itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the section parameter while acquiring the - * related header data inside this method and should not keep a copy of it. If you need the - * position of a header later on (e.g. in a click listener), use - * {@link HeaderViewHolder#getAdapterPosition()} which will have the updated adapter - * position. Then you can use {@link #getAdapterPositionSection(int)} to get section index. - * - * - * @param viewHolder The ViewHolder which should be updated to represent the contents of the - * header at the given position in the data set. - * @param section The index of the section. - */ - public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section); + int total = 0; + int sectionCount = getSectionCount(); + for (int s = 0; s < sectionCount; s++) { + final Section section = new Section(); + section.position = total; + section.itemNumber = getSectionItemCount(s); + section.length = section.itemNumber + 1; + mSections.add(section); - /** - * Called by RecyclerView to display the data at the specified position. This method should - * update the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given - * position. - *

- * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method - * again if the position of the item changes in the data set unless the item itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the offset and section parameters while acquiring the - * related data item inside this method and should not keep a copy of it. If you need the - * position of an item later on (e.g. in a click listener), use - * {@link ItemViewHolder#getAdapterPosition()} which will have the updated adapter - * position. Then you can use {@link #getAdapterPositionSection(int)} and - * {@link #getItemSectionOffset(int, int)} - * - * - * @param viewHolder The ViewHolder which should be updated to represent the contents of the - * item at the given position in the data set. - * @param section The index of the section. - * @param offset The position of the item within the section. - */ - public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset); + total += section.length; + } + mTotalItemNumber = total; - // Notify - /** - * Notify any registered observers that the data set has changed. - * - *

There are two different classes of data change events, item changes and structural - * changes. Item changes are when a single item has its data updated but no positional - * changes have occurred. Structural changes are when items are inserted, removed or moved - * within the data set.

- * - *

This event does not specify what about the data set has changed, forcing - * any observers to assume that all existing items and structure may no longer be valid. - * LayoutManagers will be forced to fully rebind and relayout all visible views.

- * - *

RecyclerView will attempt to synthesize visible structural change events - * for adapters that report that they have {@link #hasStableIds() stable IDs} when - * this method is used. This can help for the purposes of animation and visual - * object persistence but individual item views will still need to be rebound - * and relaid out.

- * - *

If you are writing an adapter it will always be more efficient to use the more - * specific change events if you can. Rely on notifyDataSetChanged() - * as a last resort.

- * - * @see #notifySectionDataSetChanged(int) - * @see #notifySectionHeaderChanged(int) - * @see #notifySectionItemChanged(int, int) - * @see #notifySectionInserted(int) - * @see #notifySectionItemInserted(int, int) - * @see #notifySectionItemRangeInserted(int, int, int) - * @see #notifySectionRemoved(int) - * @see #notifySectionItemRemoved(int, int) - * @see #notifySectionItemRangeRemoved(int, int, int) - */ - public void notifyAllSectionsDataSetChanged() { + total = 0; + mSectionIndices = new int[mTotalItemNumber]; + for (int s = 0; s < sectionCount; s++) { + final Section section = mSections.get(s); + for (int i = 0; i < section.length; i++) { + mSectionIndices[total + i] = s; + } + total += section.length; + } + } + + protected int getItemViewInternalType(int position) { + final int section = getAdapterPositionSection(position); + final Section sectionObject = mSections.get(section); + final int sectionPosition = position - sectionObject.position; + + return getItemViewInternalType(section, sectionPosition); + } + + private int getItemViewInternalType(int section, int position) { + return position == 0 ? TYPE_HEADER : TYPE_ITEM; + } + + private static int internalViewType(int type) { + return type & 0xFF; + } + + private static int externalViewType(int type) { + return type >> 8; + } + + @Override + public final int getItemCount() { + if (mSections == null) { calculateSections(); - notifyDataSetChanged(); - } + } + return mTotalItemNumber; + } - public void notifySectionDataSetChanged(int section) { + @NonNull + @Override + public final ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final int internalType = internalViewType(viewType); + final int externalType = externalViewType(viewType); + + switch (internalType) { + case TYPE_HEADER: + return onCreateHeaderViewHolder(parent, externalType); + case TYPE_ITEM: + return onCreateItemViewHolder(parent, externalType); + default: + throw new InvalidParameterException("Invalid viewType: " + viewType); + } + } + + @Override + public final void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - notifyItemRangeChanged(sectionObject.position, sectionObject.length); - } - } + } - public void notifySectionHeaderChanged(int section) { + final int section = mSectionIndices[position]; + final int internalType = internalViewType(holder.getItemViewType()); + final int externalType = externalViewType(holder.getItemViewType()); + + switch (internalType) { + case TYPE_HEADER: + onBindHeaderViewHolder((HeaderViewHolder) holder, section); + break; + case TYPE_ITEM: + final ItemViewHolder itemHolder = (ItemViewHolder) holder; + final int offset = getItemSectionOffset(section, position); + onBindItemViewHolder((ItemViewHolder) holder, section, offset); + break; + default: + throw new InvalidParameterException("invalid viewType: " + internalType); + } + } + + @Override + public final int getItemViewType(int position) { + final int section = getAdapterPositionSection(position); + final Section sectionObject = mSections.get(section); + final int sectionPosition = position - sectionObject.position; + final int internalType = getItemViewInternalType(section, sectionPosition); + int externalType = 0; + + switch (internalType) { + case TYPE_HEADER: + externalType = getSectionHeaderViewType(section); + break; + case TYPE_ITEM: + externalType = getSectionItemViewType(section, sectionPosition - 1); + break; + } + + return ((externalType & 0xFF) << 8) | (internalType & 0xFF); + } + + // Helpers + private int getItemSectionHeaderPosition(int position) { + return getSectionHeaderPosition(getAdapterPositionSection(position)); + } + + private int getAdapterPosition(int section, int offset) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - notifyItemRangeChanged(sectionObject.position, 1); - } - } + } - public void notifySectionItemChanged(int section, int position) { + if (section < 0) { + throw new IndexOutOfBoundsException("section " + section + " < 0"); + } + + if (section >= mSections.size()) { + throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); + } + + final Section sectionObject = mSections.get(section); + return sectionObject.position + offset; + } + + /** + * Given a section and an adapter position get the offset of an item + * inside section. + * + * @param section section to query + * @param position adapter position + * @return The item offset inside the section. + */ + public int getItemSectionOffset(int section, int position) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); + } - if (position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } + if (section < 0) { + throw new IndexOutOfBoundsException("section " + section + " < 0"); + } - notifyItemChanged(sectionObject.position + position + 1); - } - } + if (section >= mSections.size()) { + throw new IndexOutOfBoundsException("section " + section + " >=" + mSections.size()); + } - public void notifySectionInserted(int section) { + final Section sectionObject = mSections.get(section); + final int localPosition = position - sectionObject.position; + if (localPosition >= sectionObject.length) { + throw new IndexOutOfBoundsException( + "localPosition: " + localPosition + " >=" + sectionObject.length); + } + + return localPosition - 1; + } + + /** + * Returns the section index having item or header with provided provider position. + * + * @param position adapter position + * @return The section containing provided adapter position. + */ + public int getAdapterPositionSection(int position) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - notifyItemRangeInserted(sectionObject.position, sectionObject.length); - } - } + } - public void notifySectionItemInserted(int section, int position) { + if (getItemCount() == 0) { + return NO_POSITION; + } + + if (position < 0) { + throw new IndexOutOfBoundsException("position " + position + " < 0"); + } + + if (position >= getItemCount()) { + throw new IndexOutOfBoundsException("position " + position + " >=" + getItemCount()); + } + + return mSectionIndices[position]; + } + + /** + * Returns the adapter position for given section header. Use this only for {@link + * RecyclerView#scrollToPosition(int)} or similar functions. Never directly manipulate adapter + * items using this position. + * + * @param section section to query + * @return The adapter position. + */ + public int getSectionHeaderPosition(int section) { + return getAdapterPosition(section, 0); + } + + /** + * Returns the adapter position for given section and offset. Use this + * only for {@link RecyclerView#scrollToPosition(int)} or similar functions. Never directly + * manipulate adapter items using this position. + * + * @param section section to query + * @param position item position inside the section + * @return The adapter position. + */ + public int getSectionItemPosition(int section, int position) { + return getAdapterPosition(section, position + 1); + } + + // Overrides + /** + * Returns the total number of sections in the data set held by the adapter. + * + * @return The total number of section in this adapter. + */ + public int getSectionCount() { + return 0; + } + + /** + * Returns the number of items in the section. + * + * @param section section to query + * @return The total number of items in the section. + */ + public int getSectionItemCount(int section) { + return 0; + } + + /** + * Return the view type of the section header for the purposes of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of a single view + * type for the headers. Unlike ListView adapters, types need not be contiguous. Consider using id + * resources to uniquely identify item view types. + * + * @param section section to query + * @return integer value identifying the type of the view needed to represent the header in + * section. Type codes need not be contiguous. + */ + public int getSectionHeaderViewType(int section) { + return 0; + } + + /** + * Return the view type of the item at position in section for the + * purposes of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of a single view + * type for the adapter. Unlike ListView adapters, types need not be contiguous. Consider using id + * resources to uniquely identify item view types. + * + * @param section section to query + * @param offset section position to query + * @return integer value identifying the type of the view needed to represent the item at + * position in section. Type codes need not be contiguous. + */ + public int getSectionItemViewType(int section, int offset) { + return 0; + } + + /** + * Returns true if header in section is sticky. + * + * @param section section to query + * @return true if section header is sticky. + */ + public boolean isSectionHeaderSticky(int section) { + return true; + } + + /** + * Called when RecyclerView needs a new {@link HeaderViewHolder} of the given type to represent a + * header. + * + *

This new HeaderViewHolder should be constructed with a new View that can represent the + * headers of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + * + *

The new HeaderViewHolder will be used to display items of the adapter using {@link + * #onBindHeaderViewHolder(HeaderViewHolder, int)}. Since it will be re-used to display different + * items in the data set, it is a good idea to cache references to sub views of the View to avoid + * unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to an + * adapter position. + * @param headerType The view type of the new View. + * @return A new ViewHolder that holds a View of the given view type. + * @see #getSectionHeaderViewType(int) + * @see #onBindHeaderViewHolder(HeaderViewHolder, int) + */ + public abstract HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType); + + /** + * Called when RecyclerView needs a new {@link ItemViewHolder} of the given type to represent an + * item. + * + *

This new ViewHolder should be constructed with a new View that can represent the items of + * the given type. You can either create a new View manually or inflate it from an XML layout + * file. + * + *

The new ViewHolder will be used to display items of the adapter using {@link + * #onBindItemViewHolder(ItemViewHolder, int, int)}. Since it will be re-used to display different + * items in the data set, it is a good idea to cache references to sub views of the View to avoid + * unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to an + * adapter position. + * @param itemType The view type of the new View. + * @return A new ViewHolder that holds a View of the given view type. + * @see #getSectionItemViewType(int, int) + * @see #onBindItemViewHolder(ItemViewHolder, int, int) + */ + public abstract ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType); + + /** + * Called by RecyclerView to display the data at the specified position. This method should update + * the contents of the {@link HeaderViewHolder#itemView} to reflect the header at the given + * position. + * + *

Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the header changes in the data set unless the header itself is + * invalidated or the new position cannot be determined. For this reason, you should only use the + * section parameter while acquiring the related header data inside this method and + * should not keep a copy of it. If you need the position of a header later on (e.g. in a click + * listener), use {@link HeaderViewHolder#getAdapterPosition()} which will have the updated + * adapter position. Then you can use {@link #getAdapterPositionSection(int)} to get section + * index. + * + * @param viewHolder The ViewHolder which should be updated to represent the contents of the + * header at the given position in the data set. + * @param section The index of the section. + */ + public abstract void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section); + + /** + * Called by RecyclerView to display the data at the specified position. This method should update + * the contents of the {@link ItemViewHolder#itemView} to reflect the item at the given position. + * + *

Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is invalidated + * or the new position cannot be determined. For this reason, you should only use the offset + * and section parameters while acquiring the related data item inside this + * method and should not keep a copy of it. If you need the position of an item later on (e.g. in + * a click listener), use {@link ItemViewHolder#getAdapterPosition()} which will have the updated + * adapter position. Then you can use {@link #getAdapterPositionSection(int)} and {@link + * #getItemSectionOffset(int, int)} + * + * @param viewHolder The ViewHolder which should be updated to represent the contents of the item + * at the given position in the data set. + * @param section The index of the section. + * @param offset The position of the item within the section. + */ + public abstract void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset); + + // Notify + /** + * Notify any registered observers that the data set has changed. + * + *

There are two different classes of data change events, item changes and structural changes. + * Item changes are when a single item has its data updated but no positional changes have + * occurred. Structural changes are when items are inserted, removed or moved within the data set. + * + *

This event does not specify what about the data set has changed, forcing any observers to + * assume that all existing items and structure may no longer be valid. LayoutManagers will be + * forced to fully rebind and relayout all visible views. + * + *

RecyclerView will attempt to synthesize visible structural change events for + * adapters that report that they have {@link #hasStableIds() stable IDs} when this method is + * used. This can help for the purposes of animation and visual object persistence but individual + * item views will still need to be rebound and relaid out. + * + *

If you are writing an adapter it will always be more efficient to use the more specific + * change events if you can. Rely on notifyDataSetChanged() as a last resort. + * + * @see #notifySectionDataSetChanged(int) + * @see #notifySectionHeaderChanged(int) + * @see #notifySectionItemChanged(int, int) + * @see #notifySectionInserted(int) + * @see #notifySectionItemInserted(int, int) + * @see #notifySectionItemRangeInserted(int, int, int) + * @see #notifySectionRemoved(int) + * @see #notifySectionItemRemoved(int, int) + * @see #notifySectionItemRangeRemoved(int, int, int) + */ + public void notifyAllSectionsDataSetChanged() { + calculateSections(); + notifyDataSetChanged(); + } + + public void notifySectionDataSetChanged(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + notifyItemRangeChanged(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionHeaderChanged(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + notifyItemRangeChanged(sectionObject.position, 1); + } + } + + public void notifySectionItemChanged(int section, int position) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + + if (position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + + notifyItemChanged(sectionObject.position + position + 1); + } + } + + public void notifySectionInserted(int section) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + notifyItemRangeInserted(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionItemInserted(int section, int position) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + + notifyItemInserted(sectionObject.position + position + 1); + } + } + + public void notifySectionItemRangeInserted(int section, int position, int count) { + calculateSections(); + if (mSections == null) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); + } + if (position + count > sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); + } + + notifyItemRangeInserted(sectionObject.position + position + 1, count); + } + } + + public void notifySectionRemoved(int section) { + if (mSections == null) { calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - - notifyItemInserted(sectionObject.position + position + 1); - } - } - - public void notifySectionItemRangeInserted(int section, int position, int count) { + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); calculateSections(); - if (mSections == null) { - notifyAllSectionsDataSetChanged(); + notifyItemRangeRemoved(sectionObject.position, sectionObject.length); + } + } + + public void notifySectionItemRemoved(int section, int position) { + if (mSections == null) { + calculateSections(); + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - else { - final Section sectionObject = mSections.get(section); - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - if (position + count > sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); - } + calculateSections(); + notifyItemRemoved(sectionObject.position + position + 1); + } + } - notifyItemRangeInserted(sectionObject.position + position + 1, count); + public void notifySectionItemRangeRemoved(int section, int position, int count) { + if (mSections == null) { + calculateSections(); + notifyAllSectionsDataSetChanged(); + } else { + final Section sectionObject = mSections.get(section); + + if (position < 0 || position >= sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + position + ", size is " + sectionObject.itemNumber); } - } - - public void notifySectionRemoved(int section) { - if (mSections == null) { - calculateSections(); - notifyAllSectionsDataSetChanged(); + if (position + count > sectionObject.itemNumber) { + throw new IndexOutOfBoundsException( + "Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); } - else { - final Section sectionObject = mSections.get(section); - calculateSections(); - notifyItemRangeRemoved(sectionObject.position, sectionObject.length); - } - } - public void notifySectionItemRemoved(int section, int position) { - if (mSections == null) { - calculateSections(); - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - - calculateSections(); - notifyItemRemoved(sectionObject.position + position + 1); - } - } - - public void notifySectionItemRangeRemoved(int section, int position, int count) { - if (mSections == null) { - calculateSections(); - notifyAllSectionsDataSetChanged(); - } - else { - final Section sectionObject = mSections.get(section); - - if (position < 0 || position >= sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + position + ", size is " + sectionObject.itemNumber); - } - if (position + count > sectionObject.itemNumber) { - throw new IndexOutOfBoundsException("Invalid index " + (position + count) + ", size is " + sectionObject.itemNumber); - } - - calculateSections(); - notifyItemRangeRemoved(sectionObject.position + position + 1, count); - } - } + calculateSections(); + notifyItemRangeRemoved(sectionObject.position + position + 1, count); + } + } } diff --git a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java index 24aa99eea..2fd5c9887 100644 --- a/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java +++ b/src/main/java/com/codewaves/stickyheadergrid/StickyHeaderGridLayoutManager.java @@ -12,276 +12,270 @@ import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; - import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; - import java.util.ArrayList; import java.util.Arrays; -/** - * Created by Sergej Kravcenko on 4/24/2017. - * Copyright (c) 2017 Sergej Kravcenko - */ - +/** Created by Sergej Kravcenko on 4/24/2017. Copyright (c) 2017 Sergej Kravcenko */ @SuppressWarnings({"unused", "WeakerAccess"}) -public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager implements RecyclerView.SmoothScroller.ScrollVectorProvider { - public static final String TAG = "StickyLayoutManager"; +public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager + implements RecyclerView.SmoothScroller.ScrollVectorProvider { + public static final String TAG = "StickyLayoutManager"; - private static final int DEFAULT_ROW_COUNT = 16; + private static final int DEFAULT_ROW_COUNT = 16; - private int mSpanCount; - private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); + private int mSpanCount; + private SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); - private StickyHeaderGridAdapter mAdapter; + private StickyHeaderGridAdapter mAdapter; - private int mHeadersStartPosition; + private int mHeadersStartPosition; - private View mFloatingHeaderView; - private int mFloatingHeaderPosition; - private int mStickOffset; - private int mAverageHeaderHeight; - private int mHeaderOverlapMargin; + private View mFloatingHeaderView; + private int mFloatingHeaderPosition; + private int mStickOffset; + private int mAverageHeaderHeight; + private int mHeaderOverlapMargin; - private HeaderStateChangeListener mHeaderStateListener; - private int mStickyHeaderSection = NO_POSITION; - private View mStickyHeaderView; - private HeaderState mStickyHeadeState; + private HeaderStateChangeListener mHeaderStateListener; + private int mStickyHeaderSection = NO_POSITION; + private View mStickyHeaderView; + private HeaderState mStickyHeadeState; - private View mFillViewSet[]; + private View mFillViewSet[]; - private SavedState mPendingSavedState; - private int mPendingScrollPosition = NO_POSITION; - private int mPendingScrollPositionOffset; - private AnchorPosition mAnchor = new AnchorPosition(); + private SavedState mPendingSavedState; + private int mPendingScrollPosition = NO_POSITION; + private int mPendingScrollPositionOffset; + private AnchorPosition mAnchor = new AnchorPosition(); - private final FillResult mFillResult = new FillResult(); - private ArrayList mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT); + private final FillResult mFillResult = new FillResult(); + private ArrayList mLayoutRows = new ArrayList<>(DEFAULT_ROW_COUNT); - public enum HeaderState { - NORMAL, - STICKY, - PUSHED - } + public enum HeaderState { + NORMAL, + STICKY, + PUSHED + } - /** - * The interface to be implemented by listeners to header events from this - * LayoutManager. - */ - public interface HeaderStateChangeListener { - /** - * Called when a section header state changes. The position can be HeaderState.NORMAL, - * HeaderState.STICKY, HeaderState.PUSHED. - * - *

- *

    - *
  • NORMAL - the section header is invisible or has normal position
  • - *
  • STICKY - the section header is sticky at the top of RecyclerView
  • - *
  • PUSHED - the section header is sticky and pushed up by next header
  • - *
+ * + *
    + *
  • NORMAL - the section header is invisible or has normal position + *
  • STICKY - the section header is sticky at the top of RecyclerView + *
  • PUSHED - the section header is sticky and pushed up by next header
0) { + state.mAnchorSection = mAnchor.section; + state.mAnchorItem = mAnchor.item; + state.mAnchorOffset = mAnchor.offset; + } else { + state.invalidateAnchor(); + } - SavedState state = new SavedState(); - if (getChildCount() > 0) { - state.mAnchorSection = mAnchor.section; - state.mAnchorItem = mAnchor.item; - state.mAnchorOffset = mAnchor.offset; - } - else { - state.invalidateAnchor(); - } + return state; + } - return state; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - if (state instanceof SavedState) { - mPendingSavedState = (SavedState) state; - requestLayout(); - } - else { - Log.d(TAG, "invalid saved state class"); - } - } - - @Override - public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { - return lp instanceof LayoutParams; - } - - @Override - public boolean canScrollVertically() { - return true; - } - - /** - *

Scroll the RecyclerView to make the position visible.

- * - *

RecyclerView will scroll the minimum amount that is necessary to make the - * target position visible. - * - *

Note that scroll position change will not be reflected until the next layout call.

- * - * @param position Scroll to this adapter position - */ - @Override - public void scrollToPosition(int position) { - if (position < 0 || position > getItemCount()) { - throw new IndexOutOfBoundsException("adapter position out of range"); - } - - mPendingScrollPosition = position; - mPendingScrollPositionOffset = 0; - if (mPendingSavedState != null) { - mPendingSavedState.invalidateAnchor(); - } + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; requestLayout(); - } + } else { + Log.d(TAG, "invalid saved state class"); + } + } - private int getExtraLayoutSpace(RecyclerView.State state) { - if (state.hasTargetScrollPosition()) { - return getHeight(); - } - else { - return 0; - } - } + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } - @Override - public void smoothScrollToPosition(final RecyclerView recyclerView, RecyclerView.State state, int position) { - final LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()) { - @Override - public int calculateDyToMakeVisible(View view, int snapPreference) { + @Override + public boolean canScrollVertically() { + return true; + } + + /** + * Scroll the RecyclerView to make the position visible. + * + *

RecyclerView will scroll the minimum amount that is necessary to make the target position + * visible. + * + *

Note that scroll position change will not be reflected until the next layout call. + * + * @param position Scroll to this adapter position + */ + @Override + public void scrollToPosition(int position) { + if (position < 0 || position > getItemCount()) { + throw new IndexOutOfBoundsException("adapter position out of range"); + } + + mPendingScrollPosition = position; + mPendingScrollPositionOffset = 0; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + private int getExtraLayoutSpace(RecyclerView.State state) { + if (state.hasTargetScrollPosition()) { + return getHeight(); + } else { + return 0; + } + } + + @Override + public void smoothScrollToPosition( + final RecyclerView recyclerView, RecyclerView.State state, int position) { + final LinearSmoothScroller linearSmoothScroller = + new LinearSmoothScroller(recyclerView.getContext()) { + @Override + public int calculateDyToMakeVisible(View view, int snapPreference) { final RecyclerView.LayoutManager layoutManager = getLayoutManager(); if (layoutManager == null || !layoutManager.canScrollVertically()) { - return 0; + return 0; } final int adapterPosition = getPosition(view); @@ -291,1082 +285,1096 @@ public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager im final int start = layoutManager.getPaddingTop() + topOffset; final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); return calculateDtToFit(top, bottom, start, end, snapPreference); - } - }; - linearSmoothScroller.setTargetPosition(position); - startSmoothScroll(linearSmoothScroller); - } + } + }; + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } - @Override - public PointF computeScrollVectorForPosition(int targetPosition) { - if (getChildCount() == 0) { - return null; - } + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + if (getChildCount() == 0) { + return null; + } - final LayoutRow firstRow = getFirstVisibleRow(); - if (firstRow == null) { - return null; - } + final LayoutRow firstRow = getFirstVisibleRow(); + if (firstRow == null) { + return null; + } - return new PointF(0, targetPosition - firstRow.adapterPosition); - } + return new PointF(0, targetPosition - firstRow.adapterPosition); + } - private int getAdapterPositionFromAnchor(AnchorPosition anchor) { - if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) { - anchor.reset(); - return NO_POSITION; - } - else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) { - anchor.offset = 0; - return mAdapter.getSectionHeaderPosition(anchor.section); - } - return mAdapter.getSectionItemPosition(anchor.section, anchor.item); - } + private int getAdapterPositionFromAnchor(AnchorPosition anchor) { + if (anchor.section < 0 || anchor.section >= mAdapter.getSectionCount()) { + anchor.reset(); + return NO_POSITION; + } else if (anchor.item < 0 || anchor.item >= mAdapter.getSectionItemCount(anchor.section)) { + anchor.offset = 0; + return mAdapter.getSectionHeaderPosition(anchor.section); + } + return mAdapter.getSectionItemPosition(anchor.section, anchor.item); + } - private int getAdapterPositionChecked(int section, int offset) { - if (section < 0 || section >= mAdapter.getSectionCount()) { - return NO_POSITION; - } - else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) { - return mAdapter.getSectionHeaderPosition(section); - } - return mAdapter.getSectionItemPosition(section, offset); - } + private int getAdapterPositionChecked(int section, int offset) { + if (section < 0 || section >= mAdapter.getSectionCount()) { + return NO_POSITION; + } else if (offset < 0 || offset >= mAdapter.getSectionItemCount(section)) { + return mAdapter.getSectionHeaderPosition(section); + } + return mAdapter.getSectionItemPosition(section, offset); + } - @Override - public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { - if (mAdapter == null || state.getItemCount() == 0) { - removeAndRecycleAllViews(recycler); - clearState(); - return; - } - - int pendingAdapterPosition; - int pendingAdapterOffset; - if (mPendingScrollPosition >= 0) { - pendingAdapterPosition = mPendingScrollPosition; - pendingAdapterOffset = mPendingScrollPositionOffset; - } - else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { - pendingAdapterPosition = getAdapterPositionChecked(mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem); - pendingAdapterOffset = mPendingSavedState.mAnchorOffset; - mPendingSavedState = null; - } - else { - pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor); - pendingAdapterOffset = mAnchor.offset; - } - - if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) { - pendingAdapterPosition = 0; - pendingAdapterOffset = 0; - mPendingScrollPosition = NO_POSITION; - } - - if (pendingAdapterOffset > 0) { - pendingAdapterOffset = 0; - } - - detachAndScrapAttachedViews(recycler); + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + if (mAdapter == null || state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); clearState(); + return; + } - // Make sure mFirstViewPosition is the start of the row - pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition); - - int left = getPaddingLeft(); - int right = getWidth() - getPaddingRight(); - final int recyclerBottom = getHeight() - getPaddingBottom(); - int totalHeight = 0; - - int adapterPosition = pendingAdapterPosition; - int top = getPaddingTop() + pendingAdapterOffset; - while (true) { - if (adapterPosition >= state.getItemCount()) { - break; - } - - int bottom; - final int viewType = mAdapter.getItemViewInternalType(adapterPosition); - if (viewType == TYPE_HEADER) { - final View v = recycler.getViewForPosition(adapterPosition); - addView(v); - measureChildWithMargins(v, 0, 0); - - int height = getDecoratedMeasuredHeight(v); - final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; - bottom = top + height; - layoutDecorated(v, left, top, right, bottom); - - bottom -= margin; - height -= margin; - mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom)); - adapterPosition++; - mAverageHeaderHeight = height; - } - else { - final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); - bottom = top + result.height; - mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom)); - adapterPosition += result.length; - } - top = bottom; - - if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) { - break; - } - } - - if (getBottomRow().bottom < recyclerBottom) { - scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state); - } - else { - clearViewsAndStickHeaders(recycler, state, false); - } - - // If layout was caused by the pending scroll, adjust top item position and move it under sticky header - if (mPendingScrollPosition >= 0) { - mPendingScrollPosition = NO_POSITION; - - final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition); - if (topOffset != 0) { - scrollVerticallyBy(-topOffset, recycler, state); - } - } - } - - @Override - public void onLayoutCompleted(RecyclerView.State state) { - super.onLayoutCompleted(state); + int pendingAdapterPosition; + int pendingAdapterOffset; + if (mPendingScrollPosition >= 0) { + pendingAdapterPosition = mPendingScrollPosition; + pendingAdapterOffset = mPendingScrollPositionOffset; + } else if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + pendingAdapterPosition = + getAdapterPositionChecked( + mPendingSavedState.mAnchorSection, mPendingSavedState.mAnchorItem); + pendingAdapterOffset = mPendingSavedState.mAnchorOffset; mPendingSavedState = null; - } + } else { + pendingAdapterPosition = getAdapterPositionFromAnchor(mAnchor); + pendingAdapterOffset = mAnchor.offset; + } - private int getPositionSectionHeaderHeight(int adapterPosition) { - final int section = mAdapter.getAdapterPositionSection(adapterPosition); - if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) { - final int offset = mAdapter.getItemSectionOffset(section, adapterPosition); - if (offset >= 0) { - final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section); - if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) { - return Math.max(0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin); - } - else { - final LayoutRow header = getHeaderRow(headerAdapterPosition); - if (header != null) { - return header.getHeight(); - } - else { - // Fall back to cached header size, can be incorrect - return mAverageHeaderHeight; - } - } - } + if (pendingAdapterPosition < 0 || pendingAdapterPosition >= state.getItemCount()) { + pendingAdapterPosition = 0; + pendingAdapterOffset = 0; + mPendingScrollPosition = NO_POSITION; + } + + if (pendingAdapterOffset > 0) { + pendingAdapterOffset = 0; + } + + detachAndScrapAttachedViews(recycler); + clearState(); + + // Make sure mFirstViewPosition is the start of the row + pendingAdapterPosition = findFirstRowItem(pendingAdapterPosition); + + int left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + int totalHeight = 0; + + int adapterPosition = pendingAdapterPosition; + int top = getPaddingTop() + pendingAdapterOffset; + while (true) { + if (adapterPosition >= state.getItemCount()) { + break; + } + + int bottom; + final int viewType = mAdapter.getItemViewInternalType(adapterPosition); + if (viewType == TYPE_HEADER) { + final View v = recycler.getViewForPosition(adapterPosition); + addView(v); + measureChildWithMargins(v, 0, 0); + + int height = getDecoratedMeasuredHeight(v); + final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; + bottom = top + height; + layoutDecorated(v, left, top, right, bottom); + + bottom -= margin; + height -= margin; + mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, bottom)); + adapterPosition++; + mAverageHeaderHeight = height; + } else { + final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); + bottom = top + result.height; + mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, bottom)); + adapterPosition += result.length; + } + top = bottom; + + if (bottom >= recyclerBottom + getExtraLayoutSpace(state)) { + break; + } + } + + if (getBottomRow().bottom < recyclerBottom) { + scrollVerticallyBy(getBottomRow().bottom - recyclerBottom, recycler, state); + } else { + clearViewsAndStickHeaders(recycler, state, false); + } + + // If layout was caused by the pending scroll, adjust top item position and move it under sticky + // header + if (mPendingScrollPosition >= 0) { + mPendingScrollPosition = NO_POSITION; + + final int topOffset = getPositionSectionHeaderHeight(pendingAdapterPosition); + if (topOffset != 0) { + scrollVerticallyBy(-topOffset, recycler, state); + } + } + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; + } + + private int getPositionSectionHeaderHeight(int adapterPosition) { + final int section = mAdapter.getAdapterPositionSection(adapterPosition); + if (section >= 0 && mAdapter.isSectionHeaderSticky(section)) { + final int offset = mAdapter.getItemSectionOffset(section, adapterPosition); + if (offset >= 0) { + final int headerAdapterPosition = mAdapter.getSectionHeaderPosition(section); + if (mFloatingHeaderView != null && headerAdapterPosition == mFloatingHeaderPosition) { + return Math.max( + 0, getDecoratedMeasuredHeight(mFloatingHeaderView) - mHeaderOverlapMargin); + } else { + final LayoutRow header = getHeaderRow(headerAdapterPosition); + if (header != null) { + return header.getHeight(); + } else { + // Fall back to cached header size, can be incorrect + return mAverageHeaderHeight; + } + } + } + } + + return 0; + } + + private int findFirstRowItem(int adapterPosition) { + final int section = mAdapter.getAdapterPositionSection(adapterPosition); + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + while (sectionPosition > 0 + && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) { + sectionPosition--; + adapterPosition--; + } + + return adapterPosition; + } + + private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) { + final int spanWidth = recyclerWidth / mSpanCount; + final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; + final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize); + + return spanWidth * spanSize + widthCorrection; + } + + private int getSpanLeft(int recyclerWidth, int spanIndex) { + final int spanWidth = recyclerWidth / mSpanCount; + final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; + final int widthCorrection = Math.min(spanWidthReminder, spanIndex); + + return spanWidth * spanIndex + widthCorrection; + } + + private FillResult fillBottomRow( + RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { + final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int section = mAdapter.getAdapterPositionSection(position); + int adapterPosition = position; + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); + int count = 0; + int maxHeight = 0; + + // Create phase + Arrays.fill(mFillViewSet, null); + while (spanIndex + spanSize <= mSpanCount) { + // Create view and fill layout params + final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); + final View v = recycler.getViewForPosition(adapterPosition); + final LayoutParams params = (LayoutParams) v.getLayoutParams(); + params.mSpanIndex = spanIndex; + params.mSpanSize = spanSize; + + addView(v, mHeadersStartPosition); + mHeadersStartPosition++; + measureChildWithMargins(v, recyclerWidth - spanWidth, 0); + mFillViewSet[count] = v; + count++; + + final int height = getDecoratedMeasuredHeight(v); + if (maxHeight < height) { + maxHeight = height; + } + + // Check next + adapterPosition++; + sectionPosition++; + if (sectionPosition >= mAdapter.getSectionItemCount(section)) { + break; + } + + spanIndex += spanSize; + spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + } + + // Layout phase + int left = getPaddingLeft(); + for (int i = 0; i < count; ++i) { + final View v = mFillViewSet[i]; + final int height = getDecoratedMeasuredHeight(v); + final int width = getDecoratedMeasuredWidth(v); + layoutDecorated(v, left, top, left + width, top + height); + left += width; + } + + mFillResult.edgeView = mFillViewSet[count - 1]; + mFillResult.adapterPosition = position; + mFillResult.length = count; + mFillResult.height = maxHeight; + + return mFillResult; + } + + private FillResult fillTopRow( + RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { + final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + final int section = mAdapter.getAdapterPositionSection(position); + int adapterPosition = position; + int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); + int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); + int count = 0; + int maxHeight = 0; + + Arrays.fill(mFillViewSet, null); + while (spanIndex >= 0) { + // Create view and fill layout params + final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); + final View v = recycler.getViewForPosition(adapterPosition); + final LayoutParams params = (LayoutParams) v.getLayoutParams(); + params.mSpanIndex = spanIndex; + params.mSpanSize = spanSize; + + addView(v, 0); + mHeadersStartPosition++; + measureChildWithMargins(v, recyclerWidth - spanWidth, 0); + mFillViewSet[count] = v; + count++; + + final int height = getDecoratedMeasuredHeight(v); + if (maxHeight < height) { + maxHeight = height; + } + + // Check next + adapterPosition--; + sectionPosition--; + if (sectionPosition < 0) { + break; + } + + spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); + spanIndex -= spanSize; + } + + // Layout phase + int left = getPaddingLeft(); + for (int i = count - 1; i >= 0; --i) { + final View v = mFillViewSet[i]; + final int height = getDecoratedMeasuredHeight(v); + final int width = getDecoratedMeasuredWidth(v); + layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height)); + left += width; + } + + mFillResult.edgeView = mFillViewSet[count - 1]; + mFillResult.adapterPosition = adapterPosition + 1; + mFillResult.length = count; + mFillResult.height = maxHeight; + + return mFillResult; + } + + private void clearHiddenRows( + RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { + if (mLayoutRows.size() <= 0) { + return; + } + + final int recyclerTop = getPaddingTop(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + + if (top) { + LayoutRow row = getTopRow(); + while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) { + if (row.header) { + removeAndRecycleViewAt( + mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler); + } else { + for (int i = 0; i < row.length; ++i) { + removeAndRecycleViewAt(0, recycler); + mHeadersStartPosition--; + } + } + mLayoutRows.remove(0); + row = getTopRow(); + } + } else { + LayoutRow row = getBottomRow(); + while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) { + if (row.header) { + removeAndRecycleViewAt(getChildCount() - 1, recycler); + } else { + for (int i = 0; i < row.length; ++i) { + removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler); + mHeadersStartPosition--; + } + } + mLayoutRows.remove(mLayoutRows.size() - 1); + row = getBottomRow(); + } + } + } + + private void clearViewsAndStickHeaders( + RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { + clearHiddenRows(recycler, state, top); + if (getChildCount() > 0) { + stickTopHeader(recycler); + } + updateTopPosition(); + } + + private LayoutRow getBottomRow() { + return mLayoutRows.get(mLayoutRows.size() - 1); + } + + private LayoutRow getTopRow() { + return mLayoutRows.get(0); + } + + private void offsetRowsVertical(int offset) { + for (LayoutRow row : mLayoutRows) { + row.top += offset; + row.bottom += offset; + } + offsetChildrenVertical(offset); + } + + private void addRow( + RecyclerView.Recycler recycler, + RecyclerView.State state, + boolean isTop, + int adapterPosition, + int top) { + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + + // Reattach floating header if needed + if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) { + removeFloatingHeader(recycler); + } + + final int viewType = mAdapter.getItemViewInternalType(adapterPosition); + if (viewType == TYPE_HEADER) { + final View v = recycler.getViewForPosition(adapterPosition); + if (isTop) { + addView(v, mHeadersStartPosition); + } else { + addView(v); + } + measureChildWithMargins(v, 0, 0); + final int height = getDecoratedMeasuredHeight(v); + final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; + if (isTop) { + layoutDecorated(v, left, top - height + margin, right, top + margin); + mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top)); + } else { + layoutDecorated(v, left, top, right, top + height); + mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin)); + } + mAverageHeaderHeight = height - margin; + } else { + if (isTop) { + final FillResult result = fillTopRow(recycler, state, adapterPosition, top); + mLayoutRows.add( + 0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top)); + } else { + final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); + mLayoutRows.add( + new LayoutRow(result.adapterPosition, result.length, top, top + result.height)); + } + } + } + + private void addOffScreenRows( + RecyclerView.Recycler recycler, + RecyclerView.State state, + int recyclerTop, + int recyclerBottom, + boolean bottom) { + if (bottom) { + // Bottom + while (true) { + final LayoutRow bottomRow = getBottomRow(); + final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; + if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) + || adapterPosition >= state.getItemCount()) { + break; + } + addRow(recycler, state, false, adapterPosition, bottomRow.bottom); + } + } else { + // Top + while (true) { + final LayoutRow topRow = getTopRow(); + final int adapterPosition = topRow.adapterPosition - 1; + if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) { + break; + } + addRow(recycler, state, true, adapterPosition, topRow.top); + } + } + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + + int scrolled = 0; + int left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(); + final int recyclerTop = getPaddingTop(); + final int recyclerBottom = getHeight() - getPaddingBottom(); + + // If we have simple header stick, offset it back + final int firstHeader = getFirstVisibleSectionHeader(); + if (firstHeader != NO_POSITION) { + mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset); + } + + if (dy >= 0) { + // Up + while (scrolled < dy) { + final LayoutRow bottomRow = getBottomRow(); + final int scrollChunk = + -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled); + + offsetRowsVertical(scrollChunk); + scrolled -= scrollChunk; + + final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; + if (scrolled >= dy || adapterPosition >= state.getItemCount()) { + break; + } + + addRow(recycler, state, false, adapterPosition, bottomRow.bottom); + } + } else { + // Down + while (scrolled > dy) { + final LayoutRow topRow = getTopRow(); + final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy); + + offsetRowsVertical(scrollChunk); + scrolled -= scrollChunk; + + final int adapterPosition = topRow.adapterPosition - 1; + if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) { + break; + } + + addRow(recycler, state, true, adapterPosition, topRow.top); + } + } + + // Fill extra offscreen rows for smooth scroll + if (scrolled == dy) { + addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0); + } + + clearViewsAndStickHeaders(recycler, state, dy >= 0); + return scrolled; + } + + /** + * Returns first visible item excluding headers. + * + * @param visibleTop Whether item top edge should be visible or not + * @return The first visible item adapter position closest to top of the layout. + */ + public int getFirstVisibleItemPosition(boolean visibleTop) { + return getFirstVisiblePosition(TYPE_ITEM, visibleTop); + } + + /** + * Returns last visible item excluding headers. + * + * @return The last visible item adapter position closest to bottom of the layout. + */ + public int getLastVisibleItemPosition() { + return getLastVisiblePosition(TYPE_ITEM); + } + + /** + * Returns first visible header. + * + * @param visibleTop Whether header top edge should be visible or not + * @return The first visible header adapter position closest to top of the layout. + */ + public int getFirstVisibleHeaderPosition(boolean visibleTop) { + return getFirstVisiblePosition(TYPE_HEADER, visibleTop); + } + + /** + * Returns last visible header. + * + * @return The last visible header adapter position closest to bottom of the layout. + */ + public int getLastVisibleHeaderPosition() { + return getLastVisiblePosition(TYPE_HEADER); + } + + private int getFirstVisiblePosition(int type, boolean visibleTop) { + if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { + return NO_POSITION; + } else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { + return NO_POSITION; + } + + int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition; + int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount(); + final int recyclerTop = getPaddingTop(); + for (int i = viewFrom; i < viewTo; ++i) { + final View v = getChildAt(i); + final int adapterPosition = getPosition(v); + final int headerHeight = getPositionSectionHeaderHeight(adapterPosition); + final int top = getDecoratedTop(v); + final int bottom = getDecoratedBottom(v); + + if (visibleTop) { + if (top >= recyclerTop + headerHeight) { + return adapterPosition; + } + } else { + if (bottom >= recyclerTop + headerHeight) { + return adapterPosition; + } + } + } + + return NO_POSITION; + } + + private int getLastVisiblePosition(int type) { + if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { + return NO_POSITION; + } else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { + return NO_POSITION; + } + + int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1; + int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition; + final int recyclerBottom = getHeight() - getPaddingBottom(); + for (int i = viewFrom; i >= viewTo; --i) { + final View v = getChildAt(i); + final int top = getDecoratedTop(v); + + if (top < recyclerBottom) { + return getPosition(v); + } + } + + return NO_POSITION; + } + + private LayoutRow getFirstVisibleRow() { + final int recyclerTop = getPaddingTop(); + for (LayoutRow row : mLayoutRows) { + if (row.bottom > recyclerTop) { + return row; + } + } + return null; + } + + private int getFirstVisibleSectionHeader() { + final int recyclerTop = getPaddingTop(); + + int header = NO_POSITION; + for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header) { + header = i; + } + if (row.bottom > recyclerTop) { + return header; + } + } + return NO_POSITION; + } + + private LayoutRow getNextVisibleSectionHeader(int headerFrom) { + for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header) { + return row; + } + } + return null; + } + + private LayoutRow getHeaderRow(int adapterPosition) { + for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { + final LayoutRow row = mLayoutRows.get(i); + if (row.header && row.adapterPosition == adapterPosition) { + return row; + } + } + return null; + } + + private void removeFloatingHeader(RecyclerView.Recycler recycler) { + if (mFloatingHeaderView == null) { + return; + } + + final View view = mFloatingHeaderView; + mFloatingHeaderView = null; + mFloatingHeaderPosition = NO_POSITION; + removeAndRecycleView(view, recycler); + } + + private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) { + if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) { + onHeaderUnstick(); + } + + final boolean headerStateChanged = + mStickyHeaderSection != section + || !mStickyHeadeState.equals(state) + || state.equals(HeaderState.PUSHED); + + mStickyHeaderSection = section; + mStickyHeaderView = view; + mStickyHeadeState = state; + + if (headerStateChanged && mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset); + } + } + + private void onHeaderUnstick() { + if (mStickyHeaderSection != NO_POSITION) { + if (mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged( + mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); + } + mStickyHeaderSection = NO_POSITION; + mStickyHeaderView = null; + mStickyHeadeState = HeaderState.NORMAL; + } + } + + private void stickTopHeader(RecyclerView.Recycler recycler) { + final int firstHeader = getFirstVisibleSectionHeader(); + final int top = getPaddingTop(); + final int left = getPaddingLeft(); + final int right = getWidth() - getPaddingRight(); + + int notifySection = NO_POSITION; + View notifyView = null; + HeaderState notifyState = HeaderState.NORMAL; + int notifyOffset = 0; + + if (firstHeader != NO_POSITION) { + // Top row is header, floating header is not visible, remove + removeFloatingHeader(recycler); + + final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader); + final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition); + if (mAdapter.isSectionHeaderSticky(section)) { + final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader); + int offset = 0; + if (nextHeaderRow != null) { + final int height = firstHeaderRow.getHeight(); + offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height); + } + + mStickOffset = top - firstHeaderRow.top - offset; + firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset); + + onHeaderChanged( + section, + firstHeaderRow.headerView, + offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, + offset); + } else { + onHeaderUnstick(); + mStickOffset = 0; + } + } else { + // We don't have first visible sector header in layout, create floating + final LayoutRow firstVisibleRow = getFirstVisibleRow(); + if (firstVisibleRow != null) { + final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); + if (mAdapter.isSectionHeaderSticky(section)) { + final int headerPosition = mAdapter.getSectionHeaderPosition(section); + if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) { + removeFloatingHeader(recycler); + + // Create floating header + final View v = recycler.getViewForPosition(headerPosition); + addView(v, mHeadersStartPosition); + measureChildWithMargins(v, 0, 0); + mFloatingHeaderView = v; + mFloatingHeaderPosition = headerPosition; + } + + // Push floating header up, if needed + final int height = getDecoratedMeasuredHeight(mFloatingHeaderView); + int offset = 0; + if (getChildCount() - mHeadersStartPosition > 1) { + final View nextHeader = getChildAt(mHeadersStartPosition + 1); + final int contentHeight = Math.max(0, height - mHeaderOverlapMargin); + offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight; + } + + layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset); + onHeaderChanged( + section, + mFloatingHeaderView, + offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, + offset); + } else { + onHeaderUnstick(); + } + } else { + onHeaderUnstick(); + } + } + } + + private void updateTopPosition() { + if (getChildCount() == 0) { + mAnchor.reset(); + } + + final LayoutRow firstVisibleRow = getFirstVisibleRow(); + if (firstVisibleRow != null) { + mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); + mAnchor.item = + mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition); + mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0); + } + } + + private int getViewType(View view) { + return getItemViewType(view) & 0xFF; + } + + private int getViewType(int position) { + return mAdapter.getItemViewType(position) & 0xFF; + } + + private void clearState() { + mHeadersStartPosition = 0; + mStickOffset = 0; + mFloatingHeaderView = null; + mFloatingHeaderPosition = -1; + mAverageHeaderHeight = 0; + mLayoutRows.clear(); + + if (mStickyHeaderSection != NO_POSITION) { + if (mHeaderStateListener != null) { + mHeaderStateListener.onHeaderStateChanged( + mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); + } + mStickyHeaderSection = NO_POSITION; + mStickyHeaderView = null; + mStickyHeadeState = HeaderState.NORMAL; + } + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } + + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } + + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + final int recyclerTop = getPaddingTop(); + final LayoutRow topRow = getTopRow(); + final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0); + if (scrollChunk == 0) { + return 0; + } + + final int minPosition = Math.min(getPosition(startChild), getPosition(endChild)); + final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild)); + return Math.max(0, minPosition); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { + return 0; + } + + final View startChild = getChildAt(0); + final View endChild = getChildAt(mHeadersStartPosition - 1); + if (startChild == null || endChild == null) { + return 0; + } + + return state.getItemCount(); + } + + public static class LayoutParams extends RecyclerView.LayoutParams { + public static final int INVALID_SPAN_ID = -1; + + private int mSpanIndex = INVALID_SPAN_ID; + private int mSpanSize = 0; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + + public int getSpanIndex() { + return mSpanIndex; + } + + public int getSpanSize() { + return mSpanSize; + } + } + + public static final class DefaultSpanSizeLookup extends SpanSizeLookup { + @Override + public int getSpanSize(int section, int position) { + return 1; + } + + @Override + public int getSpanIndex(int section, int position, int spanCount) { + return position % spanCount; + } + } + + /** + * An interface to provide the number of spans each item occupies. + * + *

Default implementation sets each item to occupy exactly 1 span. + * + * @see + * StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup) + */ + public abstract static class SpanSizeLookup { + /** + * Returns the number of span occupied by the item in section at position + * . + * + * @param section The adapter section of the item + * @param position The adapter position of the item in section + * @return The number of spans occupied by the item at the provided section and position + */ + public abstract int getSpanSize(int section, int position); + + /** + * Returns the final span index of the provided position. + * + *

If you override this method, you need to make sure it is consistent with {@link + * #getSpanSize(int, int)}. StickyHeaderGridLayoutManager does not call this method for each + * item. It is called only for the reference item and rest of the items are assigned to spans + * based on the reference item. For example, you cannot assign a position to span 2 while span 1 + * is empty. + * + *

+ * + * @param section The adapter section of the item + * @param position The adapter position of the item in section + * @param spanCount The total number of spans in the grid + * @return The final span position of the item. Should be between 0 (inclusive) and + * spanCount(exclusive) + */ + public int getSpanIndex(int section, int position, int spanCount) { + // TODO: cache them? + final int positionSpanSize = getSpanSize(section, position); + if (positionSpanSize >= spanCount) { + return 0; + } + + int spanIndex = 0; + for (int i = 0; i < position; ++i) { + final int spanSize = getSpanSize(section, i); + spanIndex += spanSize; + + if (spanIndex == spanCount) { + spanIndex = 0; + } else if (spanIndex > spanCount) { + spanIndex = spanSize; + } + } + + if (spanIndex + positionSpanSize <= spanCount) { + return spanIndex; } return 0; - } - - private int findFirstRowItem(int adapterPosition) { - final int section = mAdapter.getAdapterPositionSection(adapterPosition); - int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); - while (sectionPosition > 0 && mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount) != 0) { - sectionPosition--; - adapterPosition--; - } - - return adapterPosition; - } - - private int getSpanWidth(int recyclerWidth, int spanIndex, int spanSize) { - final int spanWidth = recyclerWidth / mSpanCount; - final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; - final int widthCorrection = Math.min(Math.max(0, spanWidthReminder - spanIndex), spanSize); - - return spanWidth * spanSize + widthCorrection; - } - - private int getSpanLeft(int recyclerWidth, int spanIndex) { - final int spanWidth = recyclerWidth / mSpanCount; - final int spanWidthReminder = recyclerWidth - spanWidth * mSpanCount; - final int widthCorrection = Math.min(spanWidthReminder, spanIndex); - - return spanWidth * spanIndex + widthCorrection; - } - - private FillResult fillBottomRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { - final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); - final int section = mAdapter.getAdapterPositionSection(position); - int adapterPosition = position; - int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); - int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); - int count = 0; - int maxHeight = 0; - - // Create phase - Arrays.fill(mFillViewSet, null); - while (spanIndex + spanSize <= mSpanCount) { - // Create view and fill layout params - final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); - final View v = recycler.getViewForPosition(adapterPosition); - final LayoutParams params = (LayoutParams)v.getLayoutParams(); - params.mSpanIndex = spanIndex; - params.mSpanSize = spanSize; - - addView(v, mHeadersStartPosition); - mHeadersStartPosition++; - measureChildWithMargins(v, recyclerWidth - spanWidth, 0); - mFillViewSet[count] = v; - count++; - - final int height = getDecoratedMeasuredHeight(v); - if (maxHeight < height) { - maxHeight = height; - } - - // Check next - adapterPosition++; - sectionPosition++; - if (sectionPosition >= mAdapter.getSectionItemCount(section)) { - break; - } - - spanIndex += spanSize; - spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - } - - // Layout phase - int left = getPaddingLeft(); - for (int i = 0; i < count; ++i) { - final View v = mFillViewSet[i]; - final int height = getDecoratedMeasuredHeight(v); - final int width = getDecoratedMeasuredWidth(v); - layoutDecorated(v, left, top, left + width, top + height); - left += width; - } - - mFillResult.edgeView = mFillViewSet[count - 1]; - mFillResult.adapterPosition = position; - mFillResult.length = count; - mFillResult.height = maxHeight; - - return mFillResult; - } - - private FillResult fillTopRow(RecyclerView.Recycler recycler, RecyclerView.State state, int position, int top) { - final int recyclerWidth = getWidth() - getPaddingLeft() - getPaddingRight(); - final int section = mAdapter.getAdapterPositionSection(position); - int adapterPosition = position; - int sectionPosition = mAdapter.getItemSectionOffset(section, adapterPosition); - int spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - int spanIndex = mSpanSizeLookup.getSpanIndex(section, sectionPosition, mSpanCount); - int count = 0; - int maxHeight = 0; - - Arrays.fill(mFillViewSet, null); - while (spanIndex >= 0) { - // Create view and fill layout params - final int spanWidth = getSpanWidth(recyclerWidth, spanIndex, spanSize); - final View v = recycler.getViewForPosition(adapterPosition); - final LayoutParams params = (LayoutParams)v.getLayoutParams(); - params.mSpanIndex = spanIndex; - params.mSpanSize = spanSize; - - addView(v, 0); - mHeadersStartPosition++; - measureChildWithMargins(v, recyclerWidth - spanWidth, 0); - mFillViewSet[count] = v; - count++; - - final int height = getDecoratedMeasuredHeight(v); - if (maxHeight < height) { - maxHeight = height; - } - - // Check next - adapterPosition--; - sectionPosition--; - if (sectionPosition < 0) { - break; - } - - spanSize = mSpanSizeLookup.getSpanSize(section, sectionPosition); - spanIndex -= spanSize; - } - - // Layout phase - int left = getPaddingLeft(); - for (int i = count - 1; i >= 0; --i) { - final View v = mFillViewSet[i]; - final int height = getDecoratedMeasuredHeight(v); - final int width = getDecoratedMeasuredWidth(v); - layoutDecorated(v, left, top - maxHeight, left + width, top - (maxHeight - height)); - left += width; - } - - mFillResult.edgeView = mFillViewSet[count - 1]; - mFillResult.adapterPosition = adapterPosition + 1; - mFillResult.length = count; - mFillResult.height = maxHeight; - - return mFillResult; - } - - private void clearHiddenRows(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { - if (mLayoutRows.size() <= 0) { - return; - } - - final int recyclerTop = getPaddingTop(); - final int recyclerBottom = getHeight() - getPaddingBottom(); - - if (top) { - LayoutRow row = getTopRow(); - while (row.bottom < recyclerTop - getExtraLayoutSpace(state) || row.top > recyclerBottom) { - if (row.header) { - removeAndRecycleViewAt(mHeadersStartPosition + (mFloatingHeaderView != null ? 1 : 0), recycler); - } - else { - for (int i = 0; i < row.length; ++i) { - removeAndRecycleViewAt(0, recycler); - mHeadersStartPosition--; - } - } - mLayoutRows.remove(0); - row = getTopRow(); - } - } - else { - LayoutRow row = getBottomRow(); - while (row.bottom < recyclerTop || row.top > recyclerBottom + getExtraLayoutSpace(state)) { - if (row.header) { - removeAndRecycleViewAt(getChildCount() - 1, recycler); - } - else { - for (int i = 0; i < row.length; ++i) { - removeAndRecycleViewAt(mHeadersStartPosition - 1, recycler); - mHeadersStartPosition--; - } - } - mLayoutRows.remove(mLayoutRows.size() - 1); - row = getBottomRow(); - } - } - } - - private void clearViewsAndStickHeaders(RecyclerView.Recycler recycler, RecyclerView.State state, boolean top) { - clearHiddenRows(recycler, state, top); - if (getChildCount() > 0) { - stickTopHeader(recycler); - } - updateTopPosition(); - } - - private LayoutRow getBottomRow() { - return mLayoutRows.get(mLayoutRows.size() - 1); - } - - private LayoutRow getTopRow() { - return mLayoutRows.get(0); - } - - private void offsetRowsVertical(int offset) { - for (LayoutRow row : mLayoutRows) { - row.top += offset; - row.bottom += offset; - } - offsetChildrenVertical(offset); - } - - private void addRow(RecyclerView.Recycler recycler, RecyclerView.State state, boolean isTop, int adapterPosition, int top) { - final int left = getPaddingLeft(); - final int right = getWidth() - getPaddingRight(); - - // Reattach floating header if needed - if (isTop && mFloatingHeaderView != null && adapterPosition == mFloatingHeaderPosition) { - removeFloatingHeader(recycler); - } - - final int viewType = mAdapter.getItemViewInternalType(adapterPosition); - if (viewType == TYPE_HEADER) { - final View v = recycler.getViewForPosition(adapterPosition); - if (isTop) { - addView(v, mHeadersStartPosition); - } - else { - addView(v); - } - measureChildWithMargins(v, 0, 0); - final int height = getDecoratedMeasuredHeight(v); - final int margin = height >= mHeaderOverlapMargin ? mHeaderOverlapMargin : height; - if (isTop) { - layoutDecorated(v, left, top - height + margin, right, top + margin); - mLayoutRows.add(0, new LayoutRow(v, adapterPosition, 1, top - height + margin, top)); - } - else { - layoutDecorated(v, left, top, right, top + height); - mLayoutRows.add(new LayoutRow(v, adapterPosition, 1, top, top + height - margin)); - } - mAverageHeaderHeight = height - margin; - } - else { - if (isTop) { - final FillResult result = fillTopRow(recycler, state, adapterPosition, top); - mLayoutRows.add(0, new LayoutRow(result.adapterPosition, result.length, top - result.height, top)); - } - else { - final FillResult result = fillBottomRow(recycler, state, adapterPosition, top); - mLayoutRows.add(new LayoutRow(result.adapterPosition, result.length, top, top + result.height)); - } - } - } - - private void addOffScreenRows(RecyclerView.Recycler recycler, RecyclerView.State state, int recyclerTop, int recyclerBottom, boolean bottom) { - if (bottom) { - // Bottom - while (true) { - final LayoutRow bottomRow = getBottomRow(); - final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; - if (bottomRow.bottom >= recyclerBottom + getExtraLayoutSpace(state) || adapterPosition >= state.getItemCount()) { - break; - } - addRow(recycler, state, false, adapterPosition, bottomRow.bottom); - } - } - else { - // Top - while (true) { - final LayoutRow topRow = getTopRow(); - final int adapterPosition = topRow.adapterPosition - 1; - if (topRow.top < recyclerTop - getExtraLayoutSpace(state) || adapterPosition < 0) { - break; - } - addRow(recycler, state, true, adapterPosition, topRow.top); - } - } - } - - @Override - public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { - if (getChildCount() == 0) { - return 0; - } - - int scrolled = 0; - int left = getPaddingLeft(); - int right = getWidth() - getPaddingRight(); - final int recyclerTop = getPaddingTop(); - final int recyclerBottom = getHeight() - getPaddingBottom(); - - // If we have simple header stick, offset it back - final int firstHeader = getFirstVisibleSectionHeader(); - if (firstHeader != NO_POSITION) { - mLayoutRows.get(firstHeader).headerView.offsetTopAndBottom(-mStickOffset); - } - - if (dy >= 0) { - // Up - while (scrolled < dy) { - final LayoutRow bottomRow = getBottomRow(); - final int scrollChunk = -Math.min(Math.max(bottomRow.bottom - recyclerBottom, 0), dy - scrolled); - - offsetRowsVertical(scrollChunk); - scrolled -= scrollChunk; - - final int adapterPosition = bottomRow.adapterPosition + bottomRow.length; - if (scrolled >= dy || adapterPosition >= state.getItemCount()) { - break; - } - - addRow(recycler, state, false, adapterPosition, bottomRow.bottom); - } - } - else { - // Down - while (scrolled > dy) { - final LayoutRow topRow = getTopRow(); - final int scrollChunk = Math.min(Math.max(-topRow.top + recyclerTop, 0), scrolled - dy); - - offsetRowsVertical(scrollChunk); - scrolled -= scrollChunk; - - final int adapterPosition = topRow.adapterPosition - 1; - if (scrolled <= dy || adapterPosition >= state.getItemCount() || adapterPosition < 0) { - break; - } - - addRow(recycler, state, true, adapterPosition, topRow.top); - } - } - - // Fill extra offscreen rows for smooth scroll - if (scrolled == dy) { - addOffScreenRows(recycler, state, recyclerTop, recyclerBottom, dy >= 0); - } - - clearViewsAndStickHeaders(recycler, state, dy >= 0); - return scrolled; - } - - /** - * Returns first visible item excluding headers. - * - * @param visibleTop Whether item top edge should be visible or not - * @return The first visible item adapter position closest to top of the layout. - */ - public int getFirstVisibleItemPosition(boolean visibleTop) { - return getFirstVisiblePosition(TYPE_ITEM, visibleTop); - } - - /** - * Returns last visible item excluding headers. - * - * @return The last visible item adapter position closest to bottom of the layout. - */ - public int getLastVisibleItemPosition() { - return getLastVisiblePosition(TYPE_ITEM); - } - - /** - * Returns first visible header. - * - * @param visibleTop Whether header top edge should be visible or not - * @return The first visible header adapter position closest to top of the layout. - */ - public int getFirstVisibleHeaderPosition(boolean visibleTop) { - return getFirstVisiblePosition(TYPE_HEADER, visibleTop); - } - - /** - * Returns last visible header. - * - * @return The last visible header adapter position closest to bottom of the layout. - */ - public int getLastVisibleHeaderPosition() { - return getLastVisiblePosition(TYPE_HEADER); - } - - private int getFirstVisiblePosition(int type, boolean visibleTop) { - if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { - return NO_POSITION; - } - else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { - return NO_POSITION; - } - - int viewFrom = type == TYPE_ITEM ? 0 : mHeadersStartPosition; - int viewTo = type == TYPE_ITEM ? mHeadersStartPosition : getChildCount(); - final int recyclerTop = getPaddingTop(); - for (int i = viewFrom; i < viewTo; ++i) { - final View v = getChildAt(i); - final int adapterPosition = getPosition(v); - final int headerHeight = getPositionSectionHeaderHeight(adapterPosition); - final int top = getDecoratedTop(v); - final int bottom = getDecoratedBottom(v); - - if (visibleTop) { - if (top >= recyclerTop + headerHeight) { - return adapterPosition; - } - } - else { - if (bottom >= recyclerTop + headerHeight) { - return adapterPosition; - } - } - } - - return NO_POSITION; - } - - private int getLastVisiblePosition(int type) { - if (type == TYPE_ITEM && mHeadersStartPosition <= 0) { - return NO_POSITION; - } - else if (type == TYPE_HEADER && mHeadersStartPosition >= getChildCount()) { - return NO_POSITION; - } - - int viewFrom = type == TYPE_ITEM ? mHeadersStartPosition - 1 : getChildCount() - 1; - int viewTo = type == TYPE_ITEM ? 0 : mHeadersStartPosition; - final int recyclerBottom = getHeight() - getPaddingBottom(); - for (int i = viewFrom; i >= viewTo; --i) { - final View v = getChildAt(i); - final int top = getDecoratedTop(v); - - if (top < recyclerBottom) { - return getPosition(v); - } - } - - return NO_POSITION; - } - - private LayoutRow getFirstVisibleRow() { - final int recyclerTop = getPaddingTop(); - for (LayoutRow row : mLayoutRows) { - if (row.bottom > recyclerTop) { - return row; - } - } - return null; - } - - private int getFirstVisibleSectionHeader() { - final int recyclerTop = getPaddingTop(); - - int header = NO_POSITION; - for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { - final LayoutRow row = mLayoutRows.get(i); - if (row.header) { - header = i; - } - if (row.bottom > recyclerTop) { - return header; - } - } - return NO_POSITION; - } - - private LayoutRow getNextVisibleSectionHeader(int headerFrom) { - for (int i = headerFrom + 1, n = mLayoutRows.size(); i < n; ++i) { - final LayoutRow row = mLayoutRows.get(i); - if (row.header) { - return row; - } - } - return null; - } - - private LayoutRow getHeaderRow(int adapterPosition) { - for (int i = 0, n = mLayoutRows.size(); i < n; ++i) { - final LayoutRow row = mLayoutRows.get(i); - if (row.header && row.adapterPosition == adapterPosition) { - return row; - } - } - return null; - } - - private void removeFloatingHeader(RecyclerView.Recycler recycler) { - if (mFloatingHeaderView == null) { - return; - } - - final View view = mFloatingHeaderView; - mFloatingHeaderView = null; - mFloatingHeaderPosition = NO_POSITION; - removeAndRecycleView(view, recycler); - } - - private void onHeaderChanged(int section, View view, HeaderState state, int pushOffset) { - if (mStickyHeaderSection != NO_POSITION && section != mStickyHeaderSection) { - onHeaderUnstick(); - } - - final boolean headerStateChanged = mStickyHeaderSection != section || !mStickyHeadeState.equals(state) || state.equals(HeaderState.PUSHED); - - mStickyHeaderSection = section; - mStickyHeaderView = view; - mStickyHeadeState = state; - - if (headerStateChanged && mHeaderStateListener != null) { - mHeaderStateListener.onHeaderStateChanged(section, view, state, pushOffset); - } - } - - private void onHeaderUnstick() { - if (mStickyHeaderSection != NO_POSITION) { - if (mHeaderStateListener != null) { - mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); - } - mStickyHeaderSection = NO_POSITION; - mStickyHeaderView = null; - mStickyHeadeState = HeaderState.NORMAL; - } - } - - private void stickTopHeader(RecyclerView.Recycler recycler) { - final int firstHeader = getFirstVisibleSectionHeader(); - final int top = getPaddingTop(); - final int left = getPaddingLeft(); - final int right = getWidth() - getPaddingRight(); - - int notifySection = NO_POSITION; - View notifyView = null; - HeaderState notifyState = HeaderState.NORMAL; - int notifyOffset = 0; - - if (firstHeader != NO_POSITION) { - // Top row is header, floating header is not visible, remove - removeFloatingHeader(recycler); - - final LayoutRow firstHeaderRow = mLayoutRows.get(firstHeader); - final int section = mAdapter.getAdapterPositionSection(firstHeaderRow.adapterPosition); - if (mAdapter.isSectionHeaderSticky(section)) { - final LayoutRow nextHeaderRow = getNextVisibleSectionHeader(firstHeader); - int offset = 0; - if (nextHeaderRow != null) { - final int height = firstHeaderRow.getHeight(); - offset = Math.min(Math.max(top - nextHeaderRow.top, -height) + height, height); - } - - mStickOffset = top - firstHeaderRow.top - offset; - firstHeaderRow.headerView.offsetTopAndBottom(mStickOffset); - - onHeaderChanged(section, firstHeaderRow.headerView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); - } - else { - onHeaderUnstick(); - mStickOffset = 0; - } - } - else { - // We don't have first visible sector header in layout, create floating - final LayoutRow firstVisibleRow = getFirstVisibleRow(); - if (firstVisibleRow != null) { - final int section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); - if (mAdapter.isSectionHeaderSticky(section)) { - final int headerPosition = mAdapter.getSectionHeaderPosition(section); - if (mFloatingHeaderView == null || mFloatingHeaderPosition != headerPosition) { - removeFloatingHeader(recycler); - - // Create floating header - final View v = recycler.getViewForPosition(headerPosition); - addView(v, mHeadersStartPosition); - measureChildWithMargins(v, 0, 0); - mFloatingHeaderView = v; - mFloatingHeaderPosition = headerPosition; - } - - // Push floating header up, if needed - final int height = getDecoratedMeasuredHeight(mFloatingHeaderView); - int offset = 0; - if (getChildCount() - mHeadersStartPosition > 1) { - final View nextHeader = getChildAt(mHeadersStartPosition + 1); - final int contentHeight = Math.max(0, height - mHeaderOverlapMargin); - offset = Math.max(top - getDecoratedTop(nextHeader), -contentHeight) + contentHeight; - } - - layoutDecorated(mFloatingHeaderView, left, top - offset, right, top + height - offset); - onHeaderChanged(section, mFloatingHeaderView, offset == 0 ? HeaderState.STICKY : HeaderState.PUSHED, offset); - } - else { - onHeaderUnstick(); - } - } - else { - onHeaderUnstick(); - } - } - } - - private void updateTopPosition() { - if (getChildCount() == 0) { - mAnchor.reset(); - } - - final LayoutRow firstVisibleRow = getFirstVisibleRow(); - if (firstVisibleRow != null) { - mAnchor.section = mAdapter.getAdapterPositionSection(firstVisibleRow.adapterPosition); - mAnchor.item = mAdapter.getItemSectionOffset(mAnchor.section, firstVisibleRow.adapterPosition); - mAnchor.offset = Math.min(firstVisibleRow.top - getPaddingTop(), 0); - } - } - - private int getViewType(View view) { - return getItemViewType(view) & 0xFF; - } - - private int getViewType(int position) { - return mAdapter.getItemViewType(position) & 0xFF; - } - - private void clearState() { - mHeadersStartPosition = 0; - mStickOffset = 0; - mFloatingHeaderView = null; - mFloatingHeaderPosition = -1; - mAverageHeaderHeight = 0; - mLayoutRows.clear(); - - if (mStickyHeaderSection != NO_POSITION) { - if (mHeaderStateListener != null) { - mHeaderStateListener.onHeaderStateChanged(mStickyHeaderSection, mStickyHeaderView, HeaderState.NORMAL, 0); - } - mStickyHeaderSection = NO_POSITION; - mStickyHeaderView = null; - mStickyHeadeState = HeaderState.NORMAL; - } - } - - @Override - public int computeVerticalScrollExtent(RecyclerView.State state) { - if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { - return 0; - } - - final View startChild = getChildAt(0); - final View endChild = getChildAt(mHeadersStartPosition - 1); - if (startChild == null || endChild == null) { - return 0; - } - - return Math.abs(getPosition(startChild) - getPosition(endChild)) + 1; - } - - @Override - public int computeVerticalScrollOffset(RecyclerView.State state) { - if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { - return 0; - } - - final View startChild = getChildAt(0); - final View endChild = getChildAt(mHeadersStartPosition - 1); - if (startChild == null || endChild == null) { - return 0; - } - - final int recyclerTop = getPaddingTop(); - final LayoutRow topRow = getTopRow(); - final int scrollChunk = Math.max(-topRow.top + recyclerTop, 0); - if (scrollChunk == 0) { - return 0; - } - - final int minPosition = Math.min(getPosition(startChild), getPosition(endChild)); - final int maxPosition = Math.max(getPosition(startChild), getPosition(endChild)); - return Math.max(0, minPosition); - } - - @Override - public int computeVerticalScrollRange(RecyclerView.State state) { - if (mHeadersStartPosition == 0 || state.getItemCount() == 0) { - return 0; - } - - final View startChild = getChildAt(0); - final View endChild = getChildAt(mHeadersStartPosition - 1); - if (startChild == null || endChild == null) { - return 0; - } - - return state.getItemCount(); - } - - public static class LayoutParams extends RecyclerView.LayoutParams { - public static final int INVALID_SPAN_ID = -1; - - private int mSpanIndex = INVALID_SPAN_ID; - private int mSpanSize = 0; - - public LayoutParams(Context c, AttributeSet attrs) { - super(c, attrs); - } - - public LayoutParams(int width, int height) { - super(width, height); - } - - public LayoutParams(ViewGroup.MarginLayoutParams source) { - super(source); - } - - public LayoutParams(ViewGroup.LayoutParams source) { - super(source); - } - - public LayoutParams(RecyclerView.LayoutParams source) { - super(source); - } - - public int getSpanIndex() { - return mSpanIndex; - } - - public int getSpanSize() { - return mSpanSize; - } - } - - public static final class DefaultSpanSizeLookup extends SpanSizeLookup { - @Override - public int getSpanSize(int section, int position) { - return 1; - } - - @Override - public int getSpanIndex(int section, int position, int spanCount) { - return position % spanCount; - } - } - - /** - * An interface to provide the number of spans each item occupies. - *

- * Default implementation sets each item to occupy exactly 1 span. - * - * @see StickyHeaderGridLayoutManager#setSpanSizeLookup(StickyHeaderGridLayoutManager.SpanSizeLookup) - */ - public static abstract class SpanSizeLookup { - /** - * Returns the number of span occupied by the item in section at position. - * - * @param section The adapter section of the item - * @param position The adapter position of the item in section - * @return The number of spans occupied by the item at the provided section and position - */ - abstract public int getSpanSize(int section, int position); - - /** - * Returns the final span index of the provided position. - * - *

- * If you override this method, you need to make sure it is consistent with - * {@link #getSpanSize(int, int)}. StickyHeaderGridLayoutManager does not call this method for - * each item. It is called only for the reference item and rest of the items - * are assigned to spans based on the reference item. For example, you cannot assign a - * position to span 2 while span 1 is empty. - *

- * - * @param section The adapter section of the item - * @param position The adapter position of the item in section - * @param spanCount The total number of spans in the grid - * @return The final span position of the item. Should be between 0 (inclusive) and - * spanCount(exclusive) - */ - public int getSpanIndex(int section, int position, int spanCount) { - // TODO: cache them? - final int positionSpanSize = getSpanSize(section, position); - if (positionSpanSize >= spanCount) { - return 0; - } - - int spanIndex = 0; - for (int i = 0; i < position; ++i) { - final int spanSize = getSpanSize(section, i); - spanIndex += spanSize; - - if (spanIndex == spanCount) { - spanIndex = 0; - } - else if (spanIndex > spanCount) { - spanIndex = spanSize; - } - } - - if (spanIndex + positionSpanSize <= spanCount) { - return spanIndex; - } - - return 0; - } - } - - public static class SavedState implements Parcelable { - private int mAnchorSection; - private int mAnchorItem; - private int mAnchorOffset; - - public SavedState() { - - } - - SavedState(Parcel in) { - mAnchorSection = in.readInt(); - mAnchorItem = in.readInt(); - mAnchorOffset = in.readInt(); - } - - public SavedState(SavedState other) { - mAnchorSection = other.mAnchorSection; - mAnchorItem = other.mAnchorItem; - mAnchorOffset = other.mAnchorOffset; - } - - boolean hasValidAnchor() { - return mAnchorSection >= 0; - } - - void invalidateAnchor() { - mAnchorSection = NO_POSITION; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mAnchorSection); - dest.writeInt(mAnchorItem); - dest.writeInt(mAnchorOffset); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public SavedState createFromParcel(Parcel in) { + } + } + + public static class SavedState implements Parcelable { + private int mAnchorSection; + private int mAnchorItem; + private int mAnchorOffset; + + public SavedState() {} + + SavedState(Parcel in) { + mAnchorSection = in.readInt(); + mAnchorItem = in.readInt(); + mAnchorOffset = in.readInt(); + } + + public SavedState(SavedState other) { + mAnchorSection = other.mAnchorSection; + mAnchorItem = other.mAnchorItem; + mAnchorOffset = other.mAnchorOffset; + } + + boolean hasValidAnchor() { + return mAnchorSection >= 0; + } + + void invalidateAnchor() { + mAnchorSection = NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorSection); + dest.writeInt(mAnchorItem); + dest.writeInt(mAnchorOffset); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { return new SavedState(in); - } + } - @Override - public SavedState[] newArray(int size) { + @Override + public SavedState[] newArray(int size) { return new SavedState[size]; - } - }; - } + } + }; + } - private static class LayoutRow { - private boolean header; - private View headerView; - private int adapterPosition; - private int length; - private int top; - private int bottom; + private static class LayoutRow { + private boolean header; + private View headerView; + private int adapterPosition; + private int length; + private int top; + private int bottom; - public LayoutRow(int adapterPosition, int length, int top, int bottom) { - this.header = false; - this.headerView = null; - this.adapterPosition = adapterPosition; - this.length = length; - this.top = top; - this.bottom = bottom; - } + public LayoutRow(int adapterPosition, int length, int top, int bottom) { + this.header = false; + this.headerView = null; + this.adapterPosition = adapterPosition; + this.length = length; + this.top = top; + this.bottom = bottom; + } - public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) { - this.header = true; - this.headerView = headerView; - this.adapterPosition = adapterPosition; - this.length = length; - this.top = top; - this.bottom = bottom; - } + public LayoutRow(View headerView, int adapterPosition, int length, int top, int bottom) { + this.header = true; + this.headerView = headerView; + this.adapterPosition = adapterPosition; + this.length = length; + this.top = top; + this.bottom = bottom; + } - int getHeight() { - return bottom - top; - } - } + int getHeight() { + return bottom - top; + } + } - private static class FillResult { - private View edgeView; - private int adapterPosition; - private int length; - private int height; - } + private static class FillResult { + private View edgeView; + private int adapterPosition; + private int length; + private int height; + } - private static class AnchorPosition { - private int section; - private int item; - private int offset; + private static class AnchorPosition { + private int section; + private int item; + private int offset; - public AnchorPosition() { - reset(); - } + public AnchorPosition() { + reset(); + } - public void reset() { - section = NO_POSITION; - item = 0; - offset = 0; - } - } + public void reset() { + section = NO_POSITION; + item = 0; + offset = 0; + } + } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java index 7b12689d6..93e0a4cac 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/CanvasMatrix.java @@ -3,28 +3,27 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; - import androidx.annotation.NonNull; /** * Tracks the current matrix for a canvas. - *

- * This is because you cannot reliably call {@link Canvas#setMatrix(Matrix)}. - * {@link Canvas#getMatrix()} provides this hint in its documentation: - * "track relevant transform state outside of the canvas." - *

- * To achieve this, any changes to the canvas matrix must be done via this class, including save and - * restore operations where the matrix was altered in between. + * + *

This is because you cannot reliably call {@link Canvas#setMatrix(Matrix)}. {@link + * Canvas#getMatrix()} provides this hint in its documentation: "track relevant transform state + * outside of the canvas." + * + *

To achieve this, any changes to the canvas matrix must be done via this class, including save + * and restore operations where the matrix was altered in between. */ public final class CanvasMatrix { - private final static int STACK_HEIGHT_LIMIT = 16; + private static final int STACK_HEIGHT_LIMIT = 16; - private final Canvas canvas; - private final Matrix canvasMatrix = new Matrix(); - private final Matrix temp = new Matrix(); - private final Matrix[] stack = new Matrix[STACK_HEIGHT_LIMIT]; - private int stackHeight; + private final Canvas canvas; + private final Matrix canvasMatrix = new Matrix(); + private final Matrix temp = new Matrix(); + private final Matrix[] stack = new Matrix[STACK_HEIGHT_LIMIT]; + private int stackHeight; CanvasMatrix(Canvas canvas) { this.canvas = canvas; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java index 11706dfc0..4e3247a1c 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ColorableRenderer.java @@ -4,8 +4,8 @@ import androidx.annotation.ColorInt; /** * A renderer that can have its color changed. - *

- * For example, Lines and Text can change color. + * + *

For example, Lines and Text can change color. */ public interface ColorableRenderer extends Renderer { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java index 7fe7c0dd6..135e238da 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/DrawingSession.java @@ -2,25 +2,25 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.renderers.BezierDrawingRenderer; -/** - * Passes touch events into a {@link BezierDrawingRenderer}. - */ +/** Passes touch events into a {@link BezierDrawingRenderer}. */ class DrawingSession extends ElementEditSession { private final BezierDrawingRenderer renderer; - private DrawingSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix, @NonNull BezierDrawingRenderer renderer) { + private DrawingSession( + @NonNull EditorElement selected, + @NonNull Matrix inverseMatrix, + @NonNull BezierDrawingRenderer renderer) { super(selected, inverseMatrix); this.renderer = renderer; } - public static EditSession start(EditorElement element, BezierDrawingRenderer renderer, Matrix inverseMatrix, PointF point) { + public static EditSession start( + EditorElement element, BezierDrawingRenderer renderer, Matrix inverseMatrix, PointF point) { DrawingSession drawingSession = new DrawingSession(element, inverseMatrix, renderer); drawingSession.setScreenStartPoint(0, point); renderer.setFirstPoint(drawingSession.startPointElement[0]); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java index 3ea2148c7..5bcae29e8 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/EditSession.java @@ -2,21 +2,20 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; /** * Represents an underway edit of the image. - *

- * Accepts new touch positions, new touch points, released touch points and when complete can commit the edit. - *

- * Examples of edit session implementations are, Drag, Draw, Resize: - *

- * {@link ElementDragEditSession} for dragging with a single finger. - * {@link ElementScaleEditSession} for resize/dragging with two fingers. - * {@link DrawingSession} for drawing with a single finger. + * + *

Accepts new touch positions, new touch points, released touch points and when complete can + * commit the edit. + * + *

Examples of edit session implementations are, Drag, Draw, Resize: + * + *

{@link ElementDragEditSession} for dragging with a single finger. {@link + * ElementScaleEditSession} for resize/dragging with two fingers. {@link DrawingSession} for drawing + * with a single finger. */ interface EditSession { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java index 23bd5e961..651f17001 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementDragEditSession.java @@ -2,9 +2,7 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; final class ElementDragEditSession extends ElementEditSession { @@ -13,10 +11,14 @@ final class ElementDragEditSession extends ElementEditSession { super(selected, inverseMatrix); } - static ElementDragEditSession startDrag(@NonNull EditorElement selected, @NonNull Matrix inverseViewModelMatrix, @NonNull PointF point) { + static ElementDragEditSession startDrag( + @NonNull EditorElement selected, + @NonNull Matrix inverseViewModelMatrix, + @NonNull PointF point) { if (!selected.getFlags().isEditable()) return null; - ElementDragEditSession elementDragEditSession = new ElementDragEditSession(selected, inverseViewModelMatrix); + ElementDragEditSession elementDragEditSession = + new ElementDragEditSession(selected, inverseViewModelMatrix); elementDragEditSession.setScreenStartPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point); @@ -27,8 +29,11 @@ final class ElementDragEditSession extends ElementEditSession { public void movePoint(int p, @NonNull PointF point) { setScreenEndPoint(p, point); - selected.getEditorMatrix() - .setTranslate(endPointElement[0].x - startPointElement[0].x, endPointElement[0].y - startPointElement[0].y); + selected + .getEditorMatrix() + .setTranslate( + endPointElement[0].x - startPointElement[0].x, + endPointElement[0].y - startPointElement[0].y); } @Override diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java index 6bf0e229d..3969d5a51 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementEditSession.java @@ -2,9 +2,7 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; abstract class ElementEditSession implements EditSession { @@ -14,9 +12,9 @@ abstract class ElementEditSession implements EditSession { final EditorElement selected; final PointF[] startPointElement = newTwoPointArray(); - final PointF[] endPointElement = newTwoPointArray(); - final PointF[] startPointScreen = newTwoPointArray(); - final PointF[] endPointScreen = newTwoPointArray(); + final PointF[] endPointElement = newTwoPointArray(); + final PointF[] startPointScreen = newTwoPointArray(); + final PointF[] endPointScreen = newTwoPointArray(); ElementEditSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix) { this.selected = selected; @@ -57,12 +55,12 @@ abstract class ElementEditSession implements EditSession { /** * Map src to dst using the matrix. * - * @param dst Output point. + * @param dst Output point. * @param matrix Matrix to transform point with. - * @param src Input point. + * @param src Input point. */ private static void mapPoint(@NonNull PointF dst, @NonNull Matrix matrix, @NonNull PointF src) { - float[] in = { src.x, src.y }; + float[] in = {src.x, src.y}; float[] out = new float[2]; matrix.mapPoints(out, in); dst.set(out[0], out[1]); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java index d18934b3d..3c75c652e 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ElementScaleEditSession.java @@ -2,9 +2,7 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; final class ElementScaleEditSession extends ElementEditSession { @@ -13,9 +11,14 @@ final class ElementScaleEditSession extends ElementEditSession { super(selected, inverseMatrix); } - static ElementScaleEditSession startScale(@NonNull ElementDragEditSession session, @NonNull Matrix inverseMatrix, @NonNull PointF point, int p) { + static ElementScaleEditSession startScale( + @NonNull ElementDragEditSession session, + @NonNull Matrix inverseMatrix, + @NonNull PointF point, + int p) { session.commit(); - ElementScaleEditSession newSession = new ElementScaleEditSession(session.selected, inverseMatrix); + ElementScaleEditSession newSession = + new ElementScaleEditSession(session.selected, inverseMatrix); newSession.setScreenStartPoint(1 - p, session.endPointScreen[0]); newSession.setScreenEndPoint(1 - p, session.endPointScreen[0]); newSession.setScreenStartPoint(p, point); @@ -37,7 +40,9 @@ final class ElementScaleEditSession extends ElementEditSession { editorMatrix.postTranslate(-startPointElement[0].x, -startPointElement[0].y); editorMatrix.postScale(scale, scale); - double angle = angle(endPointElement[0], endPointElement[1]) - angle(startPointElement[0], startPointElement[1]); + double angle = + angle(endPointElement[0], endPointElement[1]) + - angle(startPointElement[0], startPointElement[1]); if (!selected.getFlags().isRotateLocked()) { editorMatrix.postRotate((float) Math.toDegrees(angle)); @@ -47,8 +52,12 @@ final class ElementScaleEditSession extends ElementEditSession { } else { editorMatrix.postTranslate(-startPointElement[0].x, -startPointElement[0].y); - float scaleX = (endPointElement[1].x - endPointElement[0].x) / (startPointElement[1].x - startPointElement[0].x); - float scaleY = (endPointElement[1].y - endPointElement[0].y) / (startPointElement[1].y - startPointElement[0].y); + float scaleX = + (endPointElement[1].x - endPointElement[0].x) + / (startPointElement[1].x - startPointElement[0].x); + float scaleY = + (endPointElement[1].y - endPointElement[0].y) + / (startPointElement[1].y - startPointElement[0].y); editorMatrix.postScale(scaleX, scaleY); @@ -78,18 +87,16 @@ final class ElementScaleEditSession extends ElementEditSession { * Find relative distance between an old and new set of Points. * * @param from Pair of points. - * @param to New pair of points. + * @param to New pair of points. * @return Scale */ private static double findScale(@NonNull PointF[] from, @NonNull PointF[] to) { float originalD2 = getDistanceSquared(from[0], from[1]); - float newD2 = getDistanceSquared(to[0], to[1]); + float newD2 = getDistanceSquared(to[0], to[1]); return Math.sqrt(newD2 / originalD2); } - /** - * Distance between two points squared. - */ + /** Distance between two points squared. */ private static float getDistanceSquared(@NonNull PointF a, @NonNull PointF b) { float dx = a.x - b.x; float dy = a.y - b.y; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java index 32a2fc51e..1b5ec70cd 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java @@ -10,33 +10,25 @@ import android.view.Gravity; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.FrameLayout; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; -/** - * Invisible {@link android.widget.EditText} that is used during in-image text editing. - */ +/** Invisible {@link android.widget.EditText} that is used during in-image text editing. */ final class HiddenEditText extends AppCompatEditText { @SuppressLint("InlinedApi") private static final int INCOGNITO_KEYBOARD_IME = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; - @Nullable - private EditorElement currentTextEditorElement; + @Nullable private EditorElement currentTextEditorElement; - @Nullable - private MultiLineTextRenderer currentTextEntity; + @Nullable private MultiLineTextRenderer currentTextEntity; - @Nullable - private Runnable onEndEdit; + @Nullable private Runnable onEndEdit; - @Nullable - private OnEditOrSelectionChange onEditOrSelectionChange; + @Nullable private OnEditOrSelectionChange onEditOrSelectionChange; public HiddenEditText(Context context) { super(context); @@ -87,21 +79,26 @@ final class HiddenEditText extends AppCompatEditText { } private void postEditOrSelectionChange() { - if (currentTextEditorElement != null && currentTextEntity != null && onEditOrSelectionChange != null) { + if (currentTextEditorElement != null + && currentTextEntity != null + && onEditOrSelectionChange != null) { onEditOrSelectionChange.onChange(currentTextEditorElement, currentTextEntity); } } - @Nullable MultiLineTextRenderer getCurrentTextEntity() { + @Nullable + MultiLineTextRenderer getCurrentTextEntity() { return currentTextEntity; } - @Nullable EditorElement getCurrentTextEditorElement() { + @Nullable + EditorElement getCurrentTextEditorElement() { return currentTextEditorElement; } public void setCurrentTextEditorElement(@Nullable EditorElement currentTextEditorElement) { - if (currentTextEditorElement != null && currentTextEditorElement.getRenderer() instanceof MultiLineTextRenderer) { + if (currentTextEditorElement != null + && currentTextEditorElement.getRenderer() instanceof MultiLineTextRenderer) { this.currentTextEditorElement = currentTextEditorElement; setCurrentTextEntity((MultiLineTextRenderer) currentTextEditorElement.getRenderer()); } else { @@ -143,10 +140,12 @@ final class HiddenEditText extends AppCompatEditText { if (currentTextEntity != null && focus) { currentTextEntity.setFocused(true); - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT); if (!imm.isAcceptingText()) { - imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY); + imm.toggleSoftInput( + InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY); } } @@ -154,24 +153,29 @@ final class HiddenEditText extends AppCompatEditText { } public void hideKeyboard() { - InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); } public void setIncognitoKeyboardEnabled(boolean incognitoKeyboardEnabled) { - setImeOptions(incognitoKeyboardEnabled ? getImeOptions() | INCOGNITO_KEYBOARD_IME - : getImeOptions() & ~INCOGNITO_KEYBOARD_IME); + setImeOptions( + incognitoKeyboardEnabled + ? getImeOptions() | INCOGNITO_KEYBOARD_IME + : getImeOptions() & ~INCOGNITO_KEYBOARD_IME); } public void setOnEndEdit(@Nullable Runnable onEndEdit) { this.onEndEdit = onEndEdit; } - public void setOnEditOrSelectionChange(@Nullable OnEditOrSelectionChange onEditOrSelectionChange) { + public void setOnEditOrSelectionChange( + @Nullable OnEditOrSelectionChange onEditOrSelectionChange) { this.onEditOrSelectionChange = onEditOrSelectionChange; } public interface OnEditOrSelectionChange { - void onChange(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer); + void onChange( + @NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer); } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java index 0eab881ee..6c2457375 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorMediaConstraints.java @@ -1,29 +1,28 @@ package org.thoughtcrime.securesms.imageeditor; import android.content.Context; - import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.util.Util; public class ImageEditorMediaConstraints extends MediaConstraints { - private static final int MAX_IMAGE_DIMEN_LOWMEM = 768; - private static final int MAX_IMAGE_DIMEN = 1536; - private static final int KB = 1024; - private static final int MB = 1024 * KB; + private static final int MAX_IMAGE_DIMEN_LOWMEM = 768; + private static final int MAX_IMAGE_DIMEN = 1536; + private static final int KB = 1024; + private static final int MB = 1024 * KB; - @Override - public int getImageMaxWidth(Context context) { - return Util.isLowMemory(context) ? MAX_IMAGE_DIMEN_LOWMEM : MAX_IMAGE_DIMEN; - } + @Override + public int getImageMaxWidth(Context context) { + return Util.isLowMemory(context) ? MAX_IMAGE_DIMEN_LOWMEM : MAX_IMAGE_DIMEN; + } - @Override - public int getImageMaxHeight(Context context) { - return getImageMaxWidth(context); - } + @Override + public int getImageMaxHeight(Context context) { + return getImageMaxWidth(context); + } - @Override - public int getImageMaxSize(Context context) { - return 4 * MB; - } + @Override + public int getImageMaxSize(Context context) { + return 4 * MB; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java index fcdc2f13a..af749c0c4 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java @@ -11,12 +11,10 @@ import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.widget.FrameLayout; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.GestureDetectorCompat; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.thoughtcrime.securesms.imageeditor.model.ThumbRenderer; @@ -25,56 +23,53 @@ import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; /** * ImageEditorView - *

- * Android {@link android.view.View} that allows manipulation of a base image, rotate/flip/crop and - * addition and manipulation of text/drawing/and other image layers that move with the base image. - *

- * Drawing - *

- * Drawing is achieved by setting the {@link #color} and putting the view in {@link Mode#Draw}. - * Touch events are then passed to a new {@link BezierDrawingRenderer} on a new {@link EditorElement}. - *

- * New images - *

- * To add new images to the base image add via the {@link EditorModel#addElementCentered(EditorElement, float)} - * which centers the new item in the current crop area. + * + *

Android {@link android.view.View} that allows manipulation of a base image, rotate/flip/crop + * and addition and manipulation of text/drawing/and other image layers that move with the base + * image. + * + *

Drawing + * + *

Drawing is achieved by setting the {@link #color} and putting the view in {@link Mode#Draw}. + * Touch events are then passed to a new {@link BezierDrawingRenderer} on a new {@link + * EditorElement}. + * + *

New images + * + *

To add new images to the base image add via the {@link + * EditorModel#addElementCentered(EditorElement, float)} which centers the new item in the current + * crop area. */ public final class ImageEditorView extends FrameLayout { private HiddenEditText editText; - @NonNull - private Mode mode = Mode.MoveAndResize; + @NonNull private Mode mode = Mode.MoveAndResize; - @ColorInt - private int color = 0xff000000; + @ColorInt private int color = 0xff000000; private float thickness = 0.02f; - @NonNull - private Paint.Cap cap = Paint.Cap.ROUND; + @NonNull private Paint.Cap cap = Paint.Cap.ROUND; private EditorModel model; private GestureDetectorCompat doubleTap; - @Nullable - private DrawingChangedListener drawingChangedListener; + @Nullable private DrawingChangedListener drawingChangedListener; - @Nullable - private UndoRedoStackListener undoRedoStackListener; + @Nullable private UndoRedoStackListener undoRedoStackListener; - private final Matrix viewMatrix = new Matrix(); - private final RectF viewPort = Bounds.newFullBounds(); - private final RectF visibleViewPort = Bounds.newFullBounds(); - private final RectF screen = new RectF(); + private final Matrix viewMatrix = new Matrix(); + private final RectF viewPort = Bounds.newFullBounds(); + private final RectF visibleViewPort = Bounds.newFullBounds(); + private final RectF screen = new RectF(); - private TapListener tapListener; + private TapListener tapListener; private RendererContext rendererContext; - @Nullable - private EditSession editSession; - private boolean moreThanOnePointerUsedInSession; + @Nullable private EditSession editSession; + private boolean moreThanOnePointerUsedInSession; public ImageEditorView(Context context) { super(context); @@ -111,7 +106,8 @@ public final class ImageEditorView extends FrameLayout { return editText; } - public void startTextEditing(@NonNull EditorElement editorElement, boolean incognitoKeyboardEnabled, boolean selectAll) { + public void startTextEditing( + @NonNull EditorElement editorElement, boolean incognitoKeyboardEnabled, boolean selectAll) { if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { editText.setIncognitoKeyboardEnabled(incognitoKeyboardEnabled); editText.setCurrentTextEditorElement(editorElement); @@ -122,8 +118,9 @@ public final class ImageEditorView extends FrameLayout { } } - private void zoomToFitText(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer) { - getModel().zoomToTextElement(editorElement, textRenderer); + private void zoomToFitText( + @NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer) { + getModel().zoomToTextElement(editorElement, textRenderer); } public boolean isTextEditing() { @@ -144,7 +141,8 @@ public final class ImageEditorView extends FrameLayout { @Override protected void onDraw(Canvas canvas) { if (rendererContext == null || rendererContext.canvas != canvas) { - rendererContext = new RendererContext(getContext(), canvas, rendererReady, rendererInvalidate); + rendererContext = + new RendererContext(getContext(), canvas, rendererReady, rendererInvalidate); } rendererContext.save(); try { @@ -156,13 +154,15 @@ public final class ImageEditorView extends FrameLayout { } } - private final RendererContext.Ready rendererReady = new RendererContext.Ready() { - @Override - public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { - model.onReady(renderer, cropMatrix, size); - invalidate(); - } - }; + private final RendererContext.Ready rendererReady = + new RendererContext.Ready() { + @Override + public void onReady( + @NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { + model.onReady(renderer, cropMatrix, size); + invalidate(); + } + }; private final RendererContext.Invalidate rendererInvalidate = renderer -> invalidate(); @@ -218,100 +218,110 @@ public final class ImageEditorView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: { - Matrix inverse = new Matrix(); - PointF point = getPoint(event); - EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse); + case MotionEvent.ACTION_DOWN: + { + Matrix inverse = new Matrix(); + PointF point = getPoint(event); + EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse); - moreThanOnePointerUsedInSession = false; - model.pushUndoPoint(); - editSession = startEdit(inverse, point, selected); + moreThanOnePointerUsedInSession = false; + model.pushUndoPoint(); + editSession = startEdit(inverse, point, selected); - if (tapListener != null && allowTaps()) { - if (editSession != null) { - tapListener.onEntityDown(editSession.getSelected()); - } else { - tapListener.onEntityDown(null); - } - } - - return true; - } - case MotionEvent.ACTION_MOVE: { - if (editSession != null) { - int historySize = event.getHistorySize(); - int pointerCount = Math.min(2, event.getPointerCount()); - - for (int h = 0; h < historySize; h++) { - for (int p = 0; p < pointerCount; p++) { - editSession.movePoint(p, getHistoricalPoint(event, p, h)); + if (tapListener != null && allowTaps()) { + if (editSession != null) { + tapListener.onEntityDown(editSession.getSelected()); + } else { + tapListener.onEntityDown(null); } } - for (int p = 0; p < pointerCount; p++) { - editSession.movePoint(p, getPoint(event, p)); - } - model.moving(editSession.getSelected()); - invalidate(); return true; } - break; - } - case MotionEvent.ACTION_POINTER_DOWN: { - if (editSession != null && event.getPointerCount() == 2) { - moreThanOnePointerUsedInSession = true; - editSession.commit(); - model.pushUndoPoint(); + case MotionEvent.ACTION_MOVE: + { + if (editSession != null) { + int historySize = event.getHistorySize(); + int pointerCount = Math.min(2, event.getPointerCount()); - Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); - if (newInverse != null) { - editSession = editSession.newPoint(newInverse, getPoint(event, event.getActionIndex()), event.getActionIndex()); - } else { - editSession = null; + for (int h = 0; h < historySize; h++) { + for (int p = 0; p < pointerCount; p++) { + editSession.movePoint(p, getHistoricalPoint(event, p, h)); + } + } + + for (int p = 0; p < pointerCount; p++) { + editSession.movePoint(p, getPoint(event, p)); + } + model.moving(editSession.getSelected()); + invalidate(); + return true; } - if (editSession == null) { + break; + } + case MotionEvent.ACTION_POINTER_DOWN: + { + if (editSession != null && event.getPointerCount() == 2) { + moreThanOnePointerUsedInSession = true; + editSession.commit(); + model.pushUndoPoint(); + + Matrix newInverse = + model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); + if (newInverse != null) { + editSession = + editSession.newPoint( + newInverse, getPoint(event, event.getActionIndex()), event.getActionIndex()); + } else { + editSession = null; + } + if (editSession == null) { + dragDropRelease(); + } + return true; + } + break; + } + case MotionEvent.ACTION_POINTER_UP: + { + if (editSession != null && event.getActionIndex() < 2) { + editSession.commit(); + model.pushUndoPoint(); + dragDropRelease(); + + Matrix newInverse = + model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); + if (newInverse != null) { + editSession = editSession.removePoint(newInverse, event.getActionIndex()); + } else { + editSession = null; + } + return true; + } + break; + } + case MotionEvent.ACTION_UP: + { + if (editSession != null) { + editSession.commit(); dragDropRelease(); - } - return true; - } - break; - } - case MotionEvent.ACTION_POINTER_UP: { - if (editSession != null && event.getActionIndex() < 2) { - editSession.commit(); - model.pushUndoPoint(); - dragDropRelease(); - Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix); - if (newInverse != null) { - editSession = editSession.removePoint(newInverse, event.getActionIndex()); - } else { editSession = null; + model.postEdit(moreThanOnePointerUsedInSession); + invalidate(); + return true; + } else { + model.postEdit(moreThanOnePointerUsedInSession); } - return true; + break; } - break; - } - case MotionEvent.ACTION_UP: { - if (editSession != null) { - editSession.commit(); - dragDropRelease(); - - editSession = null; - model.postEdit(moreThanOnePointerUsedInSession); - invalidate(); - return true; - } else { - model.postEdit(moreThanOnePointerUsedInSession); - } - break; - } } return super.onTouchEvent(event); } - private @Nullable EditSession startEdit(@NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { + private @Nullable EditSession startEdit( + @NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { if (mode == Mode.Draw || mode == Mode.Blur) { return startADrawingSession(point); } else { @@ -320,8 +330,11 @@ public final class ImageEditorView extends FrameLayout { } private EditSession startADrawingSession(@NonNull PointF point) { - BezierDrawingRenderer renderer = new BezierDrawingRenderer(color, thickness * Bounds.FULL_BOUNDS.width(), cap, model.findCropRelativeToRoot()); - EditorElement element = new EditorElement(renderer, mode == Mode.Blur ? EditorModel.Z_MASK : EditorModel.Z_DRAWING); + BezierDrawingRenderer renderer = + new BezierDrawingRenderer( + color, thickness * Bounds.FULL_BOUNDS.width(), cap, model.findCropRelativeToRoot()); + EditorElement element = + new EditorElement(renderer, mode == Mode.Blur ? EditorModel.Z_MASK : EditorModel.Z_DRAWING); model.addElementCentered(element, 1); Matrix elementInverseMatrix = model.findElementInverseMatrix(element, viewMatrix); @@ -329,7 +342,8 @@ public final class ImageEditorView extends FrameLayout { return DrawingSession.start(element, renderer, elementInverseMatrix, point); } - private EditSession startAMoveAndResizeSession(@NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { + private EditSession startAMoveAndResizeSession( + @NonNull Matrix inverse, @NonNull PointF point, @Nullable EditorElement selected) { Matrix elementInverseMatrix; if (selected == null) return null; @@ -342,7 +356,8 @@ public final class ImageEditorView extends FrameLayout { elementInverseMatrix = model.findElementInverseMatrix(selected, viewMatrix); if (elementInverseMatrix != null) { - return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumb.getControlPoint(), point); + return ThumbDragEditSession.startDrag( + selected, elementInverseMatrix, thumb.getControlPoint(), point); } else { return null; } @@ -357,7 +372,7 @@ public final class ImageEditorView extends FrameLayout { public void startDrawing(float thickness, @NonNull Paint.Cap cap, boolean blur) { this.thickness = thickness; - this.cap = cap; + this.cap = cap; setMode(blur ? Mode.Blur : Mode.Draw); } @@ -381,8 +396,8 @@ public final class ImageEditorView extends FrameLayout { } private static PointF getHistoricalPoint(MotionEvent event, int p, int historicalIndex) { - return new PointF(event.getHistoricalX(p, historicalIndex), - event.getHistoricalY(p, historicalIndex)); + return new PointF( + event.getHistoricalX(p, historicalIndex), event.getHistoricalY(p, historicalIndex)); } public EditorModel getModel() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java index 9b431e45f..ff50ab3ac 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/Renderer.java @@ -1,13 +1,14 @@ package org.thoughtcrime.securesms.imageeditor; import android.os.Parcelable; - import androidx.annotation.NonNull; /** - * Responsible for rendering a single {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement} to the canvas. - *

- * Because it knows the most about the whereabouts of the image it is also responsible for hit detection. + * Responsible for rendering a single {@link + * org.thoughtcrime.securesms.imageeditor.model.EditorElement} to the canvas. + * + *

Because it knows the most about the whereabouts of the image it is also responsible for hit + * detection. */ public interface Renderer extends Parcelable { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java index f814699f1..2f0347767 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/RendererContext.java @@ -6,38 +6,30 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.imageeditor.model.EditorElement; - import java.util.Collections; import java.util.List; +import org.thoughtcrime.securesms.imageeditor.model.EditorElement; /** * Contains all of the information required for a {@link Renderer} to do its job. - *

- * Includes a {@link #canvas}, preconfigured with the correct matrix. - *

- * The {@link #canvasMatrix} should further matrix manipulation be required. + * + *

Includes a {@link #canvas}, preconfigured with the correct matrix. + * + *

The {@link #canvasMatrix} should further matrix manipulation be required. */ public final class RendererContext { - @NonNull - public final Context context; + @NonNull public final Context context; - @NonNull - public final Canvas canvas; + @NonNull public final Canvas canvas; - @NonNull - public final CanvasMatrix canvasMatrix; + @NonNull public final CanvasMatrix canvasMatrix; - @NonNull - public final Ready rendererReady; + @NonNull public final Ready rendererReady; - @NonNull - public final Invalidate invalidate; + @NonNull public final Invalidate invalidate; private boolean blockingLoad; @@ -46,14 +38,18 @@ public final class RendererContext { private boolean isEditing = true; private List children = Collections.emptyList(); - private Paint maskPaint; + private Paint maskPaint; - public RendererContext(@NonNull Context context, @NonNull Canvas canvas, @NonNull Ready rendererReady, @NonNull Invalidate invalidate) { - this.context = context; - this.canvas = canvas; - this.canvasMatrix = new CanvasMatrix(canvas); + public RendererContext( + @NonNull Context context, + @NonNull Canvas canvas, + @NonNull Ready rendererReady, + @NonNull Invalidate invalidate) { + this.context = context; + this.canvas = canvas; + this.canvasMatrix = new CanvasMatrix(canvas); this.rendererReady = rendererReady; - this.invalidate = invalidate; + this.invalidate = invalidate; } public void setBlockingLoad(boolean blockingLoad) { @@ -61,12 +57,14 @@ public final class RendererContext { } /** - * {@link Renderer}s generally run in the foreground but can load any data they require in the background. - *

- * If they do so, they can use the {@link #invalidate} callback when ready to inform the view it needs to be redrawn. - *

- * However, when isBlockingLoad is true, the renderer is running in the background for the final render - * and must load the data immediately and block the render until done so. + * {@link Renderer}s generally run in the foreground but can load any data they require in the + * background. + * + *

If they do so, they can use the {@link #invalidate} callback when ready to inform the view + * it needs to be redrawn. + * + *

However, when isBlockingLoad is true, the renderer is running in the background for the + * final render and must load the data immediately and block the render until done so. */ public boolean isBlockingLoad() { return blockingLoad; @@ -99,9 +97,7 @@ public final class RendererContext { canvasMatrix.save(); } - /** - * Restore the current state from the stack, must match a call to {@link #save()}. - */ + /** Restore the current state from the stack, must match a call to {@link #save()}. */ public void restore() { canvasMatrix.restore(); } @@ -128,16 +124,14 @@ public final class RendererContext { public interface Ready { - Ready NULL = (renderer, cropMatrix, size) -> { - }; + Ready NULL = (renderer, cropMatrix, size) -> {}; void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size); } public interface Invalidate { - Invalidate NULL = (renderer) -> { - }; + Invalidate NULL = (renderer) -> {}; void onInvalidate(@NonNull Renderer renderer); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java index 860883e56..ed44dd04b 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/ThumbDragEditSession.java @@ -2,26 +2,31 @@ package org.thoughtcrime.securesms.imageeditor; import android.graphics.Matrix; import android.graphics.PointF; - import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.model.EditorElement; import org.thoughtcrime.securesms.imageeditor.model.ThumbRenderer; class ThumbDragEditSession extends ElementEditSession { - @NonNull - private final ThumbRenderer.ControlPoint controlPoint; + @NonNull private final ThumbRenderer.ControlPoint controlPoint; - private ThumbDragEditSession(@NonNull EditorElement selected, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull Matrix inverseMatrix) { + private ThumbDragEditSession( + @NonNull EditorElement selected, + @NonNull ThumbRenderer.ControlPoint controlPoint, + @NonNull Matrix inverseMatrix) { super(selected, inverseMatrix); this.controlPoint = controlPoint; } - static EditSession startDrag(@NonNull EditorElement selected, @NonNull Matrix inverseViewModelMatrix, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull PointF point) { + static EditSession startDrag( + @NonNull EditorElement selected, + @NonNull Matrix inverseViewModelMatrix, + @NonNull ThumbRenderer.ControlPoint controlPoint, + @NonNull PointF point) { if (!selected.getFlags().isEditable()) return null; - ElementEditSession elementDragEditSession = new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix); + ElementEditSession elementDragEditSession = + new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix); elementDragEditSession.setScreenStartPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point); return elementDragEditSession; @@ -48,13 +53,20 @@ class ThumbDragEditSession extends ElementEditSession { float defaultScale = aspectLocked ? 2 : 1; - float scaleX = controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); - float scaleY = controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); + float scaleX = + controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); + float scaleY = + controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); scale(editorMatrix, aspectLocked, scaleX, scaleY, controlPoint.opposite()); } - private void scale(Matrix editorMatrix, boolean aspectLocked, float scaleX, float scaleY, ThumbRenderer.ControlPoint around) { + private void scale( + Matrix editorMatrix, + boolean aspectLocked, + float scaleX, + float scaleY, + ThumbRenderer.ControlPoint around) { float x = around.getX(); float y = around.getY(); editorMatrix.postTranslate(-x, -y); @@ -76,4 +88,4 @@ class ThumbDragEditSession extends ElementEditSession { public EditSession removePoint(@NonNull Matrix newInverse, int p) { return null; } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java index 6aae9f88a..0bef9ac3f 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AlphaAnimation.java @@ -1,25 +1,25 @@ package org.thoughtcrime.securesms.imageeditor.model; import android.animation.ValueAnimator; -import androidx.annotation.Nullable; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; +import androidx.annotation.Nullable; final class AlphaAnimation { - private final static Interpolator interpolator = new LinearInterpolator(); + private static final Interpolator interpolator = new LinearInterpolator(); - final static AlphaAnimation NULL_1 = new AlphaAnimation(1); + static final AlphaAnimation NULL_1 = new AlphaAnimation(1); - private final float from; - private final float to; + private final float from; + private final float to; private final Runnable invalidate; - private final boolean canAnimate; - private float animatedFraction; + private final boolean canAnimate; + private float animatedFraction; private AlphaAnimation(float from, float to, @Nullable Runnable invalidate) { - this.from = from; - this.to = to; + this.from = from; + this.to = to; this.invalidate = invalidate; this.canAnimate = invalidate != null; } @@ -47,10 +47,11 @@ final class AlphaAnimation { ValueAnimator animator = ValueAnimator.ofFloat(from, to); animator.setDuration(200); animator.setInterpolator(interpolator); - animator.addUpdateListener(animation -> { - animatedFraction = (float) animation.getAnimatedValue(); - invalidate.run(); - }); + animator.addUpdateListener( + animation -> { + animatedFraction = (float) animation.getAnimatedValue(); + invalidate.run(); + }); animator.start(); } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java index 202821cc3..0cb454405 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/AnimationMatrix.java @@ -2,22 +2,19 @@ package org.thoughtcrime.securesms.imageeditor.model; import android.animation.ValueAnimator; import android.graphics.Matrix; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.view.animation.CycleInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; - +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.imageeditor.CanvasMatrix; -/** - * Animation Matrix provides a matrix that animates over time down to the identity matrix. - */ +/** Animation Matrix provides a matrix that animates over time down to the identity matrix. */ final class AnimationMatrix { - private final static float[] iValues = new float[9]; - private final static Interpolator interpolator = new DecelerateInterpolator(); - private final static Interpolator pulseInterpolator = inverse(new CycleInterpolator(0.5f)); + private static final float[] iValues = new float[9]; + private static final Interpolator interpolator = new DecelerateInterpolator(); + private static final Interpolator pulseInterpolator = inverse(new CycleInterpolator(0.5f)); static final AnimationMatrix NULL = new AnimationMatrix(); @@ -26,14 +23,14 @@ final class AnimationMatrix { } private final Runnable invalidate; - private final boolean canAnimate; - private final float[] undoValues = new float[9]; + private final boolean canAnimate; + private final float[] undoValues = new float[9]; - private final Matrix temp = new Matrix(); - private final float[] tempValues = new float[9]; + private final Matrix temp = new Matrix(); + private final float[] tempValues = new float[9]; private ValueAnimator animator; - private float animatedFraction; + private float animatedFraction; private AnimationMatrix(@NonNull Matrix undo, @NonNull Runnable invalidate) { this.invalidate = invalidate; @@ -46,7 +43,8 @@ final class AnimationMatrix { invalidate = null; } - static @NonNull AnimationMatrix animate(@NonNull Matrix from, @NonNull Matrix to, @Nullable Runnable invalidate) { + static @NonNull AnimationMatrix animate( + @NonNull Matrix from, @NonNull Matrix to, @Nullable Runnable invalidate) { if (invalidate == null) { return NULL; } @@ -65,10 +63,9 @@ final class AnimationMatrix { } } - /** - * Animate applying a matrix and then animate removing. - */ - static @NonNull AnimationMatrix singlePulse(@NonNull Matrix pulse, @Nullable Runnable invalidate) { + /** Animate applying a matrix and then animate removing. */ + static @NonNull AnimationMatrix singlePulse( + @NonNull Matrix pulse, @Nullable Runnable invalidate) { if (invalidate == null) { return NULL; } @@ -84,10 +81,11 @@ final class AnimationMatrix { animator = ValueAnimator.ofFloat(1, 0); animator.setDuration(250); animator.setInterpolator(interpolator); - animator.addUpdateListener(animation -> { - animatedFraction = (float) animation.getAnimatedValue(); - invalidate.run(); - }); + animator.addUpdateListener( + animation -> { + animatedFraction = (float) animation.getAnimatedValue(); + invalidate.run(); + }); animator.start(); } } @@ -97,18 +95,14 @@ final class AnimationMatrix { if (animator != null) animator.cancel(); } - /** - * Append the current animation value. - */ + /** Append the current animation value. */ void preConcatValueTo(@NonNull Matrix onTo) { if (!canAnimate) return; onTo.preConcat(buildTemp()); } - /** - * Append the current animation value. - */ + /** Append the current animation value. */ void preConcatValueTo(@NonNull CanvasMatrix canvasMatrix) { if (!canAnimate) return; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java index 35f3115b0..2cf05fc86 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/Bisect.java @@ -11,7 +11,7 @@ final class Bisect { private static final int MAX_ITERATIONS = 16; interface Predicate { - boolean test(); + boolean test(); } interface ModifyElement { @@ -19,95 +19,101 @@ final class Bisect { } /** - * Given a predicate function, attempts to finds the boundary between predicate true and predicate false. - * If it returns true, it will animate the element to the closest true value found to that boundary. + * Given a predicate function, attempts to finds the boundary between predicate true and predicate + * false. If it returns true, it will animate the element to the closest true value found to that + * boundary. * - * @param element The element to modify. - * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a translate. - * @param atMost A value believed to be in bounds. - * @param predicate The out of bounds predicate. - * @param modifyElement Apply the latest value to the element local matrix. - * @param invalidate For animation if finds a result. + * @param element The element to modify. + * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a + * translate. + * @param atMost A value believed to be in bounds. + * @param predicate The out of bounds predicate. + * @param modifyElement Apply the latest value to the element local matrix. + * @param invalidate For animation if finds a result. * @return true iff finds a result. */ - static boolean bisectToTest(@NonNull EditorElement element, - float outOfBoundsValue, - float atMost, - @NonNull Predicate predicate, - @NonNull ModifyElement modifyElement, - @NonNull Runnable invalidate) - { - Matrix closestSuccesful = bisectToTest(element, outOfBoundsValue, atMost, predicate, modifyElement); + static boolean bisectToTest( + @NonNull EditorElement element, + float outOfBoundsValue, + float atMost, + @NonNull Predicate predicate, + @NonNull ModifyElement modifyElement, + @NonNull Runnable invalidate) { + Matrix closestSuccesful = + bisectToTest(element, outOfBoundsValue, atMost, predicate, modifyElement); - if (closestSuccesful != null) { - element.animateLocalTo(closestSuccesful, invalidate); - return true; - } else { - return false; - } + if (closestSuccesful != null) { + element.animateLocalTo(closestSuccesful, invalidate); + return true; + } else { + return false; + } } /** - * Given a predicate function, attempts to finds the boundary between predicate true and predicate false. - * Returns new local matrix for the element if a solution is found. + * Given a predicate function, attempts to finds the boundary between predicate true and predicate + * false. Returns new local matrix for the element if a solution is found. * - * @param element The element to modify. - * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a translate. - * @param atMost A value believed to be in bounds. - * @param predicate The out of bounds predicate. - * @param modifyElement Apply the latest value to the element local matrix. + * @param element The element to modify. + * @param outOfBoundsValue The current value, known to be out of bounds. 1 for a scale and 0 for a + * translate. + * @param atMost A value believed to be in bounds. + * @param predicate The out of bounds predicate. + * @param modifyElement Apply the latest value to the element local matrix. * @return matrix to replace local matrix iff finds a result, null otherwise. */ - static @Nullable Matrix bisectToTest(@NonNull EditorElement element, - float outOfBoundsValue, - float atMost, - @NonNull Predicate predicate, - @NonNull ModifyElement modifyElement) - { - Matrix elementMatrix = element.getLocalMatrix(); - Matrix original = new Matrix(elementMatrix); - Matrix closestSuccessful = new Matrix(); - boolean haveResult = false; - int attempt = 0; - float successValue = 0; - float inBoundsValue = atMost; - float nextValueToTry = inBoundsValue; + static @Nullable Matrix bisectToTest( + @NonNull EditorElement element, + float outOfBoundsValue, + float atMost, + @NonNull Predicate predicate, + @NonNull ModifyElement modifyElement) { + Matrix elementMatrix = element.getLocalMatrix(); + Matrix original = new Matrix(elementMatrix); + Matrix closestSuccessful = new Matrix(); + boolean haveResult = false; + int attempt = 0; + float successValue = 0; + float inBoundsValue = atMost; + float nextValueToTry = inBoundsValue; - do { - attempt++; + do { + attempt++; - modifyElement.applyFactor(elementMatrix, nextValueToTry); - try { + modifyElement.applyFactor(elementMatrix, nextValueToTry); + try { - if (predicate.test()) { - inBoundsValue = nextValueToTry; + if (predicate.test()) { + inBoundsValue = nextValueToTry; - // if first success or closer to out of bounds than the current closest - if (!haveResult || Math.abs(nextValueToTry - outOfBoundsValue) < Math.abs(successValue - outOfBoundsValue)) { - haveResult = true; - successValue = nextValueToTry; - closestSuccessful.set(elementMatrix); - } - } else { - if (attempt == 1) { - // failure on first attempt means inBoundsValue is actually out of bounds and so no solution - return null; - } - outOfBoundsValue = nextValueToTry; - } - } finally { - // reset - elementMatrix.set(original); - } + // if first success or closer to out of bounds than the current closest + if (!haveResult + || Math.abs(nextValueToTry - outOfBoundsValue) + < Math.abs(successValue - outOfBoundsValue)) { + haveResult = true; + successValue = nextValueToTry; + closestSuccessful.set(elementMatrix); + } + } else { + if (attempt == 1) { + // failure on first attempt means inBoundsValue is actually out of bounds and so no + // solution + return null; + } + outOfBoundsValue = nextValueToTry; + } + } finally { + // reset + elementMatrix.set(original); + } - nextValueToTry = (inBoundsValue + outOfBoundsValue) / 2f; + nextValueToTry = (inBoundsValue + outOfBoundsValue) / 2f; - } while (attempt < MAX_ITERATIONS && Math.abs(inBoundsValue - outOfBoundsValue) > ACCURACY); - - if (haveResult) { - return closestSuccessful; - } - return null; - } + } while (attempt < MAX_ITERATIONS && Math.abs(inBoundsValue - outOfBoundsValue) > ACCURACY); + if (haveResult) { + return closestSuccessful; + } + return null; + } } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java index 34f3a8b0b..0f61ad903 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/CropThumbRenderer.java @@ -3,31 +3,29 @@ package org.thoughtcrime.securesms.imageeditor.model; import android.graphics.Matrix; import android.os.Parcel; import androidx.annotation.NonNull; - +import java.util.UUID; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import java.util.UUID; - /** * Hit tests a circle that is {@link R.dimen#crop_area_renderer_edge_size} in radius on the screen. - *

- * Does not draw anything. + * + *

Does not draw anything. */ class CropThumbRenderer implements Renderer, ThumbRenderer { private final ControlPoint controlPoint; - private final UUID toControl; + private final UUID toControl; - private final float[] centreOnScreen = new float[2]; - private final Matrix matrix = new Matrix(); - private int size; + private final float[] centreOnScreen = new float[2]; + private final Matrix matrix = new Matrix(); + private int size; CropThumbRenderer(@NonNull ControlPoint controlPoint, @NonNull UUID toControl) { this.controlPoint = controlPoint; - this.toControl = toControl; + this.toControl = toControl; } @Override @@ -44,13 +42,17 @@ class CropThumbRenderer implements Renderer, ThumbRenderer { public void render(@NonNull RendererContext rendererContext) { rendererContext.canvasMatrix.mapPoints(centreOnScreen, Bounds.CENTRE); rendererContext.canvasMatrix.copyTo(matrix); - size = rendererContext.context.getResources().getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size); + size = + rendererContext + .context + .getResources() + .getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size); } @Override public boolean hitTest(float x, float y) { float[] hitPointOnScreen = new float[2]; - matrix.mapPoints(hitPointOnScreen, new float[]{ x, y }); + matrix.mapPoints(hitPointOnScreen, new float[] {x, y}); float dx = centreOnScreen[0] - hitPointOnScreen[0]; float dy = centreOnScreen[1] - hitPointOnScreen[1]; @@ -63,17 +65,19 @@ class CropThumbRenderer implements Renderer, ThumbRenderer { return 0; } - public static final Creator CREATOR = new Creator() { - @Override - public CropThumbRenderer createFromParcel(Parcel in) { - return new CropThumbRenderer(ControlPoint.values()[in.readInt()], ParcelUtils.readUUID(in)); - } + public static final Creator CREATOR = + new Creator() { + @Override + public CropThumbRenderer createFromParcel(Parcel in) { + return new CropThumbRenderer( + ControlPoint.values()[in.readInt()], ParcelUtils.readUUID(in)); + } - @Override - public CropThumbRenderer[] newArray(int size) { - return new CropThumbRenderer[size]; - } - }; + @Override + public CropThumbRenderer[] newArray(int size) { + return new CropThumbRenderer[size]; + } + }; @Override public void writeToParcel(Parcel dest, int flags) { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java index 26d10d855..c8a0f5f59 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElement.java @@ -3,13 +3,8 @@ package org.thoughtcrime.securesms.imageeditor.model; import android.graphics.Matrix; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.imageeditor.Renderer; -import org.thoughtcrime.securesms.imageeditor.RendererContext; - import java.util.Collections; import java.util.Comparator; import java.util.Iterator; @@ -17,65 +12,62 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; +import org.thoughtcrime.securesms.imageeditor.Renderer; +import org.thoughtcrime.securesms.imageeditor.RendererContext; /** * An image consists of a tree of {@link EditorElement}s. - *

- * Each element has some persisted state: - * - An optional {@link Renderer} so that it can draw itself. - * - A list of child elements that make the tree possible. - * - Its own transformation matrix, which applies to itself and all its children. - * - A set of flags controlling visibility, selectablity etc. - *

- * Then some temporary state. - * - A editor matrix for displaying as yet uncommitted edits. - * - An animation matrix for animating from one matrix to another. - * - Deleted children to allow them to fade out on delete. - * - Temporary flags, for temporary visibility, selectablity etc. + * + *

Each element has some persisted state: - An optional {@link Renderer} so that it can draw + * itself. - A list of child elements that make the tree possible. - Its own transformation matrix, + * which applies to itself and all its children. - A set of flags controlling visibility, + * selectablity etc. + * + *

Then some temporary state. - A editor matrix for displaying as yet uncommitted edits. - An + * animation matrix for animating from one matrix to another. - Deleted children to allow them to + * fade out on delete. - Temporary flags, for temporary visibility, selectablity etc. */ public final class EditorElement implements Parcelable { - private static final Comparator Z_ORDER_COMPARATOR = (e1, e2) -> Integer.compare(e1.zOrder, e2.zOrder); + private static final Comparator Z_ORDER_COMPARATOR = + (e1, e2) -> Integer.compare(e1.zOrder, e2.zOrder); - private final UUID id; + private final UUID id; private final EditorFlags flags; - private final Matrix localMatrix = new Matrix(); - private final Matrix editorMatrix = new Matrix(); - private final int zOrder; + private final Matrix localMatrix = new Matrix(); + private final Matrix editorMatrix = new Matrix(); + private final int zOrder; - @Nullable - private final Renderer renderer; + @Nullable private final Renderer renderer; private final Matrix temp = new Matrix(); private final Matrix tempMatrix = new Matrix(); - private final List children = new LinkedList<>(); + private final List children = new LinkedList<>(); private final List deletedChildren = new LinkedList<>(); - @NonNull - private AnimationMatrix animationMatrix = AnimationMatrix.NULL; + @NonNull private AnimationMatrix animationMatrix = AnimationMatrix.NULL; - @NonNull - private AlphaAnimation alphaAnimation = AlphaAnimation.NULL_1; + @NonNull private AlphaAnimation alphaAnimation = AlphaAnimation.NULL_1; public EditorElement(@Nullable Renderer renderer) { this(renderer, 0); } public EditorElement(@Nullable Renderer renderer, int zOrder) { - this.id = UUID.randomUUID(); - this.flags = new EditorFlags(); + this.id = UUID.randomUUID(); + this.flags = new EditorFlags(); this.renderer = renderer; - this.zOrder = zOrder; + this.zOrder = zOrder; } private EditorElement(Parcel in) { - id = ParcelUtils.readUUID(in); - flags = new EditorFlags(in.readInt()); + id = ParcelUtils.readUUID(in); + flags = new EditorFlags(in.readInt()); ParcelUtils.readMatrix(localMatrix, in); renderer = in.readParcelable(Renderer.class.getClassLoader()); - zOrder = in.readInt(); + zOrder = in.readInt(); in.readTypedList(children, EditorElement.CREATOR); } @@ -88,12 +80,12 @@ public final class EditorElement implements Parcelable { } /** - * Iff Visible, - * Renders tree with the following localMatrix: - *

- * viewModelMatrix * localMatrix * editorMatrix * animationMatrix - *

- * Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * localMatrix * editorMatrix * animationMatrix + * Iff Visible, Renders tree with the following localMatrix: + * + *

viewModelMatrix * localMatrix * editorMatrix * animationMatrix + * + *

Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * localMatrix * + * editorMatrix * animationMatrix * * @param rendererContext Canvas to draw on to. */ @@ -132,7 +124,8 @@ public final class EditorElement implements Parcelable { renderer.render(rendererContext); } - private static void drawChildren(@NonNull List children, @NonNull RendererContext rendererContext) { + private static void drawChildren( + @NonNull List children, @NonNull RendererContext rendererContext) { for (EditorElement element : children) { if (element.zOrder >= 0) { element.draw(rendererContext); @@ -153,23 +146,34 @@ public final class EditorElement implements Parcelable { return editorMatrix; } - EditorElement findElement(@NonNull EditorElement toFind, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { - return findElement(viewMatrix, outInverseModelMatrix, (element, inverseMatrix) -> toFind == element); + EditorElement findElement( + @NonNull EditorElement toFind, + @NonNull Matrix viewMatrix, + @NonNull Matrix outInverseModelMatrix) { + return findElement( + viewMatrix, outInverseModelMatrix, (element, inverseMatrix) -> toFind == element); } - EditorElement findElementAt(float x, float y, @NonNull Matrix viewModelMatrix, @NonNull Matrix outInverseModelMatrix) { + EditorElement findElementAt( + float x, float y, @NonNull Matrix viewModelMatrix, @NonNull Matrix outInverseModelMatrix) { final float[] dst = new float[2]; - final float[] src = { x, y }; + final float[] src = {x, y}; - return findElement(viewModelMatrix, outInverseModelMatrix, (element, inverseMatrix) -> { - Renderer renderer = element.renderer; - if (renderer == null) return false; - inverseMatrix.mapPoints(dst, src); - return element.flags.isSelectable() && renderer.hitTest(dst[0], dst[1]); - }); + return findElement( + viewModelMatrix, + outInverseModelMatrix, + (element, inverseMatrix) -> { + Renderer renderer = element.renderer; + if (renderer == null) return false; + inverseMatrix.mapPoints(dst, src); + return element.flags.isSelectable() && renderer.hitTest(dst[0], dst[1]); + }); } - public EditorElement findElement(@NonNull Matrix viewModelMatrix, @NonNull Matrix outInverseModelMatrix, @NonNull FindElementPredicate predicate) { + public EditorElement findElement( + @NonNull Matrix viewModelMatrix, + @NonNull Matrix outInverseModelMatrix, + @NonNull FindElementPredicate predicate) { temp.set(viewModelMatrix); temp.preConcat(localMatrix); @@ -178,7 +182,8 @@ public final class EditorElement implements Parcelable { if (temp.invert(tempMatrix)) { for (int i = children.size() - 1; i >= 0; i--) { - EditorElement elementAt = children.get(i).findElement(temp, outInverseModelMatrix, predicate); + EditorElement elementAt = + children.get(i).findElement(temp, outInverseModelMatrix, predicate); if (elementAt != null) { return elementAt; } @@ -235,7 +240,8 @@ public final class EditorElement implements Parcelable { alphaAnimation = AlphaAnimation.animate(0, 1, invalidate); } - @Nullable EditorElement parentOf(@NonNull EditorElement element) { + @Nullable + EditorElement parentOf(@NonNull EditorElement element) { if (children.contains(element)) { return this; } @@ -304,10 +310,11 @@ public final class EditorElement implements Parcelable { /** * @param destination Matrix to change - * @param source Matrix value to set - * @param invalidate Callback to allow animation + * @param source Matrix value to set + * @param invalidate Callback to allow animation */ - private void setMatrixWithAnimation(@NonNull Matrix destination, @NonNull Matrix source, @Nullable Runnable invalidate) { + private void setMatrixWithAnimation( + @NonNull Matrix destination, @NonNull Matrix source, @Nullable Runnable invalidate) { Matrix old = new Matrix(destination); animationMatrix.stop(); animationMatrix.preConcatValueTo(old); @@ -325,17 +332,18 @@ public final class EditorElement implements Parcelable { animationMatrix.stop(); } - public static final Creator CREATOR = new Creator() { - @Override - public EditorElement createFromParcel(Parcel in) { - return new EditorElement(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public EditorElement createFromParcel(Parcel in) { + return new EditorElement(in); + } - @Override - public EditorElement[] newArray(int size) { - return new EditorElement[size]; - } - }; + @Override + public EditorElement[] newArray(int size) { + return new EditorElement[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java index cab52e3f4..97fe4f328 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorElementHierarchy.java @@ -4,10 +4,8 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.RectF; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.renderers.CropAreaRenderer; @@ -16,31 +14,17 @@ import org.thoughtcrime.securesms.imageeditor.renderers.OvalGuideRenderer; /** * Creates and handles a strict EditorElement Hierarchy. - *

- * root - always square, contains only temporary zooms for editing. e.g. when the whole editor zooms out for cropping - * | - * |- view - contains persisted adjustments for crops - * | | - * | |- flipRotate - contains persisted adjustments for flip and rotate operations, ensures operations are centered within the current view - * | | - * | |- imageRoot - * | | |- mainImage - * | | |- stickers/drawings/text - * | | - * | |- overlay - always square - * | | |- imageCrop - a crop to match the aspect of the main image - * | | | |- cropEditorElement - user crop, not always square, but upright, the area of the view - * | | | | | All children do not move/scale or rotate. - * | | | | |- blackout - * | | | | |- thumbs - * | | | | | |- Center left thumb - * | | | | | |- Center right thumb - * | | | | | |- Top center thumb - * | | | | | |- Bottom center thumb - * | | | | | |- Top left thumb - * | | | | | |- Top right thumb - * | | | | | |- Bottom left thumb - * | | | | | |- Bottom right thumb + * + *

root - always square, contains only temporary zooms for editing. e.g. when the whole editor + * zooms out for cropping | |- view - contains persisted adjustments for crops | | | |- flipRotate - + * contains persisted adjustments for flip and rotate operations, ensures operations are centered + * within the current view | | | |- imageRoot | | |- mainImage | | |- stickers/drawings/text | | | + * |- overlay - always square | | |- imageCrop - a crop to match the aspect of the main image | | | + * |- cropEditorElement - user crop, not always square, but upright, the area of the view | | | | | + * All children do not move/scale or rotate. | | | | |- blackout | | | | |- thumbs | | | | | |- + * Center left thumb | | | | | |- Center right thumb | | | | | |- Top center thumb | | | | | |- + * Bottom center thumb | | | | | |- Top left thumb | | | | | |- Top right thumb | | | | | |- Bottom + * left thumb | | | | | |- Bottom right thumb */ final class EditorElementHierarchy { @@ -67,15 +51,15 @@ final class EditorElementHierarchy { private final EditorElement thumbs; private EditorElementHierarchy(@NonNull EditorElement root) { - this.root = root; - this.view = this.root.getChild(0); - this.flipRotate = this.view.getChild(0); - this.imageRoot = this.flipRotate.getChild(0); - this.overlay = this.flipRotate.getChild(1); - this.imageCrop = this.overlay.getChild(0); + this.root = root; + this.view = this.root.getChild(0); + this.flipRotate = this.view.getChild(0); + this.imageRoot = this.flipRotate.getChild(0); + this.overlay = this.flipRotate.getChild(1); + this.imageCrop = this.overlay.getChild(0); this.cropEditorElement = this.imageCrop.getChild(0); - this.blackout = this.cropEditorElement.getChild(0); - this.thumbs = this.cropEditorElement.getChild(1); + this.blackout = this.cropEditorElement.getChild(0); + this.thumbs = this.cropEditorElement.getChild(1); } private static @NonNull EditorElement createRoot(boolean circleEdit) { @@ -96,32 +80,32 @@ final class EditorElementHierarchy { EditorElement imageCrop = new EditorElement(null); overlay.addElement(imageCrop); - EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit)); + EditorElement cropEditorElement = + new EditorElement( + new CropAreaRenderer(R.color.crop_area_renderer_outer_color, !circleEdit)); - cropEditorElement.getFlags() - .setRotateLocked(true) - .setAspectLocked(true) - .setSelectable(false) - .setVisible(false) - .persist(); + cropEditorElement + .getFlags() + .setRotateLocked(true) + .setAspectLocked(true) + .setSelectable(false) + .setVisible(false) + .persist(); imageCrop.addElement(cropEditorElement); EditorElement blackout = new EditorElement(new InverseFillRenderer(0xff000000)); - blackout.getFlags() - .setSelectable(false) - .setEditable(false) - .persist(); + blackout.getFlags().setSelectable(false).setEditable(false).persist(); cropEditorElement.addElement(blackout); cropEditorElement.addElement(createThumbs(cropEditorElement, !circleEdit)); if (circleEdit) { - EditorElement circle = new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color)); - circle.getFlags().setSelectable(false) - .persist(); + EditorElement circle = + new EditorElement(new OvalGuideRenderer(R.color.crop_circle_guide_color)); + circle.getFlags().setSelectable(false).persist(); cropEditorElement.addElement(circle); } @@ -129,14 +113,11 @@ final class EditorElementHierarchy { return root; } - private static @NonNull EditorElement createThumbs(EditorElement cropEditorElement, boolean centerThumbs) { + private static @NonNull EditorElement createThumbs( + EditorElement cropEditorElement, boolean centerThumbs) { EditorElement thumbs = new EditorElement(null); - thumbs.getFlags() - .setChildrenVisible(false) - .setSelectable(false) - .setVisible(false) - .persist(); + thumbs.getFlags().setChildrenVisible(false).setSelectable(false).setVisible(false).persist(); if (centerThumbs) { thumbs.addElement(newThumb(cropEditorElement, ThumbRenderer.ControlPoint.CENTER_LEFT)); @@ -154,12 +135,12 @@ final class EditorElementHierarchy { return thumbs; } - private static @NonNull EditorElement newThumb(@NonNull EditorElement toControl, @NonNull ThumbRenderer.ControlPoint controlPoint) { - EditorElement element = new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId())); + private static @NonNull EditorElement newThumb( + @NonNull EditorElement toControl, @NonNull ThumbRenderer.ControlPoint controlPoint) { + EditorElement element = + new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId())); - element.getFlags() - .setSelectable(false) - .persist(); + element.getFlags().setSelectable(false).persist(); element.getLocalMatrix().preTranslate(controlPoint.getX(), controlPoint.getY()); @@ -174,10 +155,9 @@ final class EditorElementHierarchy { return imageRoot; } - /** - * The main image, null if not yet set. - */ - @Nullable EditorElement getMainImage() { + /** The main image, null if not yet set. */ + @Nullable + EditorElement getMainImage() { return imageRoot.getChildCount() > 0 ? imageRoot.getChild(0) : null; } @@ -198,20 +178,17 @@ final class EditorElementHierarchy { } void startCrop(@NonNull Runnable invalidate) { - Matrix editor = new Matrix(); - float scaleInForCrop = 0.8f; + Matrix editor = new Matrix(); + float scaleInForCrop = 0.8f; editor.postScale(scaleInForCrop, scaleInForCrop); root.animateEditorTo(editor, invalidate); - cropEditorElement.getFlags() - .setVisible(true); + cropEditorElement.getFlags().setVisible(true); - blackout.getFlags() - .setVisible(false); + blackout.getFlags().setVisible(false); - thumbs.getFlags() - .setChildrenVisible(true); + thumbs.getFlags().setChildrenVisible(true); thumbs.forAllInTree(element -> element.getFlags().setSelectable(true)); @@ -252,10 +229,11 @@ final class EditorElementHierarchy { /** * Returns a matrix that maps points from the crop on to the visible image. - *

- * i.e. if a mapped point is in bounds, then the point is on the visible image. + * + *

i.e. if a mapped point is in bounds, then the point is on the visible image. */ - @Nullable Matrix imageMatrixRelativeToCrop() { + @Nullable + Matrix imageMatrixRelativeToCrop() { EditorElement mainImage = getMainImage(); if (mainImage == null) return null; @@ -286,7 +264,12 @@ final class EditorElementHierarchy { return dst; } - void flipRotate(int degrees, int scaleX, int scaleY, @NonNull RectF visibleViewPort, @Nullable Runnable invalidate) { + void flipRotate( + int degrees, + int scaleX, + int scaleY, + @NonNull RectF visibleViewPort, + @Nullable Runnable invalidate) { Matrix newLocal = new Matrix(flipRotate.getLocalMatrix()); if (degrees != 0) { newLocal.postRotate(degrees); @@ -296,9 +279,7 @@ final class EditorElementHierarchy { updateViewToCrop(visibleViewPort, invalidate); } - /** - * The full matrix for the {@link #getMainImage()} from {@link #root} down. - */ + /** The full matrix for the {@link #getMainImage()} from {@link #root} down. */ Matrix getMainImageFullMatrix() { Matrix matrix = new Matrix(); @@ -308,9 +289,7 @@ final class EditorElementHierarchy { return matrix; } - /** - * The full matrix for the {@link #getMainImage()} from {@link #flipRotate} down. - */ + /** The full matrix for the {@link #getMainImage()} from {@link #flipRotate} down. */ Matrix getMainImageFullMatrixFromFlipRotate() { Matrix matrix = new Matrix(); @@ -339,22 +318,21 @@ final class EditorElementHierarchy { matrix.preConcat(cropEditorElement.getEditorMatrix()); EditorElement mainImage = getMainImage(); if (mainImage != null) { - float xScale = 1f / (xScale(mainImage.getLocalMatrix()) * xScale(mainImage.getEditorMatrix())); + float xScale = + 1f / (xScale(mainImage.getLocalMatrix()) * xScale(mainImage.getEditorMatrix())); matrix.preScale(xScale, xScale); } float[] dst = new float[4]; - matrix.mapPoints(dst, new float[]{ 0, 0, inputSize.x, inputSize.y }); + matrix.mapPoints(dst, new float[] {0, 0, inputSize.x, inputSize.y}); - float widthF = Math.abs(dst[0] - dst[2]); + float widthF = Math.abs(dst[0] - dst[2]); float heightF = Math.abs(dst[1] - dst[3]); return new PointF(widthF, heightF); } - /** - * Extract the x scale from a matrix, which is the length of the first column. - */ + /** Extract the x scale from a matrix, which is the length of the first column. */ static float xScale(@NonNull Matrix matrix) { float[] values = new float[9]; matrix.getValues(values); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java index 77e4dfedf..ad59a714c 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java @@ -4,19 +4,20 @@ import androidx.annotation.NonNull; /** * Flags for an {@link EditorElement}. - *

- * Values you set are not persisted unless you call {@link #persist()}. - *

- * This allows temporary state for editing and an easy way to revert to the persisted state via {@link #reset()}. + * + *

Values you set are not persisted unless you call {@link #persist()}. + * + *

This allows temporary state for editing and an easy way to revert to the persisted state via + * {@link #reset()}. */ public final class EditorFlags { - private static final int ASPECT_LOCK = 1; - private static final int ROTATE_LOCK = 2; - private static final int SELECTABLE = 4; - private static final int VISIBLE = 8; + private static final int ASPECT_LOCK = 1; + private static final int ROTATE_LOCK = 2; + private static final int SELECTABLE = 4; + private static final int VISIBLE = 8; private static final int CHILDREN_VISIBLE = 16; - private static final int EDITABLE = 32; + private static final int EDITABLE = 32; private int flags; private int markedFlags; diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java index f9dc113d7..862d6da92 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java @@ -9,11 +9,14 @@ import android.graphics.PointF; import android.graphics.RectF; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; - +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.Renderer; @@ -21,46 +24,38 @@ import org.thoughtcrime.securesms.imageeditor.RendererContext; import org.thoughtcrime.securesms.imageeditor.UndoRedoStackListener; import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - /** * Contains a reference to the root {@link EditorElement}, maintains undo and redo stacks and has a * reference to the {@link EditorElementHierarchy}. - *

- * As such it is the entry point for all operations that change the image. + * + *

As such it is the entry point for all operations that change the image. */ public final class EditorModel implements Parcelable, RendererContext.Ready { - public static final int Z_MASK = -1; - public static final int Z_DRAWING = 0; + public static final int Z_MASK = -1; + public static final int Z_DRAWING = 0; public static final int Z_STICKERS = 0; - public static final int Z_TEXT = 1; + public static final int Z_TEXT = 1; - private static final Runnable NULL_RUNNABLE = () -> { - }; + private static final Runnable NULL_RUNNABLE = () -> {}; private static final int MINIMUM_OUTPUT_WIDTH = 1024; - private static final int MINIMUM_CROP_PIXEL_COUNT = 100; - private static final Point MINIMUM_RATIO = new Point(15, 1); + private static final int MINIMUM_CROP_PIXEL_COUNT = 100; + private static final Point MINIMUM_RATIO = new Point(15, 1); - @NonNull - private Runnable invalidate = NULL_RUNNABLE; + @NonNull private Runnable invalidate = NULL_RUNNABLE; private UndoRedoStackListener undoRedoStackListener; private final UndoRedoStacks undoRedoStacks; private final UndoRedoStacks cropUndoRedoStacks; - private final InBoundsMemory inBoundsMemory = new InBoundsMemory(); + private final InBoundsMemory inBoundsMemory = new InBoundsMemory(); private EditorElementHierarchy editorElementHierarchy; - private final RectF visibleViewPort = new RectF(); - private final Point size; + private final RectF visibleViewPort = new RectF(); + private final Point size; private final boolean circleEditing; public EditorModel() { @@ -68,21 +63,22 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } private EditorModel(@NonNull Parcel in) { - ClassLoader classLoader = getClass().getClassLoader(); - this.circleEditing = in.readByte() == 1; - this.size = new Point(in.readInt(), in.readInt()); + ClassLoader classLoader = getClass().getClassLoader(); + this.circleEditing = in.readByte() == 1; + this.size = new Point(in.readInt(), in.readInt()); //noinspection ConstantConditions this.editorElementHierarchy = EditorElementHierarchy.create(in.readParcelable(classLoader)); - this.undoRedoStacks = in.readParcelable(classLoader); - this.cropUndoRedoStacks = in.readParcelable(classLoader); + this.undoRedoStacks = in.readParcelable(classLoader); + this.cropUndoRedoStacks = in.readParcelable(classLoader); } - public EditorModel(boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) { - this.circleEditing = circleEditing; - this.size = new Point(1024, 1024); + public EditorModel( + boolean circleEditing, @NonNull EditorElementHierarchy editorElementHierarchy) { + this.circleEditing = circleEditing; + this.size = new Point(1024, 1024); this.editorElementHierarchy = editorElementHierarchy; - this.undoRedoStacks = new UndoRedoStacks(50); - this.cropUndoRedoStacks = new UndoRedoStacks(50); + this.undoRedoStacks = new UndoRedoStacks(50); + this.cropUndoRedoStacks = new UndoRedoStacks(50); } public static EditorModel create() { @@ -90,7 +86,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } public static EditorModel createForCircleEditing() { - EditorModel editorModel = new EditorModel(true, EditorElementHierarchy.createForCircleEditing()); + EditorModel editorModel = + new EditorModel(true, EditorElementHierarchy.createForCircleEditing()); editorModel.setCropAspectLock(true); return editorModel; } @@ -107,13 +104,13 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { /** * Renders tree with the following matrix: - *

- * viewModelMatrix * matrix * editorMatrix - *

- * Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * matrix * editorMatrix + * + *

viewModelMatrix * matrix * editorMatrix + * + *

Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * matrix * editorMatrix * * @param rendererContext Canvas to draw on to. - * @param renderOnTop This element will appear on top of the overlay. + * @param renderOnTop This element will appear on top of the overlay. */ public void draw(@NonNull RendererContext rendererContext, @Nullable EditorElement renderOnTop) { EditorElement root = editorElementHierarchy.getRoot(); @@ -139,7 +136,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } } - public @Nullable Matrix findElementInverseMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) { + public @Nullable Matrix findElementInverseMatrix( + @NonNull EditorElement element, @NonNull Matrix viewMatrix) { Matrix inverse = new Matrix(); if (findElement(element, viewMatrix, inverse)) { return inverse; @@ -147,7 +145,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { return null; } - private @Nullable Matrix findElementMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) { + private @Nullable Matrix findElementMatrix( + @NonNull EditorElement element, @NonNull Matrix viewMatrix) { Matrix inverse = findElementInverseMatrix(element, viewMatrix); if (inverse != null) { Matrix regular = new Matrix(); @@ -157,12 +156,19 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { return null; } - public EditorElement findElementAtPoint(@NonNull PointF point, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { - return editorElementHierarchy.getRoot().findElementAt(point.x, point.y, viewMatrix, outInverseModelMatrix); + public EditorElement findElementAtPoint( + @NonNull PointF point, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { + return editorElementHierarchy + .getRoot() + .findElementAt(point.x, point.y, viewMatrix, outInverseModelMatrix); } - private boolean findElement(@NonNull EditorElement element, @NonNull Matrix viewMatrix, @NonNull Matrix outInverseModelMatrix) { - return editorElementHierarchy.getRoot().findElement(element, viewMatrix, outInverseModelMatrix) == element; + private boolean findElement( + @NonNull EditorElement element, + @NonNull Matrix viewMatrix, + @NonNull Matrix outInverseModelMatrix) { + return editorElementHierarchy.getRoot().findElement(element, viewMatrix, outInverseModelMatrix) + == element; } public void pushUndoPoint() { @@ -175,8 +181,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } public void undo() { - boolean cropping = isCropping(); - UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); + boolean cropping = isCropping(); + UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); undoRedo(stacks.getUndoStack(), stacks.getRedoStack(), cropping); @@ -184,35 +190,42 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } public void redo() { - boolean cropping = isCropping(); - UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); + boolean cropping = isCropping(); + UndoRedoStacks stacks = getActiveUndoRedoStacks(cropping); undoRedo(stacks.getRedoStack(), stacks.getUndoStack(), cropping); updateUndoRedoAvailableState(stacks); } - private void undoRedo(@NonNull ElementStack fromStack, @NonNull ElementStack toStack, boolean keepEditorState) { + private void undoRedo( + @NonNull ElementStack fromStack, @NonNull ElementStack toStack, boolean keepEditorState) { final EditorElement oldRootElement = editorElementHierarchy.getRoot(); - final EditorElement popped = fromStack.pop(oldRootElement); + final EditorElement popped = fromStack.pop(oldRootElement); if (popped != null) { editorElementHierarchy = EditorElementHierarchy.create(popped); toStack.tryPush(oldRootElement); - restoreStateWithAnimations(oldRootElement, editorElementHierarchy.getRoot(), invalidate, keepEditorState); + restoreStateWithAnimations( + oldRootElement, editorElementHierarchy.getRoot(), invalidate, keepEditorState); invalidate.run(); // re-zoom image root as the view port might be different now editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate); - inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); + inBoundsMemory.push( + editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); } } - private static void restoreStateWithAnimations(@NonNull EditorElement fromRootElement, @NonNull EditorElement toRootElement, @NonNull Runnable onInvalidate, boolean keepEditorState) { + private static void restoreStateWithAnimations( + @NonNull EditorElement fromRootElement, + @NonNull EditorElement toRootElement, + @NonNull Runnable onInvalidate, + boolean keepEditorState) { Map fromMap = getElementMap(fromRootElement); - Map toMap = getElementMap(toRootElement); + Map toMap = getElementMap(toRootElement); for (EditorElement fromElement : fromMap.values()) { fromElement.stopAnimation(); @@ -249,7 +262,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { EditorElement root = editorElementHierarchy.getRoot(); - undoRedoStackListener.onAvailabilityChanged(currentStack.canUndo(root), currentStack.canRedo(root)); + undoRedoStackListener.onAvailabilityChanged( + currentStack.canUndo(root), currentStack.canRedo(root)); } private static Map getElementMap(@NonNull EditorElement element) { @@ -262,7 +276,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { pushUndoPoint(); cropUndoRedoStacks.clear(editorElementHierarchy.getRoot()); editorElementHierarchy.startCrop(invalidate); - inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); + inBoundsMemory.push( + editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement()); updateUndoRedoAvailableState(cropUndoRedoStacks); } @@ -273,11 +288,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { public void setCropAspectLock(boolean locked) { EditorFlags flags = editorElementHierarchy.getCropEditorElement().getFlags(); - int currentState = flags.setAspectLocked(locked).getCurrentState(); + int currentState = flags.setAspectLocked(locked).getCurrentState(); flags.reset(); - flags.setAspectLocked(locked) - .persist(); + flags.setAspectLocked(locked).persist(); flags.restoreState(currentState); } @@ -329,51 +343,62 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { /** * Attempts to scale the supplied element such that {@link #cropIsWithinMainImageBounds} is true. - *

- * Does not respect minimum scale, so does need a further check to {@link #currentCropIsAcceptable} afterwards. * - * @param element The element to be scaled. If successful, it will be animated to the correct position. - * @param scaleAtMost The amount of scale to apply at most. Use < 1 for the crop, and > 1 for the image. + *

Does not respect minimum scale, so does need a further check to {@link + * #currentCropIsAcceptable} afterwards. + * + * @param element The element to be scaled. If successful, it will be animated to the correct + * position. + * @param scaleAtMost The amount of scale to apply at most. Use < 1 for the crop, and > 1 for the + * image. * @return true if successfully scaled the element. false if the element was left unchanged. */ private boolean tryToScaleToFit(@NonNull EditorElement element, float scaleAtMost) { - return Bisect.bisectToTest(element, - 1, - scaleAtMost, - this::cropIsWithinMainImageBounds, - (matrix, scale) -> matrix.preScale(scale, scale), - invalidate); + return Bisect.bisectToTest( + element, + 1, + scaleAtMost, + this::cropIsWithinMainImageBounds, + (matrix, scale) -> matrix.preScale(scale, scale), + invalidate); } /** - * Attempts to translate the supplied element such that {@link #cropIsWithinMainImageBounds} is true. - * If you supply both x and y, it will attempt to find a fit on the diagonal with vector x, y. + * Attempts to translate the supplied element such that {@link #cropIsWithinMainImageBounds} is + * true. If you supply both x and y, it will attempt to find a fit on the diagonal with vector x, + * y. * - * @param element The element to be translated. If successful, it will be animated to the correct position. + * @param element The element to be translated. If successful, it will be animated to the correct + * position. * @param translateXAtMost The maximum translation to apply in the x axis. * @param translateYAtMost The maximum translation to apply in the y axis. - * @return a matrix if successfully translated the element. null if the element unable to be translated to fit. + * @return a matrix if successfully translated the element. null if the element unable to be + * translated to fit. */ - private Matrix tryToTranslateToFit(@NonNull EditorElement element, float translateXAtMost, float translateYAtMost) { - return Bisect.bisectToTest(element, - 0, - 1, - this::cropIsWithinMainImageBounds, - (matrix, factor) -> matrix.postTranslate(factor * translateXAtMost, factor * translateYAtMost)); + private Matrix tryToTranslateToFit( + @NonNull EditorElement element, float translateXAtMost, float translateYAtMost) { + return Bisect.bisectToTest( + element, + 0, + 1, + this::cropIsWithinMainImageBounds, + (matrix, factor) -> + matrix.postTranslate(factor * translateXAtMost, factor * translateYAtMost)); } /** * Tries to fix an element that is out of bounds by adjusting it's translation. * - * @param element Element to move. + * @param element Element to move. * @param lastKnownGoodPosition Last known good position of element. * @return true iff fixed the element. */ - private boolean tryToFixTranslationOutOfBounds(@NonNull EditorElement element, @NonNull Matrix lastKnownGoodPosition) { - final Matrix elementMatrix = element.getLocalMatrix(); - final Matrix original = new Matrix(elementMatrix); - final float[] current = new float[9]; - final float[] lastGood = new float[9]; + private boolean tryToFixTranslationOutOfBounds( + @NonNull EditorElement element, @NonNull Matrix lastKnownGoodPosition) { + final Matrix elementMatrix = element.getLocalMatrix(); + final Matrix original = new Matrix(elementMatrix); + final float[] current = new float[9]; + final float[] lastGood = new float[9]; Matrix matrix; elementMatrix.getValues(current); @@ -442,13 +467,13 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } /** - * Pixel count must be no smaller than {@link #MINIMUM_CROP_PIXEL_COUNT} (unless its original size was less than that) - * and all points must be within the bounds. + * Pixel count must be no smaller than {@link #MINIMUM_CROP_PIXEL_COUNT} (unless its original size + * was less than that) and all points must be within the bounds. */ private boolean currentCropIsAcceptable() { - Point outputSize = getOutputSize(); - int outputPixelCount = outputSize.x * outputSize.y; - int minimumPixelCount = Math.min(size.x * size.y, MINIMUM_CROP_PIXEL_COUNT); + Point outputSize = getOutputSize(); + int outputPixelCount = outputSize.x * outputSize.y; + int minimumPixelCount = Math.min(size.x * size.y, MINIMUM_CROP_PIXEL_COUNT); Point thinnestRatio = MINIMUM_RATIO; @@ -457,20 +482,19 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { thinnestRatio = size; } - return compareRatios(outputSize, thinnestRatio) >= 0 && - outputPixelCount >= minimumPixelCount && - cropIsWithinMainImageBounds(); + return compareRatios(outputSize, thinnestRatio) >= 0 + && outputPixelCount >= minimumPixelCount + && cropIsWithinMainImageBounds(); } /** - * -1 iff a is a narrower ratio than b. - * +1 iff a is a squarer ratio than b. - * 0 if the ratios are the same. + * -1 iff a is a narrower ratio than b. +1 iff a is a squarer ratio than b. 0 if the ratios are + * the same. */ private static int compareRatios(@NonNull Point a, @NonNull Point b) { int smallA = Math.min(a.x, a.y); int largeA = Math.max(a.x, a.y); - + int smallB = Math.min(b.x, b.y); int largeB = Math.max(b.x, b.y); @@ -484,9 +508,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { return Bounds.boundsRemainInBounds(editorElementHierarchy.imageMatrixRelativeToCrop()); } - /** - * Called as edits are underway. - */ + /** Called as edits are underway. */ public void moving(@NonNull EditorElement editorElement) { if (!isCropping()) return; @@ -508,27 +530,31 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { public Set getUniqueColorsIgnoringAlpha() { final Set colors = new LinkedHashSet<>(); - editorElementHierarchy.getRoot().forAllInTree(element -> { - Renderer renderer = element.getRenderer(); - if (renderer instanceof ColorableRenderer) { - colors.add(((ColorableRenderer) renderer).getColor() | 0xff000000); - } - }); + editorElementHierarchy + .getRoot() + .forAllInTree( + element -> { + Renderer renderer = element.getRenderer(); + if (renderer instanceof ColorableRenderer) { + colors.add(((ColorableRenderer) renderer).getColor() | 0xff000000); + } + }); return colors; } - public static final Creator CREATOR = new Creator() { - @Override - public EditorModel createFromParcel(Parcel in) { - return new EditorModel(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public EditorModel createFromParcel(Parcel in) { + return new EditorModel(in); + } - @Override - public EditorModel[] newArray(int size) { - return new EditorModel[size]; - } - }; + @Override + public EditorModel[] newArray(int size) { + return new EditorModel[size]; + } + }; @Override public int describeContents() { @@ -545,27 +571,25 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { dest.writeParcelable(cropUndoRedoStacks, flags); } - /** - * Blocking render of the model. - */ + /** Blocking render of the model. */ @WorkerThread public @NonNull Bitmap render(@NonNull Context context) { return render(context, null); } - /** - * Blocking render of the model. - */ + /** Blocking render of the model. */ @WorkerThread public @NonNull Bitmap render(@NonNull Context context, @Nullable Point size) { - EditorElement image = editorElementHierarchy.getFlipRotate(); - RectF cropRect = editorElementHierarchy.getCropRect(); - Point outputSize = size != null ? size : getOutputSize(); + EditorElement image = editorElementHierarchy.getFlipRotate(); + RectF cropRect = editorElementHierarchy.getCropRect(); + Point outputSize = size != null ? size : getOutputSize(); Bitmap bitmap = Bitmap.createBitmap(outputSize.x, outputSize.y, Bitmap.Config.ARGB_8888); try { Canvas canvas = new Canvas(bitmap); - RendererContext rendererContext = new RendererContext(context, canvas, RendererContext.Ready.NULL, RendererContext.Invalidate.NULL); + RendererContext rendererContext = + new RendererContext( + context, canvas, RendererContext.Ready.NULL, RendererContext.Invalidate.NULL); RectF bitmapArea = new RectF(); bitmapArea.right = bitmap.getWidth(); @@ -597,7 +621,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { private Point getOutputSize() { PointF outputSize = editorElementHierarchy.getOutputSize(size); - int width = (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x); + int width = (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x); int height = (int) (width * outputSize.y / outputSize.x); return new Point(width, height); @@ -607,22 +631,23 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { public Point getOutputSizeMaxWidth(int maxDimension) { PointF outputSize = editorElementHierarchy.getOutputSize(size); - int width = Math.min(maxDimension, (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x)); + int width = Math.min(maxDimension, (int) Math.max(MINIMUM_OUTPUT_WIDTH, outputSize.x)); int height = (int) (width * outputSize.y / outputSize.x); if (height > maxDimension) { height = maxDimension; - width = (int) (height * outputSize.x / outputSize.y); + width = (int) (height * outputSize.x / outputSize.y); } return new Point(width, height); } @Override - public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { + public void onReady( + @NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) { - boolean changedBefore = isChanged(); - Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix(); + boolean changedBefore = isChanged(); + Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix(); this.size.set(size.x, size.y); if (imageCropMatrix.isIdentity()) { imageCropMatrix.set(cropMatrix); @@ -650,8 +675,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } private boolean isRendererOfMainImage(@NonNull Renderer renderer) { - EditorElement mainImage = editorElementHierarchy.getMainImage(); - Renderer mainImageRenderer = mainImage != null ? mainImage.getRenderer() : null; + EditorElement mainImage = editorElementHierarchy.getMainImage(); + Renderer mainImageRenderer = mainImage != null ? mainImage.getRenderer() : null; return mainImageRenderer == renderer; } @@ -659,7 +684,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { * Add a new {@link EditorElement} centered in the current visible crop area. * * @param element New element to add. - * @param scale Initial scale for new element. + * @param scale Initial scale for new element. */ public void addElementCentered(@NonNull EditorElement element, float scale) { Matrix localMatrix = element.getLocalMatrix(); @@ -671,7 +696,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } /** - * Add an element to the main image, or if there is no main image, make the new element the main image. + * Add an element to the main image, or if there is no main image, make the new element the main + * image. * * @param element New element to add. */ @@ -682,7 +708,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { public void addElementWithoutPushUndo(@NonNull EditorElement element) { EditorElement mainImage = editorElementHierarchy.getMainImage(); - EditorElement parent = mainImage != null ? mainImage : editorElementHierarchy.getImageRoot(); + EditorElement parent = mainImage != null ? mainImage : editorElementHierarchy.getImageRoot(); parent.addElement(element); @@ -716,15 +742,17 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } /** - * Returns a matrix that maps points in the {@param from} element in to points in the {@param to} element. + * Returns a matrix that maps points in the {@param from} element in to points in the {@param to} + * element. * * @param from * @param to * @return */ - @Nullable Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) { + @Nullable + Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) { Matrix matrix = findElementInverseMatrix(to, new Matrix()); - Matrix outOf = findElementMatrix(from, new Matrix()); + Matrix outOf = findElementMatrix(from, new Matrix()); if (outOf != null && matrix != null) { matrix.preConcat(outOf); @@ -764,7 +792,9 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } public void delete(@NonNull EditorElement editorElement) { - editorElementHierarchy.getImageRoot().forAllInTree(element -> element.deleteChild(editorElement, invalidate)); + editorElementHierarchy + .getImageRoot() + .forAllInTree(element -> element.deleteChild(editorElement, invalidate)); } public @Nullable EditorElement findById(@NonNull UUID uuid) { @@ -774,11 +804,12 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { /** * Changes the temporary view so that the text element is centered in it. * - * @param entity Entity to center on. + * @param entity Entity to center on. * @param textRenderer The text renderer, which can make additional adjustments to the zoom matrix - * to leave space for the keyboard for example. + * to leave space for the keyboard for example. */ - public void zoomToTextElement(@NonNull EditorElement entity, @NonNull MultiLineTextRenderer textRenderer) { + public void zoomToTextElement( + @NonNull EditorElement entity, @NonNull MultiLineTextRenderer textRenderer) { Matrix elementInverseMatrix = findElementInverseMatrix(entity, new Matrix()); if (elementInverseMatrix != null) { EditorElement root = editorElementHierarchy.getRoot(); @@ -803,12 +834,12 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { return editorElementHierarchy.getCropEditorElement().getFlags().isVisible(); } - /** - * Returns a matrix that maps bounds to the crop area. - */ + /** Returns a matrix that maps bounds to the crop area. */ public Matrix getInverseCropPosition() { Matrix matrix = new Matrix(); - matrix.set(findRelativeMatrix(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement())); + matrix.set( + findRelativeMatrix( + editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement())); matrix.postConcat(editorElementHierarchy.getFlipRotate().getLocalMatrix()); Matrix positionRelativeToCrop = new Matrix(); diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java index 468e288f0..7d5bf0778 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ElementStack.java @@ -4,22 +4,22 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.Arrays; import java.util.Stack; /** * Contains a stack of elements for undo and redo stacks. - *

- * Elements are mutable, so this stack serializes the element and keeps a stack of serialized data. - *

- * The stack has a {@link #limit} and if it exceeds that limit during a push the second to earliest item - * is removed so that it can always go back to the first state. Effectively collapsing the history for - * the start of the stack. + * + *

Elements are mutable, so this stack serializes the element and keeps a stack of serialized + * data. + * + *

The stack has a {@link #limit} and if it exceeds that limit during a push the second to + * earliest item is removed so that it can always go back to the first state. Effectively collapsing + * the history for the start of the stack. */ final class ElementStack implements Parcelable { - private final int limit; + private final int limit; private final Stack stack = new Stack<>(); ElementStack(int limit) { @@ -37,15 +37,15 @@ final class ElementStack implements Parcelable { /** * Pushes an element to the stack iff the element's serialized value is different to any found at * the top of the stack. - *

- * Removes the second to earliest item if it is overflowing. + * + *

Removes the second to earliest item if it is overflowing. * * @param element new editor element state. * @return true iff the pushed item was different to the top item. */ boolean tryPush(@NonNull EditorElement element) { - byte[] bytes = getBytes(element); - boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek()); + byte[] bytes = getBytes(element); + boolean push = stack.isEmpty() || !Arrays.equals(bytes, stack.peek()); if (push) { stack.push(bytes); @@ -68,14 +68,13 @@ final class ElementStack implements Parcelable { return bytes; } - /** - * Pops the first different state from the supplied element. - */ - @Nullable EditorElement pop(@NonNull EditorElement element) { + /** Pops the first different state from the supplied element. */ + @Nullable + EditorElement pop(@NonNull EditorElement element) { if (stack.empty()) return null; byte[] elementBytes = getBytes(element); - byte[] stackData = null; + byte[] stackData = null; while (!stack.empty() && stackData == null) { byte[] topData = stack.pop(); @@ -101,17 +100,18 @@ final class ElementStack implements Parcelable { stack.clear(); } - public static final Creator CREATOR = new Creator() { - @Override - public ElementStack createFromParcel(Parcel in) { - return new ElementStack(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public ElementStack createFromParcel(Parcel in) { + return new ElementStack(in); + } - @Override - public ElementStack[] newArray(int size) { - return new ElementStack[size]; - } - }; + @Override + public ElementStack[] newArray(int size) { + return new ElementStack[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java index e048261ef..962a430f4 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/InBoundsMemory.java @@ -6,7 +6,7 @@ import androidx.annotation.Nullable; final class InBoundsMemory { - private final Matrix lastGoodUserCrop = new Matrix(); + private final Matrix lastGoodUserCrop = new Matrix(); private final Matrix lastGoodMainImage = new Matrix(); void push(@Nullable EditorElement mainImage, @NonNull EditorElement userCrop) { @@ -21,7 +21,10 @@ final class InBoundsMemory { lastGoodUserCrop.preConcat(userCrop.getEditorMatrix()); } - void restore(@Nullable EditorElement mainImage, @NonNull EditorElement cropEditorElement, @Nullable Runnable invalidate) { + void restore( + @Nullable EditorElement mainImage, + @NonNull EditorElement cropEditorElement, + @Nullable Runnable invalidate) { if (mainImage != null) { mainImage.animateLocalTo(lastGoodMainImage, invalidate); } @@ -31,4 +34,4 @@ final class InBoundsMemory { Matrix getLastKnownGoodMainImageMatrix() { return new Matrix(lastGoodMainImage); } -} \ No newline at end of file +} diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java index aad9eed7e..d43a87b47 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ParcelUtils.java @@ -4,13 +4,11 @@ import android.graphics.Matrix; import android.graphics.RectF; import android.os.Parcel; import androidx.annotation.NonNull; - import java.util.UUID; public final class ParcelUtils { - private ParcelUtils() { - } + private ParcelUtils() {} public static void writeMatrix(@NonNull Parcel dest, @NonNull Matrix matrix) { float[] values = new float[9]; @@ -38,9 +36,9 @@ public final class ParcelUtils { } public static @NonNull RectF readRectF(@NonNull Parcel in) { - float left = in.readFloat(); - float top = in.readFloat(); - float right = in.readFloat(); + float left = in.readFloat(); + float top = in.readFloat(); + float right = in.readFloat(); float bottom = in.readFloat(); return new RectF(left, top, right, bottom); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java index 144766449..a529927e3 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/ThumbRenderer.java @@ -1,31 +1,31 @@ package org.thoughtcrime.securesms.imageeditor.model; +import java.util.UUID; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; -import java.util.UUID; - /** * A special {@link Renderer} that controls another {@link EditorElement}. - *

- * It has a reference to the {@link EditorElement#getId()} and a {@link ControlPoint} which it is in control of. - *

- * The presence of this interface on the selected element is used to launch a ThumbDragEditSession. + * + *

It has a reference to the {@link EditorElement#getId()} and a {@link ControlPoint} which it is + * in control of. + * + *

The presence of this interface on the selected element is used to launch a + * ThumbDragEditSession. */ public interface ThumbRenderer extends Renderer { enum ControlPoint { + CENTER_LEFT(Bounds.LEFT, Bounds.CENTRE_Y), + CENTER_RIGHT(Bounds.RIGHT, Bounds.CENTRE_Y), - CENTER_LEFT (Bounds.LEFT, Bounds.CENTRE_Y), - CENTER_RIGHT (Bounds.RIGHT, Bounds.CENTRE_Y), + TOP_CENTER(Bounds.CENTRE_X, Bounds.TOP), + BOTTOM_CENTER(Bounds.CENTRE_X, Bounds.BOTTOM), - TOP_CENTER (Bounds.CENTRE_X, Bounds.TOP), - BOTTOM_CENTER (Bounds.CENTRE_X, Bounds.BOTTOM), - - TOP_LEFT (Bounds.LEFT, Bounds.TOP), - TOP_RIGHT (Bounds.RIGHT, Bounds.TOP), - BOTTOM_LEFT (Bounds.LEFT, Bounds.BOTTOM), - BOTTOM_RIGHT (Bounds.RIGHT, Bounds.BOTTOM); + TOP_LEFT(Bounds.LEFT, Bounds.TOP), + TOP_RIGHT(Bounds.RIGHT, Bounds.TOP), + BOTTOM_LEFT(Bounds.LEFT, Bounds.BOTTOM), + BOTTOM_RIGHT(Bounds.RIGHT, Bounds.BOTTOM); private final float x; private final float y; @@ -45,14 +45,22 @@ public interface ThumbRenderer extends Renderer { public ControlPoint opposite() { switch (this) { - case CENTER_LEFT: return CENTER_RIGHT; - case CENTER_RIGHT: return CENTER_LEFT; - case TOP_CENTER: return BOTTOM_CENTER; - case BOTTOM_CENTER: return TOP_CENTER; - case TOP_LEFT: return BOTTOM_RIGHT; - case TOP_RIGHT: return BOTTOM_LEFT; - case BOTTOM_LEFT: return TOP_RIGHT; - case BOTTOM_RIGHT: return TOP_LEFT; + case CENTER_LEFT: + return CENTER_RIGHT; + case CENTER_RIGHT: + return CENTER_LEFT; + case TOP_CENTER: + return BOTTOM_CENTER; + case BOTTOM_CENTER: + return TOP_CENTER; + case TOP_LEFT: + return BOTTOM_RIGHT; + case TOP_RIGHT: + return BOTTOM_LEFT; + case BOTTOM_LEFT: + return TOP_RIGHT; + case BOTTOM_RIGHT: + return TOP_LEFT; default: throw new RuntimeException(); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java index 8f680bdd0..51ebab173 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/model/UndoRedoStacks.java @@ -4,7 +4,6 @@ import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.Arrays; final class UndoRedoStacks implements Parcelable { @@ -12,34 +11,34 @@ final class UndoRedoStacks implements Parcelable { private final ElementStack undoStack; private final ElementStack redoStack; - @NonNull - private byte[] unchangedState; + @NonNull private byte[] unchangedState; UndoRedoStacks(int limit) { this(new ElementStack(limit), new ElementStack(limit), null); } - private UndoRedoStacks(ElementStack undoStack, ElementStack redoStack, @Nullable byte[] unchangedState) { + private UndoRedoStacks( + ElementStack undoStack, ElementStack redoStack, @Nullable byte[] unchangedState) { this.undoStack = undoStack; this.redoStack = redoStack; this.unchangedState = unchangedState != null ? unchangedState : new byte[0]; } - public static final Creator CREATOR = new Creator() { - @Override - public UndoRedoStacks createFromParcel(Parcel in) { - return new UndoRedoStacks( - in.readParcelable(ElementStack.class.getClassLoader()), - in.readParcelable(ElementStack.class.getClassLoader()), - in.createByteArray() - ); - } + public static final Creator CREATOR = + new Creator() { + @Override + public UndoRedoStacks createFromParcel(Parcel in) { + return new UndoRedoStacks( + in.readParcelable(ElementStack.class.getClassLoader()), + in.readParcelable(ElementStack.class.getClassLoader()), + in.createByteArray()); + } - @Override - public UndoRedoStacks[] newArray(int size) { - return new UndoRedoStacks[size]; - } - }; + @Override + public UndoRedoStacks[] newArray(int size) { + return new UndoRedoStacks[size]; + } + }; @Override public void writeToParcel(Parcel dest, int flags) { @@ -77,16 +76,12 @@ final class UndoRedoStacks implements Parcelable { return !Arrays.equals(ElementStack.getBytes(element), unchangedState); } - /** - * As long as there is something different in the stack somewhere, then we can undo. - */ + /** As long as there is something different in the stack somewhere, then we can undo. */ boolean canUndo(@NonNull EditorElement currentState) { return undoStack.stackContainsStateDifferentFrom(currentState); } - /** - * As long as there is something different in the stack somewhere, then we can redo. - */ + /** As long as there is something different in the stack somewhere, then we can redo. */ boolean canRedo(@NonNull EditorElement currentState) { return redoStack.stackContainsStateDifferentFrom(currentState); } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java index dedbbc3ef..210e881f4 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/AutomaticControlPointBezierLine.java @@ -5,20 +5,18 @@ import android.graphics.Paint; import android.graphics.Path; import android.os.Parcel; import android.os.Parcelable; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import java.util.Arrays; /** * Given points for a line to go though, automatically finds control points. - *

- * Based on http://www.particleincell.com/2012/bezier-splines/ - *

- * Can then draw that line to a {@link Canvas} given a {@link Paint}. - *

- * Allocation efficient so that adding new points does not result in lots of array allocations. + * + *

Based on http://www.particleincell.com/2012/bezier-splines/ + * + *

Can then draw that line to a {@link Canvas} given a {@link Paint}. + * + *

Allocation efficient so that adding new points does not result in lots of array allocations. */ final class AutomaticControlPointBezierLine implements Parcelable { @@ -57,8 +55,8 @@ final class AutomaticControlPointBezierLine implements Parcelable { /** * Adds a new point to the end of the line but ignores points that are too close to the last. * - * @param x new x point - * @param y new y point + * @param x new x point + * @param y new y point * @param thickness the maximum distance to allow, line thickness is recommended. */ void addPointFiltered(float x, float y, float thickness) { @@ -135,7 +133,7 @@ final class AutomaticControlPointBezierLine implements Parcelable { * Draw the line. * * @param canvas The canvas to draw on. - * @param paint The paint to use. + * @param paint The paint to use. */ void draw(@NonNull Canvas canvas, @NonNull Paint paint) { canvas.drawPath(path, paint); @@ -148,11 +146,11 @@ final class AutomaticControlPointBezierLine implements Parcelable { private float[] r; /** - * Based on http://www.particleincell.com/2012/bezier-splines/ + * Based on http://www.particleincell.com/2012/bezier-splines/ * - * @param k knots x or y, must be at least 2 entries - * @param p1 corresponding first control point x or y - * @param p2 corresponding second control point x or y + * @param k knots x or y, must be at least 2 entries + * @param p1 corresponding first control point x or y + * @param p2 corresponding second control point x or y * @param count number of k to process */ private void computeControlPoints(float[] k, float[] p1, float[] p2, int count) { @@ -198,19 +196,20 @@ final class AutomaticControlPointBezierLine implements Parcelable { p2[n - 1] = 0.5f * (k[n] + p1[n - 1]); } - public static final Creator CREATOR = new Creator() { - @Override - public AutomaticControlPointBezierLine createFromParcel(Parcel in) { - float[] x = in.createFloatArray(); - float[] y = in.createFloatArray(); - return new AutomaticControlPointBezierLine(x, y, x != null ? x.length : 0); - } + public static final Creator CREATOR = + new Creator() { + @Override + public AutomaticControlPointBezierLine createFromParcel(Parcel in) { + float[] x = in.createFloatArray(); + float[] y = in.createFloatArray(); + return new AutomaticControlPointBezierLine(x, y, x != null ? x.length : 0); + } - @Override - public AutomaticControlPointBezierLine[] newArray(int size) { - return new AutomaticControlPointBezierLine[size]; - } - }; + @Override + public AutomaticControlPointBezierLine[] newArray(int size) { + return new AutomaticControlPointBezierLine[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java index 30423b9c4..dd341b8d3 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/BezierDrawingRenderer.java @@ -5,41 +5,46 @@ import android.graphics.Paint; import android.graphics.PointF; import android.graphics.RectF; import android.os.Parcel; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; /** - * Renders a {@link AutomaticControlPointBezierLine} with {@link #thickness}, {@link #color} and {@link #cap} end type. + * Renders a {@link AutomaticControlPointBezierLine} with {@link #thickness}, {@link #color} and + * {@link #cap} end type. */ -public final class BezierDrawingRenderer extends InvalidateableRenderer implements ColorableRenderer { +public final class BezierDrawingRenderer extends InvalidateableRenderer + implements ColorableRenderer { - private final Paint paint; + private final Paint paint; private final AutomaticControlPointBezierLine bezierLine; - private final Paint.Cap cap; + private final Paint.Cap cap; - @Nullable - private final RectF clipRect; + @Nullable private final RectF clipRect; - private int color; - private float thickness; + private int color; + private float thickness; - private BezierDrawingRenderer(int color, float thickness, @NonNull Paint.Cap cap, @Nullable AutomaticControlPointBezierLine bezierLine, @Nullable RectF clipRect) { - this.paint = new Paint(); - this.color = color; - this.thickness = thickness; - this.cap = cap; - this.clipRect = clipRect; + private BezierDrawingRenderer( + int color, + float thickness, + @NonNull Paint.Cap cap, + @Nullable AutomaticControlPointBezierLine bezierLine, + @Nullable RectF clipRect) { + this.paint = new Paint(); + this.color = color; + this.thickness = thickness; + this.cap = cap; + this.clipRect = clipRect; this.bezierLine = bezierLine != null ? bezierLine : new AutomaticControlPointBezierLine(); updatePaint(); } - public BezierDrawingRenderer(int color, float thickness, @NonNull Paint.Cap cap, @Nullable RectF clipRect) { - this(color, thickness, cap,null, clipRect != null ? new RectF(clipRect) : null); + public BezierDrawingRenderer( + int color, float thickness, @NonNull Paint.Cap cap, @Nullable RectF clipRect) { + this(color, thickness, cap, null, clipRect != null ? new RectF(clipRect) : null); } @Override @@ -99,7 +104,10 @@ public final class BezierDrawingRenderer extends InvalidateableRenderer implemen int alpha = paint.getAlpha(); paint.setAlpha(rendererContext.getAlpha(alpha)); - paint.setXfermode(rendererContext.getMaskPaint() != null ? rendererContext.getMaskPaint().getXfermode() : null); + paint.setXfermode( + rendererContext.getMaskPaint() != null + ? rendererContext.getMaskPaint().getXfermode() + : null); bezierLine.draw(canvas, paint); @@ -112,23 +120,25 @@ public final class BezierDrawingRenderer extends InvalidateableRenderer implemen return false; } - public static final Creator CREATOR = new Creator() { - @Override - public BezierDrawingRenderer createFromParcel(Parcel in) { - int color = in.readInt(); - float thickness = in.readFloat(); - Paint.Cap cap = Paint.Cap.values()[in.readInt()]; - AutomaticControlPointBezierLine bezierLine = in.readParcelable(AutomaticControlPointBezierLine.class.getClassLoader()); - RectF clipRect = in.readParcelable(RectF.class.getClassLoader()); + public static final Creator CREATOR = + new Creator() { + @Override + public BezierDrawingRenderer createFromParcel(Parcel in) { + int color = in.readInt(); + float thickness = in.readFloat(); + Paint.Cap cap = Paint.Cap.values()[in.readInt()]; + AutomaticControlPointBezierLine bezierLine = + in.readParcelable(AutomaticControlPointBezierLine.class.getClassLoader()); + RectF clipRect = in.readParcelable(RectF.class.getClassLoader()); - return new BezierDrawingRenderer(color, thickness, cap, bezierLine, clipRect); - } + return new BezierDrawingRenderer(color, thickness, cap, bezierLine, clipRect); + } - @Override - public BezierDrawingRenderer[] newArray(int size) { - return new BezierDrawingRenderer[size]; - } - }; + @Override + public BezierDrawingRenderer[] newArray(int size) { + return new BezierDrawingRenderer[size]; + } + }; @Override public int describeContents() { @@ -143,5 +153,4 @@ public final class BezierDrawingRenderer extends InvalidateableRenderer implemen dest.writeParcelable(bezierLine, flags); dest.writeParcelable(clipRect, flags); } - } diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java index a397e06e4..f0973d0ae 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/CropAreaRenderer.java @@ -6,40 +6,38 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.os.Parcel; - import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; /** - * Renders a box outside of the current crop area using {@link R.color#crop_area_renderer_outer_color} - * and around the edge it renders the markers for the thumbs using {@link R.color#crop_area_renderer_edge_color}, - * {@link R.dimen#crop_area_renderer_edge_thickness} and {@link R.dimen#crop_area_renderer_edge_size}. - *

- * Hit tests outside of the bounds. + * Renders a box outside of the current crop area using {@link + * R.color#crop_area_renderer_outer_color} and around the edge it renders the markers for the thumbs + * using {@link R.color#crop_area_renderer_edge_color}, {@link + * R.dimen#crop_area_renderer_edge_thickness} and {@link R.dimen#crop_area_renderer_edge_size}. + * + *

Hit tests outside of the bounds. */ public final class CropAreaRenderer implements Renderer { - @ColorRes - private final int color; + @ColorRes private final int color; private final boolean renderCenterThumbs; - private final Path cropClipPath = new Path(); + private final Path cropClipPath = new Path(); private final Path screenClipPath = new Path(); - private final RectF dst = new RectF(); + private final RectF dst = new RectF(); private final Paint paint = new Paint(); @Override public void render(@NonNull RendererContext rendererContext) { rendererContext.save(); - Canvas canvas = rendererContext.canvas; + Canvas canvas = rendererContext.canvas; Resources resources = rendererContext.context.getResources(); canvas.clipPath(cropClipPath); @@ -47,10 +45,16 @@ public final class CropAreaRenderer implements Renderer { rendererContext.mapRect(dst, Bounds.FULL_BOUNDS); - final int thickness = resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_thickness); - final int size = (int) Math.min(resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size), Math.min(dst.width(), dst.height()) / 3f - 10); + final int thickness = + resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_thickness); + final int size = + (int) + Math.min( + resources.getDimensionPixelSize(R.dimen.crop_area_renderer_edge_size), + Math.min(dst.width(), dst.height()) / 3f - 10); - paint.setColor(ResourcesCompat.getColor(resources, R.color.crop_area_renderer_edge_color, null)); + paint.setColor( + ResourcesCompat.getColor(resources, R.color.crop_area_renderer_edge_color, null)); rendererContext.canvasMatrix.setToIdentity(); screenClipPath.reset(); @@ -92,7 +96,7 @@ public final class CropAreaRenderer implements Renderer { } public CropAreaRenderer(@ColorRes int color, boolean renderCenterThumbs) { - this.color = color; + this.color = color; this.renderCenterThumbs = renderCenterThumbs; cropClipPath.toggleInverseFillType(); @@ -109,18 +113,18 @@ public final class CropAreaRenderer implements Renderer { return !Bounds.contains(x, y); } - public static final Creator CREATOR = new Creator() { - @Override - public @NonNull CropAreaRenderer createFromParcel(@NonNull Parcel in) { - return new CropAreaRenderer(in.readInt(), - in.readByte() == 1); - } + public static final Creator CREATOR = + new Creator() { + @Override + public @NonNull CropAreaRenderer createFromParcel(@NonNull Parcel in) { + return new CropAreaRenderer(in.readInt(), in.readByte() == 1); + } - @Override - public @NonNull CropAreaRenderer[] newArray(int size) { - return new CropAreaRenderer[size]; - } - }; + @Override + public @NonNull CropAreaRenderer[] newArray(int size) { + return new CropAreaRenderer[size]; + } + }; @Override public void writeToParcel(Parcel dest, int flags) { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java index fd255e4d2..ff44f93be 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InvalidateableRenderer.java @@ -1,14 +1,13 @@ package org.thoughtcrime.securesms.imageeditor.renderers; import androidx.annotation.NonNull; - +import java.lang.ref.WeakReference; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import java.lang.ref.WeakReference; - /** - * Maintains a weak reference to the an invalidate callback allowing future invalidation without memory leak risk. + * Maintains a weak reference to the an invalidate callback allowing future invalidation without + * memory leak risk. */ abstract class InvalidateableRenderer implements Renderer { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java index 6c662e9d1..b110360fc 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/InverseFillRenderer.java @@ -4,15 +4,14 @@ import android.graphics.Path; import android.os.Parcel; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; - import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; /** * Renders the {@link color} outside of the {@link Bounds}. - *

- * Hit tests outside of the bounds. + * + *

Hit tests outside of the bounds. */ public final class InverseFillRenderer implements Renderer { @@ -47,17 +46,18 @@ public final class InverseFillRenderer implements Renderer { return !Bounds.contains(x, y); } - public static final Creator CREATOR = new Creator() { - @Override - public InverseFillRenderer createFromParcel(Parcel in) { - return new InverseFillRenderer(in); - } + public static final Creator CREATOR = + new Creator() { + @Override + public InverseFillRenderer createFromParcel(Parcel in) { + return new InverseFillRenderer(in); + } - @Override - public InverseFillRenderer[] newArray(int size) { - return new InverseFillRenderer[size]; - } - }; + @Override + public InverseFillRenderer[] newArray(int size) { + return new InverseFillRenderer[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java index b5b7f350e..59651e0af 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java @@ -10,44 +10,40 @@ import android.graphics.RectF; import android.graphics.Typeface; import android.os.Parcel; import android.view.animation.Interpolator; - import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import java.util.ArrayList; +import java.util.List; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.ColorableRenderer; import org.thoughtcrime.securesms.imageeditor.RendererContext; -import java.util.ArrayList; -import java.util.List; - /** * Renders multiple lines of {@link #text} in the specified {@link #color}. - *

- * Scales down the text size of long lines to fit inside the {@link Bounds} width. + * + *

Scales down the text size of long lines to fit inside the {@link Bounds} width. */ -public final class MultiLineTextRenderer extends InvalidateableRenderer implements ColorableRenderer { +public final class MultiLineTextRenderer extends InvalidateableRenderer + implements ColorableRenderer { - @NonNull - private String text = ""; + @NonNull private String text = ""; - @ColorInt - private int color; + @ColorInt private int color; - private final Paint paint = new Paint(); + private final Paint paint = new Paint(); private final Paint selectionPaint = new Paint(); private final float textScale; - private int selStart; - private int selEnd; + private int selStart; + private int selEnd; private boolean hasFocus; private List lines = emptyList(); private ValueAnimator cursorAnimator; - private float cursorAnimatedValue; + private float cursorAnimatedValue; private final Matrix recommendedEditorMatrix = new Matrix(); @@ -133,20 +129,20 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen } private class Line { - private final Matrix accentMatrix = new Matrix(); - private final Matrix decentMatrix = new Matrix(); - private final Matrix projectionMatrix = new Matrix(); + private final Matrix accentMatrix = new Matrix(); + private final Matrix decentMatrix = new Matrix(); + private final Matrix projectionMatrix = new Matrix(); private final Matrix inverseProjectionMatrix = new Matrix(); - private final RectF selectionBounds = new RectF(); - private final RectF textBounds = new RectF(); + private final RectF selectionBounds = new RectF(); + private final RectF textBounds = new RectF(); private String text; - private int selStart; - private int selEnd; - private float ascentInBounds; - private float descentInBounds; - private float scale = 1f; - private float heightInBounds; + private int selStart; + private int selEnd; + private float ascentInBounds; + private float descentInBounds; + private float scale = 1f; + private float heightInBounds; Line(String text) { this.text = text; @@ -155,7 +151,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private void recalculate() { RectF maxTextBounds = new RectF(); - Rect temp = new Rect(); + Rect temp = new Rect(); getTextBoundsWithoutTrim(text, 0, text.length(), temp); textBounds.set(temp); @@ -183,23 +179,24 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen paint.getTextBounds("|", 0, 1, temp); int width = temp.width(); - temp.left -= width; + temp.left -= width; temp.right -= width; } - temp.left += startTemp.right; + temp.left += startTemp.right; temp.right += startTemp.right; selectionBounds.set(temp); } - projectionMatrix.setRectToRect(new RectF(maxTextBounds), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); + projectionMatrix.setRectToRect( + new RectF(maxTextBounds), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER); removeTranslate(projectionMatrix); - float[] pts = { 0, paint.ascent(), 0, paint.descent() }; + float[] pts = {0, paint.ascent(), 0, paint.descent()}; projectionMatrix.mapPoints(pts); - ascentInBounds = pts[1]; + ascentInBounds = pts[1]; descentInBounds = pts[3]; - heightInBounds = descentInBounds - ascentInBounds; + heightInBounds = descentInBounds - ascentInBounds; projectionMatrix.preTranslate(-textBounds.centerX(), 0); projectionMatrix.invert(inverseProjectionMatrix); @@ -220,20 +217,20 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen } private boolean showSelectionOrCursor() { - return (selStart >= 0 || selEnd >= 0) && - (selStart <= text.length() || selEnd <= text.length()); + return (selStart >= 0 || selEnd >= 0) + && (selStart <= text.length() || selEnd <= text.length()); } private boolean containsSelectionEnd() { - return (selEnd >= 0) && - (selEnd <= text.length()); + return (selEnd >= 0) && (selEnd <= text.length()); } private void getTextBoundsWithoutTrim(String text, int start, int end, Rect result) { - Rect extra = new Rect(); + Rect extra = new Rect(); Rect xBounds = new Rect(); - String cannotBeTrimmed = "x" + text.substring(Math.max(0, start), Math.min(text.length(), end)) + "x"; + String cannotBeTrimmed = + "x" + text.substring(Math.max(0, start), Math.min(text.length(), end)) + "x"; paint.getTextBounds(cannotBeTrimmed, 0, cannotBeTrimmed.length(), extra); paint.getTextBounds("x", 0, 1, xBounds); @@ -241,14 +238,14 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen result.right -= 2 * xBounds.width(); int temp = result.left; - result.left -= temp; + result.left -= temp; result.right -= temp; } public boolean contains(float x, float y) { float[] dst = new float[2]; - inverseProjectionMatrix.mapPoints(dst, new float[]{ x, y }); + inverseProjectionMatrix.mapPoints(dst, new float[] {x, y}); return textBounds.contains(dst[0], dst[1]); } @@ -293,7 +290,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen void setSelection(int selStart, int selEnd) { if (selStart != this.selStart || selEnd != this.selEnd) { this.selStart = selStart; - this.selEnd = selEnd; + this.selEnd = selEnd; recalculate(); } } @@ -333,7 +330,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen int length = line.text.length() + 1; // one for new line selStart -= length; - selEnd -= length; + selEnd -= length; } } @@ -349,10 +346,11 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen cursorAnimator.setInterpolator(pulseInterpolator()); cursorAnimator.setRepeatCount(ValueAnimator.INFINITE); cursorAnimator.setDuration(1000); - cursorAnimator.addUpdateListener(animation -> { - cursorAnimatedValue = (float) animation.getAnimatedValue(); - invalidate(); - }); + cursorAnimator.addUpdateListener( + animation -> { + cursorAnimatedValue = (float) animation.getAnimatedValue(); + invalidate(); + }); cursorAnimator.start(); } else { invalidate(); @@ -360,17 +358,18 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen } } - public static final Creator CREATOR = new Creator() { - @Override - public MultiLineTextRenderer createFromParcel(Parcel in) { - return new MultiLineTextRenderer(in.readString(), in.readInt()); - } + public static final Creator CREATOR = + new Creator() { + @Override + public MultiLineTextRenderer createFromParcel(Parcel in) { + return new MultiLineTextRenderer(in.readString(), in.readInt()); + } - @Override - public MultiLineTextRenderer[] newArray(int size) { - return new MultiLineTextRenderer[size]; - } - }; + @Override + public MultiLineTextRenderer[] newArray(int size) { + return new MultiLineTextRenderer[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java index 643e83882..4f14a7c04 100644 --- a/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java +++ b/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/OvalGuideRenderer.java @@ -5,11 +5,9 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.os.Parcel; - import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.imageeditor.Bounds; import org.thoughtcrime.securesms.imageeditor.Renderer; @@ -17,8 +15,8 @@ import org.thoughtcrime.securesms.imageeditor.RendererContext; /** * Renders an oval inside of the {@link Bounds}. - *

- * Hit tests outside of the bounds. + * + *

Hit tests outside of the bounds. */ public final class OvalGuideRenderer implements Renderer { @@ -32,16 +30,20 @@ public final class OvalGuideRenderer implements Renderer { public void render(@NonNull RendererContext rendererContext) { rendererContext.save(); - Canvas canvas = rendererContext.canvas; - Context context = rendererContext.context; - int stroke = context.getResources().getDimensionPixelSize(R.dimen.oval_guide_stroke_width); - float halfStroke = stroke / 2f; + Canvas canvas = rendererContext.canvas; + Context context = rendererContext.context; + int stroke = context.getResources().getDimensionPixelSize(R.dimen.oval_guide_stroke_width); + float halfStroke = stroke / 2f; this.paint.setStrokeWidth(stroke); paint.setColor(ContextCompat.getColor(context, ovalGuideColor)); rendererContext.mapRect(dst, Bounds.FULL_BOUNDS); - dst.set(dst.left + halfStroke, dst.top + halfStroke, dst.right - halfStroke, dst.bottom - halfStroke); + dst.set( + dst.left + halfStroke, + dst.top + halfStroke, + dst.right - halfStroke, + dst.bottom - halfStroke); rendererContext.canvasMatrix.setToIdentity(); canvas.drawOval(dst, paint); @@ -62,17 +64,18 @@ public final class OvalGuideRenderer implements Renderer { return !Bounds.contains(x, y); } - public static final Creator CREATOR = new Creator() { - @Override - public @NonNull OvalGuideRenderer createFromParcel(@NonNull Parcel in) { - return new OvalGuideRenderer(in.readInt()); - } + public static final Creator CREATOR = + new Creator() { + @Override + public @NonNull OvalGuideRenderer createFromParcel(@NonNull Parcel in) { + return new OvalGuideRenderer(in.readInt()); + } - @Override - public @NonNull OvalGuideRenderer[] newArray(int size) { - return new OvalGuideRenderer[size]; - } - }; + @Override + public @NonNull OvalGuideRenderer[] newArray(int size) { + return new OvalGuideRenderer[size]; + } + }; @Override public int describeContents() { diff --git a/src/main/res/anim/animation_toggle_in.xml b/src/main/res/anim/animation_toggle_in.xml index ffffd02d1..463e1658e 100644 --- a/src/main/res/anim/animation_toggle_in.xml +++ b/src/main/res/anim/animation_toggle_in.xml @@ -1,15 +1,8 @@ - - + - \ No newline at end of file + diff --git a/src/main/res/anim/animation_toggle_out.xml b/src/main/res/anim/animation_toggle_out.xml index 1e6f7ed63..568709d8f 100644 --- a/src/main/res/anim/animation_toggle_out.xml +++ b/src/main/res/anim/animation_toggle_out.xml @@ -1,15 +1,8 @@ - - + - \ No newline at end of file + diff --git a/src/main/res/anim/fade_scale_in.xml b/src/main/res/anim/fade_scale_in.xml index 0f2def07d..f0defd29e 100644 --- a/src/main/res/anim/fade_scale_in.xml +++ b/src/main/res/anim/fade_scale_in.xml @@ -2,16 +2,9 @@ - - + - \ No newline at end of file + diff --git a/src/main/res/anim/fade_scale_out.xml b/src/main/res/anim/fade_scale_out.xml index 2ee729071..64e67f6f3 100644 --- a/src/main/res/anim/fade_scale_out.xml +++ b/src/main/res/anim/fade_scale_out.xml @@ -2,16 +2,9 @@ - - + - \ No newline at end of file + diff --git a/src/main/res/anim/slide_from_right.xml b/src/main/res/anim/slide_from_right.xml index 7dbec61f3..35bbd8616 100644 --- a/src/main/res/anim/slide_from_right.xml +++ b/src/main/res/anim/slide_from_right.xml @@ -1,9 +1,7 @@ - + - \ No newline at end of file + diff --git a/src/main/res/anim/slide_to_right.xml b/src/main/res/anim/slide_to_right.xml index c655fcd12..490df366c 100644 --- a/src/main/res/anim/slide_to_right.xml +++ b/src/main/res/anim/slide_to_right.xml @@ -1,9 +1,7 @@ - + - \ No newline at end of file + diff --git a/src/main/res/animator/bottom_pause_to_play_animation.xml b/src/main/res/animator/bottom_pause_to_play_animation.xml index f5b474bb7..7d0dba8c8 100644 --- a/src/main/res/animator/bottom_pause_to_play_animation.xml +++ b/src/main/res/animator/bottom_pause_to_play_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/animator/bottom_play_to_pause_animation.xml b/src/main/res/animator/bottom_play_to_pause_animation.xml index 4f2778d68..31f41823d 100644 --- a/src/main/res/animator/bottom_play_to_pause_animation.xml +++ b/src/main/res/animator/bottom_play_to_pause_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/animator/rotate_90_animation.xml b/src/main/res/animator/rotate_90_animation.xml index 7d44ce690..ca7fcba88 100644 --- a/src/main/res/animator/rotate_90_animation.xml +++ b/src/main/res/animator/rotate_90_animation.xml @@ -3,7 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@integer/play_button_animation_duration" android:interpolator="@android:anim/accelerate_decelerate_interpolator" - android:propertyName="rotation" - android:valueFrom="0" - android:valueTo="90" - android:valueType="floatType"/> \ No newline at end of file + android:propertyName="rotation" android:valueFrom="0" + android:valueTo="90" android:valueType="floatType" /> diff --git a/src/main/res/animator/rotate_minus_90_animation.xml b/src/main/res/animator/rotate_minus_90_animation.xml index ef9e1b6f1..d996528cb 100644 --- a/src/main/res/animator/rotate_minus_90_animation.xml +++ b/src/main/res/animator/rotate_minus_90_animation.xml @@ -3,7 +3,5 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@integer/play_button_animation_duration" android:interpolator="@android:anim/accelerate_decelerate_interpolator" - android:propertyName="rotation" - android:valueFrom="90" - android:valueTo="0" - android:valueType="floatType"/> \ No newline at end of file + android:propertyName="rotation" android:valueFrom="90" + android:valueTo="0" android:valueType="floatType" /> diff --git a/src/main/res/animator/upper_pause_to_play_animation.xml b/src/main/res/animator/upper_pause_to_play_animation.xml index 880c7b0b8..8dd9a7ca7 100644 --- a/src/main/res/animator/upper_pause_to_play_animation.xml +++ b/src/main/res/animator/upper_pause_to_play_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/animator/upper_play_to_pause_animation.xml b/src/main/res/animator/upper_play_to_pause_animation.xml index ffa933231..8a5aee5cb 100644 --- a/src/main/res/animator/upper_play_to_pause_animation.xml +++ b/src/main/res/animator/upper_play_to_pause_animation.xml @@ -1,8 +1,9 @@ - \ No newline at end of file + diff --git a/src/main/res/color/text_color_dark_theme.xml b/src/main/res/color/text_color_dark_theme.xml index 95308fe1a..1e257eec6 100644 --- a/src/main/res/color/text_color_dark_theme.xml +++ b/src/main/res/color/text_color_dark_theme.xml @@ -1,5 +1,6 @@ - - - - \ No newline at end of file + + + + diff --git a/src/main/res/color/text_color_secondary_dark_theme.xml b/src/main/res/color/text_color_secondary_dark_theme.xml index 5510b324c..dba7d4abf 100644 --- a/src/main/res/color/text_color_secondary_dark_theme.xml +++ b/src/main/res/color/text_color_secondary_dark_theme.xml @@ -1,5 +1,6 @@ - - - - \ No newline at end of file + + + + diff --git a/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml b/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml index fff6b14b6..830612ee8 100644 --- a/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml +++ b/src/main/res/drawable-ldrtl/message_bubble_background_received_alone.xml @@ -1,15 +1,13 @@ - + - - + diff --git a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml index 196688a09..e1482dd48 100644 --- a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml +++ b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone.xml @@ -1,15 +1,13 @@ - + - - + diff --git a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml index 315f63174..1ead16e9b 100644 --- a/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml +++ b/src/main/res/drawable-ldrtl/message_bubble_background_sent_alone_with_border.xml @@ -1,16 +1,14 @@ - + - - + diff --git a/src/main/res/drawable-night/button_background.xml b/src/main/res/drawable-night/button_background.xml index df5d0c54c..ee45e9b3d 100644 --- a/src/main/res/drawable-night/button_background.xml +++ b/src/main/res/drawable-night/button_background.xml @@ -1,6 +1,7 @@ - - - + + + diff --git a/src/main/res/drawable-night/button_secondary_background.xml b/src/main/res/drawable-night/button_secondary_background.xml index c6256b4c7..bec26b087 100644 --- a/src/main/res/drawable-night/button_secondary_background.xml +++ b/src/main/res/drawable-night/button_secondary_background.xml @@ -1,6 +1,7 @@ - - - + + + diff --git a/src/main/res/drawable-night/jumpto_btn_bg.xml b/src/main/res/drawable-night/jumpto_btn_bg.xml index be9119122..d13e03c9c 100644 --- a/src/main/res/drawable-night/jumpto_btn_bg.xml +++ b/src/main/res/drawable-night/jumpto_btn_bg.xml @@ -1,5 +1,5 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/archived_indicator_background.xml b/src/main/res/drawable/archived_indicator_background.xml index 046602984..412ae183b 100644 --- a/src/main/res/drawable/archived_indicator_background.xml +++ b/src/main/res/drawable/archived_indicator_background.xml @@ -1,6 +1,8 @@ - - - - + + + + diff --git a/src/main/res/drawable/attachment_selector_shadow.xml b/src/main/res/drawable/attachment_selector_shadow.xml index 34e2aa8cd..1ac5df7b0 100644 --- a/src/main/res/drawable/attachment_selector_shadow.xml +++ b/src/main/res/drawable/attachment_selector_shadow.xml @@ -1,11 +1,8 @@ - - + - \ No newline at end of file + diff --git a/src/main/res/drawable/badge_divider.xml b/src/main/res/drawable/badge_divider.xml index 9db9b4665..fc8a65056 100644 --- a/src/main/res/drawable/badge_divider.xml +++ b/src/main/res/drawable/badge_divider.xml @@ -1,5 +1,4 @@ - + diff --git a/src/main/res/drawable/baseline_bookmark_24.xml b/src/main/res/drawable/baseline_bookmark_24.xml index 77251324b..b20fc7ccb 100644 --- a/src/main/res/drawable/baseline_bookmark_24.xml +++ b/src/main/res/drawable/baseline_bookmark_24.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/baseline_bookmark_border_24.xml b/src/main/res/drawable/baseline_bookmark_border_24.xml index d64b0ea27..75b0d50f2 100644 --- a/src/main/res/drawable/baseline_bookmark_border_24.xml +++ b/src/main/res/drawable/baseline_bookmark_border_24.xml @@ -1,5 +1,10 @@ - - - - + + + + diff --git a/src/main/res/drawable/baseline_bookmark_remove_24.xml b/src/main/res/drawable/baseline_bookmark_remove_24.xml index 1fb5c5a92..b1b5dd00a 100644 --- a/src/main/res/drawable/baseline_bookmark_remove_24.xml +++ b/src/main/res/drawable/baseline_bookmark_remove_24.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/baseline_call_24.xml b/src/main/res/drawable/baseline_call_24.xml index 2aba8addc..2f628e63a 100644 --- a/src/main/res/drawable/baseline_call_24.xml +++ b/src/main/res/drawable/baseline_call_24.xml @@ -1,5 +1,7 @@ - + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/baseline_call_end_24.xml b/src/main/res/drawable/baseline_call_end_24.xml index fa4cd2c10..d7bcb121d 100644 --- a/src/main/res/drawable/baseline_call_end_24.xml +++ b/src/main/res/drawable/baseline_call_end_24.xml @@ -1,5 +1,10 @@ - - - - + + + + diff --git a/src/main/res/drawable/button_background.xml b/src/main/res/drawable/button_background.xml index 1e8569c2a..51218ce3b 100644 --- a/src/main/res/drawable/button_background.xml +++ b/src/main/res/drawable/button_background.xml @@ -1,6 +1,7 @@ - - - + + + diff --git a/src/main/res/drawable/button_secondary_background.xml b/src/main/res/drawable/button_secondary_background.xml index e8f915706..91b60c3bb 100644 --- a/src/main/res/drawable/button_secondary_background.xml +++ b/src/main/res/drawable/button_secondary_background.xml @@ -1,6 +1,7 @@ - - - + + + diff --git a/src/main/res/drawable/circle_alpha.xml b/src/main/res/drawable/circle_alpha.xml index 82142dd34..a96042d79 100644 --- a/src/main/res/drawable/circle_alpha.xml +++ b/src/main/res/drawable/circle_alpha.xml @@ -1,5 +1,5 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/circle_tintable.xml b/src/main/res/drawable/circle_tintable.xml index 6c5c36063..d043d2650 100644 --- a/src/main/res/drawable/circle_tintable.xml +++ b/src/main/res/drawable/circle_tintable.xml @@ -1,5 +1,5 @@ + android:shape="oval"> diff --git a/src/main/res/drawable/circle_touch_highlight_background.xml b/src/main/res/drawable/circle_touch_highlight_background.xml index fe392b45e..35ec52275 100644 --- a/src/main/res/drawable/circle_touch_highlight_background.xml +++ b/src/main/res/drawable/circle_touch_highlight_background.xml @@ -1,9 +1,10 @@ - + - \ No newline at end of file + diff --git a/src/main/res/drawable/circle_universal_overlay.xml b/src/main/res/drawable/circle_universal_overlay.xml index 32c516245..1f9ffc11c 100644 --- a/src/main/res/drawable/circle_universal_overlay.xml +++ b/src/main/res/drawable/circle_universal_overlay.xml @@ -1,4 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/src/main/res/drawable/circle_white.xml b/src/main/res/drawable/circle_white.xml index 631057c46..d043d2650 100644 --- a/src/main/res/drawable/circle_white.xml +++ b/src/main/res/drawable/circle_white.xml @@ -1,4 +1,5 @@ - - - \ No newline at end of file + + + diff --git a/src/main/res/drawable/compose_divider_background.xml b/src/main/res/drawable/compose_divider_background.xml index 0046fe4e4..4c348824d 100644 --- a/src/main/res/drawable/compose_divider_background.xml +++ b/src/main/res/drawable/compose_divider_background.xml @@ -1,8 +1,6 @@ - diff --git a/src/main/res/drawable/contact_blocked_24.xml b/src/main/res/drawable/contact_blocked_24.xml index 3368f8111..37d7ccca2 100644 --- a/src/main/res/drawable/contact_blocked_24.xml +++ b/src/main/res/drawable/contact_blocked_24.xml @@ -1,5 +1,10 @@ - - - - + + + + diff --git a/src/main/res/drawable/contact_list_divider_dark.xml b/src/main/res/drawable/contact_list_divider_dark.xml index ab63ce24a..2e6462d48 100644 --- a/src/main/res/drawable/contact_list_divider_dark.xml +++ b/src/main/res/drawable/contact_list_divider_dark.xml @@ -1,11 +1,13 @@ - + - + - + diff --git a/src/main/res/drawable/contact_list_divider_light.xml b/src/main/res/drawable/contact_list_divider_light.xml index f28cf73f6..64aac47cd 100644 --- a/src/main/res/drawable/contact_list_divider_light.xml +++ b/src/main/res/drawable/contact_list_divider_light.xml @@ -1,12 +1,14 @@ - + - + - + - \ No newline at end of file + diff --git a/src/main/res/drawable/contact_photo_background.xml b/src/main/res/drawable/contact_photo_background.xml index ff1153bf2..18105d4b0 100644 --- a/src/main/res/drawable/contact_photo_background.xml +++ b/src/main/res/drawable/contact_photo_background.xml @@ -1,5 +1,6 @@ - + @@ -7,4 +8,4 @@ - \ No newline at end of file + diff --git a/src/main/res/drawable/conversation_attachment_close_circle.xml b/src/main/res/drawable/conversation_attachment_close_circle.xml index 86741f86e..024b0276f 100644 --- a/src/main/res/drawable/conversation_attachment_close_circle.xml +++ b/src/main/res/drawable/conversation_attachment_close_circle.xml @@ -1,15 +1,16 @@ - + - - + + + android:width="3dp" /> + android:gravity="center" /> diff --git a/src/main/res/drawable/conversation_attachment_edit.xml b/src/main/res/drawable/conversation_attachment_edit.xml index a72bb14ed..74d48a538 100644 --- a/src/main/res/drawable/conversation_attachment_edit.xml +++ b/src/main/res/drawable/conversation_attachment_edit.xml @@ -1,16 +1,14 @@ - + - - + + + android:left="7dp" android:right="7dp" android:top="7dp" + android:bottom="7dp" android:scaleType="centerInside"> diff --git a/src/main/res/drawable/conversation_item_background.xml b/src/main/res/drawable/conversation_item_background.xml index 6f1ea5abf..0d8064e49 100644 --- a/src/main/res/drawable/conversation_item_background.xml +++ b/src/main/res/drawable/conversation_item_background.xml @@ -1,5 +1,8 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_background_animated.xml b/src/main/res/drawable/conversation_item_background_animated.xml index cb8539799..3bd618117 100644 --- a/src/main/res/drawable/conversation_item_background_animated.xml +++ b/src/main/res/drawable/conversation_item_background_animated.xml @@ -1,5 +1,9 @@ - - - + + + diff --git a/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml b/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml index 59da5934a..f2de1a1f5 100644 --- a/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml +++ b/src/main/res/drawable/conversation_item_sent_indicator_text_shape.xml @@ -1,17 +1,21 @@ - + - + - + - + diff --git a/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml b/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml index d02a39156..934db050d 100644 --- a/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml +++ b/src/main/res/drawable/conversation_item_sent_indicator_text_shape_dark.xml @@ -1,17 +1,21 @@ - + - + - + - + diff --git a/src/main/res/drawable/conversation_item_update_background.xml b/src/main/res/drawable/conversation_item_update_background.xml index 6a7c72c11..ae561ba75 100644 --- a/src/main/res/drawable/conversation_item_update_background.xml +++ b/src/main/res/drawable/conversation_item_update_background.xml @@ -1,7 +1,8 @@ - + diff --git a/src/main/res/drawable/conversation_list_divider_shape.xml b/src/main/res/drawable/conversation_list_divider_shape.xml index 459e9420a..62473faf1 100644 --- a/src/main/res/drawable/conversation_list_divider_shape.xml +++ b/src/main/res/drawable/conversation_list_divider_shape.xml @@ -1,5 +1,6 @@ - + diff --git a/src/main/res/drawable/conversation_list_divider_shape_dark.xml b/src/main/res/drawable/conversation_list_divider_shape_dark.xml index 0ca1786e2..31d385415 100644 --- a/src/main/res/drawable/conversation_list_divider_shape_dark.xml +++ b/src/main/res/drawable/conversation_list_divider_shape_dark.xml @@ -1,5 +1,6 @@ - + diff --git a/src/main/res/drawable/conversation_list_item_background.xml b/src/main/res/drawable/conversation_list_item_background.xml index 0c39d2b4e..7780ba747 100644 --- a/src/main/res/drawable/conversation_list_item_background.xml +++ b/src/main/res/drawable/conversation_list_item_background.xml @@ -1,10 +1,13 @@ - - + + - + diff --git a/src/main/res/drawable/conversation_list_item_background_dark.xml b/src/main/res/drawable/conversation_list_item_background_dark.xml index 0c39d2b4e..7780ba747 100644 --- a/src/main/res/drawable/conversation_list_item_background_dark.xml +++ b/src/main/res/drawable/conversation_list_item_background_dark.xml @@ -1,10 +1,13 @@ - - + + - + diff --git a/src/main/res/drawable/delete_account_item_background.xml b/src/main/res/drawable/delete_account_item_background.xml index 583b62b94..9b197647d 100644 --- a/src/main/res/drawable/delete_account_item_background.xml +++ b/src/main/res/drawable/delete_account_item_background.xml @@ -1,7 +1,8 @@ - + diff --git a/src/main/res/drawable/dismiss_background.xml b/src/main/res/drawable/dismiss_background.xml index d373de262..0da640648 100644 --- a/src/main/res/drawable/dismiss_background.xml +++ b/src/main/res/drawable/dismiss_background.xml @@ -1,5 +1,5 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/floating_mini_bg_dark.xml b/src/main/res/drawable/floating_mini_bg_dark.xml index 617e35d5e..d6e0ad106 100644 --- a/src/main/res/drawable/floating_mini_bg_dark.xml +++ b/src/main/res/drawable/floating_mini_bg_dark.xml @@ -1,6 +1,7 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/floating_mini_bg_light.xml b/src/main/res/drawable/floating_mini_bg_light.xml index 3b24b52c7..a8dabab0a 100644 --- a/src/main/res/drawable/floating_mini_bg_light.xml +++ b/src/main/res/drawable/floating_mini_bg_light.xml @@ -1,6 +1,7 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/ic_advanced_24dp.xml b/src/main/res/drawable/ic_advanced_24dp.xml index 4fb9e1ab0..a3028d6f9 100644 --- a/src/main/res/drawable/ic_advanced_24dp.xml +++ b/src/main/res/drawable/ic_advanced_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_apps_24.xml b/src/main/res/drawable/ic_apps_24.xml index f578daeb2..5f18e4566 100644 --- a/src/main/res/drawable/ic_apps_24.xml +++ b/src/main/res/drawable/ic_apps_24.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/ic_baseline_devices_24.xml b/src/main/res/drawable/ic_baseline_devices_24.xml index 28a718506..fb66dd52e 100644 --- a/src/main/res/drawable/ic_baseline_devices_24.xml +++ b/src/main/res/drawable/ic_baseline_devices_24.xml @@ -1,5 +1,7 @@ - + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/ic_baseline_eye.xml b/src/main/res/drawable/ic_baseline_eye.xml index c74c5a758..9ddf987be 100644 --- a/src/main/res/drawable/ic_baseline_eye.xml +++ b/src/main/res/drawable/ic_baseline_eye.xml @@ -1,9 +1,8 @@ - - + diff --git a/src/main/res/drawable/ic_brightness_6_24dp.xml b/src/main/res/drawable/ic_brightness_6_24dp.xml index 548ad986f..aaa84c514 100644 --- a/src/main/res/drawable/ic_brightness_6_24dp.xml +++ b/src/main/res/drawable/ic_brightness_6_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_chevron_up.xml b/src/main/res/drawable/ic_chevron_up.xml index c54db3bcc..d59670421 100644 --- a/src/main/res/drawable/ic_chevron_up.xml +++ b/src/main/res/drawable/ic_chevron_up.xml @@ -1,9 +1,7 @@ - - + diff --git a/src/main/res/drawable/ic_circle_status_online.xml b/src/main/res/drawable/ic_circle_status_online.xml index b481cbb18..d3635375e 100644 --- a/src/main/res/drawable/ic_circle_status_online.xml +++ b/src/main/res/drawable/ic_circle_status_online.xml @@ -1,6 +1,5 @@ - diff --git a/src/main/res/drawable/ic_delivery_status_failed.xml b/src/main/res/drawable/ic_delivery_status_failed.xml index fbd4009fd..7681e0478 100644 --- a/src/main/res/drawable/ic_delivery_status_failed.xml +++ b/src/main/res/drawable/ic_delivery_status_failed.xml @@ -1,5 +1,7 @@ - + android:width="16dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/ic_donate_24dp.xml b/src/main/res/drawable/ic_donate_24dp.xml index 07ab6aae9..eaf5f502f 100644 --- a/src/main/res/drawable/ic_donate_24dp.xml +++ b/src/main/res/drawable/ic_donate_24dp.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/ic_forum_24dp.xml b/src/main/res/drawable/ic_forum_24dp.xml index e48c05fd4..7318e8df8 100644 --- a/src/main/res/drawable/ic_forum_24dp.xml +++ b/src/main/res/drawable/ic_forum_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_forward_white_24dp.xml b/src/main/res/drawable/ic_forward_white_24dp.xml index 4b84d9a42..9ee2bfe8e 100644 --- a/src/main/res/drawable/ic_forward_white_24dp.xml +++ b/src/main/res/drawable/ic_forward_white_24dp.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/ic_help_24dp.xml b/src/main/res/drawable/ic_help_24dp.xml index 66449b72f..b733dcf5f 100644 --- a/src/main/res/drawable/ic_help_24dp.xml +++ b/src/main/res/drawable/ic_help_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_launcher_foreground_monochrome.xml b/src/main/res/drawable/ic_launcher_foreground_monochrome.xml index 8ef65e2a8..a12b6328c 100644 --- a/src/main/res/drawable/ic_launcher_foreground_monochrome.xml +++ b/src/main/res/drawable/ic_launcher_foreground_monochrome.xml @@ -1,15 +1,11 @@ - - - - + + + diff --git a/src/main/res/drawable/ic_link_24.xml b/src/main/res/drawable/ic_link_24.xml index 54d9934a3..a0ca66534 100644 --- a/src/main/res/drawable/ic_link_24.xml +++ b/src/main/res/drawable/ic_link_24.xml @@ -1,8 +1,11 @@ - - + - + diff --git a/src/main/res/drawable/ic_lock.xml b/src/main/res/drawable/ic_lock.xml index b642ae75e..b739bf044 100644 --- a/src/main/res/drawable/ic_lock.xml +++ b/src/main/res/drawable/ic_lock.xml @@ -1,9 +1,7 @@ - - + diff --git a/src/main/res/drawable/ic_map_white_24dp.xml b/src/main/res/drawable/ic_map_white_24dp.xml index efd6dc37e..b7862e2e5 100644 --- a/src/main/res/drawable/ic_map_white_24dp.xml +++ b/src/main/res/drawable/ic_map_white_24dp.xml @@ -1,5 +1,7 @@ - + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/ic_notifications_24dp.xml b/src/main/res/drawable/ic_notifications_24dp.xml index 56cd14d7a..9788adcc3 100644 --- a/src/main/res/drawable/ic_notifications_24dp.xml +++ b/src/main/res/drawable/ic_notifications_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_outline_email.xml b/src/main/res/drawable/ic_outline_email.xml index 19ab62ba4..491aaeaaa 100644 --- a/src/main/res/drawable/ic_outline_email.xml +++ b/src/main/res/drawable/ic_outline_email.xml @@ -1,11 +1,9 @@ + android:height="18sp" android:width="18sp" android:viewportHeight="24" + android:viewportWidth="24" android:tint="#FFFFFF"> - + - \ No newline at end of file + diff --git a/src/main/res/drawable/ic_person_large.xml b/src/main/res/drawable/ic_person_large.xml index c3324be1d..eae7ea4a7 100644 --- a/src/main/res/drawable/ic_person_large.xml +++ b/src/main/res/drawable/ic_person_large.xml @@ -1,4 +1,6 @@ - + android:viewportWidth="24.0" android:width="260dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/ic_proxy_disabled_24.xml b/src/main/res/drawable/ic_proxy_disabled_24.xml index 287bc46d2..dca174b60 100644 --- a/src/main/res/drawable/ic_proxy_disabled_24.xml +++ b/src/main/res/drawable/ic_proxy_disabled_24.xml @@ -1,5 +1,10 @@ - - - - + + + + diff --git a/src/main/res/drawable/ic_proxy_enabled_24.xml b/src/main/res/drawable/ic_proxy_enabled_24.xml index 81c20e883..9638e5495 100644 --- a/src/main/res/drawable/ic_proxy_enabled_24.xml +++ b/src/main/res/drawable/ic_proxy_enabled_24.xml @@ -1,5 +1,10 @@ - - - - + + + + diff --git a/src/main/res/drawable/ic_qr_code_24.xml b/src/main/res/drawable/ic_qr_code_24.xml index f95c18d61..b502f081a 100644 --- a/src/main/res/drawable/ic_qr_code_24.xml +++ b/src/main/res/drawable/ic_qr_code_24.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/ic_qr_code_scanner_24.xml b/src/main/res/drawable/ic_qr_code_scanner_24.xml index 725b35ea7..2afb21ed8 100644 --- a/src/main/res/drawable/ic_qr_code_scanner_24.xml +++ b/src/main/res/drawable/ic_qr_code_scanner_24.xml @@ -1,5 +1,10 @@ - + - + diff --git a/src/main/res/drawable/ic_share_white_24dp.xml b/src/main/res/drawable/ic_share_white_24dp.xml index 045bbc0c0..07e50097e 100644 --- a/src/main/res/drawable/ic_share_white_24dp.xml +++ b/src/main/res/drawable/ic_share_white_24dp.xml @@ -1,5 +1,7 @@ - + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/ic_sticker_24.xml b/src/main/res/drawable/ic_sticker_24.xml index 102dedfd5..e84f09af2 100644 --- a/src/main/res/drawable/ic_sticker_24.xml +++ b/src/main/res/drawable/ic_sticker_24.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/ic_swap_vert_24dp.xml b/src/main/res/drawable/ic_swap_vert_24dp.xml index c54dc343d..a67392a3d 100644 --- a/src/main/res/drawable/ic_swap_vert_24dp.xml +++ b/src/main/res/drawable/ic_swap_vert_24dp.xml @@ -1,4 +1,5 @@ - + android:tint="?attr/pref_icon_tint" /> diff --git a/src/main/res/drawable/ic_timer_gray_18dp.xml b/src/main/res/drawable/ic_timer_gray_18dp.xml index 52a5c9218..02e268d7a 100644 --- a/src/main/res/drawable/ic_timer_gray_18dp.xml +++ b/src/main/res/drawable/ic_timer_gray_18dp.xml @@ -1,5 +1,7 @@ - + android:width="18dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/ic_verified.xml b/src/main/res/drawable/ic_verified.xml index c8b706df6..78d1019c3 100644 --- a/src/main/res/drawable/ic_verified.xml +++ b/src/main/res/drawable/ic_verified.xml @@ -1,8 +1,10 @@ + android:viewportWidth="1325.6" android:width="18sp" + xmlns:android="http://schemas.android.com/apk/res/android"> + android:strokeColor="#00000000" android:strokeWidth="206.103" /> + android:pathData="M643.6,1303C555.7,1295.1 479.5,1229.9 452.5,1146.8 365.5,1173.3 257.1,1169.4 192.1,1098 135.6,1041.4 126.1,954.9 133.2,879.5 28,840.4 -23.6,710.9 10.9,606.9 31.5,539.3 89.5,491.4 151,461.3 132.5,383.7 109.5,287.3 176.8,208.6 245.9,117.6 351.5,123.1 457,148.5 487.4,54.1 587.2,-8.2 685.1,1.1 768.6,4.6 848,60.7 874.9,140.6c74.6,-12.9 160.9,-20.5 223.1,31.8 50.7,39.9 88.8,99.2 87.8,165.5 0.9,40.1 -11.3,80.4 -17.3,118.5 91,17 150.8,110.4 156,198.8 -1.4,112 -39.9,161.5 -142.2,226.4 -23.1,9.2 -7.6,-23.6 0.1,0.2 15,101.5 -12.8,196.7 -94.3,247 -61.8,38 -138.4,38.2 -207.8,25.9 -31.3,91.2 -125,155 -221.5,149.4 -5.1,0.5 -10.1,-0.7 -15.1,-1.1zM1035.6,509.5 L915.2,385.5 612.9,685.4 411,498.7 289.1,614.8 603.1,931.9Z" + android:strokeWidth="1.33333" /> diff --git a/src/main/res/drawable/ic_videocam_white_24dp.xml b/src/main/res/drawable/ic_videocam_white_24dp.xml index f75fa71ae..a9d24df81 100644 --- a/src/main/res/drawable/ic_videocam_white_24dp.xml +++ b/src/main/res/drawable/ic_videocam_white_24dp.xml @@ -1,5 +1,7 @@ - + android:width="24dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + diff --git a/src/main/res/drawable/image_shade.xml b/src/main/res/drawable/image_shade.xml index 07a18cb4d..3c88c4d23 100644 --- a/src/main/res/drawable/image_shade.xml +++ b/src/main/res/drawable/image_shade.xml @@ -1,8 +1,7 @@ - - \ No newline at end of file + diff --git a/src/main/res/drawable/jumpto_btn_bg.xml b/src/main/res/drawable/jumpto_btn_bg.xml index 82142dd34..a96042d79 100644 --- a/src/main/res/drawable/jumpto_btn_bg.xml +++ b/src/main/res/drawable/jumpto_btn_bg.xml @@ -1,5 +1,5 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/message_bubble_background_received_alone.xml b/src/main/res/drawable/message_bubble_background_received_alone.xml index 196688a09..e1482dd48 100644 --- a/src/main/res/drawable/message_bubble_background_received_alone.xml +++ b/src/main/res/drawable/message_bubble_background_received_alone.xml @@ -1,15 +1,13 @@ - + - - + diff --git a/src/main/res/drawable/message_bubble_background_sent_alone.xml b/src/main/res/drawable/message_bubble_background_sent_alone.xml index fff6b14b6..830612ee8 100644 --- a/src/main/res/drawable/message_bubble_background_sent_alone.xml +++ b/src/main/res/drawable/message_bubble_background_sent_alone.xml @@ -1,15 +1,13 @@ - + - - + diff --git a/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml b/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml index e8ac29c9a..57ab9be81 100644 --- a/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml +++ b/src/main/res/drawable/message_bubble_background_sent_alone_with_border.xml @@ -1,16 +1,14 @@ - + - - + diff --git a/src/main/res/drawable/pause_icon.xml b/src/main/res/drawable/pause_icon.xml index e7f9fce53..83ca71873 100644 --- a/src/main/res/drawable/pause_icon.xml +++ b/src/main/res/drawable/pause_icon.xml @@ -1,35 +1,25 @@ - + - + android:strokeLineCap="butt" android:strokeLineJoin="miter" + android:strokeMiterLimit="10" /> - + android:strokeLineCap="butt" android:strokeLineJoin="miter" + android:strokeMiterLimit="10" /> - \ No newline at end of file + diff --git a/src/main/res/drawable/pause_to_play_animation.xml b/src/main/res/drawable/pause_to_play_animation.xml index aa010cee9..97e7ca673 100644 --- a/src/main/res/drawable/pause_to_play_animation.xml +++ b/src/main/res/drawable/pause_to_play_animation.xml @@ -1,16 +1,14 @@ - - + - + - + diff --git a/src/main/res/drawable/pinned_list_item_background.xml b/src/main/res/drawable/pinned_list_item_background.xml index 87ed120e6..e6afee5b8 100644 --- a/src/main/res/drawable/pinned_list_item_background.xml +++ b/src/main/res/drawable/pinned_list_item_background.xml @@ -1,10 +1,13 @@ - - + + - + diff --git a/src/main/res/drawable/pinned_list_item_background_dark.xml b/src/main/res/drawable/pinned_list_item_background_dark.xml index 56643308f..b2de727fe 100644 --- a/src/main/res/drawable/pinned_list_item_background_dark.xml +++ b/src/main/res/drawable/pinned_list_item_background_dark.xml @@ -1,10 +1,13 @@ - - + + - + diff --git a/src/main/res/drawable/play_icon.xml b/src/main/res/drawable/play_icon.xml index 7472ac7de..825a2a641 100644 --- a/src/main/res/drawable/play_icon.xml +++ b/src/main/res/drawable/play_icon.xml @@ -1,34 +1,25 @@ - - + android:strokeLineCap="butt" android:strokeLineJoin="miter" + android:strokeMiterLimit="10" /> - + android:strokeLineCap="butt" android:strokeLineJoin="miter" + android:strokeMiterLimit="10" /> - \ No newline at end of file + diff --git a/src/main/res/drawable/play_to_pause_animation.xml b/src/main/res/drawable/play_to_pause_animation.xml index 173fed50f..bd06f4040 100644 --- a/src/main/res/drawable/play_to_pause_animation.xml +++ b/src/main/res/drawable/play_to_pause_animation.xml @@ -1,16 +1,14 @@ - - + - + - + diff --git a/src/main/res/drawable/reaction_pill_background.xml b/src/main/res/drawable/reaction_pill_background.xml index bad81d54c..baf0303d1 100644 --- a/src/main/res/drawable/reaction_pill_background.xml +++ b/src/main/res/drawable/reaction_pill_background.xml @@ -2,5 +2,6 @@ - + diff --git a/src/main/res/drawable/reaction_pill_background_selected.xml b/src/main/res/drawable/reaction_pill_background_selected.xml index b52af9b3d..e7f3b717d 100644 --- a/src/main/res/drawable/reaction_pill_background_selected.xml +++ b/src/main/res/drawable/reaction_pill_background_selected.xml @@ -2,5 +2,6 @@ - + diff --git a/src/main/res/drawable/recording_lock_background_dark.xml b/src/main/res/drawable/recording_lock_background_dark.xml index 5dda37904..50211a61c 100644 --- a/src/main/res/drawable/recording_lock_background_dark.xml +++ b/src/main/res/drawable/recording_lock_background_dark.xml @@ -1,12 +1,9 @@ - - + - + diff --git a/src/main/res/drawable/recording_lock_background_light.xml b/src/main/res/drawable/recording_lock_background_light.xml index d2e3a753e..c8daf884f 100644 --- a/src/main/res/drawable/recording_lock_background_light.xml +++ b/src/main/res/drawable/recording_lock_background_light.xml @@ -1,12 +1,9 @@ - - + - + diff --git a/src/main/res/drawable/rounded_arrow_forward_24.xml b/src/main/res/drawable/rounded_arrow_forward_24.xml index 29b2576d4..6598f995b 100644 --- a/src/main/res/drawable/rounded_arrow_forward_24.xml +++ b/src/main/res/drawable/rounded_arrow_forward_24.xml @@ -1,3 +1,8 @@ - - + + diff --git a/src/main/res/drawable/search_toolbar_shadow.xml b/src/main/res/drawable/search_toolbar_shadow.xml index 5afdc2a2d..9855fd398 100644 --- a/src/main/res/drawable/search_toolbar_shadow.xml +++ b/src/main/res/drawable/search_toolbar_shadow.xml @@ -1,7 +1,5 @@ - - \ No newline at end of file + android:shape="rectangle"> + + diff --git a/src/main/res/drawable/send_button_bg_dark.xml b/src/main/res/drawable/send_button_bg_dark.xml index 79ecf53d3..13bb88de4 100644 --- a/src/main/res/drawable/send_button_bg_dark.xml +++ b/src/main/res/drawable/send_button_bg_dark.xml @@ -1,6 +1,7 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/send_button_bg_light.xml b/src/main/res/drawable/send_button_bg_light.xml index 1b8f655e9..e7347259a 100644 --- a/src/main/res/drawable/send_button_bg_light.xml +++ b/src/main/res/drawable/send_button_bg_light.xml @@ -1,6 +1,7 @@ - + android:shape="oval"> + diff --git a/src/main/res/drawable/sticker_missing_background.xml b/src/main/res/drawable/sticker_missing_background.xml index c9ce98f2b..033295ea6 100644 --- a/src/main/res/drawable/sticker_missing_background.xml +++ b/src/main/res/drawable/sticker_missing_background.xml @@ -1,6 +1,5 @@ - diff --git a/src/main/res/drawable/sticky_date_header_background_dark.xml b/src/main/res/drawable/sticky_date_header_background_dark.xml index d5be2c288..38d2b2168 100644 --- a/src/main/res/drawable/sticky_date_header_background_dark.xml +++ b/src/main/res/drawable/sticky_date_header_background_dark.xml @@ -1,8 +1,7 @@ - - \ No newline at end of file + diff --git a/src/main/res/drawable/sticky_date_header_background_light.xml b/src/main/res/drawable/sticky_date_header_background_light.xml index b015b381b..c039ee5c6 100644 --- a/src/main/res/drawable/sticky_date_header_background_light.xml +++ b/src/main/res/drawable/sticky_date_header_background_light.xml @@ -1,8 +1,7 @@ - - \ No newline at end of file + diff --git a/src/main/res/drawable/touch_highlight_background.xml b/src/main/res/drawable/touch_highlight_background.xml index 80e27edf3..10fa65265 100644 --- a/src/main/res/drawable/touch_highlight_background.xml +++ b/src/main/res/drawable/touch_highlight_background.xml @@ -1,7 +1,7 @@ - - - \ No newline at end of file + diff --git a/src/main/res/drawable/touch_highlight_background_strong.xml b/src/main/res/drawable/touch_highlight_background_strong.xml index 315d3537f..dd0755508 100644 --- a/src/main/res/drawable/touch_highlight_background_strong.xml +++ b/src/main/res/drawable/touch_highlight_background_strong.xml @@ -1,12 +1,15 @@ - - + - + - + diff --git a/src/main/res/drawable/triangle_right.xml b/src/main/res/drawable/triangle_right.xml index d6f003ef8..8367676de 100644 --- a/src/main/res/drawable/triangle_right.xml +++ b/src/main/res/drawable/triangle_right.xml @@ -1,13 +1,11 @@ - + android:strokeColor="#00000000" android:fillColor="#ffffff" /> diff --git a/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 36f6533f9..b4b122543 100644 --- a/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,9 @@ - - - - + + + + diff --git a/src/main/res/xml-v29/preferences_appearance.xml b/src/main/res/xml-v29/preferences_appearance.xml index b5dc15490..ce40e4241 100644 --- a/src/main/res/xml-v29/preferences_appearance.xml +++ b/src/main/res/xml-v29/preferences_appearance.xml @@ -1,18 +1,18 @@ - - + + - + - + diff --git a/src/main/res/xml/file_provider_paths.xml b/src/main/res/xml/file_provider_paths.xml index 77df3dcae..79b97d51c 100644 --- a/src/main/res/xml/file_provider_paths.xml +++ b/src/main/res/xml/file_provider_paths.xml @@ -3,8 +3,8 @@ - - - - - \ No newline at end of file + + + + + diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index be94c2100..8134bc508 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -1,51 +1,52 @@ - + + android:key="preference_category_profile" /> + android:title="@string/pref_chats" + android:icon="@drawable/ic_forum_24dp" + android:layout="@layout/preference_item" /> + android:title="@string/pref_notifications" + android:icon="@drawable/ic_notifications_24dp" + android:layout="@layout/preference_item" /> + android:title="@string/pref_appearance" + android:icon="@drawable/ic_brightness_6_24dp" + android:layout="@layout/preference_item" /> + android:title="@string/multidevice_title" + android:icon="@drawable/ic_baseline_devices_24" + android:layout="@layout/preference_item" /> + android:title="@string/connectivity" + android:icon="@drawable/ic_swap_vert_24dp" + android:layout="@layout/preference_item" /> + android:title="@string/menu_advanced" + android:icon="@drawable/ic_advanced_24dp" + android:layout="@layout/preference_item" /> - + - + diff --git a/src/main/res/xml/preferences_advanced.xml b/src/main/res/xml/preferences_advanced.xml index 7a1a8dad3..028250a62 100644 --- a/src/main/res/xml/preferences_advanced.xml +++ b/src/main/res/xml/preferences_advanced.xml @@ -1,89 +1,84 @@ - + + android:title="@string/pref_view_log" /> - + + android:title="@string/transports" /> + android:title="@string/proxy_settings" /> + android:summary="@string/pref_multidevice_explain" /> - + + android:defaultValue="false" android:key="pref_calls_enabled" + android:title="Debug Calls" /> + android:defaultValue="false" android:key="pref_new_broadcast_list" + android:title="@string/channels" /> + android:title="@string/pref_on_demand_location_streaming" /> + android:summary="@string/none" /> - + - + android:defaultValue="true" android:key="pref_mvbox_move" + android:title="@string/pref_auto_folder_moves" /> diff --git a/src/main/res/xml/preferences_appearance.xml b/src/main/res/xml/preferences_appearance.xml index d41fca347..889d9aef5 100644 --- a/src/main/res/xml/preferences_appearance.xml +++ b/src/main/res/xml/preferences_appearance.xml @@ -1,18 +1,18 @@ - - + + - + - + diff --git a/src/main/res/xml/preferences_chats.xml b/src/main/res/xml/preferences_chats.xml index 35a73f505..ec7d07006 100644 --- a/src/main/res/xml/preferences_chats.xml +++ b/src/main/res/xml/preferences_chats.xml @@ -1,54 +1,50 @@ - + - + android:title="@string/pref_enter_sends" /> - - + android:entryValues="@array/pref_download_limit_values" /> - - + + android:entryValues="@array/autodel_device_values" /> - - + + android:title="@string/export_backup_desktop" /> diff --git a/src/main/res/xml/preferences_notifications.xml b/src/main/res/xml/preferences_notifications.xml index b96873fc6..27d050bde 100644 --- a/src/main/res/xml/preferences_notifications.xml +++ b/src/main/res/xml/preferences_notifications.xml @@ -1,73 +1,66 @@ - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + android:key="pref_reliable_service2" android:defaultValue="false" + android:title="@string/pref_reliable_service" + android:summary="@string/pref_reliable_service_explain" /> + diff --git a/src/main/res/xml/shortcuts.xml b/src/main/res/xml/shortcuts.xml index a1528dae5..4719e27b9 100644 --- a/src/main/res/xml/shortcuts.xml +++ b/src/main/res/xml/shortcuts.xml @@ -1,6 +1,8 @@ - - - - - + + + + +