From 208f10e5429b42a2af0dbfa204bc21914f9dd1e3 Mon Sep 17 00:00:00 2001 From: Sayed Owais Ali Date: Mon, 23 Jun 2025 18:16:22 +0530 Subject: [PATCH] NTP-71867 | Amc fund details revamp (#16660) --- .../amc/fundbuy/adapters/FundTagsAdapter.kt | 59 ++++ .../fundbuy/fragments/FundDetailsFragment.kt | 246 +++++++++++++--- .../fundbuy/models/FundDetailScreenData.kt | 32 ++- .../amc/fundbuy/models/FundGraphDetails.kt | 8 + .../amc/fundbuy/models/FundInfoCardsData.kt | 20 ++ .../amc/fundbuy/models/FundManagerData.kt | 4 + .../fundbuy/models/ListItemProgressData.kt | 3 + .../fundbuy/viewmodel/FundDetailViewModel.kt | 8 + .../navi/amc/fundbuy/views/FundDetailView.kt | 35 +++ .../amc/fundbuy/views/FundGraphToolTipView.kt | 269 +++++++++++++----- .../navi/amc/fundbuy/views/FundGraphView.kt | 130 ++++++++- .../navi/amc/fundbuy/views/FundManagerView.kt | 78 +++++ .../amc/fundbuy/views/ListItemProgressView.kt | 51 +++- .../java/com/navi/amc/utils/AccordionUtils.kt | 77 +++++ .../java/com/navi/amc/utils/ColorUtils.kt | 2 +- .../res/drawable/fund_graph_tooltip_bg.xml | 81 +----- .../res/drawable/tag_green_background.xml | 6 + .../res/layout/download_document_layout.xml | 57 ++-- .../res/layout/fund_detail_screen_layout.xml | 19 +- .../res/layout/fund_detail_view_layout.xml | 40 ++- .../src/main/res/layout/fund_graph_layout.xml | 46 ++- .../fund_graph_tool_tip_details_layout.xml | 51 ++++ .../fund_graph_tool_tip_view_layout.xml | 71 +---- .../res/layout/fund_info_card_item_layout.xml | 40 +++ .../main/res/layout/fund_manager_layout.xml | 106 ++++--- .../res/layout/information_view_layout.xml | 24 +- .../src/main/res/layout/item_fund_tag.xml | 13 + .../main/res/layout/list_item_progress.xml | 127 +++++---- .../layout/vertical_dotted_line_layout.xml | 18 ++ .../main/res/drawable/divider_margin_4.xml | 14 + .../naviwidgets/models/FundDetailsWidget.kt | 3 + 31 files changed, 1367 insertions(+), 371 deletions(-) create mode 100644 android/navi-amc/src/main/java/com/navi/amc/fundbuy/adapters/FundTagsAdapter.kt create mode 100644 android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundInfoCardsData.kt create mode 100644 android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundManagerView.kt create mode 100644 android/navi-amc/src/main/java/com/navi/amc/utils/AccordionUtils.kt create mode 100644 android/navi-amc/src/main/res/drawable/tag_green_background.xml create mode 100644 android/navi-amc/src/main/res/layout/fund_graph_tool_tip_details_layout.xml create mode 100644 android/navi-amc/src/main/res/layout/fund_info_card_item_layout.xml create mode 100644 android/navi-amc/src/main/res/layout/item_fund_tag.xml create mode 100644 android/navi-amc/src/main/res/layout/vertical_dotted_line_layout.xml create mode 100644 android/navi-common/src/main/res/drawable/divider_margin_4.xml diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/adapters/FundTagsAdapter.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/adapters/FundTagsAdapter.kt new file mode 100644 index 0000000000..fe3ed7e619 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/adapters/FundTagsAdapter.kt @@ -0,0 +1,59 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.navi.amc.databinding.ItemFundTagBinding +import com.navi.amc.fundbuy.models.FundTagData +import com.navi.design.textview.model.TextWithStyle +import com.navi.design.utils.dpToPxInInt +import com.navi.design.utils.getNaviDrawable +import com.navi.design.utils.parseColorSafe +import com.navi.design.utils.setSpannableString + +class FundTagsAdapter : RecyclerView.Adapter() { + + private val pills = mutableListOf() + + fun updatePills(newPills: List) { + pills.clear() + pills.addAll(newPills) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TagViewHolder { + val binding = ItemFundTagBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return TagViewHolder(binding) + } + + override fun onBindViewHolder(holder: TagViewHolder, position: Int) { + holder.bind(pills[position]) + } + + override fun getItemCount(): Int = pills.size + + class TagViewHolder(private val binding: ItemFundTagBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(pill: FundTagData) { + binding.apply { + tagText.setSpannableString(TextWithStyle(pill.text, pill.span)) + + root.background = + getNaviDrawable( + backgroundColor = pill.bgColor.parseColorSafe(), + cornerRadius = dpToPxInInt(16), + ) + + root.setPadding(dpToPxInInt(4), dpToPxInInt(2), dpToPxInInt(4), dpToPxInInt(2)) + } + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt index 8d72b68626..76c97a813d 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/FundDetailsFragment.kt @@ -8,12 +8,12 @@ package com.navi.amc.fundbuy.fragments import android.content.Context -import android.graphics.Rect import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewStub +import android.widget.ImageView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams @@ -34,14 +34,16 @@ import com.navi.amc.common.view.InformationView import com.navi.amc.compose.feature.ftue.model.FundSelectionData import com.navi.amc.databinding.DownloadDocumentLayoutBinding import com.navi.amc.databinding.FundDetailScreenLayoutBinding -import com.navi.amc.databinding.FundManagerLayoutBinding import com.navi.amc.databinding.InvestUspLayoutBinding +import com.navi.amc.fundbuy.models.AmcHeaderData import com.navi.amc.fundbuy.models.FundReturn import com.navi.amc.fundbuy.viewmodel.FundBuyFlowViewModel import com.navi.amc.fundbuy.viewmodel.FundDetailViewModel import com.navi.amc.fundbuy.views.FundDetailCarouselView import com.navi.amc.fundbuy.views.FundDetailView +import com.navi.amc.fundbuy.views.FundGraphToolTipView import com.navi.amc.fundbuy.views.FundGraphView +import com.navi.amc.fundbuy.views.FundManagerView import com.navi.amc.fundbuy.views.ListItemProgressView import com.navi.amc.navigator.NaviAmcDeeplinkNavigator import com.navi.amc.utils.AmcAnalytics @@ -64,7 +66,6 @@ import com.navi.base.model.Padding import com.navi.base.utils.isNotNull import com.navi.base.utils.isNull import com.navi.base.utils.orFalse -import com.navi.base.utils.orTrue import com.navi.base.utils.orZero import com.navi.common.listeners.FragmentInterchangeListener import com.navi.common.listeners.HeaderInteractionListener @@ -148,27 +149,25 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { viewModel.fundDetailScreenData.observe(viewLifecycleOwner) { fundDetailScreenData -> hideLoader() binding.apply { + binding.fundOnscrollView.apply { + left.setSpannableString(viewModel.fundReturn.value?.leftText) + right.setSpannableString(viewModel.fundReturn.value?.rightText) + root.visibility = View.GONE + } + scroll.viewTreeObserver.addOnScrollChangedListener { - val rect = Rect() - scroll.getHitRect(rect) - val view = binding.container.findViewWithTag("graph")?.getTitle() - val returnView = - binding.container - .findViewWithTag("graph") - ?.getReturnDetailsView() - if ( - !view?.getLocalVisibleRect(rect).orTrue() && - !returnView?.getLocalVisibleRect(rect).orTrue() - ) { - binding.fundOnscrollView.apply { - left.setSpannableString(viewModel.fundReturn.value?.leftText) - right.setSpannableString(viewModel.fundReturn.value?.rightText) + val scrollY = scroll.scrollY + binding.fundOnscrollView.apply { + left.setSpannableString(viewModel.fundReturn.value?.leftText) + right.setSpannableString(viewModel.fundReturn.value?.rightText) + + if (scrollY > 0) { if (left.isVisible && right.isVisible) { root.visibility = View.VISIBLE } + } else { + root.visibility = View.GONE } - } else { - binding.fundOnscrollView.root.visibility = View.GONE } } scroll.updateLayoutParams { @@ -249,7 +248,36 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { fundDetailScreenData?.content?.amcHeaderData?.let { viewModel.fundName = it.title?.text.orEmpty() - binding.header.setProperties(it) + binding.header.setProperties( + AmcHeaderData( + title = it.title, + subtitle = it.subtitle, + icon = it.icon, + bgColor = it.bgColor, + filter = it.filter, + label = it.label, + iconSize = it.iconSize, + ) + ) + } + + fundDetailScreenData?.content?.amcHeaderData?.fundTags?.let { fundTags -> + if (binding.fundTags.adapter == null) { + val tagsAdapter = com.navi.amc.fundbuy.adapters.FundTagsAdapter() + binding.fundTags.adapter = tagsAdapter + binding.fundTags.layoutManager = + androidx.recyclerview.widget.LinearLayoutManager( + context, + androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL, + false, + ) + } + + (binding.fundTags.adapter as? com.navi.amc.fundbuy.adapters.FundTagsAdapter) + ?.updatePills(fundTags) + + binding.fundTags.visibility = + if (fundTags.isNotEmpty()) View.VISIBLE else View.GONE } } binding.companies.isVisible = @@ -257,6 +285,7 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { binding.companies.showWhenDataIsAvailable(it) true } ?: run { false } + container.apply { removeAllViews() val inflater = LayoutInflater.from(context) @@ -271,6 +300,7 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { viewModel.chipIdToGraphDataMap[viewModel.selectedChipId]!! ) } else FundGraphUiState.Loading + val view = FundGraphView(context) view.setProperties( data = fundDetailScreenData.content.fundGraphDetails, @@ -283,9 +313,58 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { vmSelectedKey = viewModel.selectedChipId, setRadioAction = ::setSelectedRadio, vmSelectedRadio = viewModel.selectedRadio, + fundInfoCards = fundDetailScreenData.content.fundInfoCards, ) view.tag = "graph" + view.setPadding( + view.paddingLeft, + view.paddingTop + dpToPxInInt(10), + view.paddingRight, + view.paddingBottom + dpToPxInInt(10), + ) addView(view) + + val showGraphTextView = view.getShowGraphTextView() + val showGraphIconView = view.getShowGraphIconView() + val showGraphContainer = view.getShowGraphContainer() + + fundDetailScreenData.content.fundGraphDetails.showGraphTag?.let { + showGraphTag -> + showGraphTextView.setSpannableString(showGraphTag.title) + showGraphTextView.isVisible = true + showGraphIconView.isVisible = true + + val shouldShowGraph = + fundDetailScreenData.content.fundGraphDetails.shouldShowGraph == + true + viewModel.setIsGraphVisible(shouldShowGraph) + + val chartComponent = view.findViewById(R.id.chart) + val xAxisLabel = view.findViewById(R.id.x_axis_label) + val clErrorLoader = view.findViewById(R.id.cl_error_loader) + + if (!shouldShowGraph) { + chartComponent?.visibility = View.GONE + xAxisLabel?.visibility = View.GONE + clErrorLoader?.visibility = View.GONE + + showGraphIconView.showWhenDataIsAvailable( + showGraphTag.unSelectedIconCode + ) + } + + showGraphContainer.setOnClickListener { + if (viewModel.isGraphVisible.value == true) { + viewModel.setIsGraphVisible(false) + } else { + viewModel.setIsGraphVisible(true) + } + } + } + ?: run { + showGraphTextView.isVisible = false + showGraphIconView.isVisible = false + } } fundDetailScreenData?.content?.usp?.let { @@ -326,8 +405,7 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { fundDetailScreenData?.content?.fundHoldingDetails?.let { data -> val view = ListItemProgressView(context) view.setProperties(data) { - val bundle = - Bundle().apply { putString(Constant.DATA, Gson().toJson(data)) } + val bundle = Bundle().apply { putString(DATA, Gson().toJson(data)) } ListItemProgressBottomSheet.newInstance(bundle).let { bottomSheet -> safelyShowBottomSheet( bottomSheet, @@ -337,21 +415,11 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { } addView(view) } - fundDetailScreenData?.content?.fundManagerDetailData?.let { - val childBinding = - DataBindingUtil.inflate( - inflater, - R.layout.fund_manager_layout, - container, - false, - ) - childBinding.apply { - title.setSpannableString(it.title) - name.setSpannableString(it.name) - experience.setSpannableString(it.experience) - icon.showWhenDataIsAvailable(it.profile) - } - addView(childBinding.root) + fundDetailScreenData?.content?.fundManagerDetailData?.let { managerData -> + val fundManagerView = FundManagerView(requireContext()) + + fundManagerView.setProperties(managerData) + addView(fundManagerView) } fundDetailScreenData?.content?.documentData?.let { val childBinding = @@ -365,6 +433,11 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { title.setSpannableString(it.title) subtitle.setSpannableString(it.subtitle) icon.showWhenDataIsAvailable(it.icon?.iconCode) + subtitleLinearLayout.background = + getNaviDrawable( + backgroundColor = it.bgColor.parseColorSafe("#F5F5F5"), + cornerRadius = dpToPxInInt(4), + ) touch.setOnClickListener { view -> it.icon?.downloadUrlList?.let { downloadUrlList -> downloadUrlList.forEach { downloadUrl -> @@ -393,6 +466,14 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { view.updateRootViewTopPadding( Padding(startDp = 0f, topDp = 0f, endDp = 0f, bottomDp = 24f) ) + view + .findViewById(R.id.compliance_text) + .setSpannableString(fundDetailScreenData.content.regulatoryInfo?.title) + view + .findViewById(R.id.compliance_icon) + .showWhenDataIsAvailable( + fundDetailScreenData.content.regulatoryInfo?.iconCode + ) addView(view) } fundDetailScreenData?.footer?.backCta?.let { @@ -424,12 +505,66 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { viewLifecycleOwner.lifecycleScope.launchWhenResumed { viewModel.fundGraphData.collect { graphUiState -> updateGraph(graphUiState) } } + viewModel.isGraphVisible.observeNonNull(viewLifecycleOwner) { isVisible -> + val graphView = binding.container.findViewWithTag("graph") + + graphView?.let { view -> + val chartComponent = view.findViewById(R.id.chart) + val xAxisLabel = view.findViewById(R.id.x_axis_label) + val chipGroup = view.findViewById(R.id.chip_group) + val clErrorLoader = view.findViewById(R.id.cl_error_loader) + + val chart = + view.findViewById(R.id.chart) + val marker = chart?.marker as? FundGraphToolTipView + marker?.updateVisibility(isVisible) + + if (isVisible) { + chartComponent?.visibility = View.VISIBLE + xAxisLabel?.visibility = View.VISIBLE + clErrorLoader?.visibility = View.GONE + + chipGroup?.let { group -> + val params = group.layoutParams as? ConstraintLayout.LayoutParams + params?.let { + it.topMargin = dpToPxInInt(317) + group.layoutParams = it + } + } + } else { + chartComponent?.visibility = View.GONE + xAxisLabel?.visibility = View.GONE + clErrorLoader?.visibility = View.GONE + + chipGroup?.let { group -> + val params = group.layoutParams as? ConstraintLayout.LayoutParams + params?.let { + it.topMargin = dpToPxInInt(24) + group.layoutParams = it + } + } + } + + val showGraphIconView = view.getShowGraphIconView() + val showGraphTag = + viewModel.fundDetailScreenData.value?.content?.fundGraphDetails?.showGraphTag + + if (isVisible) { + showGraphIconView.showWhenDataIsAvailable(showGraphTag?.selectedIconCode) + } else { + showGraphIconView.showWhenDataIsAvailable(showGraphTag?.unSelectedIconCode) + } + } + } } private fun updateGraph(graphUiState: FundGraphUiState) { - binding.container - .findViewWithTag("graph") - ?.setProperties( + val graphView = binding.container.findViewWithTag("graph") + + val wasGraphVisible = viewModel.isGraphVisible.value ?: true + + graphView?.let { view -> + view.setProperties( data = viewModel.fundDetailScreenData.value?.content?.fundGraphDetails, graphUiState = graphUiState, fundInvestmentDetailData = @@ -441,6 +576,35 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { setRadioAction = ::setSelectedRadio, vmSelectedRadio = viewModel.selectedRadio, ) + + if (!wasGraphVisible) { + viewModel.setIsGraphVisible(false) + + val chartComponent = view.findViewById(R.id.chart) + val xAxisLabel = view.findViewById(R.id.x_axis_label) + val clErrorLoader = view.findViewById(R.id.cl_error_loader) + + chartComponent?.visibility = View.GONE + xAxisLabel?.visibility = View.GONE + clErrorLoader?.visibility = View.GONE + + val chipGroup = view.findViewById(R.id.chip_group) + chipGroup?.let { group -> + val params = group.layoutParams as? ConstraintLayout.LayoutParams + params?.let { + it.topMargin = dpToPxInInt(24) + group.layoutParams = it + } + } + + val showGraphIconView = view.getShowGraphIconView() + val showGraphTag = + viewModel.fundDetailScreenData.value?.content?.fundGraphDetails?.showGraphTag + showGraphTag?.unSelectedIconCode?.let { iconCode -> + showGraphIconView.showWhenDataIsAvailable(iconCode) + } + } + } } private fun navigate(action: ActionData) { @@ -450,7 +614,7 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { private fun onActionIconClick(actionData: ActionData) { val url = actionData?.url sendEvent(actionData?.metaData?.clickedData) - if (url == Constant.SHOW_BOTTOMSHEET) { + if (url == SHOW_BOTTOMSHEET) { val data = actionData.parameters?.getOrNull(0)?.value val key = actionData.parameters?.getOrNull(0)?.key val bundle = Bundle().apply { putString(Constant.DATA, data) } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt index bcc1659047..2711010ef4 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenData.kt @@ -7,14 +7,18 @@ package com.navi.amc.fundbuy.models +import android.os.Parcelable import com.google.gson.annotations.SerializedName import com.navi.amc.common.model.Footer import com.navi.amc.common.model.FundInvestmentDetailData import com.navi.amc.common.model.InformationCardData import com.navi.common.model.Header +import com.navi.design.textview.model.NaviSpan import com.navi.design.textview.model.TextWithStyle import com.navi.naviwidgets.models.FundDetailData import com.navi.naviwidgets.models.response.amc.TagData +import java.io.Serializable +import kotlinx.parcelize.Parcelize data class FundDetails( @SerializedName("header") val header: Header? = null, @@ -23,7 +27,7 @@ data class FundDetails( ) data class FundDetailScreenData( - @SerializedName("fundHeader") val amcHeaderData: AmcHeaderData? = null, + @SerializedName("fundHeader") val amcHeaderData: FundDetailsHeaderData? = null, @SerializedName("fundDetails") val fundDetailData: FundDetailData? = null, @SerializedName("fundManagerDetails") val fundManagerDetailData: FundManagerData? = null, @SerializedName("fundHoldingDetails") val fundHoldingDetails: ListItemProgressData? = null, @@ -36,11 +40,37 @@ data class FundDetailScreenData( @SerializedName("companiesLogo") val companiesLogo: String? = null, @SerializedName("usp") val usp: FundUspData? = null, @SerializedName("disclaimerData") val disclaimerData: InformationCardData? = null, + @SerializedName("fundInfoCards") val fundInfoCards: FundInfoCardsData? = null, @SerializedName("fundInvestmentDetails") val fundInvestmentDetails: FundInvestmentDetailData? = null, + @SerializedName("regulatoryInfo") val regulatoryInfo: RegulatoryInfoData? = null, ) +@Parcelize +data class FundDetailsHeaderData( + @SerializedName("title") val title: TextWithStyle? = null, + @SerializedName("subtitle", alternate = ["subTitle"]) val subtitle: TextWithStyle? = null, + @SerializedName("filter") val filter: IconData? = null, + @SerializedName("label") val label: LabelData? = null, + @SerializedName("icon") val icon: ActionIcon? = null, + @SerializedName("bgColor") val bgColor: String? = null, + @SerializedName("iconSize") val iconSize: Double? = null, + @SerializedName("fundTags") val fundTags: List? = null, +) : Parcelable + data class FundUspData( @SerializedName("title") val title: TextWithStyle? = null, @SerializedName("items") val items: List? = null, ) + +@Parcelize +data class FundTagData( + @SerializedName("bgColor") val bgColor: String? = null, + @SerializedName("text") var text: String? = null, + @SerializedName("span") val span: List? = null, +) : Serializable, Parcelable + +data class RegulatoryInfoData( + @SerializedName("title") val title: TextWithStyle? = null, + @SerializedName("iconCode") val iconCode: String? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundGraphDetails.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundGraphDetails.kt index be55f043c9..8631ecc027 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundGraphDetails.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundGraphDetails.kt @@ -16,6 +16,8 @@ data class FundGraphDetails( @SerializedName("items") val items: List? = null, @SerializedName("fundDuration") val fundDuration: List? = null, @SerializedName("graphBgColor") val graphBgColor: String? = null, + @SerializedName("showGraphTag") val showGraphTag: ShowGraphTagData? = null, + @SerializedName("shouldShowGraph") val shouldShowGraph: Boolean? = null, ) data class FundGraphData( @@ -80,3 +82,9 @@ data class XAxisLabelData( @SerializedName("mid") val mid: String? = null, @SerializedName("end") val end: String? = null, ) + +data class ShowGraphTagData( + @SerializedName("title") val title: TextWithStyle? = null, + @SerializedName("selectedIconCode") val selectedIconCode: String? = null, + @SerializedName("unSelectedIconCode") val unSelectedIconCode: String? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundInfoCardsData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundInfoCardsData.kt new file mode 100644 index 0000000000..e1ecb84b4f --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundInfoCardsData.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.models + +import com.google.gson.annotations.SerializedName +import com.navi.design.textview.model.TextWithStyle + +data class FundInfoCardsData(@SerializedName("items") val items: List? = null) + +data class FundInfoCardItem( + @SerializedName("title") val title: TextWithStyle? = null, + @SerializedName("subtitle") val subtitle: TextWithStyle? = null, + @SerializedName("topTag") val topTag: String? = null, + @SerializedName("bgColor") val bgColor: String? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundManagerData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundManagerData.kt index 435e2a120f..37f41d0f7c 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundManagerData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundManagerData.kt @@ -15,4 +15,8 @@ data class FundManagerData( @SerializedName("name") val name: TextWithStyle? = null, @SerializedName("experience") val experience: TextWithStyle? = null, @SerializedName("profile") val profile: String? = null, + @SerializedName("isExpanded") val isExpanded: Boolean? = null, + @SerializedName("rightChevronDown") val rightChevronDown: String? = null, + @SerializedName("rightChevronUp") val rightChevronUp: String? = null, + @SerializedName("assetsDetails") val assetDetails: TextWithStyle? = null, ) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/ListItemProgressData.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/ListItemProgressData.kt index fe1c24fabd..917e80f817 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/ListItemProgressData.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/ListItemProgressData.kt @@ -18,6 +18,9 @@ data class ListItemProgressData( @SerializedName("listData") val listData: List? = null, @SerializedName("displayCount") val displayCount: Int? = null, @SerializedName("action") val action: ActionData? = null, + @SerializedName("rightChevronDown") val rightChevronDown: String? = null, + @SerializedName("rightChevronUp") val rightChevronUp: String? = null, + @SerializedName("isExpanded") val isExpanded: Boolean? = null, ) data class ListData( diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt index bd921caef1..28a68a56b0 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailViewModel.kt @@ -52,6 +52,14 @@ class FundDetailViewModel @Inject constructor(private val repository: FundDetail var fundName: String? = null var selectedRadio: String? = null + private val _isGraphVisible = MutableLiveData(true) + val isGraphVisible: LiveData + get() = _isGraphVisible + + fun setIsGraphVisible(isVisible: Boolean) { + _isGraphVisible.value = isVisible + } + fun getFundScreenData(isin: String, source: String?, screenName: String) { viewModelScope.launch { val response = diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundDetailView.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundDetailView.kt index 4feabf4844..3a24ecc73f 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundDetailView.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundDetailView.kt @@ -7,6 +7,7 @@ package com.navi.amc.fundbuy.views +import android.animation.LayoutTransition import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater @@ -18,6 +19,8 @@ import com.navi.amc.R import com.navi.amc.common.model.InformationCardData import com.navi.amc.databinding.FundDetailItemLayoutBinding import com.navi.amc.databinding.FundDetailViewLayoutBinding +import com.navi.amc.utils.toggleContentVisibility +import com.navi.amc.utils.updateChevronImage import com.navi.base.model.ActionData import com.navi.base.utils.orZero import com.navi.design.utils.setSpannableString @@ -27,11 +30,37 @@ import com.navi.naviwidgets.models.FundDetailData class FundDetailView(context: Context, attributeSet: AttributeSet? = null) : LinearLayout(context, attributeSet) { + companion object { + private const val TAG = "FundDetailView" + } + private val binding: FundDetailViewLayoutBinding + private var isExpanded: Boolean = false init { val inflater = LayoutInflater.from(context) binding = DataBindingUtil.inflate(inflater, R.layout.fund_detail_view_layout, this, true) + + this.layoutTransition = + LayoutTransition().apply { setDuration(LayoutTransition.CHANGING, 300) } + } + + private fun setupAccordion(data: FundDetailData) { + if (data.isExpanded != null) { + binding.titleBar.setOnClickListener { + isExpanded = !isExpanded + toggleContentVisibility( + isExpanded = isExpanded, + content = binding.itemContainer, + chevronImage = binding.chevronImage, + ) + } + updateChevronImage( + isExpanded = isExpanded, + chevronImage = binding.chevronImage, + chevronDown = data.rightChevronDown, + ) + } } fun setProperties( @@ -40,8 +69,12 @@ class FundDetailView(context: Context, attributeSet: AttributeSet? = null) : action: ((ActionData) -> Unit)? = null, iconClickAction: ((ActionData) -> Unit)? = null, ) { + isExpanded = data.isExpanded ?: true binding.title.setSpannableString(data.title) + setupAccordion(data) + binding.itemContainer.apply { + removeAllViews() data.items?.forEachIndexed { index, itemData -> val inflater = LayoutInflater.from(context) val childBinding: FundDetailItemLayoutBinding = @@ -78,5 +111,7 @@ class FundDetailView(context: Context, attributeSet: AttributeSet? = null) : addView(childBinding.root) } } + + binding.itemContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphToolTipView.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphToolTipView.kt index a28af20ff8..430ca3721d 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphToolTipView.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphToolTipView.kt @@ -8,109 +8,244 @@ package com.navi.amc.fundbuy.views import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView -import android.widget.TextView import androidx.core.view.isVisible import com.github.mikephil.charting.components.MarkerView import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.utils.MPPointF +import com.navi.amc.R +import com.navi.amc.databinding.FundGraphToolTipDetailsLayoutBinding import com.navi.amc.utils.Constant.RUPEE_SYMBOL import com.navi.design.textview.model.NaviSpan -import com.navi.design.utils.doAnimate import com.navi.design.utils.dpToPx import com.navi.design.utils.spannedText -import com.navi.naviwidgets.R -import timber.log.Timber class FundGraphToolTipView(context: Context, layout: Int) : MarkerView(context, layout) { - private var uiScreenWidth = 0 - private var title: TextView? = null - private var subtitle: TextView? = null - private var circleIndicatorView: ImageView? = null + private var pointerCircle: ImageView? = null + private var tooltipDetailsView: View? = null + private var tooltipBinding: FundGraphToolTipDetailsLayoutBinding? = null + + private var verticalLineView: View? = null + private var duplicatePointerCircle: ImageView? = null + + private val TOP_GAP = dpToPx(4) + private val BOTTOM_GAP = dpToPx(24) + private var chartParent: ViewGroup? = null init { - title = findViewById(R.id.title) - subtitle = findViewById(R.id.subtitle) - circleIndicatorView = findViewById(R.id.scroll_indicator) - uiScreenWidth = resources.displayMetrics.widthPixels + pointerCircle = findViewById(R.id.pointer_circle) + tooltipBinding = FundGraphToolTipDetailsLayoutBinding.inflate(LayoutInflater.from(context)) + tooltipDetailsView = tooltipBinding?.root + tooltipDetailsView?.visibility = View.INVISIBLE + } + + private fun setupTooltipDetailsView() { + if (tooltipDetailsView?.parent == null && chartView != null) { + chartParent = chartView?.parent as? ViewGroup + chartParent?.let { parent -> + if (verticalLineView == null) { + val lineInflater = LayoutInflater.from(context) + verticalLineView = + lineInflater.inflate(R.layout.vertical_dotted_line_layout, parent, false) + + val lineLayoutParams = + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + chartView?.height ?: ViewGroup.LayoutParams.MATCH_PARENT, + ) + + parent.addView(verticalLineView, lineLayoutParams) + } + + if (duplicatePointerCircle == null) { + duplicatePointerCircle = + ImageView(context).apply { + layoutParams = + FrameLayout.LayoutParams(dpToPx(14).toInt(), dpToPx(14).toInt()) + setImageResource(com.navi.naviwidgets.R.drawable.black_border_circle) + elevation = dpToPx(4).toFloat() + visibility = View.INVISIBLE + } + parent.addView(duplicatePointerCircle) + } + + val layoutParams = + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + parent.addView(tooltipDetailsView, layoutParams) + } + } } override fun refreshContent(e: Entry?, highlight: Highlight?) { - try { - var nav = RUPEE_SYMBOL.plus(e?.y.toString()) - val date = e?.data as? String - val titleText = "NAV: $nav" - title?.text = - titleText.spannedText( + setupTooltipDetailsView() + + val nav = RUPEE_SYMBOL + (e?.y?.toString() ?: "0.0") + val date = e?.data as? String + val titleText = "NAV: $nav" + + tooltipBinding?.title?.text = + titleText.spannedText( + context = context, + span = + listOf( + NaviSpan( + startSpan = 0, + endSpan = 4, + fontName = "NAVI_BODY_REGULAR", + fontSize = 12.0, + spanColor = "#6B6B6B", + ), + NaviSpan( + startSpan = 4, + endSpan = titleText.length, + fontName = "NAVI_BODY_DEMI_BOLD", + fontSize = 12.0, + spanColor = "#191919", + ), + ), + ) + + date?.let { + tooltipBinding?.subtitle?.isVisible = true + val subtitleText = "$date" + tooltipBinding?.subtitle?.text = + subtitleText.spannedText( context = context, span = listOf( NaviSpan( startSpan = 0, - endSpan = 4, + endSpan = subtitleText.length, fontName = "NAVI_BODY_REGULAR", fontSize = 12.0, - spanColor = "#6B6B6B", - ), - NaviSpan( - startSpan = 4, - endSpan = 25, - fontName = "NAVI_BODY_DEMI_BOLD", - fontSize = 12.0, spanColor = "#191919", - ), + ) ), ) - date?.let { - subtitle?.isVisible = true - val subtitleText = "on $date" - subtitle?.text = - subtitleText.spannedText( - context = context, - span = - listOf( - NaviSpan( - startSpan = 0, - endSpan = 30, - fontName = "NAVI_BODY_REGULAR", - fontSize = 12.0, - spanColor = "#191919", - ) - ), - ) - } - } catch (e: Exception) { - Timber.e(e) - } + } ?: run { tooltipBinding?.subtitle?.isVisible = false } + + tooltipDetailsView?.visibility = View.VISIBLE super.refreshContent(e, highlight) } override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF { - var offsetX = 0f - val leftBound = posX - (width.toFloat() / 2) - val rightBound = posX + (width.toFloat() / 2) - val indicatorHeight = circleIndicatorView?.measuredHeight?.toFloat() - val indicatorWidth = circleIndicatorView?.measuredWidth?.toFloat() - val indicatorOffset = if (indicatorHeight != null) indicatorHeight / 2 else 0f + val pointerOffsetX = -(pointerCircle?.width?.div(2) ?: 0).toFloat() + val pointerOffsetY = -(pointerCircle?.height?.div(2) ?: 0).toFloat() - if (leftBound <= 0f) { - offsetX = -dpToPx(6) - if (indicatorWidth != null) { - circleIndicatorView?.doAnimate(translationX = -(width.toFloat() / 2 - dpToPx(6))) + offset.x = pointerOffsetX + offset.y = pointerOffsetY + + pointerCircle?.visibility = View.INVISIBLE + + tooltipDetailsView?.let { tooltipView -> + val tooltipWidth = tooltipView.width.toFloat() + var tooltipX = chartView.left + posX - (tooltipWidth / 2) + + val actualDataPointX = chartView.left + posX + val actualDataPointY = chartView.top + posY + + if (tooltipX < chartView.left + dpToPx(16)) { + tooltipX = chartView.left.toFloat() + dpToPx(16) + } else if (tooltipX + tooltipWidth > chartView.right - dpToPx(16)) { + tooltipX = chartView.right.toFloat() - tooltipWidth - dpToPx(16) } - } else if (rightBound > chartView.measuredWidth) { - offsetX = -(width.toFloat() - dpToPx(6)) - if (indicatorWidth != null) { - circleIndicatorView?.doAnimate(translationX = width.toFloat() / 2 - dpToPx(6)) + + duplicatePointerCircle?.let { duplicateCircle -> + duplicateCircle.x = actualDataPointX - (duplicateCircle.width / 2) + duplicateCircle.y = actualDataPointY - (duplicateCircle.height / 2) + duplicateCircle.visibility = View.VISIBLE } - } else { - offsetX = (-(width / 2)).toFloat() - circleIndicatorView?.doAnimate(translationX = 0f) + + verticalLineView?.let { line -> + val legendHeight = dpToPx(20) + val chartAreaHeight = chartView.height - legendHeight + + val layoutParams = line.layoutParams + layoutParams.height = chartAreaHeight.toInt() + + line.x = actualDataPointX - (line.width / 2) + line.y = chartView.top.toFloat() + + line.layoutParams = layoutParams + + line.visibility = tooltipDetailsView?.visibility ?: View.INVISIBLE + } + + val tooltipHeight = tooltipView.height.toFloat() + + val upperPosition = chartView.top.toFloat() + TOP_GAP + val lowerPosition = + (chartView.top + chartView.height).toFloat() - tooltipHeight - BOTTOM_GAP + + val upperTooltipBottom = upperPosition + tooltipHeight + + val pointerY = chartView.top + posY + + val yPosition = if (pointerY < upperTooltipBottom) lowerPosition else upperPosition + + tooltipView.x = tooltipX + tooltipView.y = yPosition + + tooltipView.visibility = View.VISIBLE + verticalLineView?.visibility = View.VISIBLE + + return offset } - offset.x = offsetX - offset.y = -(height.toFloat() - indicatorOffset - dpToPx(4)) + return offset } + + fun hideTooltip() { + tooltipDetailsView?.visibility = View.INVISIBLE + verticalLineView?.visibility = View.INVISIBLE + duplicatePointerCircle?.visibility = View.INVISIBLE + pointerCircle?.visibility = View.VISIBLE + } + + fun updateVisibility(visible: Boolean) { + if (!visible) { + hideTooltip() + + val parent = tooltipDetailsView?.parent as? ViewGroup + parent?.removeView(tooltipDetailsView) + parent?.removeView(verticalLineView) + parent?.removeView(duplicatePointerCircle) + + verticalLineView = null + duplicatePointerCircle = null + + chartView?.highlightValues(null) + } else { + if (tooltipDetailsView?.parent == null && chartView != null) { + setupTooltipDetailsView() + } + hideTooltip() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + cleanupTooltip() + } + + private fun cleanupTooltip() { + val parent = tooltipDetailsView?.parent as? ViewGroup + parent?.removeView(tooltipDetailsView) + parent?.removeView(verticalLineView) + parent?.removeView(duplicatePointerCircle) + verticalLineView = null + duplicatePointerCircle = null + tooltipDetailsView = null + tooltipBinding = null + chartParent = null + } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphView.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphView.kt index 9533a630f5..32817be92c 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphView.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphView.kt @@ -10,6 +10,7 @@ package com.navi.amc.fundbuy.views import android.content.Context import android.content.res.ColorStateList import android.graphics.drawable.GradientDrawable +import android.text.TextUtils import android.util.AttributeSet import android.view.LayoutInflater import android.view.View @@ -20,9 +21,11 @@ import android.view.ViewTreeObserver import android.widget.ImageView import android.widget.LinearLayout import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.graphics.toColorInt import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.core.view.forEach +import androidx.core.view.isNotEmpty import androidx.core.view.isVisible import androidx.databinding.DataBindingUtil import androidx.transition.Fade @@ -42,6 +45,7 @@ import com.navi.amc.databinding.FundGraphLayoutBinding import com.navi.amc.fundbuy.models.FundDuration import com.navi.amc.fundbuy.models.FundGraphData import com.navi.amc.fundbuy.models.FundGraphDetails +import com.navi.amc.fundbuy.models.FundInfoCardsData import com.navi.amc.fundbuy.models.FundReturn import com.navi.amc.fundbuy.models.ReturnsData import com.navi.amc.utils.ColorUtils @@ -53,6 +57,7 @@ import com.navi.base.model.ActionData import com.navi.base.utils.orFalse import com.navi.base.utils.orZero import com.navi.common.animation.fadeIn +import com.navi.design.textview.NaviTextView import com.navi.design.textview.model.TextWithStyle import com.navi.design.utils.CornerRadius import com.navi.design.utils.dpToPxInInt @@ -78,12 +83,22 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : private var setRadioAction: ((String) -> Unit)? = null private var fundDurationData: FundDuration? = null private var selectedId: Int = 0 + private var hasDisplayedFundInfoCards = false + + private val toolTipMarkerView: FundGraphToolTipView by lazy { + FundGraphToolTipView(context = context, R.layout.fund_graph_tool_tip_view_layout) + } init { val inflater = LayoutInflater.from(context) binding = DataBindingUtil.inflate(inflater, R.layout.fund_graph_layout, this, true) layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT) + tag = "graph" + binding.showGraphText.tag = "showGraphText" + binding.showGraphIcon.tag = "showGraphIcon" + binding.showGraphContainer.tag = "showGraphContainer" + binding.chipTooltipContent.radioGroup.setOnCheckedChangeListener { group, checkedId -> when (checkedId) { R.id.radio_option_one_time -> { @@ -107,6 +122,7 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : vmSelectedKey: String? = null, setRadioAction: ((String) -> Unit)? = null, vmSelectedRadio: String? = null, + fundInfoCards: FundInfoCardsData? = null, ) { this.apiClickAction = apiClickAction this.setRadioAction = setRadioAction @@ -115,6 +131,8 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : setFundGraphPillsData(data, vmSelectedKey) } + setFundInfoCardsData(fundInfoCards) + when (graphUiState) { is FundGraphUiState.Loading -> { showLoader() @@ -167,6 +185,92 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : } } + private fun setFundInfoCardsData(fundInfoCards: FundInfoCardsData?) { + val cardsContainer = binding.fundInfoCards + + if (fundInfoCards == null || fundInfoCards.items.isNullOrEmpty()) { + if (!hasDisplayedFundInfoCards) { + cardsContainer.visibility = View.GONE + } + return + } + + if (hasDisplayedFundInfoCards && cardsContainer.isVisible && cardsContainer.isNotEmpty()) { + return + } + + cardsContainer.visibility = View.VISIBLE + + cardsContainer.removeAllViews() + + val cardWeight = 1.0f / fundInfoCards.items.size + + fundInfoCards.items.forEachIndexed { index, cardItem -> + val inflater = LayoutInflater.from(context) + val cardView = + inflater.inflate(R.layout.fund_info_card_item_layout, cardsContainer, false) + + val layoutParams = + LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply { + weight = cardWeight + + val marginDp = dpToPxInInt(2) + marginStart = marginDp + marginEnd = marginDp + } + cardView.layoutParams = layoutParams + + val title = cardView.findViewById(R.id.title) + val subtitle = cardView.findViewById(R.id.subtitle) + val infoTag = cardView.findViewById(R.id.info_tag) + + cardItem.bgColor?.let { colorString -> + cardView.setBackgroundColor(colorString.parseColorSafe()) + } + + cardItem.title?.let { title.setSpannableString(it) } + title.maxLines = 1 + title.ellipsize = TextUtils.TruncateAt.END + + cardItem.subtitle?.let { subtitle.setSpannableString(it) } + + cardItem.topTag?.let { infoTag.showWhenDataIsAvailable(it) } + + if (index == 0) { + cardView.background = + getNaviDrawable( + radii = + CornerRadius( + leftTop = dpToPxInInt(4).toFloat(), + rightTop = 0f, + leftBottom = dpToPxInInt(4).toFloat(), + rightBottom = 0f, + ), + backgroundColor = (cardItem.bgColor ?: COLOR_WHITE).toColorInt(), + ) + } + + if (index == fundInfoCards.items.size - 1) { + cardView.background = + getNaviDrawable( + radii = + CornerRadius( + leftTop = 0f, + rightTop = dpToPxInInt(4).toFloat(), + leftBottom = 0f, + rightBottom = dpToPxInInt(4).toFloat(), + ), + backgroundColor = (cardItem.bgColor ?: COLOR_WHITE).toColorInt(), + ) + } + + cardsContainer.addView(cardView) + } + + cardsContainer.visibility = View.VISIBLE + hasDisplayedFundInfoCards = true + } + private fun setFundGraphPillsData(data: FundGraphDetails?, vmSelectedKey: String?) { isGraphPillUpdated = true chipIdToDataMap.clear() @@ -330,8 +434,6 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : fillDrawable = gradientDrawable } - val toolTipMarkerView = - FundGraphToolTipView(context = context, R.layout.fund_graph_tool_tip_view_layout) binding.chart.setOnChartValueSelectedListener( object : OnChartValueSelectedListener { override fun onValueSelected(e: Entry, h: Highlight?) { @@ -341,7 +443,9 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : } } - override fun onNothingSelected() {} + override fun onNothingSelected() { + toolTipMarkerView.hideTooltip() + } } ) val lineData = LineData(graphInitialize) @@ -417,6 +521,14 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : } private fun updateSelectedChips(group: ChipGroup, checkedIds: List) { + toolTipMarkerView.hideTooltip() + binding.chart.highlightValues(null) + + if (binding.chart.data != null && binding.chart.data.entryCount > 0) { + binding.chart.marker = null + binding.chart.marker = toolTipMarkerView + } + for (childView in group.children) { if (childView is Chip) { val childId = childView.id @@ -447,6 +559,18 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : return binding.fundReturnDetails } + fun getShowGraphTextView(): NaviTextView { + return binding.showGraphText + } + + fun getShowGraphIconView(): ImageView { + return binding.showGraphIcon + } + + fun getShowGraphContainer(): LinearLayout { + return binding.showGraphContainer + } + private fun updateGraph(key: String) { selectedKey = key apiClickAction?.invoke(key, false) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundManagerView.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundManagerView.kt new file mode 100644 index 0000000000..345794a9b7 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundManagerView.kt @@ -0,0 +1,78 @@ +/* + * + * * Copyright © 2024-2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.views + +import android.animation.LayoutTransition +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.databinding.DataBindingUtil +import com.navi.amc.R +import com.navi.amc.databinding.FundManagerLayoutBinding +import com.navi.amc.fundbuy.models.FundManagerData +import com.navi.amc.utils.toggleContentVisibility +import com.navi.amc.utils.updateChevronImage +import com.navi.design.utils.setSpannableString +import com.navi.naviwidgets.extensions.showWhenDataIsAvailable + +class FundManagerView(context: Context, attributeSet: AttributeSet? = null) : + ConstraintLayout(context, attributeSet) { + + companion object { + private const val TAG = "FundManagerView" + } + + private val binding: FundManagerLayoutBinding + private var isExpanded: Boolean = true + + init { + val inflater = LayoutInflater.from(context) + binding = DataBindingUtil.inflate(inflater, R.layout.fund_manager_layout, this, true) + + this.layoutTransition = + LayoutTransition().apply { setDuration(LayoutTransition.CHANGING, 300) } + } + + private fun setupAccordion(data: FundManagerData) { + if (data.isExpanded != null) { + binding.titleContainer.setOnClickListener { + isExpanded = !isExpanded + toggleContentVisibility( + isExpanded = isExpanded, + content = binding.contentContainer, + chevronImage = binding.chevron, + ) + } + + updateChevronImage( + isExpanded = isExpanded, + chevronImage = binding.chevron, + chevronDown = data.rightChevronDown, + ) + } + } + + fun setProperties(data: FundManagerData) { + isExpanded = data.isExpanded ?: true + + binding.apply { + title.setSpannableString(data.title) + + name.setSpannableString(data.name) + experience.setSpannableString(data.experience) + icon.showWhenDataIsAvailable(data.profile) + assetDetails.setSpannableString(data.assetDetails) + + contentContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE + + setupAccordion(data) + } + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/ListItemProgressView.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/ListItemProgressView.kt index a4203ded0d..4288fe80c9 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/ListItemProgressView.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/ListItemProgressView.kt @@ -7,10 +7,11 @@ package com.navi.amc.fundbuy.views +import android.animation.LayoutTransition import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater -import android.view.ViewGroup +import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.databinding.DataBindingUtil @@ -18,6 +19,8 @@ import com.navi.amc.R import com.navi.amc.databinding.ListItemProgressBinding import com.navi.amc.fundbuy.adapters.ListItemProgressAdapter import com.navi.amc.fundbuy.models.ListItemProgressData +import com.navi.amc.utils.toggleContentVisibility +import com.navi.amc.utils.updateChevronImage import com.navi.base.utils.orZero import com.navi.design.R as DesignR import com.navi.design.utils.getNaviDrawable @@ -26,32 +29,61 @@ import com.navi.design.utils.setSpannableString class ListItemProgressView(context: Context, attributeSet: AttributeSet? = null) : ConstraintLayout(context, attributeSet) { + + companion object { + private const val TAG = "ListItemProgressView" + } + private val binding: ListItemProgressBinding private val adapter = ListItemProgressAdapter() - var expandState: Boolean = false - private var expandListener: ((Boolean) -> Unit)? = null + private var isExpanded: Boolean = true init { val inflater = LayoutInflater.from(context) binding = DataBindingUtil.inflate(inflater, R.layout.list_item_progress, this, true) - layoutParams = - ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, + + this.layoutTransition = + LayoutTransition().apply { setDuration(LayoutTransition.CHANGING, 300) } + } + + private fun setupAccordion(data: ListItemProgressData) { + if (data.isExpanded != null) { + binding.titleContainer.setOnClickListener { + isExpanded = !isExpanded + toggleContentVisibility( + isExpanded = isExpanded, + content = binding.contentContainer, + chevronImage = binding.chevron, + ) + } + + updateChevronImage( + isExpanded = isExpanded, + chevronImage = binding.chevron, + chevronDown = data.rightChevronDown, ) + } } fun setProperties(data: ListItemProgressData, onClick: () -> Unit) { + isExpanded = data.isExpanded ?: true + binding.apply { title.setSpannableString(data.title) leftTitle.setSpannableString(data.leftHeader) rightTitle.setSpannableString(data.rightHeader) + + setupAccordion(data) + items.adapter = adapter.apply { data.listData?.let { data.displayCount?.let { count -> update(it.take(count)) } } } + + contentContainer.visibility = if (isExpanded) View.VISIBLE else View.GONE + button.root.isVisible = if (data.listData?.size.orZero() > 3) { button.apply { @@ -63,10 +95,7 @@ class ListItemProgressView(context: Context, attributeSet: AttributeSet? = null) cornerRadius = resources.getDimension(DesignR.dimen.dp_4).toInt(), ) } - button.root.setOnClickListener { - onClick.invoke() - expandListener?.invoke(expandState) - } + button.root.setOnClickListener { onClick.invoke() } true } else { false diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/AccordionUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/AccordionUtils.kt new file mode 100644 index 0000000000..e366ef5496 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/AccordionUtils.kt @@ -0,0 +1,77 @@ +/* + * + * * Copyright © 2025 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.utils + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import com.navi.naviwidgets.extensions.showWhenDataIsAvailable + +fun expand(view: View) { + view.measure( + View.MeasureSpec.makeMeasureSpec((view.parent as View).width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + ) + val targetHeight = view.measuredHeight + + view.layoutParams.height = 0 + view.visibility = View.VISIBLE + + val animator = ValueAnimator.ofInt(0, targetHeight) + animator.addUpdateListener { valueAnimator -> + val value = valueAnimator.animatedValue as Int + view.layoutParams.height = value + view.requestLayout() + } + animator.duration = 300 + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + view.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + } + } + ) + animator.start() +} + +fun collapse(view: View) { + val initialHeight = view.measuredHeight + val animator = ValueAnimator.ofInt(initialHeight, 0) + animator.addUpdateListener { valueAnimator -> + val value = valueAnimator.animatedValue as Int + view.layoutParams.height = value + view.requestLayout() + } + animator.duration = 300 + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + view.visibility = View.GONE + } + } + ) + animator.start() +} + +fun toggleContentVisibility(isExpanded: Boolean, content: View, chevronImage: ImageView) { + if (isExpanded) { + expand(content) + } else { + collapse(content) + } + chevronImage.animate().rotation(if (isExpanded) 180f else 0f).setDuration(300).start() +} + +fun updateChevronImage(isExpanded: Boolean, chevronImage: ImageView, chevronDown: String? = null) { + chevronImage.showWhenDataIsAvailable(chevronDown) + chevronImage.visibility = View.VISIBLE + chevronImage.rotation = if (isExpanded) 180f else 0f +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/ColorUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/ColorUtils.kt index 3affe55982..fc84a7ea43 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/ColorUtils.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/ColorUtils.kt @@ -24,7 +24,7 @@ object ColorUtils { const val KEY_COLOR_NEGATIVE = "negative" const val DEFAULT_COLOR_VARIATION_SELECTED = "#22A940" const val DEFAULT_COLOR_VARIATION_UNSELECTED = "#E3E5E5" - const val FUND_GRAPH_BG_START_GRADIENT_COLOR = "#64FFFCEC" + const val FUND_GRAPH_BG_START_GRADIENT_COLOR = "#F9F9FA" const val FUND_GRAPH_BG_END_GRADIENT_COLOR = "#FFFFFF" const val FUND_GRAPH_CHIP_SELECTED_COLOR = "#1F002A" diff --git a/android/navi-amc/src/main/res/drawable/fund_graph_tooltip_bg.xml b/android/navi-amc/src/main/res/drawable/fund_graph_tooltip_bg.xml index 8fb71db194..d326deff64 100644 --- a/android/navi-amc/src/main/res/drawable/fund_graph_tooltip_bg.xml +++ b/android/navi-amc/src/main/res/drawable/fund_graph_tooltip_bg.xml @@ -1,75 +1,10 @@ - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/drawable/tag_green_background.xml b/android/navi-amc/src/main/res/drawable/tag_green_background.xml new file mode 100644 index 0000000000..a8119f10cd --- /dev/null +++ b/android/navi-amc/src/main/res/drawable/tag_green_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/navi-amc/src/main/res/layout/download_document_layout.xml b/android/navi-amc/src/main/res/layout/download_document_layout.xml index d22c6d2548..e4b518b7c5 100644 --- a/android/navi-amc/src/main/res/layout/download_document_layout.xml +++ b/android/navi-amc/src/main/res/layout/download_document_layout.xml @@ -7,8 +7,7 @@ + android:padding="@dimen/_16dp"> - + - + + + + + + app:layout_constraintBottom_toBottomOf="@id/subtitle_linear_layout" + app:layout_constraintEnd_toEndOf="@id/subtitle_linear_layout" + app:layout_constraintStart_toStartOf="@id/subtitle_linear_layout" + app:layout_constraintTop_toTopOf="@id/subtitle_linear_layout" /> + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_detail_screen_layout.xml b/android/navi-amc/src/main/res/layout/fund_detail_screen_layout.xml index e77690b9a8..ae1afd4c9b 100644 --- a/android/navi-amc/src/main/res/layout/fund_detail_screen_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_detail_screen_layout.xml @@ -47,11 +47,24 @@ app:layout_goneMarginTop="@dimen/dp_0" app:layout_constraintTop_toBottomOf="@id/tag_container"/> + + + android:layout_marginTop="@dimen/_16dp" + app:layout_constraintTop_toBottomOf="@id/fund_tags" /> diff --git a/android/navi-amc/src/main/res/layout/fund_detail_view_layout.xml b/android/navi-amc/src/main/res/layout/fund_detail_view_layout.xml index 2534f5aaa6..9e91a7e8ae 100644 --- a/android/navi-amc/src/main/res/layout/fund_detail_view_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_detail_view_layout.xml @@ -9,20 +9,48 @@ android:orientation="vertical" android:paddingHorizontal="@dimen/dp_16"> - + + android:background="@color/white" + android:clickable="true" + android:focusable="true" + android:paddingVertical="@dimen/_20dp" + android:minHeight="@dimen/_24dp"> + + + + + + diff --git a/android/navi-amc/src/main/res/layout/fund_graph_layout.xml b/android/navi-amc/src/main/res/layout/fund_graph_layout.xml index 2c39bc76eb..41c46390e9 100644 --- a/android/navi-amc/src/main/res/layout/fund_graph_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_graph_layout.xml @@ -30,6 +30,31 @@ app:layout_constraintBottom_toBottomOf="@id/title" tools:text="+ 200.75%" /> + + + + + + + + @@ -168,7 +193,8 @@ android:layout_height="wrap_content" android:translationZ="@dimen/dp_8" android:gravity="center_horizontal" - android:paddingTop="@dimen/dp_8" + android:paddingTop="@dimen/_12dp" + android:paddingBottom="@dimen/_16dp" android:layout_marginTop="317dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -220,6 +246,20 @@ app:layout_constraintTop_toTopOf="@id/chip_group" /> + + + app:layout_constraintTop_toBottomOf="@id/fund_info_cards" /> \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_details_layout.xml b/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_details_layout.xml new file mode 100644 index 0000000000..e521c2ffd1 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_details_layout.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_view_layout.xml b/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_view_layout.xml index 08dd12e365..8a0f6c383f 100644 --- a/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_view_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_view_layout.xml @@ -1,70 +1,25 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:translationZ="@dimen/_1dp" + android:elevation="@dimen/_1dp" + android:layout_height="wrap_content"> - - - - - - - - + app:layout_constraintBottom_toBottomOf="parent" /> + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_info_card_item_layout.xml b/android/navi-amc/src/main/res/layout/fund_info_card_item_layout.xml new file mode 100644 index 0000000000..905171460e --- /dev/null +++ b/android/navi-amc/src/main/res/layout/fund_info_card_item_layout.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_manager_layout.xml b/android/navi-amc/src/main/res/layout/fund_manager_layout.xml index 726d7e9923..439dfe6c37 100644 --- a/android/navi-amc/src/main/res/layout/fund_manager_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_manager_layout.xml @@ -3,45 +3,85 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - - + android:orientation="vertical"> - + + app:layout_constraintTop_toTopOf="parent"> - + - + + - + android:paddingBottom="@dimen/_20dp" + android:paddingHorizontal="@dimen/dp_16" + android:orientation="horizontal"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/information_view_layout.xml b/android/navi-amc/src/main/res/layout/information_view_layout.xml index 41337b1315..e4661ad502 100644 --- a/android/navi-amc/src/main/res/layout/information_view_layout.xml +++ b/android/navi-amc/src/main/res/layout/information_view_layout.xml @@ -15,7 +15,7 @@ android:layout_width="@dimen/dp_24" android:layout_height="@dimen/dp_24" android:layout_marginStart="@dimen/dp_16" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toBottomOf="@id/sub_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/refund_image" /> @@ -85,5 +85,27 @@ app:layout_constraintTop_toTopOf="parent" app:tilt_direction="forward" /> + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/item_fund_tag.xml b/android/navi-amc/src/main/res/layout/item_fund_tag.xml new file mode 100644 index 0000000000..87c1ffd8d3 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/item_fund_tag.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/navi-amc/src/main/res/layout/list_item_progress.xml b/android/navi-amc/src/main/res/layout/list_item_progress.xml index db301775b1..95e80dd8c9 100644 --- a/android/navi-amc/src/main/res/layout/list_item_progress.xml +++ b/android/navi-amc/src/main/res/layout/list_item_progress.xml @@ -3,59 +3,90 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - + android:layout_height="wrap_content" + android:orientation="vertical"> - - - - - - - + app:layout_constraintEnd_toEndOf="parent"> - + + + + + - + android:paddingBottom="@dimen/_20dp" + android:orientation="vertical"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/vertical_dotted_line_layout.xml b/android/navi-amc/src/main/res/layout/vertical_dotted_line_layout.xml new file mode 100644 index 0000000000..2fe34a43a0 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/vertical_dotted_line_layout.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/android/navi-common/src/main/res/drawable/divider_margin_4.xml b/android/navi-common/src/main/res/drawable/divider_margin_4.xml new file mode 100644 index 0000000000..e6055300c3 --- /dev/null +++ b/android/navi-common/src/main/res/drawable/divider_margin_4.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FundDetailsWidget.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FundDetailsWidget.kt index 9f2460fbfa..4e32ece97d 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FundDetailsWidget.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/models/FundDetailsWidget.kt @@ -30,6 +30,9 @@ data class FundDetailsWidget( data class FundDetailData( @SerializedName("title") val title: TextWithStyle? = null, @SerializedName("items") val items: List? = null, + @SerializedName("rightChevronUp") val rightChevronUp: String? = null, + @SerializedName("rightChevronDown") val rightChevronDown: String? = null, + @SerializedName("isExpanded") val isExpanded: Boolean? = null, ) data class FundItemData(