Skip to content

Commit

Permalink
Merge "Adds loading and error screens, and swipe to refresh functiona…
Browse files Browse the repository at this point in the history
…lity"
  • Loading branch information
Manuel Vivo authored and Gerrit Code Review committed Apr 21, 2020
2 parents 94ef364 + c2370f7 commit 0fbf067
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ import androidx.ui.graphics.imageFromResource
import com.example.jetnews.data.Result
import com.example.jetnews.data.posts.PostsRepository
import com.example.jetnews.model.Post
import java.lang.IllegalStateException
import java.util.concurrent.ExecutorService
import kotlin.random.Random

/**
* Implementation of PostsRepository that returns a hardcoded list of
* posts with resources after some delay in a background thread.
* 1/3 of the times will throw an error.
*
* The result is posted to the resultThreadHandler passed as a parameter.
*/
Expand All @@ -53,15 +56,21 @@ class FakePostsRepository(

override fun getPost(postId: String, callback: (Result<Post?>) -> Unit) {
executeInBackground(callback) {
resultThreadHandler.post { callback(Result.Success(
postsWithResources.find { it.id == postId }
)) }
resultThreadHandler.post {
callback(Result.Success(
postsWithResources.find { it.id == postId }
))
}
}
}

override fun getPosts(callback: (Result<List<Post>>) -> Unit) {
executeInBackground(callback) {
simulateNetworkRequest()
Thread.sleep(1500L)
if (shouldRandomlyFail()) {
throw IllegalStateException()
}
resultThreadHandler.post { callback(Result.Success(postsWithResources)) }
}
}
Expand All @@ -86,8 +95,13 @@ class FakePostsRepository(
private var networkRequestDone = false
private fun simulateNetworkRequest() {
if (!networkRequestDone) {
Thread.sleep(5000)
Thread.sleep(2000L)
networkRequestDone = true
}
}

/**
* 1/3 requests should fail loading
*/
private fun shouldRandomlyFail(): Boolean = Random.nextFloat() < 0.33f
}
160 changes: 160 additions & 0 deletions JetNews/app/src/main/java/com/example/jetnews/ui/SwipeToRefresh.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright 2020 Google, Inc.
*
* 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
*
* http://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.example.jetnews.ui

import androidx.animation.AnimatedFloat
import androidx.animation.AnimationBuilder
import androidx.animation.AnimationEndReason
import androidx.animation.TweenBuilder
import androidx.compose.Composable
import androidx.compose.onCommit
import androidx.compose.remember
import androidx.compose.state
import androidx.ui.animation.animatedFloat
import androidx.ui.core.Alignment
import androidx.ui.core.DensityAmbient
import androidx.ui.core.Modifier
import androidx.ui.core.PassThroughLayout
import androidx.ui.foundation.Box
import androidx.ui.foundation.animation.AnchorsFlingConfig
import androidx.ui.foundation.animation.fling
import androidx.ui.foundation.gestures.DragDirection
import androidx.ui.foundation.gestures.draggable
import androidx.ui.layout.Stack
import androidx.ui.layout.offset
import androidx.ui.unit.dp
import androidx.ui.unit.px

private val SWIPE_DISTANCE_SIZE = 100.dp
private const val SWIPE_DOWN_OFFSET = 1.2f

@Composable
fun SwipeToRefreshLayout(
refreshingState: Boolean,
onRefresh: () -> Unit,
refreshIndicator: @Composable() () -> Unit,
content: @Composable() () -> Unit
) {
val size = with(DensityAmbient.current) { SWIPE_DISTANCE_SIZE.toPx().value }
// min is below negative to hide
val min = -size
val max = size * SWIPE_DOWN_OFFSET
StateDraggable(
state = refreshingState,
onStateChange = { shouldRefresh -> if (shouldRefresh) onRefresh() },
anchorsToState = listOf(min to false, max to true),
animationBuilder = TweenBuilder(),
dragDirection = DragDirection.Vertical,
minValue = min,
maxValue = max
) { dragPosition ->
val dpOffset = with(DensityAmbient.current) {
(dragPosition.value * 0.5).px.toDp()
}
Stack {
content()
Box(Modifier.gravity(Alignment.TopCenter).offset(0.dp, dpOffset)) {
if (dragPosition.value != min) {
refreshIndicator()
}
}
}
}
}

// Copied from ui/ui-material/src/main/java/androidx/ui/material/internal/StateDraggable.kt

/**
* Higher-level component that allows dragging around anchored positions binded to different states
*
* Example might be a Switch which you can drag between two states (true or false).
*
* Additional features compared to regular [draggable] modifier:
* 1. The AnimatedFloat hosted inside and its value will be in sync with call site state
* 2. When the anchor is reached, [onStateChange] will be called with state mapped to this anchor
* 3. When the anchor is reached and [onStateChange] with corresponding state is called, but
* call site didn't update state to the reached one for some reason,
* this component performs rollback to the previous (correct) state.
* 4. When new [state] is provided, component will be animated to state's anchor
*
* children of this composable will receive [AnimatedFloat] class from which
* they can read current value when they need or manually animate.
*
* @param T type with which state is represented
* @param state current state to represent Float value with
* @param onStateChange callback to update call site's state
* @param anchorsToState pairs of anchors to states to map anchors to state and vise versa
* @param animationBuilder animation which will be used for animations
* @param dragDirection direction in which drag should be happening.
* Either [DragDirection.Vertical] or [DragDirection.Horizontal]
* @param minValue lower bound for draggable value in this component
* @param maxValue upper bound for draggable value in this component
* @param enabled whether or not this Draggable is enabled and should consume events
*/
// TODO(malkov/tianliu) (figure our how to make it better and make public)
@Composable
internal fun <T> StateDraggable(
state: T,
onStateChange: (T) -> Unit,
anchorsToState: List<Pair<Float, T>>,
animationBuilder: AnimationBuilder<Float>,
dragDirection: DragDirection,
enabled: Boolean = true,
minValue: Float = Float.MIN_VALUE,
maxValue: Float = Float.MAX_VALUE,
content: @Composable() (AnimatedFloat) -> Unit
) {
val forceAnimationCheck = state { true }

val anchors = remember(anchorsToState) { anchorsToState.map { it.first } }
val currentValue = anchorsToState.firstOrNull { it.second == state }!!.first
val flingConfig =
AnchorsFlingConfig(anchors, animationBuilder, onAnimationEnd = { reason, finalValue, _ ->
if (reason != AnimationEndReason.Interrupted) {
val newState = anchorsToState.firstOrNull { it.first == finalValue }?.second
if (newState != null && newState != state) {
onStateChange(newState)
forceAnimationCheck.value = !forceAnimationCheck.value
}
}
})
val position = animatedFloat(currentValue)
position.setBounds(minValue, maxValue)

// This state is to force this component to be recomposed and trigger onCommit below
// This is needed to stay in sync with drag state that caller side holds
onCommit(currentValue, forceAnimationCheck.value) {
position.animateTo(currentValue, animationBuilder)
}
val draggable = draggable(
dragDirection = dragDirection,
onDragDeltaConsumptionRequested = { delta ->
val old = position.value
position.snapTo(position.value + delta)
position.value - old
},
onDragStopped = { position.fling(flingConfig, it) },
enabled = enabled,
startDragImmediately = position.isRunning
)
// TODO(b/150706555): This layout is temporary and should be removed once Semantics
// is implemented with modifiers.
@Suppress("DEPRECATION")
PassThroughLayout(draggable) {
content(position)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ import com.example.jetnews.data.successOr
import com.example.jetnews.model.Post
import com.example.jetnews.ui.Screen
import com.example.jetnews.ui.ThemedPreview
import com.example.jetnews.ui.UiState
import com.example.jetnews.ui.darkThemeColors
import com.example.jetnews.ui.effect.fetchPost
import com.example.jetnews.ui.home.BookmarkButton
import com.example.jetnews.ui.home.isFavorite
import com.example.jetnews.ui.home.toggleBookmark
import com.example.jetnews.ui.navigateTo
import com.example.jetnews.ui.state.UiState

@Composable
fun ArticleScreen(postId: String, postsRepository: PostsRepository) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import androidx.compose.state
import com.example.jetnews.data.Result
import com.example.jetnews.data.posts.PostsRepository
import com.example.jetnews.model.Post
import com.example.jetnews.ui.UiState
import com.example.jetnews.ui.state.UiState

/**
* Effect that interacts with the repository to obtain a post with postId to display on the screen.
Expand Down
Loading

0 comments on commit 0fbf067

Please sign in to comment.