diff --git a/android/navi-amc/src/main/java/com/navi/amc/common/model/FundGraphUiState.kt b/android/navi-amc/src/main/java/com/navi/amc/common/model/FundGraphUiState.kt new file mode 100644 index 0000000000..de5cb8b588 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/common/model/FundGraphUiState.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.common.model + +import com.navi.amc.fundbuy.models.FundGraphData + +sealed interface FundGraphUiState { + data object Loading : FundGraphUiState + + data class Success(val data: FundGraphData) : FundGraphUiState + + data class Error( + val error: FundGraphData? = null, + ) : FundGraphUiState +} 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 fdcbcc038e..09c86c9451 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 @@ -18,11 +18,13 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.google.gson.Gson import com.navi.amc.R import com.navi.amc.common.activity.CheckerActivity import com.navi.amc.common.fragment.AmcBaseFragment import com.navi.amc.common.listener.FooterInteractionListener +import com.navi.amc.common.model.FundGraphUiState import com.navi.amc.common.taskProcessor.AmcTaskManager import com.navi.amc.common.taskProcessor.FundLandingPrefetchTask import com.navi.amc.common.view.InformationView @@ -51,6 +53,7 @@ import com.navi.amc.utils.showTimerInFormat import com.navi.base.model.ActionData 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 @@ -130,8 +133,7 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { scroll.viewTreeObserver.addOnScrollChangedListener { val rect = Rect() scroll.getHitRect(rect) - val view = - binding.container.findViewWithTag("graph")?.getSubtitle() + val view = binding.container.findViewWithTag("graph")?.getTitle() val returnView = binding.container .findViewWithTag("graph") @@ -143,7 +145,9 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { binding.fundOnscrollView.apply { left.setSpannableString(viewModel.fundReturn.value?.leftText) right.setSpannableString(viewModel.fundReturn.value?.rightText) - root.visibility = View.VISIBLE + if (left.isVisible && right.isVisible) { + root.visibility = View.VISIBLE + } } } else { binding.fundOnscrollView.root.visibility = View.GONE @@ -226,6 +230,7 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { } ?: run { tagContainer.isVisible = false } fundDetailScreenData?.content?.amcHeaderData?.let { + viewModel.fundName = it.title?.text.orEmpty() binding.header.setProperties(it) } } @@ -238,13 +243,25 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { removeAllViews() val inflater = LayoutInflater.from(context) fundDetailScreenData?.content?.fundGraphDetails?.let { + val graphUiState = + if ( + viewModel.selectedChipId.isNotNull() && + viewModel.chipIdToGraphDataMap[viewModel.selectedChipId] + .isNotNull() + ) { + FundGraphUiState.Success( + viewModel.chipIdToGraphDataMap[viewModel.selectedChipId]!! + ) + } else FundGraphUiState.Loading val view = FundGraphView(context) view.setProperties( data = fundDetailScreenData.content.fundGraphDetails, + graphUiState = graphUiState, fundInvestmentDetailData = fundDetailScreenData.content.fundInvestmentDetails, action = ::setFundReturns, - navigateAction = ::navigate + navigateAction = ::navigate, + apiClickAction = ::pillClickAction ) view.tag = "graph" addView(view) @@ -379,6 +396,23 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { } } } + viewLifecycleOwner.lifecycleScope.launchWhenResumed { + viewModel.fundGraphData.collect { graphUiState -> updateGraph(graphUiState) } + } + } + + private fun updateGraph(graphUiState: FundGraphUiState) { + binding.container + .findViewWithTag("graph") + ?.setProperties( + data = viewModel.fundDetailScreenData.value?.content?.fundGraphDetails, + graphUiState = graphUiState, + fundInvestmentDetailData = + viewModel.fundDetailScreenData.value?.content?.fundInvestmentDetails, + action = ::setFundReturns, + navigateAction = ::navigate, + apiClickAction = ::pillClickAction + ) } private fun navigate(action: ActionData) { @@ -412,6 +446,22 @@ class FundDetailsFragment : AmcBaseFragment(), FooterInteractionListener { isin?.let { viewModel.getFundScreenData(isin) } } + private fun pillClickAction(key: String?, errorRefresh: Boolean? = null) { + key?.let { + viewModel.selectedChipId = key + if (viewModel.chipIdToGraphDataMap[key] != null && errorRefresh.orFalse().not()) { + viewModel.chipIdToGraphDataMap[key]?.let { + if (it.errorTitle.isNull()) { + updateGraph(FundGraphUiState.Success(it)) + } else updateGraph(FundGraphUiState.Error(it)) + } + } else { + val isIn = arguments?.getString(ISIN) + viewModel.fetchFundGraphData(key, isIn) + } + } + } + private fun setFundReturns(fundReturn: FundReturn) { viewModel.fundReturn.value = fundReturn } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/ListItemProgressBottomSheet.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/ListItemProgressBottomSheet.kt index 71017bac8c..25a3e703d0 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/ListItemProgressBottomSheet.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/fragments/ListItemProgressBottomSheet.kt @@ -68,10 +68,10 @@ class ListItemProgressBottomSheet() : BaseBottomSheet() { sheetBehavior.isDraggable = false sheetBehavior.isHideable = true val buttonHeight = - binding.btn.height + 40 // button height + experimental distance from bottom + binding.btn.height + 20 // button height + experimental distance from bottom binding.title.updateLayoutParams { bottomMargin = 16 } binding.items.updateLayoutParams { - bottomMargin = (buttonHeight + 24).toInt() + bottomMargin = (buttonHeight).toInt() } } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenDataV2.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenDataV2.kt index 9760edaa69..3b1cde6ac4 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenDataV2.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/models/FundDetailScreenDataV2.kt @@ -10,6 +10,7 @@ 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.base.model.ActionData import com.navi.common.model.Header @@ -39,7 +40,9 @@ data class FundDetailScreenDataV2( @SerializedName("rewards") val rewards: RewardsData? = null, @SerializedName("tags") val tags: List? = null, @SerializedName("fundDetailsCarousel") val fundDetailsCarousel: FundDetailCarouselData? = null, - @SerializedName("companiesLogo") val companiesLogo: String? = null + @SerializedName("companiesLogo") val companiesLogo: String? = null, + @SerializedName("fundInvestmentDetails") + val fundInvestmentDetails: FundInvestmentDetailData? = null ) data class FundManagerDataV2( 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 581a0ee754..2f57beed7d 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 @@ -21,13 +21,16 @@ data class FundGraphDetails( data class FundGraphData( @SerializedName("title") val title: TextWithStyle? = null, @SerializedName("subtitle") val subtitle: TextWithStyle? = null, - @SerializedName("duration") val duration: List, + @SerializedName("duration") val duration: List? = null, @SerializedName("graphColor") val graphColor: Gradient? = null, @SerializedName("graphHighlightColor") val graphHighlightColor: String? = null, @SerializedName("selected") val selected: Boolean? = null, @SerializedName("key") val key: String? = null, + @SerializedName("xAxis") val xAxisData: XAxisLabelData? = null, @SerializedName("fundReturn") val fundReturn: FundReturn? = null, - @SerializedName("fundReturnDetails") val fundReturnDetails: FundReturnDetails? = null + @SerializedName("fundReturnDetails") val fundReturnDetails: FundReturnDetails? = null, + @SerializedName("validTill") val validTill: Long? = null, + var errorTitle: TextWithStyle? = null ) data class FundReturn( @@ -52,3 +55,9 @@ data class FundDuration( @SerializedName("selected") var selected: Boolean? = null, @SerializedName("bgColorVariations") val bgColorVariations: Map? = null ) + +data class XAxisLabelData( + @SerializedName("start") val start: String? = null, + @SerializedName("mid") val mid: String? = null, + @SerializedName("end") val end: String? = null, +) diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt index 0ea7992627..ea0da8c657 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/repository/FundDetailRepository.kt @@ -7,16 +7,90 @@ package com.navi.amc.fundbuy.repository +import com.google.gson.reflect.TypeToken +import com.navi.amc.fundbuy.models.FundGraphDetails import com.navi.amc.network.retrofit.RetrofitService +import com.navi.amc.utils.getFundGraphItemCacheKey +import com.navi.amc.utils.getJsonObject +import com.navi.base.cache.model.NaviCacheEntity +import com.navi.base.cache.repository.NaviCacheRepositoryImpl +import com.navi.base.utils.isNotNull +import com.navi.common.network.models.RepoResult import com.navi.common.network.retrofit.ResponseCallback +import com.navi.common.utils.isValidResponse import javax.inject.Inject +import kotlin.math.absoluteValue +import kotlin.time.Duration.Companion.hours +import live.hms.video.utils.GsonUtils.gson +import org.joda.time.DateTime +import org.joda.time.Hours -class FundDetailRepository @Inject constructor(private val retrofitService: RetrofitService) : - ResponseCallback() { +class FundDetailRepository +@Inject +constructor( + private val retrofitService: RetrofitService, + private val naviCacheRepository: NaviCacheRepositoryImpl +) : ResponseCallback() { suspend fun getFundDetail(isin: String) = apiResponseCallback(retrofitService.fetchFundDetails(isin)) - suspend fun getFundDetailV2(isin: String) = - apiResponseCallback(retrofitService.fetchFundDetailsV2(isin)) + suspend fun getFundGraphData( + key: String, + isin: String?, + fundName: String, + loaderAction: suspend (Boolean) -> Unit + ): RepoResult { + val fundGraphItemCacheKey = getFundGraphItemCacheKey(key, fundName) + val fundGraphItemDataFromCache = getFundGraphItemDataFromCache(fundGraphItemCacheKey) + if (fundGraphItemDataFromCache.isNotNull()) { + return RepoResult(fundGraphItemDataFromCache) + } else { + loaderAction.invoke(true) + val apiResponse = apiResponseCallback(retrofitService.fetchFundGraphData(isin, key)) + if (apiResponse.isValidResponse()) { + val fundGraphItemData = apiResponse.data as FundGraphDetails + val cacheTtl = getFundGraphCacheTtl(fundGraphItemData.items?.get(0)?.validTill) + saveFundGraphItemDataInCache(fundGraphItemData, fundGraphItemCacheKey, cacheTtl) + } + return apiResponse + } + } + + private suspend fun getFundGraphItemDataFromCache(cacheKey: String): FundGraphDetails? { + return naviCacheRepository.get(cacheKey, 1)?.let { + getJsonObject( + type = object : TypeToken() {}.type, + jsonString = it.value + ) + } + } + + private suspend fun saveFundGraphItemDataInCache( + graphItemData: FundGraphDetails, + cacheKey: String, + ttl: Long + ) { + naviCacheRepository.save( + NaviCacheEntity( + key = cacheKey, + value = gson.toJson(graphItemData), + version = 1, + ttl = ttl + ) + ) + } + + private fun getFundGraphCacheTtl(validTill: Long?): Long { + validTill?.let { + return it - System.currentTimeMillis() / 1000 // validTill is in epochs + } + ?: run { + val now = DateTime.now() + val nearestMidnight = now.withTimeAtStartOfDay().plusDays(1) + val hoursTillMidnight = + Hours.hoursBetween(now, nearestMidnight).hours.absoluteValue.toLong() + return hoursTillMidnight.hours.inWholeMilliseconds + } + } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailV2ViewModel.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailV2ViewModel.kt deleted file mode 100644 index b826291880..0000000000 --- a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/viewmodel/FundDetailV2ViewModel.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * - * * Copyright © 2024 by Navi Technologies Limited - * * All rights reserved. Strictly confidential - * - */ - -package com.navi.amc.fundbuy.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.navi.amc.fundbuy.models.FundDetailsV2 -import com.navi.amc.fundbuy.models.FundReturn -import com.navi.amc.fundbuy.repository.FundDetailRepository -import com.navi.amc.utils.generateFundDocumentName -import com.navi.common.viewmodel.BaseVM -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.launch - -@HiltViewModel -class FundDetailV2ViewModel @Inject constructor(private val repository: FundDetailRepository) : - BaseVM() { - - private val _fundDetailScreenData = MutableLiveData() - val fundDetailScreenData: LiveData - get() = _fundDetailScreenData - - val fundReturn = MutableLiveData() - - fun getFundScreenData(isin: String) { - viewModelScope.launch { - val response = repository.getFundDetailV2(isin) - if (response.error == null && response.errors.isNullOrEmpty()) { - _fundDetailScreenData.value = response.data - } else { - setErrorData(response.errors, response.error) - } - } - } - - override fun onCleared() { - errorResponse.value = null - _fundDetailScreenData.value = null - fundReturn.value = null - } - - fun getDocumentName() = - generateFundDocumentName( - _fundDetailScreenData.value?.content?.amcHeaderData?.title?.text.orEmpty() - ) -} 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 f884ae96ac..ab538e4865 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 @@ -10,13 +10,24 @@ package com.navi.amc.fundbuy.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import com.navi.amc.common.model.FundGraphUiState import com.navi.amc.fundbuy.models.FundDetails +import com.navi.amc.fundbuy.models.FundGraphData +import com.navi.amc.fundbuy.models.FundGraphDetails import com.navi.amc.fundbuy.models.FundReturn import com.navi.amc.fundbuy.repository.FundDetailRepository import com.navi.amc.utils.generateFundDocumentName +import com.navi.base.model.ActionData +import com.navi.common.utils.isValidResponse import com.navi.common.viewmodel.BaseVM +import com.navi.design.textview.model.NaviSpan +import com.navi.design.textview.model.TextWithStyle import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel @@ -27,7 +38,13 @@ class FundDetailViewModel @Inject constructor(private val repository: FundDetail val fundDetailScreenData: LiveData get() = _fundDetailScreenData + private val _fundGraphData = MutableStateFlow(FundGraphUiState.Loading) + val fundGraphData = _fundGraphData.asStateFlow() + val fundReturn = MutableLiveData() + val chipIdToGraphDataMap = mutableMapOf() + var selectedChipId: String? = null + var fundName: String? = null fun getFundScreenData(isin: String) { viewModelScope.launch { @@ -40,6 +57,64 @@ class FundDetailViewModel @Inject constructor(private val repository: FundDetail } } + fun fetchFundGraphData(key: String, isin: String?) { + viewModelScope.launch(Dispatchers.IO) { + val graphResponse = + repository.getFundGraphData(key, isin, fundName.orEmpty()) { showLoader -> + if (showLoader) { + _fundGraphData.emit(FundGraphUiState.Loading) + } + } + if (graphResponse.isValidResponse()) { + val fundGraphDetails = graphResponse.data as FundGraphDetails + val fundGraphData = fundGraphDetails.items?.get(0) + fundGraphData?.let { fundGraphItemData -> + chipIdToGraphDataMap[key] = fundGraphItemData + _fundGraphData.update { FundGraphUiState.Success(fundGraphItemData) } + } + } else { + val errorUnifiedResponse = + getErrorUnifiedResponse( + errors = graphResponse.errors, + error = graphResponse.error + ) + sendFailureEvent("FUND_GRAPH_{$key}_DATA", errorUnifiedResponse) + val fundGraphErrorData = getFundGraphErrorData(key) + chipIdToGraphDataMap[key] = fundGraphErrorData + _fundGraphData.update { FundGraphUiState.Error(fundGraphErrorData) } + } + } + } + + private fun getFundGraphErrorData(key: String): FundGraphData { + return FundGraphData( + errorTitle = + TextWithStyle( + text = "Failed to load the data, please refresh", + style = + listOf( + NaviSpan( + startSpan = 0, + endSpan = 32, + fontName = "NAVI_BOLD", + fontSize = 12.0, + spanColor = "#000000" + ), + NaviSpan( + startSpan = 32, + endSpan = 45, + fontName = "NAVI_BOLD", + fontSize = 12.0, + spanColor = "#000000", + underline = true, + cta = ActionData(url = key) + ) + ) + ), + key = key + ) + } + override fun onCleared() { errorResponse.value = null _fundDetailScreenData.value = null 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 1613f94957..33e5e1b64c 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 @@ -58,7 +58,7 @@ class FundDetailView(context: Context, attributeSet: AttributeSet? = null) : iconTitleCl.isVisible = true title.setSpannableString(itemData.leftTitle) actionIv.showWhenDataIsAvailable(actionIcon.iconCode) - actionIv.setOnClickListener { + iconTitleCl.setOnClickListener { actionIcon.actionData?.let { iconClickAction?.invoke(it) } } } 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 new file mode 100644 index 0000000000..aeaa32d393 --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphToolTipView.kt @@ -0,0 +1,116 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.views + +import android.content.Context +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.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 + + init { + title = findViewById(R.id.title) + subtitle = findViewById(R.id.subtitle) + circleIndicatorView = findViewById(R.id.scroll_indicator) + uiScreenWidth = resources.displayMetrics.widthPixels + } + + 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( + context = context, + span = + listOf( + NaviSpan( + startSpan = 0, + endSpan = 4, + fontName = "NAVI_REGULAR", + fontSize = 12.0, + spanColor = "#6B6B6B" + ), + NaviSpan( + startSpan = 4, + endSpan = 25, + fontName = "NAVI_EXTRA_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_REGULAR", + fontSize = 12.0, + spanColor = "#191919" + ) + ) + ) + } + } catch (e: Exception) { + Timber.e(e) + } + 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 + + if (leftBound <= 0f) { + offsetX = -dpToPx(6) + if (indicatorWidth != null) { + circleIndicatorView?.doAnimate(translationX = -(width.toFloat() / 2 - dpToPx(6))) + } + } else if (rightBound > chartView.measuredWidth) { + offsetX = -(width.toFloat() - dpToPx(6)) + if (indicatorWidth != null) { + circleIndicatorView?.doAnimate(translationX = width.toFloat() / 2 - dpToPx(6)) + } + } else { + offsetX = (-(width / 2)).toFloat() + circleIndicatorView?.doAnimate(translationX = 0f) + } + offset.x = offsetX + offset.y = -(height.toFloat() - indicatorOffset - dpToPx(4)) + return offset + } +} 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 29924b4e89..b93b35d3c2 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 @@ -8,7 +8,9 @@ package com.navi.amc.fundbuy.views import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.GradientDrawable +import android.os.Build import android.util.AttributeSet import android.view.LayoutInflater import android.view.View @@ -22,9 +24,12 @@ import androidx.databinding.DataBindingUtil import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.highlight.Highlight +import com.github.mikephil.charting.listener.OnChartValueSelectedListener import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.navi.amc.R +import com.navi.amc.common.model.FundGraphUiState import com.navi.amc.common.model.FundInvestmentDetailData import com.navi.amc.databinding.ChipBinding import com.navi.amc.databinding.FundGraphLayoutBinding @@ -33,44 +38,119 @@ import com.navi.amc.fundbuy.models.FundGraphData import com.navi.amc.fundbuy.models.FundGraphDetails import com.navi.amc.fundbuy.models.FundReturn import com.navi.amc.utils.ColorUtils -import com.navi.amc.utils.createChipBg +import com.navi.amc.utils.ColorUtils.FUND_GRAPH_BG_END_GRADIENT_COLOR +import com.navi.amc.utils.ColorUtils.FUND_GRAPH_BG_START_GRADIENT_COLOR +import com.navi.amc.utils.ColorUtils.FUND_GRAPH_CHIP_SELECTED_COLOR import com.navi.base.model.ActionData import com.navi.base.utils.orFalse import com.navi.base.utils.orZero -import com.navi.design.utils.* +import com.navi.design.utils.CornerRadius +import com.navi.design.utils.dpToPxInInt +import com.navi.design.utils.getNaviDrawable +import com.navi.design.utils.parseColorSafe +import com.navi.design.utils.setSpannableString +import com.navi.naviwidgets.widgets.InfoWithTimerWidgetLayout.Companion.COLOR_WHITE class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : ConstraintLayout(context, attributeSet) { private val binding: FundGraphLayoutBinding - private var fundGraphData: List? = null - var action: ((FundReturn) -> Unit)? = null - + private var apiClickAction: ((String, Boolean?) -> Unit)? = null + private var investmentDetailNavigateAction: ((ActionData) -> Unit)? = null private val chipIdToDataMap = mutableMapOf() + private val keysAvailable = mutableMapOf() private var isChipCheckedByUser: Boolean = false private var fundDurationSelectedListener: ((FundDuration, Boolean) -> Unit)? = null + private var isInvestmentDetailsUpdated: Boolean = false + private var isGraphPillUpdated: Boolean = false + private var selectedKey: String? = null init { val inflater = LayoutInflater.from(context) binding = DataBindingUtil.inflate(inflater, R.layout.fund_graph_layout, this, true) layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT) - binding.options.setOnCheckedStateChangeListener(::updateSelectedChips) } fun setProperties( data: FundGraphDetails?, - fundInvestmentDetailData: FundInvestmentDetailData? = null, + graphUiState: FundGraphUiState, + fundInvestmentDetailData: FundInvestmentDetailData?, action: ((FundReturn) -> Unit)? = null, - navigateAction: ((ActionData) -> Unit)? = null + navigateAction: ((ActionData) -> Unit)? = null, + apiClickAction: ((String?, Boolean?) -> Unit)? = null, ) { + this.apiClickAction = apiClickAction + if (isGraphPillUpdated.not()) { + setFundGraphPillsData(data) + } + + when (graphUiState) { + is FundGraphUiState.Loading -> { + showLoader() + } + is FundGraphUiState.Success -> { + if ( + graphUiState.data.key + ?.equals(selectedKey.orEmpty(), ignoreCase = true) + .orFalse() + ) { + hideError() + hideLoader() + val graphData = graphUiState.data + graphData.fundReturn?.let { fundReturn -> action?.invoke(fundReturn) } + showGraph(graphData) + graphData.xAxisData?.let { xAxisLabelData -> + binding.xAxisLabel.isVisible = true + binding.xAxisLabel.setProperties(xAxisLabelData) + } + } + } + is FundGraphUiState.Error -> { + if ( + graphUiState.error + ?.key + ?.equals(selectedKey.orEmpty(), ignoreCase = true) + .orFalse() + ) { + showError() + binding.errorTitle.setSpannableString(graphUiState.error?.errorTitle) { cta -> + apiClickAction?.invoke(cta.url, true) + } + } + } + } + + this.investmentDetailNavigateAction = navigateAction + if (isInvestmentDetailsUpdated.not()) { + setFundInvestmentDetailsData(fundInvestmentDetailData) + } + } + + private fun setFundInvestmentDetailsData(fundInvestmentDetailData: FundInvestmentDetailData?) { + fundInvestmentDetailData?.let { + isInvestmentDetailsUpdated = true + binding.fundInvestmentDetails.isVisible = true + binding.fundInvestmentDetails.setProperties(it) { actionData -> + investmentDetailNavigateAction?.invoke(actionData) + } + } + } + + private fun setFundGraphPillsData(data: FundGraphDetails?) { + isGraphPillUpdated = true chipIdToDataMap.clear() + binding.options.setOnCheckedStateChangeListener(::updateSelectedChips) data?.graphBgColor?.let { bgColor -> binding.root.setBackgroundColor(bgColor.parseColorSafe()) } - - data?.items?.let { fundGraphData = it } - this.action = action - data?.fundDuration?.forEach { addChipView(it) } + data?.fundDuration?.forEach { + if (keysAvailable.containsKey(it.key.orEmpty())) { + return + } else { + addChipView(it) + it.key?.let { it1 -> keysAvailable.put(it1, true) } + } + } isChipCheckedByUser = false chipIdToDataMap @@ -78,12 +158,43 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : .map { it.key } .forEach { selectedChipId -> binding.options.check(selectedChipId) } isChipCheckedByUser = true - fundInvestmentDetailData?.let { - binding.fundInvestmentDetails.isVisible = true - binding.fundInvestmentDetails.setProperties(it) { actionData -> - navigateAction?.invoke(actionData) - } - } + } + + private fun showError() { + binding.clErrorLoader.isVisible = true + binding.loaderLv.isVisible = false + binding.errorIcon.isVisible = true + binding.errorTitle.isVisible = true + binding.chart.visibility = View.INVISIBLE + binding.xAxisLabel.isVisible = false + binding.subtitle.isVisible = false + } + + private fun hideError() { + binding.clErrorLoader.isVisible = false + binding.chart.visibility = View.VISIBLE + binding.xAxisLabel.isVisible = true + binding.subtitle.isVisible = true + } + + private fun showLoader() { + binding.clErrorLoader.isVisible = true + binding.loaderLv.isVisible = true + binding.loaderLv.playAnimation() + binding.errorTitle.isVisible = false + binding.errorIcon.isVisible = false + binding.chart.visibility = View.INVISIBLE + binding.xAxisLabel.isVisible = false + binding.subtitle.isVisible = false + } + + private fun hideLoader() { + binding.clErrorLoader.isVisible = false + binding.loaderLv.isVisible = false + binding.loaderLv.pauseAnimation() + binding.chart.visibility = View.VISIBLE + binding.xAxisLabel.isVisible = true + binding.subtitle.isVisible = true } fun setFundDurationSelectedListener(listener: ((FundDuration, Boolean) -> Unit)?) { @@ -136,7 +247,7 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : fundReturnDetails.isVisible = false } val dataPoints = arrayListOf() - for (i in data.duration.indices) { + for (i in data.duration?.indices!!) { dataPoints.add(Entry(i.toFloat(), data.duration[i].y.orZero())) } val graphInitialize = @@ -146,6 +257,8 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : setDrawFilled(true) setDrawValues(false) setDrawCircles(false) + setDrawHorizontalHighlightIndicator(false) + setDrawVerticalHighlightIndicator(false) circleRadius = 4F color = data.graphHighlightColor.parseColorSafe() val gradientDrawable = @@ -158,6 +271,21 @@ 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?) { + binding.chart.setOnTouchListener { view, event -> + view.parent.requestDisallowInterceptTouchEvent(true) + false + } + } + + override fun onNothingSelected() {} + } + ) val lineData = LineData(graphInitialize) binding.chart.apply { axisLeft.setDrawGridLines(false) @@ -166,12 +294,29 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : xAxis.isEnabled = false axisLeft.isEnabled = false axisRight.isEnabled = false + xAxis.axisMinimum = 0f + extraTopOffset = 42f legend.isEnabled = false description.isEnabled = false - setTouchEnabled(false) + isDoubleTapToZoomEnabled = false + setPinchZoom(false) + setScaleEnabled(false) + marker = toolTipMarkerView animateX(1000) } + binding.chart.background = + GradientDrawable( + GradientDrawable.Orientation.TOP_BOTTOM, + intArrayOf( + FUND_GRAPH_BG_START_GRADIENT_COLOR.parseColorSafe(), + FUND_GRAPH_BG_END_GRADIENT_COLOR.parseColorSafe() + ) + ) + toolTipMarkerView.chartView = binding.chart binding.chart.data = lineData + binding.chart.legend.isEnabled = false + + binding.chart.requestLayout() } } @@ -188,9 +333,7 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : chipBinding.chip.id = ViewCompat.generateViewId() chipIdToDataMap[chipBinding.chip.id] = data - if (data.bgColorVariations != null) { - chipBinding.chip.chipBackgroundColor = createChipBg(data, context) - } + chipBinding.chip.chipBackgroundColor = createChipBg(data) chipBinding.chip.setSpannableString(data.title) binding.options.addView(chipBinding.root) @@ -200,7 +343,14 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : for (childView in group.children) { if (childView is Chip) { val childId = childView.id - val childData = chipIdToDataMap.getOrElse(childId) { null } + val childData = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + chipIdToDataMap.getOrDefault(childId, null) + } else { + if (chipIdToDataMap[childId] != null) { + chipIdToDataMap[childId] + } else null + } if (childData != null) { if (checkedIds.contains(childId)) { fundDurationSelectedListener?.invoke(childData, isChipCheckedByUser) @@ -216,8 +366,8 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : } } - fun getSubtitle(): View { - return binding.subtitle + fun getTitle(): View { + return binding.title } fun getReturnDetailsView(): View { @@ -225,11 +375,22 @@ class FundGraphView(context: Context, attributeSet: AttributeSet? = null) : } private fun updateGraph(key: String) { - fundGraphData - ?.singleOrNull { it.key == key } - ?.let { - it.fundReturn?.let { action?.invoke(it) } - showGraph(it) - } + selectedKey = key + apiClickAction?.invoke(key, false) + } + + private fun createChipBg(data: FundDuration): ColorStateList { + val defaultColor = data.bgColorVariations?.get(ColorUtils.KEY_COLOR_DEFAULT) ?: COLOR_WHITE + val selectedColor = + data.bgColorVariations?.get(ColorUtils.KEY_COLOR_SELECTED) + ?: FUND_GRAPH_CHIP_SELECTED_COLOR + + return ColorStateList( + arrayOf( + intArrayOf(-android.R.attr.state_checked), + intArrayOf(android.R.attr.state_checked) + ), + intArrayOf(defaultColor.parseColorSafe(), selectedColor.parseColorSafe()) + ) } } diff --git a/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphXAxisLabelView.kt b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphXAxisLabelView.kt new file mode 100644 index 0000000000..a3251019cf --- /dev/null +++ b/android/navi-amc/src/main/java/com/navi/amc/fundbuy/views/FundGraphXAxisLabelView.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.amc.fundbuy.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.databinding.DataBindingUtil +import com.navi.amc.R +import com.navi.amc.databinding.FundGraphXaxisLabelLayoutBinding +import com.navi.amc.fundbuy.models.XAxisLabelData + +class FundGraphXAxisLabelView(context: Context, attributeSet: AttributeSet? = null) : + ConstraintLayout(context, attributeSet) { + val binding: FundGraphXaxisLabelLayoutBinding + + init { + val inflater = LayoutInflater.from(context) + binding = + DataBindingUtil.inflate(inflater, R.layout.fund_graph_xaxis_label_layout, this, true) + } + + fun setProperties(data: XAxisLabelData) { + data.start?.let { labelStart -> binding.startTitle.text = labelStart } + data.mid?.let { labelMid -> binding.midTitle.text = labelMid } + binding.endTitle.text = data.end + } +} diff --git a/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt b/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt index 9e23ce14ff..36c43d5fde 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/network/retrofit/RetrofitService.kt @@ -515,4 +515,10 @@ interface RetrofitService { suspend fun fetchAutoPaySetupDetailsV2( @Body map: Map? ): Response> + + @GET("/fund/v1/fund-graph/{isin}") + suspend fun fetchFundGraphData( + @Path("isin") isin: String?, + @Query("duration") duration: String + ): Response> } 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 f39df142c0..9df25d3771 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,6 +24,9 @@ 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_END_GRADIENT_COLOR = "#FFFFFF" + const val FUND_GRAPH_CHIP_SELECTED_COLOR = "#1F002A" fun getPurpleRoundedDrawable(context: Context) = getNaviDrawable( diff --git a/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt b/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt index 646345a4d3..9b989e15e9 100644 --- a/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt +++ b/android/navi-amc/src/main/java/com/navi/amc/utils/CommonUtils.kt @@ -23,6 +23,8 @@ import com.navi.amc.network.deserializer.FundListDeserializer import com.navi.amc.utils.Constant.UPI_APP_INTENT_URL import com.navi.base.model.ActionData import com.navi.base.utils.BaseUtils +import com.navi.base.utils.SPACE +import com.navi.base.utils.UNDERSCORE import com.navi.base.utils.orFalse import com.navi.base.utils.orZero import com.navi.common.CommonLibManager @@ -188,3 +190,13 @@ fun downloadDocuments(context: Context, downloadUrl: String?, fileName: String) URI.create(path) ) } + +fun getFundGraphItemCacheKey(key: String, fundName: String): String { + return "FUND_GRAPH" + .plus(UNDERSCORE) + .plus(fundName.replace(SPACE, UNDERSCORE)) + .plus(UNDERSCORE) + .plus(key) + .plus(UNDERSCORE) + .plus("CACHE_KEY") +} 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 new file mode 100644 index 0000000000..25bb3fb58c --- /dev/null +++ b/android/navi-amc/src/main/res/drawable/fund_graph_tooltip_bg.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/navi-amc/src/main/res/drawable/vertical_dash_5dp_gray.xml b/android/navi-amc/src/main/res/drawable/vertical_dash_5dp_gray.xml new file mode 100644 index 0000000000..a823a4cab1 --- /dev/null +++ b/android/navi-amc/src/main/res/drawable/vertical_dash_5dp_gray.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/navi-amc/src/main/res/layout/fund_detail_item_layout.xml b/android/navi-amc/src/main/res/layout/fund_detail_item_layout.xml index f3070bf96f..7193c878d7 100644 --- a/android/navi-amc/src/main/res/layout/fund_detail_item_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_detail_item_layout.xml @@ -58,6 +58,7 @@ android:id="@+id/right_title" android:layout_width="@dimen/dp_0" android:layout_height="wrap_content" + android:layout_marginStart="@dimen/dp_8" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/left_title" app:layout_constraintTop_toTopOf="parent" @@ -68,6 +69,7 @@ android:layout_width="@dimen/dp_0" android:layout_height="wrap_content" android:layout_marginTop="@dimen/dp_8" + android:layout_marginStart="@dimen/dp_8" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/left_subtitle" app:layout_constraintTop_toBottomOf="@id/right_title" 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 3dd1b1bfcb..00b1b50d12 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 @@ -12,19 +12,22 @@ + android:text="Returns:" + tools:text="Returns:" /> + app:constraint_referenced_ids="fund_return_details,subtitle,title"/> + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/chart_label_barrier"> @@ -124,6 +194,5 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/option_container" /> - \ 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 new file mode 100644 index 0000000000..d898132cad --- /dev/null +++ b/android/navi-amc/src/main/res/layout/fund_graph_tool_tip_view_layout.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_graph_xaxis_label_layout.xml b/android/navi-amc/src/main/res/layout/fund_graph_xaxis_label_layout.xml new file mode 100644 index 0000000000..a240164b22 --- /dev/null +++ b/android/navi-amc/src/main/res/layout/fund_graph_xaxis_label_layout.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/navi-amc/src/main/res/layout/fund_investment_detail_view_layout.xml b/android/navi-amc/src/main/res/layout/fund_investment_detail_view_layout.xml index 69f0c966de..690d17c993 100644 --- a/android/navi-amc/src/main/res/layout/fund_investment_detail_view_layout.xml +++ b/android/navi-amc/src/main/res/layout/fund_investment_detail_view_layout.xml @@ -11,31 +11,32 @@ + tools:text="Your investment" /> + tools:text="₹50,00,000.988" /> + tools:text="Current value" /> + tools:text="₹54,305.45 (-18.10%)" /> + android:layout_height="wrap_content"> 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 eeef7a96b1..db301775b1 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 @@ -20,7 +20,7 @@ android:id="@+id/left_title" android:layout_width="@dimen/dp_0" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/dp_16" + android:layout_marginTop="@dimen/dp_24" android:layout_marginStart="@dimen/dp_16" app:layout_constraintEnd_toStartOf="@id/right_title" app:layout_constraintStart_toStartOf="parent" @@ -32,7 +32,7 @@ android:layout_width="@dimen/dp_0" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" - android:layout_marginEnd="@dimen/dp_16" + android:layout_marginEnd="@dimen/dp_16" app:layout_constraintStart_toEndOf="@id/left_title" app:layout_constraintTop_toTopOf="@id/left_title" android:gravity="right" @@ -42,18 +42,17 @@ android:id="@+id/items" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/dp_16" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/left_title" /> - + android:paddingBottom="@dimen/dp_24">