NTP-390 | Fund details - graph + caching changes (#11810)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>>
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user