Sharing animation

This commit is contained in:
2021-09-08 00:48:20 +02:00
parent 3397efe47f
commit 09ce7b6fb8
15 changed files with 469 additions and 99 deletions
+4 -7
View File
@@ -1,9 +1,9 @@
<h1 align="center">Lottiefiles App</h1></br>
<h1 align="center">Lottiefiles App</h1><br>
<p align="center">
Lottiefiles App is an Android application built using jetpack compose and Hilt, based on modern Android technology stacks and the MVI architecture.
The application is a clone of the [Lottiefiles](https://lottiefiles.com/mobile) mobile application and allows the user to retrieve and save animations, animators and articles locally from an API rest
</p>
</br>
<br>
<p align="center">
<a href="https://opensource.org/licenses/Apache-2.0"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"/></a>
@@ -46,12 +46,9 @@ Go to the [Releases](https://github.com/eric-ampire/lottiefiles-app/releases) to
* The API was not complete and the stories and animators returned by the API did not have IDs, I had to find a strategy to assign IDs to data that did not have them to avoid duplication in the database.
* Animations didn't come with any information about whether an animation was popular, recent or featured, so I had to add an extra field to make the queries properly.
## Find this repository useful? :heart:
Support it by joining __[stargazers](https://github.com/eric-ampire/lottiefiles-app/stargazers)__ for this repository. :star: <br>
And __[follow](https://github.com/eric-ampire)__ me for my next creations! 🤩
# License
```xml
```
Designed and developed by 2020 ericampire (Eric Ampire)
Licensed under the Apache License, Version 2.0 (the "License");
+1 -1
View File
@@ -130,5 +130,5 @@ dependencies {
testImplementation(Libs.mockk_core)
androidTestImplementation(Libs.mockk_android)
debugImplementation(Libs.code_scanner)
implementation(Libs.code_scanner)
}
+13
View File
@@ -39,6 +39,16 @@
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
@@ -50,6 +60,9 @@
<meta-data
android:name="com.ericampire.android.androidstudycase.app.initializer.MavericksInitializer"
android:value="androidx.startup" />
<meta-data
android:name="com.ericampire.android.androidstudycase.app.initializer.DownloaderInitializer"
android:value="androidx.startup" />
</provider>
</application>
@@ -0,0 +1,19 @@
package com.ericampire.android.androidstudycase.app.initializer
import android.content.Context
import androidx.startup.Initializer
import com.downloader.PRDownloader
import com.downloader.PRDownloaderConfig
class DownloaderInitializer : Initializer<Unit> {
override fun create(context: Context) {
val config = PRDownloaderConfig.newBuilder()
.setDatabaseEnabled(true)
.build()
PRDownloader.initialize(context, config)
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
@@ -32,6 +32,8 @@ import com.ericampire.android.androidstudycase.presentation.screen.explore.busin
import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewModel
import com.ericampire.android.androidstudycase.presentation.screen.explore.business.ExploreViewState
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
import com.ericampire.android.androidstudycase.util.extension.copyTextToClipboard
import com.ericampire.android.androidstudycase.util.extension.downloadAndShare
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset
@@ -55,6 +57,11 @@ fun ExploreScreen(
val tabItems = stringArrayResource(id = R.array.explore_item)
val pagerState = rememberPagerState(pageCount = tabItems.size)
val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
var selectedAnimation by remember { mutableStateOf<Lottiefile?>(null) }
var fileUrl by remember { mutableStateOf<String?>(null) }
LaunchedEffect(viewModel) {
viewModel.submitAction(ExploreAction.FindRecentFile)
}
@@ -69,96 +76,144 @@ fun ExploreScreen(
}
}
Scaffold(
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = AppColor.Black001),
content = {
TopActionBar()
TabRow(
modifier = Modifier.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage,
backgroundColor = Color.Transparent,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier
.pagerTabIndicatorOffset(pagerState, tabPositions)
.padding(horizontal = 32.dp)
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
height = 3.dp,
color = MaterialTheme.colors.primary
)
},
tabs = {
tabItems.forEachIndexed { index, title ->
Tab(
selected = index == pagerState.currentPage,
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = Color.White,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
content = {
Text(
modifier = Modifier.padding(vertical = 12.dp),
text = title,
style = MaterialTheme.typography.h6.copy(
fontWeight = FontWeight.Normal
),
textAlign = TextAlign.Center,
)
}
)
}
}
)
LaunchedEffect(fileUrl) {
if (fileUrl != null) {
context.downloadAndShare(
url = fileUrl!!,
onError = {
Timber.e(it)
},
onSuccess = {
selectedAnimation = null
fileUrl = null
coroutineScope.launch {
bottomSheetState.hide()
}
}
)
}
}
ModalBottomSheetLayout(
sheetShape = RoundedCornerShape(topEnd = 24.dp, topStart = 24.dp),
sheetState = bottomSheetState,
sheetContent = {
ShareBottomSheet(
lottiefile = selectedAnimation,
isLoading = fileUrl != null,
onCopyLink = {
context.copyTextToClipboard(it)
coroutineScope.launch {
bottomSheetState.hide()
}
},
onShareGifFile = { fileUrl = it },
onShareJsonFile = { fileUrl = it },
onShareVideoFile = { fileUrl = it }
)
},
content = { contentPadding ->
Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
when (it) {
Uninitialized -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Loading -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Success -> {
val animations = it.invoke()
if (animations.isEmpty()) {
// Todo: Empty instead of Loading View
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
content = {
Scaffold(
topBar = {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = AppColor.Black001),
content = {
TopActionBar()
TabRow(
modifier = Modifier.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage,
backgroundColor = Color.Transparent,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier
.pagerTabIndicatorOffset(pagerState, tabPositions)
.padding(horizontal = 32.dp)
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp)),
height = 3.dp,
color = MaterialTheme.colors.primary
)
} else {
HorizontalPager(state = pagerState) {
ExploreContent(files = animations)
},
tabs = {
tabItems.forEachIndexed { index, title ->
Tab(
selected = index == pagerState.currentPage,
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = Color.White,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
content = {
Text(
modifier = Modifier.padding(vertical = 12.dp),
text = title,
style = MaterialTheme.typography.h6.copy(
fontWeight = FontWeight.Normal
),
textAlign = TextAlign.Center,
)
}
)
}
}
)
}
)
},
content = { contentPadding ->
Crossfade(modifier = Modifier.padding(contentPadding), targetState = state) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
when (it) {
Uninitialized -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Loading -> {
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
}
is Success -> {
val animations = it.invoke()
if (animations.isEmpty()) {
// Todo: Empty instead of Loading View
LoadingAnimation(
waveColor = MaterialTheme.colors.primary.copy(alpha = 0.5f),
arcColor = MaterialTheme.colors.primaryVariant
)
} else {
HorizontalPager(state = pagerState) {
ExploreContent(
files = animations,
onShareClick = {
selectedAnimation = it
coroutineScope.launch {
bottomSheetState.animateTo(ModalBottomSheetValue.Expanded)
}
}
)
}
}
}
is Fail -> {
Timber.e(it.error.localizedMessage)
}
}
}
is Fail -> {
Timber.e(it.error.localizedMessage)
}
}
)
}
)
}
}
)
}
)
}
@@ -167,6 +222,7 @@ fun ExploreScreen(
@Composable
private fun ExploreContent(
modifier: Modifier = Modifier,
onShareClick: (Lottiefile) -> Unit,
files: List<Lottiefile>
) {
LazyColumn(
@@ -184,7 +240,8 @@ private fun ExploreContent(
items(items = files, key = { it.toString() }) { lottieFile ->
LottieFileItemView(
lottiefile = lottieFile,
onClick = {}
onClick = {},
onShareClick = onShareClick,
)
Divider(
@@ -31,7 +31,8 @@ import com.ericampire.android.androidstudycase.util.LottieFileProvider
fun LottieFileItemView(
modifier: Modifier = Modifier,
lottiefile: Lottiefile,
onClick: (Lottiefile) -> Unit
onClick: (Lottiefile) -> Unit,
onShareClick: (Lottiefile) -> Unit,
) {
Card(
modifier = modifier,
@@ -89,7 +90,9 @@ fun LottieFileItemView(
onLikeClick = { /*TODO*/ },
onCommentClick = { /*TODO*/ },
onAddCollectionClick = { /*TODO*/ },
onShareClick = { }
onShareClick = {
onShareClick(lottiefile)
}
)
}
)
@@ -203,7 +206,8 @@ fun LottiefileItemViewPreview(@PreviewParameter(LottieFileProvider::class) data:
content = {
LottieFileItemView(
lottiefile = data,
onClick = {}
onClick = {},
onShareClick = {},
)
}
)
@@ -0,0 +1,201 @@
package com.ericampire.android.androidstudycase.presentation.screen.explore.ui
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.InsertDriveFile
import androidx.compose.material.icons.rounded.Link
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.ericampire.android.androidstudycase.R
import com.ericampire.android.androidstudycase.domain.entity.Lottiefile
import com.ericampire.android.androidstudycase.presentation.theme.AndroidStudyCaseTheme
import com.ericampire.android.androidstudycase.presentation.theme.AppColor
import com.ericampire.android.androidstudycase.util.LottieFileProvider
@Composable
fun ShareBottomSheet(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
lottiefile: Lottiefile?,
onCopyLink: (String) -> Unit,
onShareJsonFile: (String?) -> Unit,
onShareGifFile: (String?) -> Unit,
onShareVideoFile: (String?) -> Unit,
) {
val progressBarAlpha = if (isLoading) 1f else 0f
Column(
modifier = modifier
.background(AppColor.Black001),
content = {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.alpha(progressBarAlpha)
)
Column(
modifier = modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
content = {
Text(
text = stringResource(id = R.string.txt_share_animation),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.h4.copy(
color = MaterialTheme.colors.onSurface
),
)
Column(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(AppColor.Arsenic)
.clickable { onCopyLink(lottiefile?.lottieUrl ?: "") }
.padding(18.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
content = {
Icon(
modifier = Modifier.size(48.dp),
tint = AppColor.SlateGray,
imageVector = Icons.Rounded.Link,
contentDescription = null
)
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.txt_share_animation),
style = MaterialTheme.typography.body1.copy(
color = MaterialTheme.colors.onSurface
),
)
}
)
Column(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
content = {
ShareItem(
modifier = Modifier.clickable {
onShareJsonFile(lottiefile?.lottieUrl)
},
title = "Lottie JSON",
fileExtension = ".json",
icon = Icons.Rounded.InsertDriveFile
)
Divider(modifier = Modifier
.fillMaxWidth()
.background(Color.DarkGray))
ShareItem(
modifier = Modifier.clickable {
onShareGifFile(lottiefile?.gifUrl)
},
title = "Animated GIF",
fileExtension = ".gif",
icon = Icons.Rounded.InsertDriveFile
)
Divider(modifier = Modifier
.fillMaxWidth()
.background(Color.DarkGray))
ShareItem(
modifier = Modifier.clickable {
onShareVideoFile(lottiefile?.videoUrl)
},
title = "Video MP4",
fileExtension = ".mp4",
icon = Icons.Rounded.InsertDriveFile
)
}
)
}
)
}
)
}
@Composable
private fun ShareItem(
modifier: Modifier = Modifier,
title: String,
fileExtension: String,
icon: ImageVector
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.background(AppColor.Arsenic)
.padding(24.dp)
.fillMaxWidth(),
content = {
Row(
horizontalArrangement = Arrangement.spacedBy(24.dp),
verticalAlignment = Alignment.CenterVertically,
content = {
Icon(
imageVector = icon,
tint = AppColor.SlateGray,
contentDescription = null
)
Text(
text = title,
style = MaterialTheme.typography.h4.copy(
color = MaterialTheme.colors.onSurface
),
)
}
)
Text(
text = fileExtension,
style = MaterialTheme.typography.body1.copy(
color = Color.Gray
),
)
}
)
}
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Preview()
@Composable
fun ShareBottomSheetPreview(@PreviewParameter(LottieFileProvider::class) data: Lottiefile) {
AndroidStudyCaseTheme(darkTheme = true) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
content = {
ShareBottomSheet(
lottiefile = data,
isLoading = true,
onCopyLink = {},
onShareGifFile = {},
onShareJsonFile = {},
onShareVideoFile = {}
)
}
)
}
}
@@ -1,6 +1,5 @@
package com.ericampire.android.androidstudycase.presentation.screen.home.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -12,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -32,6 +32,8 @@ fun LoginBottomSheet(
onLoginClick: () -> Unit,
) {
val progressBarAlpha = if (isLoading) 1f else 0f
val composition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.people_communicating)
)
@@ -45,9 +47,11 @@ fun LoginBottomSheet(
.clip(MaterialTheme.shapes.medium)
.background(AppColor.Black001),
content = {
AnimatedVisibility(isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.alpha(progressBarAlpha)
)
Column(
modifier = Modifier.padding(24.dp),
@@ -100,7 +104,7 @@ fun LoginBottomSheet(
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@Preview()
@Preview
@Composable
fun LoginDialogViewPreview() {
AndroidStudyCaseTheme(darkTheme = true) {
@@ -5,6 +5,8 @@ import androidx.compose.ui.graphics.Color
object AppColor {
val SlateGray = Color(0xFF606E7C)
val Arsenic = Color(0xFF3C3C3C)
val Teal = Color(0xFF1C7373)
val PaleBlue = Color(0xFFD3F6F6)
val PrimaryColor = Color(0xFF2BEAED)
+1
View File
@@ -5,4 +5,5 @@
<string name="txt_login_title">1000s of lottie animations</string>
<string name="txt_login_description">from top creators waiting to be discover, save and share from the palm of your hand</string>
<string name="txt_connect_with_facebook">Continue with Facebook</string>
<string name="txt_share_animation">Share animation</string>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_files"
path="." />
</paths>
+1
View File
@@ -148,4 +148,5 @@ object Libs {
const val joda_time = "net.danlew:android.joda:2.10.9"
const val code_scanner = "com.budiyev.android:code-scanner:2.1.0"
const val fetch = "com.mindorks.android:prdownloader:0.6.0"
}
+1
View File
@@ -23,4 +23,5 @@
<string name="txt_permission_denied">Camera permission denied. See this FAQ with information about why we need this permission. Please, grant us access on the Settings screen.</string>
<string name="txt_connect_with_google">Continue with Google</string>
<string name="txt_get_started">Get started for free</string>
<string name="txt_share_lottie_file">Share Lottiefile</string>
</resources>
+1
View File
@@ -51,4 +51,5 @@ dependencies {
kapt(Libs.hilt_android_compiler)
api(Libs.timber)
api(Libs.fetch)
}
@@ -0,0 +1,63 @@
package com.ericampire.android.androidstudycase.util.extension
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Environment
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import com.downloader.Error
import com.downloader.OnDownloadListener
import com.downloader.PRDownloader
import com.downloader.utils.Utils
import org.zxconnect.android.beserve.util.R
import java.io.File
fun Context.downloadAndShare(
url: String,
onSuccess: () -> Unit,
onError: (String?) -> Unit
) {
val dirPath = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString()
val fileName = url.substringAfterLast("/")
val filePath = Utils.getPath(dirPath, fileName)
val downloadRequest = PRDownloader.download(url, dirPath, fileName).build()
downloadRequest.start(object : OnDownloadListener {
override fun onDownloadComplete() {
shareFile(filePath)
onSuccess()
}
override fun onError(error: Error) {
onError(error.serverErrorMessage)
}
})
}
fun Context.copyTextToClipboard(text: String) {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("text", text)
clipboardManager.setPrimaryClip(clipData)
Toast.makeText(this, "Link copied to clipboard", Toast.LENGTH_LONG).show()
}
private fun Context.shareFile(filePath: String) {
val file = File(filePath)
val fileUri = FileProvider.getUriForFile(
this,
this.applicationContext.packageName.toString() + ".provider",
file
)
val shareIntent = ShareCompat.IntentBuilder(this)
.setChooserTitle(getString(R.string.txt_share_lottie_file))
.setStream(fileUri)
.setType("application/pdf")
.createChooserIntent()
startActivity(shareIntent)
}