common: initial commit of LobstersItem

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2021-06-04 08:58:57 +05:30
parent 9cb43dec7a
commit 284dd8c67a
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
4 changed files with 553 additions and 7 deletions

View file

@ -21,16 +21,13 @@ kotlin {
api(compose.runtime) api(compose.runtime)
api(compose.foundation) api(compose.foundation)
api(compose.material) api(compose.material)
api(projects.database)
implementation("com.alialbaali.kamel:kamel-image:0.2.1")
} }
} }
val commonTest by getting { dependencies { implementation(kotlin("test")) } } val commonTest by getting { dependencies { implementation(kotlin("test")) } }
val androidMain by getting { val androidMain by getting
dependencies { val androidTest by getting
api("androidx.appcompat:appcompat:1.4.0-alpha02")
api("androidx.core:core-ktx:1.6.0-beta02")
}
}
val androidTest by getting { dependencies { implementation("junit:junit:4.13.2") } }
val desktopMain by getting val desktopMain by getting
val desktopTest by getting val desktopTest by getting
} }

View file

@ -0,0 +1,306 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.flowlayout
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlin.math.max
/**
* A composable that places its children in a horizontal flow. Unlike [Row], if the horizontal space
* is too small to put all the children in one row, multiple rows may be used.
*
* Note that just like [Row], flex values cannot be used with [FlowRow].
*
* @param modifier The modifier to be applied to the FlowRow.
* @param mainAxisSize The size of the layout in the main axis direction.
* @param mainAxisAlignment The alignment of each row's children in the main axis direction.
* @param mainAxisSpacing The main axis spacing between the children of each row.
* @param crossAxisAlignment The alignment of each row's children in the cross axis direction.
* @param crossAxisSpacing The cross axis spacing between the rows of the layout.
* @param lastLineMainAxisAlignment Overrides the main axis alignment of the last row.
*/
@Composable
public fun FlowRow(
modifier: Modifier = Modifier,
mainAxisSize: SizeMode = SizeMode.Wrap,
mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start,
mainAxisSpacing: Dp = 0.dp,
crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start,
crossAxisSpacing: Dp = 0.dp,
lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment,
content: @Composable () -> Unit
) {
Flow(
modifier = modifier,
orientation = LayoutOrientation.Horizontal,
mainAxisSize = mainAxisSize,
mainAxisAlignment = mainAxisAlignment,
mainAxisSpacing = mainAxisSpacing,
crossAxisAlignment = crossAxisAlignment,
crossAxisSpacing = crossAxisSpacing,
lastLineMainAxisAlignment = lastLineMainAxisAlignment,
content = content
)
}
/**
* A composable that places its children in a vertical flow. Unlike [Column], if the vertical space
* is too small to put all the children in one column, multiple columns may be used.
*
* Note that just like [Column], flex values cannot be used with [FlowColumn].
*
* @param modifier The modifier to be applied to the FlowColumn.
* @param mainAxisSize The size of the layout in the main axis direction.
* @param mainAxisAlignment The alignment of each column's children in the main axis direction.
* @param mainAxisSpacing The main axis spacing between the children of each column.
* @param crossAxisAlignment The alignment of each column's children in the cross axis direction.
* @param crossAxisSpacing The cross axis spacing between the columns of the layout.
* @param lastLineMainAxisAlignment Overrides the main axis alignment of the last column.
*/
@Composable
public fun FlowColumn(
modifier: Modifier = Modifier,
mainAxisSize: SizeMode = SizeMode.Wrap,
mainAxisAlignment: FlowMainAxisAlignment = FlowMainAxisAlignment.Start,
mainAxisSpacing: Dp = 0.dp,
crossAxisAlignment: FlowCrossAxisAlignment = FlowCrossAxisAlignment.Start,
crossAxisSpacing: Dp = 0.dp,
lastLineMainAxisAlignment: FlowMainAxisAlignment = mainAxisAlignment,
content: @Composable () -> Unit
) {
Flow(
modifier = modifier,
orientation = LayoutOrientation.Vertical,
mainAxisSize = mainAxisSize,
mainAxisAlignment = mainAxisAlignment,
mainAxisSpacing = mainAxisSpacing,
crossAxisAlignment = crossAxisAlignment,
crossAxisSpacing = crossAxisSpacing,
lastLineMainAxisAlignment = lastLineMainAxisAlignment,
content = content
)
}
/** Used to specify the alignment of a layout's children, in cross axis direction. */
public enum class FlowCrossAxisAlignment {
/** Place children such that their center is in the middle of the cross axis. */
Center,
/** Place children such that their start edge is aligned to the start edge of the cross axis. */
Start,
/** Place children such that their end edge is aligned to the end edge of the cross axis. */
End,
}
public typealias FlowMainAxisAlignment = MainAxisAlignment
/** Layout model that arranges its children in a horizontal or vertical flow. */
@Composable
private fun Flow(
modifier: Modifier,
orientation: LayoutOrientation,
mainAxisSize: SizeMode,
mainAxisAlignment: FlowMainAxisAlignment,
mainAxisSpacing: Dp,
crossAxisAlignment: FlowCrossAxisAlignment,
crossAxisSpacing: Dp,
lastLineMainAxisAlignment: FlowMainAxisAlignment,
content: @Composable () -> Unit
) {
fun Placeable.mainAxisSize() = if (orientation == LayoutOrientation.Horizontal) width else height
fun Placeable.crossAxisSize() = if (orientation == LayoutOrientation.Horizontal) height else width
Layout(content, modifier) { measurables, outerConstraints ->
val sequences = mutableListOf<List<Placeable>>()
val crossAxisSizes = mutableListOf<Int>()
val crossAxisPositions = mutableListOf<Int>()
var mainAxisSpace = 0
var crossAxisSpace = 0
val currentSequence = mutableListOf<Placeable>()
var currentMainAxisSize = 0
var currentCrossAxisSize = 0
val constraints = OrientationIndependentConstraints(outerConstraints, orientation)
val childConstraints =
if (orientation == LayoutOrientation.Horizontal) {
Constraints(maxWidth = constraints.mainAxisMax)
} else {
Constraints(maxHeight = constraints.mainAxisMax)
}
// Return whether the placeable can be added to the current sequence.
fun canAddToCurrentSequence(placeable: Placeable) =
currentSequence.isEmpty() ||
currentMainAxisSize + mainAxisSpacing.roundToPx() + placeable.mainAxisSize() <=
constraints.mainAxisMax
// Store current sequence information and start a new sequence.
fun startNewSequence() {
if (sequences.isNotEmpty()) {
crossAxisSpace += crossAxisSpacing.roundToPx()
}
sequences += currentSequence.toList()
crossAxisSizes += currentCrossAxisSize
crossAxisPositions += crossAxisSpace
crossAxisSpace += currentCrossAxisSize
mainAxisSpace = max(mainAxisSpace, currentMainAxisSize)
currentSequence.clear()
currentMainAxisSize = 0
currentCrossAxisSize = 0
}
for (measurable in measurables) {
// Ask the child for its preferred size.
val placeable = measurable.measure(childConstraints)
// Start a new sequence if there is not enough space.
if (!canAddToCurrentSequence(placeable)) startNewSequence()
// Add the child to the current sequence.
if (currentSequence.isNotEmpty()) {
currentMainAxisSize += mainAxisSpacing.roundToPx()
}
currentSequence.add(placeable)
currentMainAxisSize += placeable.mainAxisSize()
currentCrossAxisSize = max(currentCrossAxisSize, placeable.crossAxisSize())
}
if (currentSequence.isNotEmpty()) startNewSequence()
val mainAxisLayoutSize =
if (constraints.mainAxisMax != Constraints.Infinity && mainAxisSize == SizeMode.Expand) {
constraints.mainAxisMax
} else {
max(mainAxisSpace, constraints.mainAxisMin)
}
val crossAxisLayoutSize = max(crossAxisSpace, constraints.crossAxisMin)
val layoutWidth =
if (orientation == LayoutOrientation.Horizontal) {
mainAxisLayoutSize
} else {
crossAxisLayoutSize
}
val layoutHeight =
if (orientation == LayoutOrientation.Horizontal) {
crossAxisLayoutSize
} else {
mainAxisLayoutSize
}
layout(layoutWidth, layoutHeight) {
sequences.forEachIndexed { i, placeables ->
val childrenMainAxisSizes =
IntArray(placeables.size) { j ->
placeables[j].mainAxisSize() +
if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0
}
val arrangement =
if (i < sequences.lastIndex) {
mainAxisAlignment.arrangement
} else {
lastLineMainAxisAlignment.arrangement
}
// TODO(soboleva): rtl support
// Handle vertical direction
val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 }
with(arrangement) { arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions) }
placeables.forEachIndexed { j, placeable ->
val crossAxis =
when (crossAxisAlignment) {
FlowCrossAxisAlignment.Start -> 0
FlowCrossAxisAlignment.End -> crossAxisSizes[i] - placeable.crossAxisSize()
FlowCrossAxisAlignment.Center ->
Alignment.Center.align(
IntSize.Zero,
IntSize(width = 0, height = crossAxisSizes[i] - placeable.crossAxisSize()),
LayoutDirection.Ltr
)
.y
}
if (orientation == LayoutOrientation.Horizontal) {
placeable.place(x = mainAxisPositions[j], y = crossAxisPositions[i] + crossAxis)
} else {
placeable.place(x = crossAxisPositions[i] + crossAxis, y = mainAxisPositions[j])
}
}
}
}
}
}
/** Used to specify how a layout chooses its own size when multiple behaviors are possible. */
// TODO(popam): remove this when Flow is reworked
public enum class SizeMode {
/**
* Minimize the amount of free space by wrapping the children, subject to the incoming layout
* constraints.
*/
Wrap,
/**
* Maximize the amount of free space by expanding to fill the available space, subject to the
* incoming layout constraints.
*/
Expand
}
/** Used to specify the alignment of a layout's children, in main axis direction. */
public enum class MainAxisAlignment(internal val arrangement: Arrangement.Vertical) {
// TODO(soboleva) support RTl in Flow
// workaround for now - use Arrangement that equals to previous Arrangement
/** Place children such that they are as close as possible to the middle of the main axis. */
Center(Arrangement.Center),
/** Place children such that they are as close as possible to the start of the main axis. */
Start(Arrangement.Top),
/** Place children such that they are as close as possible to the end of the main axis. */
End(Arrangement.Bottom),
/**
* Place children such that they are spaced evenly across the main axis, including free space
* before the first child and after the last child.
*/
SpaceEvenly(Arrangement.SpaceEvenly),
/**
* Place children such that they are spaced evenly across the main axis, without free space before
* the first child or after the last child.
*/
SpaceBetween(Arrangement.SpaceBetween),
/**
* Place children such that they are spaced evenly across the main axis, including free space
* before the first child and after the last child, but half the amount of space existing
* otherwise between two consecutive children.
*/
SpaceAround(Arrangement.SpaceAround)
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.accompanist.flowlayout
import androidx.compose.ui.unit.Constraints
internal enum class LayoutOrientation {
Horizontal,
Vertical
}
internal data class OrientationIndependentConstraints(
val mainAxisMin: Int,
val mainAxisMax: Int,
val crossAxisMin: Int,
val crossAxisMax: Int
) {
constructor(
c: Constraints,
orientation: LayoutOrientation
) : this(
if (orientation === LayoutOrientation.Horizontal) c.minWidth else c.minHeight,
if (orientation === LayoutOrientation.Horizontal) c.maxWidth else c.maxHeight,
if (orientation === LayoutOrientation.Horizontal) c.minHeight else c.minWidth,
if (orientation === LayoutOrientation.Horizontal) c.maxHeight else c.maxWidth
)
}

View file

@ -0,0 +1,202 @@
@file:Suppress("FunctionName")
package dev.msfjarvis.claw.common.posts
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.IconButton
import androidx.compose.material.IconToggleButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
import dev.msfjarvis.lobsters.data.local.SavedPost
import io.kamel.image.KamelImage
import io.kamel.image.lazyImageResource
@Composable
fun LobstersItem(
post: SavedPost,
isSaved: Boolean,
viewPost: () -> Unit,
viewComments: (String) -> Unit,
toggleSave: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
modifier = Modifier.clickable { viewPost.invoke() }.then(modifier),
) {
Column(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
) {
PostTitle(
title = post.title,
modifier = Modifier.padding(bottom = 4.dp),
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
TagRow(
tags = post.tags,
modifier = Modifier.weight(0.65f),
)
SaveButton(
isSaved = isSaved,
onClick = toggleSave,
)
Spacer(
modifier = Modifier.width(8.dp),
)
CommentsButton(
onClick = { viewComments(post.shortId) },
)
}
SubmitterName(
text = "Submitted by %s".format(post.submitterName),
avatarUrl = post.submitterAvatarUrl,
contentDescription = "Submitted by %s".format(post.submitterName),
)
}
}
}
@Composable
fun PostTitle(
title: String,
modifier: Modifier = Modifier,
) {
Text(
text = title,
color = Color.Black,
fontWeight = FontWeight.Bold,
modifier = Modifier.then(modifier),
)
}
@Composable
fun SubmitterName(
text: String,
avatarUrl: String,
contentDescription: String,
modifier: Modifier = Modifier,
) {
Row(
modifier = Modifier.then(modifier),
verticalAlignment = Alignment.CenterVertically,
) {
SubmitterAvatar(
avatarUrl = avatarUrl,
contentDescription = contentDescription,
)
SubmitterNameText(
text = text,
)
}
}
@Composable
fun SubmitterAvatar(
avatarUrl: String,
contentDescription: String,
) {
KamelImage(
resource = lazyImageResource(avatarUrl),
contentDescription = contentDescription,
modifier = Modifier.requiredSize(24.dp),
crossfade = true,
)
}
@Composable
fun SubmitterNameText(
text: String,
) {
Text(
text = text,
modifier = Modifier.padding(start = 4.dp),
)
}
@Composable
fun SaveButton(
isSaved: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
IconToggleButton(
checked = isSaved,
onCheckedChange = { onClick.invoke() },
modifier = Modifier.requiredSize(32.dp).then(modifier),
) {
Crossfade(targetState = isSaved) { saved ->
/*IconResource(
resourceId = if (saved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px,
tint = MaterialTheme.colors.secondary,
contentDescription =
if (saved) Strings.RemoveFromSavedPosts.get() else Strings.AddToSavedPosts.get(),
)*/
}
}
}
@Composable
fun CommentsButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
IconButton(
onClick = onClick,
modifier = Modifier.requiredSize(32.dp).then(modifier),
) {
/*IconResource(
resourceId = R.drawable.ic_insert_comment_24px,
tint = MaterialTheme.colors.secondary,
contentDescription = Strings.OpenComments.get(),
)*/
}
}
@Composable
fun TagRow(
tags: List<String>,
modifier: Modifier = Modifier,
) {
Box(
modifier = Modifier.then(modifier),
) {
FlowRow(
mainAxisSpacing = 8.dp,
crossAxisSpacing = 8.dp,
) {
tags.forEach { tag ->
Text(
text = tag,
modifier =
Modifier.background(
MaterialTheme.colors.secondary.copy(alpha = 0.75f),
RoundedCornerShape(8.dp)
)
.padding(vertical = 2.dp, horizontal = 6.dp),
color = MaterialTheme.colors.onSecondary,
)
}
}
}
}