Merge remote-tracking branch 'origin/fenix/116.0' into iceraven
commit
b45e8cd324
@ -1 +1 @@
|
||||
Subproject commit ac015fe2d5ef0700f93e40b62094f1cf79edcf86
|
||||
Subproject commit 51fe6a7e4c1e4d01946aadddf21b788e8ce7fff7
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,91 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.ui
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.customannotations.SmokeTest
|
||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS
|
||||
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
|
||||
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
|
||||
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
|
||||
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
|
||||
import org.mozilla.fenix.helpers.TestHelper.mDevice
|
||||
import org.mozilla.fenix.ui.robots.clickPageObject
|
||||
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||
|
||||
class PDFViewerTest {
|
||||
private lateinit var mockWebServer: MockWebServer
|
||||
|
||||
@get:Rule
|
||||
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
mockWebServer = MockWebServer().apply {
|
||||
dispatcher = AndroidAssetDispatcher()
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
@SmokeTest
|
||||
@Test
|
||||
fun openPDFInBrowserTest() {
|
||||
val genericURL =
|
||||
TestAssetHelper.getGenericAsset(mockWebServer, 3)
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(genericURL.url) {
|
||||
clickPageObject(itemContainingText("PDF form file"))
|
||||
verifyPageContent("Washington Crossing the Delaware")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun pdfViewerOpenInAppTest() {
|
||||
val genericURL = getGenericAsset(mockWebServer, 3)
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(genericURL.url) {
|
||||
clickPageObject(itemWithText("PDF form file"))
|
||||
verifyPDFReaderToolbarItems()
|
||||
clickPageObject(itemWithResIdAndText("openInApp", "Open in app"))
|
||||
assertExternalAppOpens(GOOGLE_DOCS)
|
||||
}
|
||||
}
|
||||
|
||||
// Download PDF file using the download toolbar button
|
||||
@Test
|
||||
fun pdfViewerDownloadButtonTest() {
|
||||
val genericURL = getGenericAsset(mockWebServer, 3)
|
||||
val downloadFile = "pdfForm.pdf"
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(genericURL.url) {
|
||||
clickPageObject(itemWithText("PDF form file"))
|
||||
}.clickDownloadPDFButton {
|
||||
verifyDownloadedFileName(downloadFile)
|
||||
}.clickOpen("application/pdf") {
|
||||
assertExternalAppOpens(GOOGLE_DOCS)
|
||||
}
|
||||
deleteDownloadedFileOnStorage(downloadFile)
|
||||
}
|
||||
}
|
@ -0,0 +1,291 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.ui
|
||||
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.customannotations.SmokeTest
|
||||
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
|
||||
import org.mozilla.fenix.helpers.HomeActivityTestRule
|
||||
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
|
||||
import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
|
||||
import org.mozilla.fenix.helpers.TestHelper.mDevice
|
||||
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
|
||||
import org.mozilla.fenix.ui.robots.browserScreen
|
||||
import org.mozilla.fenix.ui.robots.homeScreen
|
||||
import org.mozilla.fenix.ui.robots.navigationToolbar
|
||||
|
||||
/**
|
||||
* Tests for verifying basic functionality of recently closed tabs history
|
||||
*
|
||||
*/
|
||||
class RecentlyClosedTabsTest {
|
||||
private lateinit var mockWebServer: MockWebServer
|
||||
|
||||
@get:Rule
|
||||
val activityTestRule = AndroidComposeTestRule(
|
||||
HomeActivityTestRule.withDefaultSettingsOverrides(
|
||||
tabsTrayRewriteEnabled = true,
|
||||
),
|
||||
) { it.activity }
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockWebServer = MockWebServer().apply {
|
||||
dispatcher = AndroidAssetDispatcher()
|
||||
start()
|
||||
}
|
||||
|
||||
Intents.init()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
// This test verifies the Recently Closed Tabs List and items
|
||||
@Test
|
||||
fun verifyRecentlyClosedTabsListTest() {
|
||||
val website = getGenericAsset(mockWebServer, 1)
|
||||
|
||||
homeScreen {
|
||||
}.openNavigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(website.url) {
|
||||
mDevice.waitForIdle()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
closeTab()
|
||||
}
|
||||
homeScreen {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
registerAndCleanupIdlingResources(
|
||||
RecyclerViewIdlingResource(
|
||||
activityTestRule.activity.findViewById(R.id.recently_closed_list),
|
||||
1,
|
||||
),
|
||||
) {
|
||||
verifyRecentlyClosedTabsMenuView()
|
||||
}
|
||||
verifyRecentlyClosedTabsPageTitle("Test_Page_1")
|
||||
verifyRecentlyClosedTabsUrl(website.url)
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that a recently closed item is properly opened
|
||||
@SmokeTest
|
||||
@Test
|
||||
fun openRecentlyClosedItemTest() {
|
||||
val website = getGenericAsset(mockWebServer, 1)
|
||||
|
||||
homeScreen {
|
||||
}.openNavigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(website.url) {
|
||||
mDevice.waitForIdle()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
closeTab()
|
||||
}
|
||||
homeScreen {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
registerAndCleanupIdlingResources(
|
||||
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
|
||||
) {
|
||||
verifyRecentlyClosedTabsMenuView()
|
||||
}
|
||||
}.clickRecentlyClosedItem("Test_Page_1") {
|
||||
verifyUrl(website.url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that tapping the "x" button removes a recently closed item from the list
|
||||
@SmokeTest
|
||||
@Test
|
||||
fun deleteRecentlyClosedTabsItemTest() {
|
||||
val website = getGenericAsset(mockWebServer, 1)
|
||||
|
||||
homeScreen {
|
||||
}.openNavigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(website.url) {
|
||||
mDevice.waitForIdle()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
closeTab()
|
||||
}
|
||||
homeScreen {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
registerAndCleanupIdlingResources(
|
||||
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
|
||||
) {
|
||||
verifyRecentlyClosedTabsMenuView()
|
||||
}
|
||||
clickDeleteRecentlyClosedTabs()
|
||||
verifyEmptyRecentlyClosedTabsList()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openInNewTabRecentlyClosedTabsTest() {
|
||||
val firstPage = getGenericAsset(mockWebServer, 1)
|
||||
val secondPage = getGenericAsset(mockWebServer, 2)
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(firstPage.url) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openNewTab {
|
||||
}.submitQuery(secondPage.url.toString()) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openThreeDotMenu {
|
||||
}.closeAllTabs {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
longTapSelectItem(firstPage.url)
|
||||
longTapSelectItem(secondPage.url)
|
||||
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
|
||||
}.clickOpenInNewTab(activityTestRule) {
|
||||
// URL verification to be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1839179 is fixed.
|
||||
browserScreen {
|
||||
verifyPageContent(secondPage.content)
|
||||
verifyUrl(secondPage.url.toString())
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
verifyNormalBrowsingButtonIsSelected(true)
|
||||
verifyExistingOpenTabs(firstPage.title, secondPage.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openInPrivateTabRecentlyClosedTabsTest() {
|
||||
val firstPage = getGenericAsset(mockWebServer, 1)
|
||||
val secondPage = getGenericAsset(mockWebServer, 2)
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(firstPage.url) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openNewTab {
|
||||
}.submitQuery(secondPage.url.toString()) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openThreeDotMenu {
|
||||
}.closeAllTabs {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
longTapSelectItem(firstPage.url)
|
||||
longTapSelectItem(secondPage.url)
|
||||
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
|
||||
}.clickOpenInPrivateTab(activityTestRule) {
|
||||
// URL verification to be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1839179 is fixed.
|
||||
browserScreen {
|
||||
verifyPageContent(secondPage.content)
|
||||
verifyUrl(secondPage.url.toString())
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
verifyPrivateBrowsingButtonIsSelected(true)
|
||||
verifyExistingOpenTabs(firstPage.title, secondPage.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shareMultipleRecentlyClosedTabsTest() {
|
||||
val firstPage = getGenericAsset(mockWebServer, 1)
|
||||
val secondPage = getGenericAsset(mockWebServer, 2)
|
||||
val sharingApp = "Gmail"
|
||||
val urlString = "${firstPage.url}\n\n${secondPage.url}"
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(firstPage.url) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openNewTab {
|
||||
}.submitQuery(secondPage.url.toString()) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openThreeDotMenu {
|
||||
}.closeAllTabs {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
longTapSelectItem(firstPage.url)
|
||||
longTapSelectItem(secondPage.url)
|
||||
}.clickShare {
|
||||
verifyShareTabsOverlay(firstPage.title, secondPage.title)
|
||||
verifySharingWithSelectedApp(sharingApp, urlString, "${firstPage.title}, ${secondPage.title}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun privateBrowsingNotSavedInRecentlyClosedTabsTest() {
|
||||
val firstPage = getGenericAsset(mockWebServer, 1)
|
||||
val secondPage = getGenericAsset(mockWebServer, 2)
|
||||
|
||||
homeScreen {}.togglePrivateBrowsingMode()
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(firstPage.url) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openNewTab {
|
||||
}.submitQuery(secondPage.url.toString()) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openThreeDotMenu {
|
||||
}.closeAllTabs {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
verifyEmptyRecentlyClosedTabsList()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteHistoryClearsRecentlyClosedTabsListTest() {
|
||||
val firstPage = getGenericAsset(mockWebServer, 1)
|
||||
val secondPage = getGenericAsset(mockWebServer, 2)
|
||||
|
||||
navigationToolbar {
|
||||
}.enterURLAndEnterToBrowser(firstPage.url) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openNewTab {
|
||||
}.submitQuery(secondPage.url.toString()) {
|
||||
waitForPageToLoad()
|
||||
}.openComposeTabDrawer(activityTestRule) {
|
||||
}.openThreeDotMenu {
|
||||
}.closeAllTabs {
|
||||
}.openThreeDotMenu {
|
||||
}.openHistory {
|
||||
}.openRecentlyClosedTabs {
|
||||
waitForListToExist()
|
||||
}.goBackToHistoryMenu {
|
||||
clickDeleteAllHistoryButton()
|
||||
selectEverythingOption()
|
||||
confirmDeleteAllHistory()
|
||||
verifyEmptyHistoryView()
|
||||
}.openRecentlyClosedTabs {
|
||||
verifyEmptyRecentlyClosedTabsList()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
# Espresso/UI Automator Tests on All Channels
|
||||
|
||||
When writing Espresso/UI Automator tests, by default, the tests are expected to run on all channels unless otherwise targeted. The provided code snippet below demonstrates a conditional check before running the tests on specific channels.
|
||||
|
||||
```
|
||||
runWithCondition(
|
||||
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
|
||||
// Once this feature lands in RC we should remove the wrapper.
|
||||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
|
||||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
|
||||
)
|
||||
```
|
||||
The code uses the `runWithCondition()` function to determine the appropriate channel for the test. It checks if the current version's release channel is either Nightly or Beta using the `activityIntentTestRule.activity.components.core.engine.version.releaseChannel` property.
|
||||
|
||||
If the release channel is Nightly or Beta, the test is executed within the `runWithCondition()` block. However, once the feature under test lands in the Release Candidate (RC) channel, we suggest removing the wrapper and allowing the tests to run without any channel-specific condition.
|
||||
|
||||
This approach ensures that the tests are executed on all channels during the development and testing phase. However, when the feature stabilizes and reaches the RC channel, the conditional check can be removed to ensure the tests run consistently across all channels.
|
||||
|
||||
Please note that the actual implementation of the tests and their behavior may vary depending on the specific testing framework, project structure, and requirements. The provided code snippet serves as an example to showcase the concept of targeting specific channels during test execution.
|
@ -0,0 +1,563 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt" >
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:width="432dp"
|
||||
android:height="432dp"
|
||||
android:viewportWidth="432"
|
||||
android:viewportHeight="432">
|
||||
<group
|
||||
android:name="a1_t"
|
||||
android:pivotX="216"
|
||||
android:pivotY="216">
|
||||
<path
|
||||
android:pathData="M303.8,251.4h-79.2c-2.3,0 -4.2,1.9 -4.2,4.2v12.7c0,18.7 15.1,33.8 33.8,33.8l0,0H298c14,0 25.3,-11.4 25.3,-25.3v-11.1C323.3,259.3 317,251.4 303.8,251.4L303.8,251.4z"
|
||||
android:fillColor="#008787"/>
|
||||
<path
|
||||
android:pathData="M303.8,251.4h-79.2c-2.3,0 -4.2,1.9 -4.2,4.2v12.7c0,18.7 15.1,33.8 33.8,33.8l0,0H298c14,0 25.3,-11.4 25.3,-25.3v-11.1C323.3,259.3 317,251.4 303.8,251.4L303.8,251.4z"
|
||||
android:strokeAlpha="0.9"
|
||||
android:fillAlpha="0.9">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="301.7"
|
||||
android:startY="300.03"
|
||||
android:endX="274.29"
|
||||
android:endY="274.08"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#7F054096"/>
|
||||
<item android:offset="0.05" android:color="#700F3D9C"/>
|
||||
<item android:offset="0.26" android:color="#3F2F35B1"/>
|
||||
<item android:offset="0.47" android:color="#1C462FBF"/>
|
||||
<item android:offset="0.67" android:color="#07542BC8"/>
|
||||
<item android:offset="0.86" android:color="#00592ACB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.2c-12.3,-30.5 -33,-42.8 -50,-69.7c-0.9,-1.4 -1.7,-2.7 -2.6,-4.1c-0.4,-0.7 -0.8,-1.5 -1.2,-2.3c-0.7,-1.4 -1.3,-2.8 -1.6,-4.3c0,-0.1 -0.1,-0.3 -0.2,-0.3c0,0 -0.1,0 -0.2,0l0,0c0,0 0,0 -0.1,0l0,0c-27.2,16 -36.5,45.4 -37.3,60.2c-10.9,0.8 -21.3,4.7 -29.8,11.5c-0.9,-0.8 -1.8,-1.4 -2.8,-2.1c-2.5,-8.7 -2.6,-17.8 -0.3,-26.5c-11.1,5 -19.8,13.1 -26.1,20.2l0,0c-4.3,-5.5 -4,-23.4 -3.7,-27.1c0,-0.2 -3.2,1.6 -3.6,1.9c-3.8,2.7 -7.3,5.7 -10.6,9.1c-3.7,3.7 -7.1,7.8 -10.1,12.1l0,0l0,0c-7,9.9 -11.9,21 -14.5,32.8c0,0.2 -0.1,0.5 -0.1,0.7c-0.2,0.9 -0.9,5.7 -1.1,6.8c0,0.1 0,0.1 0,0.2c-0.9,4.9 -1.5,9.9 -1.8,15c0,0.2 0,0.4 0,0.5c0,59.7 48.5,108.2 108.3,108.2c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2zM191.5,263.9c0.5,0.2 1,0.5 1.5,0.7c0,0 0,0 0.1,0C192.6,264.5 192.1,264.2 191.5,263.9zM305.7,184.8L305.7,184.8L305.7,184.8L305.7,184.8z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="296.59"
|
||||
android:startY="138.83"
|
||||
android:endX="128.16"
|
||||
android:endY="312.38"
|
||||
android:type="linear">
|
||||
<item android:offset="0.05" android:color="#FFFFF44F"/>
|
||||
<item android:offset="0.37" android:color="#FFFF980E"/>
|
||||
<item android:offset="0.53" android:color="#FFFF3647"/>
|
||||
<item android:offset="0.7" android:color="#FFE31587"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.1v0.1c10.2,27.7 4.6,55.9 -3.4,73.1c-12.4,26.6 -42.5,54 -89.5,52.6c-50.8,-1.4 -95.5,-39.2 -103.9,-88.6c-1.5,-7.8 0,-11.8 0.8,-18.1c-0.9,4.9 -1.3,6.3 -1.8,15c0,0.2 0,0.4 0,0.5c0,59.8 48.5,108.3 108.3,108.3c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="297.63"
|
||||
android:centerY="151.73"
|
||||
android:gradientRadius="221.61"
|
||||
android:type="radial">
|
||||
<item android:offset="0.13" android:color="#FFFFBD4F"/>
|
||||
<item android:offset="0.28" android:color="#FFFF980E"/>
|
||||
<item android:offset="0.47" android:color="#FFFF3750"/>
|
||||
<item android:offset="0.78" android:color="#FFEB0878"/>
|
||||
<item android:offset="0.86" android:color="#FFE50080"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.1v0.1c10.2,27.7 4.6,55.9 -3.4,73.1c-12.4,26.6 -42.5,54 -89.5,52.6c-50.8,-1.4 -95.5,-39.2 -103.9,-88.6c-1.5,-7.8 0,-11.8 0.8,-18.1c-0.9,4.9 -1.3,6.3 -1.8,15c0,0.2 0,0.4 0,0.5c0,59.8 48.5,108.3 108.3,108.3c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="213.81"
|
||||
android:centerY="222.16"
|
||||
android:gradientRadius="227.15"
|
||||
android:type="radial">
|
||||
<item android:offset="0.3" android:color="#FF960E18"/>
|
||||
<item android:offset="0.35" android:color="#BCB11927"/>
|
||||
<item android:offset="0.43" android:color="#56DB293D"/>
|
||||
<item android:offset="0.5" android:color="#16F5334B"/>
|
||||
<item android:offset="0.53" android:color="#00FF3750"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M263.7,191.9c0.2,0.2 0.5,0.3 0.7,0.5c-2.7,-4.8 -6.1,-9.2 -10.1,-13.1c-33.7,-33.7 -8.8,-73 -4.6,-75l0,0c-27.2,16 -36.5,45.4 -37.3,60.2c1.3,-0.1 2.5,-0.2 3.8,-0.2C236.6,164.2 254.3,175.4 263.7,191.9L263.7,191.9z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="237.41"
|
||||
android:centerY="93.62"
|
||||
android:gradientRadius="72.74"
|
||||
android:type="radial">
|
||||
<item android:offset="0.13" android:color="#FFFFF44F"/>
|
||||
<item android:offset="0.53" android:color="#FFFF980E"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M216.4,198.6c-0.2,2.7 -9.7,12 -13,12c-30.8,0 -35.8,18.6 -35.8,18.6c1.4,15.7 12.3,28.6 25.5,35.4c0.6,0.3 1.2,0.6 1.8,0.9c1,0.5 2.1,0.9 3.2,1.3c4.6,1.6 9.3,2.5 14.1,2.7c54,2.5 64.5,-64.6 25.5,-84c10,-1.8 20.3,2.3 26.1,6.4c-9.5,-16.5 -27.1,-27.7 -47.5,-27.7c-1.3,0 -2.5,0.1 -3.8,0.2c-10.9,0.8 -21.3,4.7 -29.8,11.5c1.7,1.4 3.5,3.2 7.4,7.1C197.5,190.2 216.3,197.8 216.4,198.6L216.4,198.6z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="189.41"
|
||||
android:centerY="280.33"
|
||||
android:gradientRadius="96.24"
|
||||
android:type="radial">
|
||||
<item android:offset="0.35" android:color="#FF3A8EE6"/>
|
||||
<item android:offset="0.67" android:color="#FF9059FF"/>
|
||||
<item android:offset="1" android:color="#FFC139E6"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M216.4,198.6c-0.2,2.7 -9.7,12 -13,12c-30.8,0 -35.8,18.6 -35.8,18.6c1.4,15.7 12.3,28.6 25.5,35.4c0.6,0.3 1.2,0.6 1.8,0.9c1,0.5 2.1,0.9 3.2,1.3c4.6,1.6 9.3,2.5 14.1,2.7c54,2.5 64.5,-64.6 25.5,-84c10,-1.8 20.3,2.3 26.1,6.4c-9.5,-16.5 -27.1,-27.7 -47.5,-27.7c-1.3,0 -2.5,0.1 -3.8,0.2c-10.9,0.8 -21.3,4.7 -29.8,11.5c1.7,1.4 3.5,3.2 7.4,7.1C197.5,190.2 216.3,197.8 216.4,198.6L216.4,198.6z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="212.55"
|
||||
android:centerY="199"
|
||||
android:gradientRadius="51.11"
|
||||
android:type="radial">
|
||||
<item android:offset="0.21" android:color="#009059FF"/>
|
||||
<item android:offset="0.97" android:color="#996E008B"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M177.6,172.2c0.8,0.5 1.5,1 2.3,1.5c-2.5,-8.7 -2.6,-17.8 -0.3,-26.5c-11.1,5 -19.8,13.1 -26.1,20.2C154,167.4 169.7,167.1 177.6,172.2L177.6,172.2z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="208.87"
|
||||
android:centerY="120.79"
|
||||
android:gradientRadius="76.52"
|
||||
android:type="radial">
|
||||
<item android:offset="0.1" android:color="#FFFFE226"/>
|
||||
<item android:offset="0.79" android:color="#FFFF7139"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M108.8,222c8.4,49.4 53.2,87.1 104,88.5c47.1,1.4 77.1,-26 89.5,-52.6c8,-17.2 13.6,-45.4 3.4,-73.1l0,0v-0.1c0,-0.1 0,-0.1 0,-0.1s0,0 0,0.2c3.8,25.1 -8.9,49.4 -28.9,65.8v0.1c-38.9,31.6 -76.1,19.1 -83.6,14c-0.5,-0.3 -1,-0.5 -1.6,-0.8c-22.7,-10.8 -32.1,-31.5 -30,-49.2c-19.2,0 -25.7,-16.1 -25.7,-16.1s17.2,-12.3 39.9,-1.6c21,9.9 40.7,1.6 40.7,1.6c0,-0.9 -18.9,-8.4 -26.2,-15.6c-3.9,-3.9 -5.8,-5.7 -7.4,-7.1c-0.9,-0.8 -1.8,-1.4 -2.8,-2.1c-0.7,-0.5 -1.5,-1 -2.3,-1.5c-7.9,-5.1 -23.6,-4.9 -24.1,-4.8l0,0c-4.3,-5.5 -4,-23.4 -3.7,-27.1c0,-0.2 -3.2,1.6 -3.6,1.9c-3.8,2.7 -7.3,5.7 -10.6,9.1c-3.7,3.7 -7.1,7.8 -10.1,12.1l0,0l0,0c-7,9.9 -11.9,21 -14.5,32.8C110.7,196.5 106.9,213.3 108.8,222L108.8,222z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="278.74"
|
||||
android:centerY="70.41"
|
||||
android:gradientRadius="365.3"
|
||||
android:type="radial">
|
||||
<item android:offset="0.11" android:color="#FFFFF44F"/>
|
||||
<item android:offset="0.46" android:color="#FFFF980E"/>
|
||||
<item android:offset="0.72" android:color="#FFFF3647"/>
|
||||
<item android:offset="0.9" android:color="#FFE31587"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M254.4,179.3c4,3.9 7.3,8.3 10.1,13.1c0.6,0.5 1.2,0.9 1.6,1.4c24.6,22.6 11.7,54.6 10.7,56.9c20,-16.5 32.7,-40.8 28.9,-65.8c-12.3,-30.5 -33,-42.8 -50,-69.7c-0.9,-1.4 -1.7,-2.7 -2.6,-4.1c-0.4,-0.7 -0.8,-1.5 -1.2,-2.3c-0.7,-1.4 -1.3,-2.8 -1.6,-4.3c0,-0.1 -0.1,-0.3 -0.2,-0.3c0,0 -0.1,0 -0.2,0l0,0c0,0 0,0 -0.1,0C245.5,106.2 220.7,145.6 254.4,179.3L254.4,179.3z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="247.82"
|
||||
android:centerY="76.43"
|
||||
android:gradientRadius="240.17"
|
||||
android:type="radial">
|
||||
<item android:offset="0" android:color="#FFFFF44F"/>
|
||||
<item android:offset="0.3" android:color="#FFFF980E"/>
|
||||
<item android:offset="0.57" android:color="#FFFF3647"/>
|
||||
<item android:offset="0.74" android:color="#FFE31587"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M266,193.8c-0.5,-0.5 -1,-0.9 -1.6,-1.4c-0.2,-0.2 -0.5,-0.3 -0.7,-0.5c-5.8,-4.1 -16.1,-8.1 -26.1,-6.4c39,19.5 28.5,86.6 -25.5,84c-4.8,-0.2 -9.6,-1.1 -14.1,-2.7c-1.1,-0.4 -2.1,-0.9 -3.2,-1.3c-0.6,-0.3 -1.2,-0.5 -1.8,-0.9c0,0 0,0 0.1,0c7.5,5.1 44.7,17.7 83.6,-14v-0.1C277.8,248.4 290.6,216.4 266,193.8L266,193.8z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="209.65"
|
||||
android:centerY="148.39"
|
||||
android:gradientRadius="209.74"
|
||||
android:type="radial">
|
||||
<item android:offset="0.14" android:color="#FFFFF44F"/>
|
||||
<item android:offset="0.48" android:color="#FFFF980E"/>
|
||||
<item android:offset="0.66" android:color="#FFFF3647"/>
|
||||
<item android:offset="0.9" android:color="#FFE31587"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M167.5,229.2c0,0 5,-18.6 35.8,-18.6c3.3,0 12.8,-9.3 13,-12s-19.7,8.3 -40.7,-1.6c-22.6,-10.6 -39.9,1.6 -39.9,1.6s6.5,16.1 25.7,16.1c-2,17.7 7.3,38.4 30,49.2c0.5,0.2 1,0.5 1.5,0.7C179.8,257.9 168.9,245 167.5,229.2z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:centerX="264.09"
|
||||
android:centerY="160.45"
|
||||
android:gradientRadius="252.1"
|
||||
android:type="radial">
|
||||
<item android:offset="0.09" android:color="#FFFFF44F"/>
|
||||
<item android:offset="0.63" android:color="#FFFF980E"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.2c-12.3,-30.5 -33,-42.8 -50,-69.7c-0.9,-1.4 -1.7,-2.7 -2.6,-4.1c-0.4,-0.7 -0.8,-1.5 -1.2,-2.3c-0.7,-1.4 -1.3,-2.8 -1.6,-4.3c0,-0.1 -0.1,-0.3 -0.2,-0.3c0,0 -0.1,0 -0.2,0l0,0c0,0 0,0 -0.1,0l0,0c-27.2,16 -36.5,45.4 -37.3,60.2c1.3,-0.1 2.5,-0.2 3.8,-0.2c20.3,0 38,11.2 47.5,27.7c-5.8,-4.1 -16.1,-8.1 -26.1,-6.4c39,19.5 28.5,86.6 -25.5,84c-4.8,-0.2 -9.6,-1.1 -14.1,-2.7c-1.1,-0.4 -2.1,-0.9 -3.2,-1.3c-0.6,-0.3 -1.2,-0.5 -1.8,-0.9c0,0 0,0 0.1,0c-0.5,-0.3 -1,-0.5 -1.6,-0.8c0.5,0.2 1,0.5 1.5,0.7c-13.3,-6.9 -24.2,-19.7 -25.5,-35.4c0,0 5,-18.6 35.8,-18.6c3.3,0 12.8,-9.3 13,-12c0,-0.9 -18.9,-8.4 -26.2,-15.6c-3.9,-3.9 -5.8,-5.7 -7.4,-7.1c-0.9,-0.8 -1.8,-1.4 -2.8,-2.1c-2.5,-8.7 -2.6,-17.8 -0.3,-26.5c-11.1,5 -19.8,13.1 -26.1,20.2l0,0c-4.3,-5.5 -4,-23.4 -3.7,-27.1c0,-0.2 -3.2,1.6 -3.6,1.9c-3.8,2.7 -7.3,5.7 -10.6,9.1c-3.7,3.7 -7.1,7.8 -10.1,12.1l0,0l0,0c-7,9.9 -11.9,21 -14.5,32.8c0,0.2 -0.1,0.5 -0.1,0.7c-0.2,0.9 -1.1,5.8 -1.3,6.9c0,0.1 0,-0.1 0,0c-0.9,5 -1.4,10.1 -1.6,15.1c0,0.2 0,0.4 0,0.5c0,59.7 48.5,108.2 108.3,108.2c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2zM305.7,184.7v0.1l0,0V184.7z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="285.98"
|
||||
android:startY="140.07"
|
||||
android:endX="151.82"
|
||||
android:endY="296.61"
|
||||
android:type="linear">
|
||||
<item android:offset="0.17" android:color="#CCFFF44F"/>
|
||||
<item android:offset="0.6" android:color="#00FFF44F"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M319.1,269.9h-72.8c-17.2,0 -31.2,14 -31.2,31.2l0,0v15.2c0,2.3 1.9,4.2 4.2,4.2h72.8c17.2,0 31.2,-14 31.2,-31.2v-23.7C323.3,268 321.4,269.9 319.1,269.9L319.1,269.9z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="227.01"
|
||||
android:startY="274.49"
|
||||
android:endX="325.95"
|
||||
android:endY="316.35"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF54FFBD"/>
|
||||
<item android:offset="1" android:color="#FF00DDFF"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M247.6,295c1.5,-0.9 2.5,-2.5 2.4,-4.2c0,-3.7 -2.6,-5.9 -7.3,-5.9H234v21h8.7c4.6,0 7.5,-2.1 7.5,-6.2C250.4,297.4 249.4,295.8 247.6,295L247.6,295zM238.1,288.4h4.8c2.1,0 3.1,0.9 3.1,2.3s-0.9,2.5 -3.1,2.5h-4.8V288.4zM242.9,302.2h-4.8V297h4.6c2.6,0 3.5,0.9 3.5,2.6C246.3,301.1 245.1,302.2 242.9,302.2L242.9,302.2zM254.3,305.8h14.1V302h-10.1v-4.8h10.1v-3.8h-10.1v-4.7h10.1v-3.8h-14.1L254.3,305.8L254.3,305.8zM287.4,284.9h-15.6v3.7h5.8v17.2h4v-17.3h5.8L287.4,284.9zM298.3,284.9h-4l-7.9,21h4.1l1.4,-3.8h8.7l1.4,3.8h4.1L298.3,284.9zM293.3,298.3l3.1,-8.3l3,8.3H293.3z"
|
||||
android:fillColor="#20123A"/>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target
|
||||
android:name="a1_t">
|
||||
<aapt:attr
|
||||
name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="300"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="0.75"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/linear"/>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="700"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="300"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="1000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="2000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="3000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="4000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="5000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="6000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="7000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="8000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="9000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="10000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleX"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="11000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="300"
|
||||
android:valueFrom="0.75002400000000002"
|
||||
android:valueTo="0.75002400000000002"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/linear"/>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="700"
|
||||
android:valueFrom="0.75002400000000002"
|
||||
android:valueTo="1"
|
||||
android:startOffset="300"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="1000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="2000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="3000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="4000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="5000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="6000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="7000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="8000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="9000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0.75"
|
||||
android:valueTo="1"
|
||||
android:startOffset="10000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="1000"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="0.75"
|
||||
android:startOffset="11000"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr
|
||||
name="android:interpolator">
|
||||
<pathInterpolator
|
||||
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,74 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.compose
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* A handle present on top of a bottom sheet. This is selectable when talkback is enabled.
|
||||
*
|
||||
* @param onRequestDismiss Invoked on clicking the handle when talkback is enabled.
|
||||
* @param contentDescription Content Description of the composable.
|
||||
* @param modifier The modifier to be applied to the Composable.
|
||||
* @param color Color of the handle.
|
||||
*/
|
||||
@Composable
|
||||
fun BottomSheetHandle(
|
||||
onRequestDismiss: () -> Unit,
|
||||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = FirefoxTheme.colors.textSecondary,
|
||||
) {
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.height(dimensionResource(id = R.dimen.bottom_sheet_handle_height))
|
||||
.semantics(mergeDescendants = true) {
|
||||
this.contentDescription = contentDescription
|
||||
onClick {
|
||||
onRequestDismiss()
|
||||
true
|
||||
}
|
||||
},
|
||||
) {
|
||||
drawRect(color = color)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun BottomSheetHandlePreview() {
|
||||
FirefoxTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = FirefoxTheme.colors.layer1)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
BottomSheetHandle(
|
||||
onRequestDismiss = {},
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.compose
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material.DismissDirection
|
||||
import androidx.compose.material.DismissDirection.EndToStart
|
||||
import androidx.compose.material.DismissDirection.StartToEnd
|
||||
import androidx.compose.material.DismissState
|
||||
import androidx.compose.material.DismissValue
|
||||
import androidx.compose.material.DismissValue.Default
|
||||
import androidx.compose.material.DismissValue.DismissedToEnd
|
||||
import androidx.compose.material.DismissValue.DismissedToStart
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FixedThreshold
|
||||
import androidx.compose.material.FractionalThreshold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ThresholdConfig
|
||||
import androidx.compose.material.rememberDismissState
|
||||
import androidx.compose.material.swipeable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* A composable that can be dismissed by swiping left or right
|
||||
*
|
||||
* @param state The state of this component.
|
||||
* @param modifier Optional [Modifier] for this component.
|
||||
* @param enabled [Boolean] controlling whether the content is swipeable or not.
|
||||
* @param directions The set of directions in which the component can be dismissed.
|
||||
* @param dismissThreshold The threshold the item needs to be swiped in order to be dismissed.
|
||||
* @param backgroundContent A composable that is stacked behind the primary content and is exposed
|
||||
* when the content is swiped. You can/should use the [state] to have different backgrounds on each side.
|
||||
* @param dismissContent The content that can be dismissed.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterialApi
|
||||
fun SwipeToDismiss(
|
||||
state: DismissState,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
|
||||
dismissThreshold: ThresholdConfig = FractionalThreshold(DISMISS_THRESHOLD),
|
||||
backgroundContent: @Composable RowScope.() -> Unit,
|
||||
dismissContent: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val swipeWidth = with(LocalDensity.current) {
|
||||
LocalConfiguration.current.screenWidthDp.dp.toPx()
|
||||
}
|
||||
val anchors = mutableMapOf(0f to Default)
|
||||
val thresholds = { _: DismissValue, _: DismissValue ->
|
||||
dismissThreshold
|
||||
}
|
||||
|
||||
if (StartToEnd in directions) anchors += swipeWidth to DismissedToEnd
|
||||
if (EndToStart in directions) anchors += -swipeWidth to DismissedToStart
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.swipeable(
|
||||
state = state,
|
||||
anchors = anchors,
|
||||
thresholds = thresholds,
|
||||
orientation = Orientation.Horizontal,
|
||||
enabled = state.currentValue == Default && enabled,
|
||||
reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
resistance = null,
|
||||
)
|
||||
.then(modifier),
|
||||
) {
|
||||
Row(
|
||||
content = backgroundContent,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
)
|
||||
|
||||
Row(
|
||||
content = dismissContent,
|
||||
modifier = Modifier.offset { IntOffset(state.offset.value.roundToInt(), 0) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DISMISS_THRESHOLD = 0.5f
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SwipeablePreview(directions: Set<DismissDirection>, text: String, threshold: ThresholdConfig) {
|
||||
val state = rememberDismissState()
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(30.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
SwipeToDismiss(
|
||||
state = state,
|
||||
directions = directions,
|
||||
dismissThreshold = threshold,
|
||||
backgroundContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FirefoxTheme.colors.layerAccent),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(FirefoxTheme.colors.layer1),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
private fun SwipeToDismissPreview() {
|
||||
FirefoxTheme {
|
||||
Column {
|
||||
SwipeablePreview(
|
||||
directions = setOf(StartToEnd),
|
||||
text = "Swipe to right 50% ->",
|
||||
FractionalThreshold(.5f),
|
||||
)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
SwipeablePreview(
|
||||
directions = setOf(EndToStart),
|
||||
text = "<- Swipe to left 100%",
|
||||
FractionalThreshold(1f),
|
||||
)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
SwipeablePreview(
|
||||
directions = setOf(StartToEnd, EndToStart),
|
||||
text = "<- Swipe both ways 20dp ->",
|
||||
FixedThreshold(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
private const val THUMBNAIL_SIZE = 108
|
||||
private const val FALLBACK_ICON_SIZE = 36
|
||||
|
||||
/**
|
||||
* Thumbnail belonging to a [tab]. If a thumbnail is not available, the favicon
|
||||
* will be displayed until the thumbnail is loaded.
|
||||
*
|
||||
* @param tab The given [TabSessionState] to render a thumbnail for.
|
||||
* @param size [Dp] size of the thumbnail.
|
||||
* @param backgroundColor [Color] used for the background of the favicon.
|
||||
* @param modifier [Modifier] used to draw the image content.
|
||||
* @param contentDescription Text used by accessibility services
|
||||
* to describe what this image represents.
|
||||
* @param contentScale [ContentScale] used to draw image content.
|
||||
* @param alignment [Alignment] used to draw the image content.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun TabThumbnail(
|
||||
tab: TabSessionState,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = THUMBNAIL_SIZE.dp,
|
||||
backgroundColor: Color = FirefoxTheme.colors.layer2,
|
||||
contentDescription: String? = null,
|
||||
contentScale: ContentScale = ContentScale.FillWidth,
|
||||
alignment: Alignment = Alignment.TopCenter,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
backgroundColor = backgroundColor,
|
||||
) {
|
||||
ThumbnailImage(
|
||||
key = tab.id,
|
||||
size = size,
|
||||
modifier = modifier,
|
||||
contentScale = contentScale,
|
||||
alignment = alignment,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(FALLBACK_ICON_SIZE.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val icon = tab.content.icon
|
||||
if (icon != null) {
|
||||
icon.prepareToDraw()
|
||||
Image(
|
||||
bitmap = icon.asImageBitmap(),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier
|
||||
.size(FALLBACK_ICON_SIZE.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentScale = contentScale,
|
||||
)
|
||||
} else {
|
||||
Favicon(
|
||||
url = tab.content.url,
|
||||
size = FALLBACK_ICON_SIZE.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ThumbnailCardPreview() {
|
||||
FirefoxTheme {
|
||||
TabThumbnail(
|
||||
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
|
||||
modifier = Modifier
|
||||
.size(THUMBNAIL_SIZE.dp, 80.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.compose
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.concept.base.images.ImageLoadRequest
|
||||
import org.mozilla.fenix.components.components
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* Thumbnail belonging to a [key]. Asynchronously fetches the bitmap from storage.
|
||||
*
|
||||
* @param key Key used to remember the thumbnail for future compositions.
|
||||
* @param size [Dp] size of the thumbnail.
|
||||
* @param modifier [Modifier] used to draw the image content.
|
||||
* @param contentScale [ContentScale] used to draw image content.
|
||||
* @param alignment [Alignment] used to draw the image content.
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
fun ThumbnailImage(
|
||||
key: String,
|
||||
size: Dp,
|
||||
modifier: Modifier,
|
||||
contentScale: ContentScale,
|
||||
alignment: Alignment,
|
||||
fallbackContent: @Composable () -> Unit,
|
||||
) {
|
||||
if (inComposePreview) {
|
||||
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer3))
|
||||
} else {
|
||||
val thumbnailSize = LocalDensity.current.run { size.toPx().toInt() }
|
||||
val request = ImageLoadRequest(key, thumbnailSize)
|
||||
val storage = components.core.thumbnailStorage
|
||||
var state by remember { mutableStateOf(ThumbnailImageState(null, false)) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
if (!state.hasLoaded) {
|
||||
scope.launch {
|
||||
val thumbnailBitmap = storage.loadThumbnail(request).await()
|
||||
thumbnailBitmap?.prepareToDraw()
|
||||
state = ThumbnailImageState(
|
||||
bitmap = thumbnailBitmap,
|
||||
hasLoaded = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onDispose {
|
||||
// Recycle the bitmap to liberate the RAM. Without this, a list of [ThumbnailImage]
|
||||
// will bloat the memory. This is a trade-off, however, as the bitmap
|
||||
// will be re-fetched if this Composable is disposed and re-loaded.
|
||||
state.bitmap?.recycle()
|
||||
state = ThumbnailImageState(
|
||||
bitmap = null,
|
||||
hasLoaded = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.bitmap == null && state.hasLoaded) {
|
||||
fallbackContent()
|
||||
} else {
|
||||
state.bitmap?.let { bitmap ->
|
||||
Image(
|
||||
painter = BitmapPainter(bitmap.asImageBitmap()),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
contentScale = contentScale,
|
||||
alignment = alignment,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State wrapper for [ThumbnailImage].
|
||||
*/
|
||||
private data class ThumbnailImageState(
|
||||
val bitmap: Bitmap?,
|
||||
val hasLoaded: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* This preview does not demo anything. This is to ensure that [ThumbnailImage] does not break other previews.
|
||||
*/
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ThumbnailImagePreview() {
|
||||
FirefoxTheme {
|
||||
ThumbnailImage(
|
||||
key = "",
|
||||
size = 1.dp,
|
||||
modifier = Modifier,
|
||||
contentScale = ContentScale.Crop,
|
||||
alignment = Alignment.Center,
|
||||
fallbackContent = {},
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.compose.tabstray
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.DismissDirection
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import mozilla.components.feature.tab.collections.Tab
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.compose.annotation.LightDarkPreview
|
||||
import org.mozilla.fenix.theme.FirefoxTheme
|
||||
|
||||
/**
|
||||
* The background of a [Tab] that is being swiped left or right.
|
||||
*
|
||||
* @param dismissDirection [DismissDirection] of the ongoing swipe. Depending on the direction,
|
||||
* the background will also include a warning icon at the start of the swipe gesture.
|
||||
* If `null` the warning icon will be shown at both ends.
|
||||
* @param shape Shape of the background.
|
||||
*/
|
||||
@Composable
|
||||
fun DismissedTabBackground(
|
||||
dismissDirection: DismissDirection?,
|
||||
shape: Shape,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
backgroundColor = FirefoxTheme.colors.layer3,
|
||||
shape = shape,
|
||||
elevation = 0.dp,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
// Only show the delete icon for where the swipe starts.
|
||||
.alpha(
|
||||
if (dismissDirection == DismissDirection.StartToEnd || dismissDirection == null) 1f else 0f,
|
||||
),
|
||||
tint = FirefoxTheme.colors.iconWarning,
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_delete),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp)
|
||||
// Only show the delete icon for where the swipe starts.
|
||||
.alpha(
|
||||
if (dismissDirection == DismissDirection.EndToStart || dismissDirection == null) 1f else 0f,
|
||||
),
|
||||
tint = FirefoxTheme.colors.iconWarning,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@LightDarkPreview
|
||||
private fun DismissedTabBackgroundPreview() {
|
||||
FirefoxTheme {
|
||||
Column {
|
||||
Box(modifier = Modifier.height(56.dp)) {
|
||||
DismissedTabBackground(
|
||||
dismissDirection = DismissDirection.StartToEnd,
|
||||
shape = RoundedCornerShape(0.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Box(modifier = Modifier.height(56.dp)) {
|
||||
DismissedTabBackground(
|
||||
dismissDirection = DismissDirection.EndToStart,
|
||||
shape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Box(modifier = Modifier.height(56.dp)) {
|
||||
DismissedTabBackground(
|
||||
dismissDirection = null,
|
||||
shape = RoundedCornerShape(0.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.crashes
|
||||
|
||||
import mozilla.components.concept.base.crash.Breadcrumb
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
import mozilla.components.feature.contextmenu.facts.ContextMenuFacts
|
||||
import mozilla.components.feature.downloads.facts.DownloadsFacts
|
||||
import mozilla.components.feature.prompts.facts.AddressAutofillDialogFacts
|
||||
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
|
||||
import mozilla.components.feature.prompts.facts.PromptFacts
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsFacts
|
||||
import mozilla.components.support.base.Component
|
||||
import mozilla.components.support.base.facts.Fact
|
||||
import mozilla.components.support.base.facts.FactProcessor
|
||||
import mozilla.components.support.base.facts.Facts
|
||||
|
||||
/**
|
||||
* Collects facts and record bread crumbs for the events.
|
||||
*/
|
||||
class CrashFactCollector(
|
||||
private val crashReporter: CrashReporting,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Starts collecting facts.
|
||||
*/
|
||||
fun start() {
|
||||
Facts.registerProcessor(
|
||||
object : FactProcessor {
|
||||
override fun process(fact: Fact) {
|
||||
fact.process()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Fact.process(): Unit = when (component to item) {
|
||||
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.MENU,
|
||||
Component.FEATURE_DOWNLOADS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_SHOWN,
|
||||
Component.FEATURE_DOWNLOADS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN,
|
||||
Component.FEATURE_DOWNLOADS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED,
|
||||
Component.FEATURE_DOWNLOADS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_SHOWN,
|
||||
Component.FEATURE_DOWNLOADS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED,
|
||||
Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.PROMPT,
|
||||
Component.FEATURE_SITEPERMISSIONS to SitePermissionsFacts.Items.PERMISSIONS,
|
||||
Component.FEATURE_PROMPTS to PromptFacts.Items.PROMPT,
|
||||
-> {
|
||||
crashReporter.recordCrashBreadcrumb(Breadcrumb("$component $action $value"))
|
||||
}
|
||||
else -> {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue