From 743e4fe207a591780cd8d277e25506aa23d4f089 Mon Sep 17 00:00:00 2001 From: Simone Arpe Date: Thu, 1 Jun 2023 18:32:00 +0200 Subject: [PATCH] (Wallet) Add market tab (#18691) Implements new market tab section showing the first 250 assets. Implements shimmer effect while loading. --- android/brave_java_resources.gni | 7 + android/brave_java_sources.gni | 4 + .../browser/app/domain/MarketModel.java | 91 +++++++++ .../browser/app/domain/WalletModel.java | 7 + .../adapters/CryptoFragmentPageAdapter.java | 6 +- .../adapters/MarketCoinAdapter.java | 110 +++++++++++ .../fragments/MarketFragment.java | 177 ++++++++++++++++++ .../LockableNestedScrollView.java | 57 ++++++ android/java/res/drawable/circle_shimmer.xml | 10 + android/java/res/drawable/ic_bear_arrow.xml | 9 + android/java/res/drawable/ic_bull_arrow.xml | 9 + .../java/res/layout/brave_wallet_divider.xml | 14 ++ android/java/res/layout/fragment_market.xml | 71 +++++++ .../java/res/layout/market_coin_list_item.xml | 68 +++++++ .../res/layout/market_coin_shimmer_item.xml | 74 ++++++++ android/java/res/values/brave_colors.xml | 2 + .../android/strings/android_brave_strings.grd | 3 + 17 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 android/java/org/chromium/chrome/browser/app/domain/MarketModel.java create mode 100644 android/java/org/chromium/chrome/browser/crypto_wallet/adapters/MarketCoinAdapter.java create mode 100644 android/java/org/chromium/chrome/browser/crypto_wallet/fragments/MarketFragment.java create mode 100644 android/java/org/chromium/chrome/browser/custom_layout/LockableNestedScrollView.java create mode 100644 android/java/res/drawable/circle_shimmer.xml create mode 100644 android/java/res/drawable/ic_bear_arrow.xml create mode 100644 android/java/res/drawable/ic_bull_arrow.xml create mode 100644 android/java/res/layout/brave_wallet_divider.xml create mode 100644 android/java/res/layout/fragment_market.xml create mode 100644 android/java/res/layout/market_coin_list_item.xml create mode 100644 android/java/res/layout/market_coin_shimmer_item.xml diff --git a/android/brave_java_resources.gni b/android/brave_java_resources.gni index 7edd1c3696d..bd2d3d6667d 100644 --- a/android/brave_java_resources.gni +++ b/android/brave_java_resources.gni @@ -426,6 +426,7 @@ brave_java_resources = [ "java/res/drawable/brave_sync_warning.xml", "java/res/drawable/card_bg.xml", "java/res/drawable/checked_circle_filled.xml", + "java/res/drawable/circle_shimmer.xml", "java/res/drawable/circular_progress.xml", "java/res/drawable/cookie_consent_tooltip_background.xml", "java/res/drawable/country_spinner_background.xml", @@ -459,6 +460,7 @@ brave_java_resources = [ "java/res/drawable/ic_basic_check.xml", "java/res/drawable/ic_bat_color.xml", "java/res/drawable/ic_bat_unverified_off.xml", + "java/res/drawable/ic_bear_arrow.xml", "java/res/drawable/ic_bell_icon.xml", "java/res/drawable/ic_bg.xml", "java/res/drawable/ic_bg_video_playback.xml", @@ -478,6 +480,7 @@ brave_java_resources = [ "java/res/drawable/ic_brave_smiley_happy.xml", "java/res/drawable/ic_brave_smiley_sad.xml", "java/res/drawable/ic_brave_vpn_graphic.xml", + "java/res/drawable/ic_bull_arrow.xml", "java/res/drawable/ic_calendar.xml", "java/res/drawable/ic_carat_down.xml", "java/res/drawable/ic_carat_down_country_spinner.xml", @@ -838,6 +841,7 @@ brave_java_resources = [ "java/res/layout/brave_wallet_autolock_layout.xml", "java/res/layout/brave_wallet_connected_sites_list_item.xml", "java/res/layout/brave_wallet_connected_sites_preference_layout.xml", + "java/res/layout/brave_wallet_divider.xml", "java/res/layout/brave_wallet_edit_gas.xml", "java/res/layout/brave_wallet_network_item.xml", "java/res/layout/brave_wallet_networks_preference_layout.xml", @@ -874,6 +878,7 @@ brave_java_resources = [ "java/res/layout/fragment_dormant_users_engagement_dialog.xml", "java/res/layout/fragment_encryption_key.xml", "java/res/layout/fragment_link_subscription_dialog.xml", + "java/res/layout/fragment_market.xml", "java/res/layout/fragment_nft_grid.xml", "java/res/layout/fragment_onboarding_v2.xml", "java/res/layout/fragment_playlist_warning_dialog.xml", @@ -904,6 +909,8 @@ brave_java_resources = [ "java/res/layout/item_news_settings_section.xml", "java/res/layout/layout_search_widget_promo.xml", "java/res/layout/location_status_incognito_badge.xml", + "java/res/layout/market_coin_list_item.xml", + "java/res/layout/market_coin_shimmer_item.xml", "java/res/layout/menu_separator.xml", "java/res/layout/monthly_contribution_tooltip.xml", "java/res/layout/network_spinner_items.xml", diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni index 57ffed8ac6e..86a8b44b060 100644 --- a/android/brave_java_sources.gni +++ b/android/brave_java_sources.gni @@ -52,6 +52,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/app/domain/CryptoSharedData.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/DappsModel.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/KeyringModel.java", + "../../brave/android/java/org/chromium/chrome/browser/app/domain/MarketModel.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/NetworkModel.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/NetworkSelectorModel.java", "../../brave/android/java/org/chromium/chrome/browser/app/domain/PortfolioModel.java", @@ -115,6 +116,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CryptoFragmentPageAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CryptoWalletOnboardingPagerAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/FragmentNavigationItemAdapter.java", + "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/MarketCoinAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/NetworkSelectorAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/NetworkSpinnerAdapter.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/RecoveryPhraseAdapter.java", @@ -127,6 +129,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/ApproveTxBottomSheetDialogFragment.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/CreateAccountBottomSheetFragment.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/EditVisibleAssetsBottomSheetDialogFragment.java", + "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/MarketFragment.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/NftGridFragment.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/PortfolioFragment.java", "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/SolanaTxDetailsFragment.java", @@ -200,6 +203,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/crypto_wallet/util/WalletUtils.java", "../../brave/android/java/org/chromium/chrome/browser/custom_layout/AutoFitVerticalGridLayoutManager.java", "../../brave/android/java/org/chromium/chrome/browser/custom_layout/HeightWrappingViewPager.java", + "../../brave/android/java/org/chromium/chrome/browser/custom_layout/LockableNestedScrollView.java", "../../brave/android/java/org/chromium/chrome/browser/custom_layout/NonSwipeableViewPager.java", "../../brave/android/java/org/chromium/chrome/browser/custom_layout/VerticalViewPager.java", "../../brave/android/java/org/chromium/chrome/browser/custom_layout/popup_window_tooltip/ArrowColorDrawable.java", diff --git a/android/java/org/chromium/chrome/browser/app/domain/MarketModel.java b/android/java/org/chromium/chrome/browser/app/domain/MarketModel.java new file mode 100644 index 00000000000..9f439a64f36 --- /dev/null +++ b/android/java/org/chromium/chrome/browser/app/domain/MarketModel.java @@ -0,0 +1,91 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * 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 https://mozilla.org/MPL/2.0/. */ + +package org.chromium.chrome.browser.app.domain; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import org.chromium.brave_wallet.mojom.AssetRatioService; +import org.chromium.brave_wallet.mojom.CoinMarket; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +public class MarketModel { + private static final String CURRENCY = "USD"; + // Fetch the first 250 assets. + private static final byte ASSETS_REQUEST_LIMIT = (byte) (250 & 0xFF); + private AssetRatioService mAssetRatioService; + + private final DecimalFormatSymbols mSymbols; + private final DecimalFormat mPriceFormatter; + private final DecimalFormat mPercentageFormatter; + + public final LiveData mCoinMarkets; + private final MutableLiveData _mCoinMarkets; + + public MarketModel(AssetRatioService assetRatioService) { + mAssetRatioService = assetRatioService; + + _mCoinMarkets = new MutableLiveData<>(); + mCoinMarkets = _mCoinMarkets; + + mSymbols = new DecimalFormatSymbols(Locale.ENGLISH); + mPriceFormatter = new DecimalFormat("$0.00##########", mSymbols); + mPriceFormatter.setGroupingSize(3); + mPriceFormatter.setGroupingUsed(true); + + mPercentageFormatter = new DecimalFormat("0.00", mSymbols); + mPercentageFormatter.setRoundingMode(RoundingMode.UP); + } + + public void resetService(AssetRatioService assetRatioService) { + mAssetRatioService = assetRatioService; + } + + /** + * Gets the first 250 market assets. + * See {@link #mCoinMarkets} the live data used to emit the assets. + */ + public void getCoinMarkets() { + if (mAssetRatioService == null) { + return; + } + mAssetRatioService.getCoinMarkets( + CURRENCY, ASSETS_REQUEST_LIMIT, (success, coinMarkets) -> { + if (success) { + _mCoinMarkets.postValue(coinMarkets); + } else { + _mCoinMarkets.postValue(null); + } + }); + } + + /** + * Gets formatted price using the format `$0.00##########` (e.g. $27,160.00, or + * $0.523453456789). + * @param price Price to format. + * @return Formatted price. + */ + @NonNull + public String getFormattedPrice(double price) { + return mPriceFormatter.format(price); + } + + /** + * Gets formatted percentage change rounded up and using the format `0.00` (e.g. 0.05%). + * @param percentageChange Percentage change to format. + * @return Formatted percentage change. + */ + @NonNull + public String getFormattedPercentageChange(double percentageChange) { + double absoluteChange = Math.abs(percentageChange); + return String.format(Locale.ENGLISH, "%s%%", mPercentageFormatter.format(absoluteChange)); + } +} diff --git a/android/java/org/chromium/chrome/browser/app/domain/WalletModel.java b/android/java/org/chromium/chrome/browser/app/domain/WalletModel.java index 7310e05d7c9..a0e13c54d47 100644 --- a/android/java/org/chromium/chrome/browser/app/domain/WalletModel.java +++ b/android/java/org/chromium/chrome/browser/app/domain/WalletModel.java @@ -33,6 +33,7 @@ public class WalletModel { private final CryptoModel mCryptoModel; private final DappsModel mDappsModel; private final KeyringModel mKeyringModel; + private final MarketModel mMarketModel; private Context mContext; private CryptoActions mCryptoActions; @@ -60,6 +61,7 @@ public class WalletModel { mCryptoModel.getPendingTxHelper()); mKeyringModel = new KeyringModel(mContext, mKeyringService, mCryptoModel.getSharedData(), mBraveWalletService, mCryptoActions); + mMarketModel = new MarketModel(mAssetRatioService); // be careful with dependencies, must avoid cycles mCryptoModel.setAccountInfosFromKeyRingModel(mKeyringModel.mAccountInfos); init(); @@ -86,6 +88,7 @@ public class WalletModel { mDappsModel.resetServices( mJsonRpcService, mBraveWalletService, mCryptoModel.getPendingTxHelper()); mKeyringModel.resetService(mContext, mKeyringService, braveWalletService); + mMarketModel.resetService(mAssetRatioService); init(); } @@ -120,6 +123,10 @@ public class WalletModel { return mDappsModel; } + public MarketModel getMarketModel() { + return mMarketModel; + } + public KeyringService getKeyringService() { return mKeyringService; } diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CryptoFragmentPageAdapter.java b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CryptoFragmentPageAdapter.java index dd796e58c73..9ee6037e9fc 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CryptoFragmentPageAdapter.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/CryptoFragmentPageAdapter.java @@ -17,6 +17,7 @@ import androidx.fragment.app.FragmentStatePagerAdapter; import org.chromium.base.ContextUtils; import org.chromium.chrome.R; import org.chromium.chrome.browser.crypto_wallet.fragments.AccountsFragment; +import org.chromium.chrome.browser.crypto_wallet.fragments.MarketFragment; import org.chromium.chrome.browser.crypto_wallet.fragments.NftGridFragment; import org.chromium.chrome.browser.crypto_wallet.fragments.PortfolioFragment; @@ -28,6 +29,7 @@ public class CryptoFragmentPageAdapter extends FragmentStatePagerAdapter { private static final int PORTFOLIO_FRAGMENT_POSITION = 0; private static final int NFT_GRID_FRAGMENT_POSITION = 1; private static final int ACCOUNTS_FRAGMENT_POSITION = 2; + private static final int MARKET_FRAGMENT_POSITION = 3; private final List mTitles; @@ -38,7 +40,7 @@ public class CryptoFragmentPageAdapter extends FragmentStatePagerAdapter { Resources resources = context.getResources(); mTitles = new ArrayList<>(Arrays.asList(resources.getString(R.string.portfolio), resources.getString(R.string.brave_wallet_nfts), - resources.getString(R.string.accounts))); + resources.getString(R.string.accounts), resources.getString(R.string.market))); } @NonNull @@ -52,6 +54,8 @@ public class CryptoFragmentPageAdapter extends FragmentStatePagerAdapter { return NftGridFragment.newInstance(); case ACCOUNTS_FRAGMENT_POSITION: return AccountsFragment.newInstance(); + case MARKET_FRAGMENT_POSITION: + return MarketFragment.newInstance(); default: throw new IllegalStateException( String.format("No fragment found for position %d.", position)); diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/MarketCoinAdapter.java b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/MarketCoinAdapter.java new file mode 100644 index 00000000000..4b8a783f44d --- /dev/null +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/adapters/MarketCoinAdapter.java @@ -0,0 +1,110 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * 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 https://mozilla.org/MPL/2.0/. */ + +package org.chromium.chrome.browser.crypto_wallet.adapters; + +import static org.chromium.ui.base.ViewUtils.dpToPx; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import org.chromium.brave_wallet.mojom.CoinMarket; +import org.chromium.chrome.R; +import org.chromium.chrome.browser.app.domain.MarketModel; +import org.chromium.chrome.browser.app.helpers.ImageLoader; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class MarketCoinAdapter extends RecyclerView.Adapter { + private static final int PERCENTAGE_CHANGE_DRAWABLE_PADDING_PX = 8; + private final MarketModel mMarketModel; + private final List mCoinMarkets; + + private final Context mContext; + + public MarketCoinAdapter( + @NonNull final Context context, @NonNull final MarketModel marketModel) { + mContext = context; + mMarketModel = marketModel; + mCoinMarkets = new ArrayList<>(); + } + + @Override + public @NonNull ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(mContext); + View marketCoinView = inflater.inflate(R.layout.market_coin_list_item, parent, false); + return new ViewHolder(marketCoinView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + CoinMarket coinMarket = mCoinMarkets.get(position); + holder.coinName.setText(coinMarket.name); + holder.symbol.setText(coinMarket.symbol.toUpperCase(Locale.ENGLISH)); + + holder.price.setText(mMarketModel.getFormattedPrice(coinMarket.currentPrice)); + setPriceIndicator(holder.changePercentage, coinMarket.priceChangePercentage24h); + + ImageLoader.downloadImage( + coinMarket.image, Glide.with(mContext), true, 0, holder.icon, null); + } + + @Override + public int getItemCount() { + return mCoinMarkets.size(); + } + + public void setCoinMarkets(@NonNull final List coinMarkets) { + final int itemCount = mCoinMarkets.size(); + mCoinMarkets.clear(); + mCoinMarkets.addAll(coinMarkets); + notifyItemRangeRemoved(0, itemCount); + notifyItemRangeInserted(0, coinMarkets.size()); + } + + private void setPriceIndicator(TextView textView, double percentageChange) { + if (percentageChange > 0) { + textView.setTextColor(mContext.getColor(R.color.brave_wallet_day_night_bull)); + textView.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_bull_arrow, 0, 0, 0); + } else { + textView.setTextColor(mContext.getColor(R.color.brave_wallet_day_night_bear)); + textView.setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_bear_arrow, 0, 0, 0); + } + + textView.setText(mMarketModel.getFormattedPercentageChange(percentageChange)); + textView.setCompoundDrawablePadding(dpToPx(mContext.getResources().getDisplayMetrics(), + PERCENTAGE_CHANGE_DRAWABLE_PADDING_PX)); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final ImageView icon; + private final TextView coinName; + private final TextView symbol; + private final TextView price; + private final TextView changePercentage; + + public ViewHolder(View itemView) { + super(itemView); + icon = itemView.findViewById(R.id.icon); + coinName = itemView.findViewById(R.id.coin_name); + symbol = itemView.findViewById(R.id.symbol); + price = itemView.findViewById(R.id.price); + changePercentage = itemView.findViewById(R.id.change_percentage); + } + } +} diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/MarketFragment.java b/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/MarketFragment.java new file mode 100644 index 00000000000..3c1c5309792 --- /dev/null +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/MarketFragment.java @@ -0,0 +1,177 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * 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 https://mozilla.org/MPL/2.0/. */ + +package org.chromium.chrome.browser.crypto_wallet.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.cardview.widget.CardView; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import org.chromium.base.Log; +import org.chromium.brave_wallet.mojom.AccountInfo; +import org.chromium.brave_wallet.mojom.BraveWalletConstants; +import org.chromium.brave_wallet.mojom.CoinMarket; +import org.chromium.brave_wallet.mojom.KeyringInfo; +import org.chromium.brave_wallet.mojom.KeyringService; +import org.chromium.chrome.R; +import org.chromium.chrome.browser.app.BraveActivity; +import org.chromium.chrome.browser.app.domain.MarketModel; +import org.chromium.chrome.browser.app.domain.WalletModel; +import org.chromium.chrome.browser.app.helpers.Api33AndPlusBackPressHelper; +import org.chromium.chrome.browser.app.shimmer.ShimmerFrameLayout; +import org.chromium.chrome.browser.crypto_wallet.activities.AccountDetailActivity; +import org.chromium.chrome.browser.crypto_wallet.activities.AddAccountActivity; +import org.chromium.chrome.browser.crypto_wallet.activities.BraveWalletActivity; +import org.chromium.chrome.browser.crypto_wallet.adapters.MarketCoinAdapter; +import org.chromium.chrome.browser.crypto_wallet.adapters.WalletCoinAdapter; +import org.chromium.chrome.browser.crypto_wallet.listeners.OnWalletListItemClick; +import org.chromium.chrome.browser.crypto_wallet.model.WalletListItemModel; +import org.chromium.chrome.browser.crypto_wallet.util.AndroidUtils; +import org.chromium.chrome.browser.crypto_wallet.util.Utils; +import org.chromium.chrome.browser.custom_layout.LockableNestedScrollView; +import org.chromium.chrome.browser.settings.BraveWalletPreferences; +import org.chromium.chrome.browser.settings.SettingsLauncherImpl; +import org.chromium.components.browser_ui.settings.SettingsLauncher; + +import java.util.Arrays; + +public class MarketFragment extends Fragment { + private static final String TAG = "MarketFragment"; + + private LockableNestedScrollView mRootView; + private RecyclerView mCoinMarkets; + private ShimmerFrameLayout mShimmerViewContainer; + private LinearLayout mSingleItemShimmer; + private CardView mShimmerCardView; + private LinearLayout mShimmerListContainer; + private CardView mMarketListCardView; + + private MarketCoinAdapter mMarketCoinAdapter; + + private WalletModel mWalletModel; + private MarketModel mMarketModel; + + private boolean mShimmerActive = true; + + public static MarketFragment newInstance() { + return new MarketFragment(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Api33AndPlusBackPressHelper.create( + this, (FragmentActivity) requireActivity(), () -> requireActivity().finish()); + } + try { + BraveActivity activity = BraveActivity.getBraveActivity(); + mWalletModel = activity.getWalletModel(); + mMarketModel = mWalletModel.getMarketModel(); + } catch (BraveActivity.BraveActivityNotFoundException exception) { + Log.e(TAG, "onCreate", exception); + } + mMarketCoinAdapter = new MarketCoinAdapter(getContext(), mMarketModel); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + mRootView = (LockableNestedScrollView) inflater.inflate( + R.layout.fragment_market, container, false); + mRootView.setScrollingEnabled(false); + mShimmerViewContainer = mRootView.findViewById(R.id.shimmer_view_container); + mSingleItemShimmer = mRootView.findViewById(R.id.market_coin_item); + mShimmerCardView = mRootView.findViewById(R.id.card_view_market_placeholder); + mShimmerListContainer = mRootView.findViewById(R.id.shimmer_list); + mMarketListCardView = mRootView.findViewById(R.id.market_list_card_view_view_container); + + mCoinMarkets = mRootView.findViewById(R.id.coin_markets_recycler_view); + mCoinMarkets.addItemDecoration( + new DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)); + mCoinMarkets.setAdapter(mMarketCoinAdapter); + mCoinMarkets.setLayoutManager(new LinearLayoutManager(getActivity())); + + final int visibleShimmerItems = + getVisibleShimmerItems(mSingleItemShimmer, mShimmerCardView); + for (int i = 1; i < visibleShimmerItems; i++) { + inflater.inflate(R.layout.brave_wallet_divider, mShimmerListContainer, true); + inflater.inflate(R.layout.market_coin_shimmer_item, mShimmerListContainer, true); + } + + mMarketModel.mCoinMarkets.observe(getViewLifecycleOwner(), coinMarkets -> { + if (coinMarkets == null) { + Log.e(TAG, "Failed to load coin market list."); + } else { + mMarketCoinAdapter.setCoinMarkets(Arrays.asList(coinMarkets)); + } + disableShimmerEffect(); + }); + mMarketModel.getCoinMarkets(); + + return mRootView; + } + + private int getVisibleShimmerItems( + final ViewGroup mSingleItemShimmer, final CardView shimmerCardView) { + int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + + mSingleItemShimmer.measure(widthMeasureSpec, heightMeasureSpec); + int itemHeight = mSingleItemShimmer.getMeasuredHeight(); + int totalHeight = AndroidUtils.getScreenHeight(); + + int totalHeightNoPadding = + totalHeight - shimmerCardView.getPaddingTop() - shimmerCardView.getPaddingBottom(); + + return Math.max(1, (int) Math.ceil((float) totalHeightNoPadding / itemHeight)); + } + + @Override + public void onResume() { + super.onResume(); + if (mShimmerActive) { + mShimmerViewContainer.startShimmer(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (mShimmerActive) { + mShimmerViewContainer.stopShimmer(); + } + } + + private void disableShimmerEffect() { + if (mShimmerActive) { + mShimmerViewContainer.hideShimmer(); + mShimmerViewContainer.setVisibility(View.GONE); + mRootView.setScrollingEnabled(true); + mMarketListCardView.setVisibility(View.VISIBLE); + mShimmerActive = false; + } + } +} diff --git a/android/java/org/chromium/chrome/browser/custom_layout/LockableNestedScrollView.java b/android/java/org/chromium/chrome/browser/custom_layout/LockableNestedScrollView.java new file mode 100644 index 00000000000..8a914d7c58c --- /dev/null +++ b/android/java/org/chromium/chrome/browser/custom_layout/LockableNestedScrollView.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2023 The Brave Authors. All rights reserved. + * 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 https://mozilla.org/MPL/2.0/. + */ +package org.chromium.chrome.browser.custom_layout; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.widget.NestedScrollView; + +/** + * {@link NestedScrollView} whose scrolling can be temporarily locked using + * the method {@link #setScrollingEnabled(boolean)}. The scrolling is enabled by default. + */ +public class LockableNestedScrollView extends NestedScrollView { + private boolean scrollable = true; + + public LockableNestedScrollView(@NonNull Context context) { + super(context); + } + + public LockableNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public LockableNestedScrollView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(@NonNull MotionEvent ev) { + return scrollable && super.onTouchEvent(ev); + } + + @Override + public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) { + return scrollable && super.onInterceptTouchEvent(ev); + } + + /** + * Enables or disables the scrolling depending on the value set. + * + * @param enabled {@code true} to enable the scrolling, {@code false} to disable it. + */ + public void setScrollingEnabled(boolean enabled) { + scrollable = enabled; + } +} diff --git a/android/java/res/drawable/circle_shimmer.xml b/android/java/res/drawable/circle_shimmer.xml new file mode 100644 index 00000000000..a18ffe270ee --- /dev/null +++ b/android/java/res/drawable/circle_shimmer.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/android/java/res/drawable/ic_bear_arrow.xml b/android/java/res/drawable/ic_bear_arrow.xml new file mode 100644 index 00000000000..3c13ca567a1 --- /dev/null +++ b/android/java/res/drawable/ic_bear_arrow.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/java/res/drawable/ic_bull_arrow.xml b/android/java/res/drawable/ic_bull_arrow.xml new file mode 100644 index 00000000000..add49f1ea60 --- /dev/null +++ b/android/java/res/drawable/ic_bull_arrow.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/java/res/layout/brave_wallet_divider.xml b/android/java/res/layout/brave_wallet_divider.xml new file mode 100644 index 00000000000..cf3cd907dc3 --- /dev/null +++ b/android/java/res/layout/brave_wallet_divider.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/android/java/res/layout/fragment_market.xml b/android/java/res/layout/fragment_market.xml new file mode 100644 index 00000000000..09fd45beea4 --- /dev/null +++ b/android/java/res/layout/fragment_market.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/java/res/layout/market_coin_list_item.xml b/android/java/res/layout/market_coin_list_item.xml new file mode 100644 index 00000000000..84243a4145d --- /dev/null +++ b/android/java/res/layout/market_coin_list_item.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/java/res/layout/market_coin_shimmer_item.xml b/android/java/res/layout/market_coin_shimmer_item.xml new file mode 100644 index 00000000000..2e43e1fd36a --- /dev/null +++ b/android/java/res/layout/market_coin_shimmer_item.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/java/res/values/brave_colors.xml b/android/java/res/values/brave_colors.xml index 19468a7f16e..c27321e429e 100644 --- a/android/java/res/values/brave_colors.xml +++ b/android/java/res/values/brave_colors.xml @@ -33,6 +33,8 @@ #F0F2FF #FFFFFF #F8F9FA + #FF2AC194 + #FFEE6374 #868E96 diff --git a/browser/ui/android/strings/android_brave_strings.grd b/browser/ui/android/strings/android_brave_strings.grd index 865d557f7a5..f4928191aff 100644 --- a/browser/ui/android/strings/android_brave_strings.grd +++ b/browser/ui/android/strings/android_brave_strings.grd @@ -2500,6 +2500,9 @@ If you don't accept this request, VPN will not reconnect and your internet conne Portfolio + + Market +