NTP-390 | Fund details - graph + caching changes (#11810)

This commit is contained in:
Apoorv Nigam
2024-07-17 16:16:31 +05:30
committed by GitHub
parent 266cab00b6
commit 948b9cf7ad
28 changed files with 872 additions and 127 deletions

View File

@@ -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
}

View File

@@ -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<FundGraphView>("graph")?.getSubtitle()
val view = binding.container.findViewWithTag<FundGraphView>("graph")?.getTitle()
val returnView =
binding.container
.findViewWithTag<FundGraphView>("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<FundGraphView>("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
}

View File

@@ -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<ConstraintLayout.LayoutParams> { bottomMargin = 16 }
binding.items.updateLayoutParams<FrameLayout.LayoutParams> {
bottomMargin = (buttonHeight + 24).toInt()
bottomMargin = (buttonHeight).toInt()
}
}
}

View File

@@ -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<TagData>? = 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(

View File

@@ -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<GraphPoint>,
@SerializedName("duration") val duration: List<GraphPoint>? = 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<String, String>? = null
)
data class XAxisLabelData(
@SerializedName("start") val start: String? = null,
@SerializedName("mid") val mid: String? = null,
@SerializedName("end") val end: String? = null,
)

View File

@@ -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<FundGraphDetails> {
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<FundGraphDetails>(
type = object : TypeToken<FundGraphDetails>() {}.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
}
}
}

View File

@@ -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<FundDetailsV2?>()
val fundDetailScreenData: LiveData<FundDetailsV2?>
get() = _fundDetailScreenData
val fundReturn = MutableLiveData<FundReturn?>()
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()
)
}

View File

@@ -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<FundDetails?>
get() = _fundDetailScreenData
private val _fundGraphData = MutableStateFlow<FundGraphUiState>(FundGraphUiState.Loading)
val fundGraphData = _fundGraphData.asStateFlow()
val fundReturn = MutableLiveData<FundReturn?>()
val chipIdToGraphDataMap = mutableMapOf<String, FundGraphData>()
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

View File

@@ -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) }
}
}

View File

@@ -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
}
}

View File

@@ -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<FundGraphData>? = null
var action: ((FundReturn) -> Unit)? = null
private var apiClickAction: ((String, Boolean?) -> Unit)? = null
private var investmentDetailNavigateAction: ((ActionData) -> Unit)? = null
private val chipIdToDataMap = mutableMapOf<Int, FundDuration>()
private val keysAvailable = mutableMapOf<String, Boolean>()
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<Entry>()
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())
)
}
}

View File

@@ -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
}
}

View File

@@ -515,4 +515,10 @@ interface RetrofitService {
suspend fun fetchAutoPaySetupDetailsV2(
@Body map: Map<String, String>?
): Response<GenericResponse<AutoPaySetupScreenData>>
@GET("/fund/v1/fund-graph/{isin}")
suspend fun fetchFundGraphData(
@Path("isin") isin: String?,
@Query("duration") duration: String
): Response<GenericResponse<FundGraphDetails>>
}

View File

@@ -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(

View File

@@ -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")
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="113dp"
android:height="48dp"
android:viewportWidth="113"
android:viewportHeight="48">
<path
android:pathData="M4,0L109,0A4,4 0,0 1,113 4L113,44A4,4 0,0 1,109 48L4,48A4,4 0,0 1,0 44L0,4A4,4 0,0 1,4 0z"
android:fillColor="#F5F5F5"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="2dp"
android:height="5dp"
android:viewportWidth="2"
android:viewportHeight="5">
<path
android:strokeWidth="1"
android:pathData="M1,0L1,5"
android:fillColor="#00000000"
android:strokeColor="#E3E5E5"/>
</vector>

View File

@@ -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"

View File

@@ -12,19 +12,22 @@
<com.navi.design.textview.NaviTextView
android:id="@+id/title"
style="@style/TitleSp16Color191919"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="NAVi fund" />
android:text="Returns:"
tools:text="Returns:" />
<com.navi.design.textview.NaviTextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
android:layout_marginStart="@dimen/dp_8"
app:layout_constraintStart_toEndOf="@id/title"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title"
tools:text="+ 200.75%" />
<androidx.constraintlayout.widget.ConstraintLayout
@@ -79,12 +82,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="fund_return_details,subtitle"/>
app:constraint_referenced_ids="fund_return_details,subtitle,title"/>
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="@dimen/layout_dp_200"
android:layout_height="264dp"
android:layout_marginStart="-16dp"
android:layout_marginTop="@dimen/dp_20"
android:layout_marginEnd="-16dp"
@@ -92,6 +95,73 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/return_barrier" />
<com.navi.amc.fundbuy.views.FundGraphXAxisLabelView
android:id="@+id/x_axis_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/chart"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/chart_label_barrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="chart,subtitle,x_axis_label" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_error_loader"
android:layout_width="match_parent"
android:layout_height="264dp"
android:layout_marginTop="@dimen/dp_16"
android:layout_marginBottom="@dimen/dp_36"
app:layout_constraintTop_toTopOf="@id/chart"
app:layout_constraintBottom_toTopOf="@id/option_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="#FFFCEC"
android:visibility="gone"
tools:visibility="visible">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/error_icon"
android:layout_width="@dimen/dp_16"
android:layout_height="@dimen/dp_16"
android:layout_marginBottom="@dimen/dp_20"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:src="@drawable/ic_alert_error_red"
/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/loader_lv"
android:layout_width="@dimen/dp_42"
android:layout_height="@dimen/dp_42"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:lottie_autoPlay="false"
app:lottie_loop="true"
app:lottie_rawRes="@raw/black_gradient_loader"
app:lottie_speed="1.0"/>
<com.navi.design.textview.NaviTextView
android:id="@+id/error_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_10"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/error_icon"
tools:text="Failed to load the data, please refresh" />
</androidx.constraintlayout.widget.ConstraintLayout>
<HorizontalScrollView
android:id="@+id/option_container"
android:layout_width="@dimen/dp_0"
@@ -100,7 +170,7 @@
android:paddingTop="@dimen/dp_8"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chart">
app:layout_constraintTop_toBottomOf="@id/chart_label_barrier">
<com.google.android.material.chip.ChipGroup
android:id="@+id/options"
@@ -109,9 +179,9 @@
android:layout_gravity="center"
android:layout_marginStart="@dimen/dp_8"
android:layout_marginEnd="@dimen/dp_8"
app:chipSpacingHorizontal="@dimen/dp_8"
app:selectionRequired="true"
app:chipSpacingHorizontal="@dimen/dp_16"
app:singleLine="true"
app:selectionRequired="true"
app:singleSelection="true" />
</HorizontalScrollView>
@@ -124,6 +194,5 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/option_container" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingHorizontal="@dimen/dp_6"
android:paddingBottom="@dimen/dp_4">
<View
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/fund_graph_tooltip_bg"
app:layout_constraintBottom_toBottomOf="@+id/subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
style="@style/WhiteSemiBoldFontStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:minWidth="@dimen/dp_90"
android:paddingHorizontal="@dimen/dp_8"
android:paddingTop="@dimen/dp_4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/subtitle"
style="@style/WhiteRegularFontStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible"
android:gravity="center"
android:minWidth="@dimen/dp_90"
android:paddingHorizontal="@dimen/dp_8"
android:paddingBottom="@dimen/dp_4"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/scroll_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/dp_6"
android:src="@drawable/black_border_circle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/start_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/start_title"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/vertical_dash_5dp_gray"/>
<com.navi.design.textview.NaviTextView
android:id="@+id/start_title"
android:textSize="10sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="@dimen/dp_85"
android:layout_marginTop="@dimen/dp_2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/start_iv"
android:gravity="center|top"
tools:text="No lock"
android:breakStrategy="simple" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/mid_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/mid_title"
app:layout_constraintEnd_toEndOf="@id/mid_title"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/vertical_dash_5dp_gray"/>
<com.navi.design.textview.NaviTextView
android:id="@+id/mid_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:maxWidth="@dimen/dp_85"
app:layout_constraintEnd_toStartOf="@id/end_title"
android:layout_marginTop="@dimen/dp_2"
app:layout_constraintStart_toEndOf="@id/start_title"
android:gravity="center|top"
tools:text="No lock"
android:breakStrategy="simple"
app:layout_constraintTop_toBottomOf="@+id/mid_iv" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/end_iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@id/end_title"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/vertical_dash_5dp_gray"/>
<com.navi.design.textview.NaviTextView
android:id="@+id/end_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:maxWidth="@dimen/dp_85"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="@dimen/dp_2"
android:gravity="center|top"
tools:text="No lock"
android:breakStrategy="simple"
app:layout_constraintTop_toBottomOf="@+id/end_iv" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -11,31 +11,32 @@
<com.navi.design.textview.NaviTextView
android:id="@+id/left_title"
android:layout_width="@dimen/dp_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toStartOf="@id/right_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="NAV: 11th Jul 2022" />
tools:text="Your investment" />
<com.navi.design.textview.NaviTextView
android:id="@+id/left_subtitle"
android:layout_width="@dimen/dp_0"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_2"
app:layout_constraintEnd_toStartOf="@id/right_subtitle"
app:layout_constraintEnd_toEndOf="@id/left_title"
app:layout_constraintStart_toStartOf="@id/left_title"
app:layout_constraintTop_toBottomOf="@id/left_title"
tools:text="₹1500" />
tools:text="₹50,00,000.988" />
<com.navi.design.textview.NaviTextView
android:id="@+id/right_title"
android:layout_width="@dimen/dp_0"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dp_32"
app:layout_constraintEnd_toStartOf="@id/icon"
app:layout_constraintStart_toEndOf="@id/left_title"
app:layout_constraintTop_toTopOf="parent"
tools:text="NAV: 11th Jul 2022" />
tools:text="Current value" />
<com.navi.design.textview.NaviTextView
android:id="@+id/right_subtitle"
@@ -43,9 +44,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_2"
app:layout_constraintEnd_toEndOf="@id/right_title"
app:layout_constraintStart_toEndOf="@id/left_subtitle"
app:layout_constraintStart_toStartOf="@id/right_title"
app:layout_constraintTop_toBottomOf="@id/right_title"
tools:text="₹1500" />
tools:text="₹54,305.45 (-18.10%)" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"

View File

@@ -5,8 +5,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dp_12">
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/image"

View File

@@ -20,7 +20,7 @@
<include
android:id="@+id/item1"
layout="@layout/image_text_layout"
android:layout_width="wrap_content"
android:layout_width="85dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_24"
app:layout_constraintStart_toStartOf="parent"
@@ -29,7 +29,7 @@
<include
android:id="@+id/item2"
layout="@layout/image_text_layout"
android:layout_width="wrap_content"
android:layout_width="85dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toStartOf="@id/item3"
app:layout_constraintStart_toEndOf="@id/item1"
@@ -38,7 +38,7 @@
<include
android:id="@+id/item3"
layout="@layout/image_text_layout"
android:layout_width="wrap_content"
android:layout_width="85dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/item2" />

View File

@@ -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" />
<include
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
layout="@layout/amc_button"
android:layout_marginTop="@dimen/dp_32"
android:layout_marginHorizontal="@dimen/dp_16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -79,6 +79,7 @@
android:id="@+id/items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/dp_16"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:scrollbars="vertical"
android:scrollbarSize="@dimen/dp_4"

View File

@@ -7,7 +7,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dp_16"
android:paddingTop="@dimen/dp_24">
android:paddingBottom="@dimen/dp_24">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/horizontal_guideline"

View File

@@ -29,7 +29,7 @@ data class NaviSpan(
@SerializedName("spanBgColor") val spanBgColor: String? = null,
@SerializedName("replacementSpan") val replacementSpan: Boolean? = null,
@SerializedName("cta", alternate = ["action"]) val cta: ActionData? = null,
@SerializedName("letterSpacing") val letterSpacing: Float? = null
@SerializedName("letterSpacing") val letterSpacing: Float? = null,
) : SpanInterface, Serializable, Parcelable {
override fun endSpan(): Int? = endSpan