jetpack compose lazy custom layout - android-jetpack-compose

I tried to develop a simple custom layout just like the documentation
#Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
it works fine when I know exact number of items
but what about lazy items?
there is nothing in documentation about how can I develop a LazyCustomLayout

You don't exactly have to know how many items are in the Layout, since even for dynamic lists, there's always a 'current number of items' which can be computed. Let's say you download a list of texts from a server, and then intend to use this Layout to render those. Even in that case, while the server may vary the length of the list, i.e., the list is dynamic in size, you would presumably have a LiveData object keeping track of the list items. From there, you can easily use the collectAsState() method inside a Composable, or the observeAsState() method tied to a LifecycleOwner to convert it into the Compose-compatible MutableState<T> variable. Hence, whenever the LiveData notifies a new value (addition, or deletion), the MutableState<T> variable will also be updated to reflect those values. This, you can use inside the said Layout, which is also a Composable and hence, will update along-side the server-values, in real-time.
The thing is, no matter how you get your list, in order to show it on-screen, or use it anywhere in your app, you would always have a list object, which can be exploited using Compose's declarative and reactive nature.

Related

Is there a way for lazy loading with FlowRow?

I use FlowRow in the accompanist project to auto wrap my text items to next line. It works as intended. However, when I have a large dataset (which I already load with paging), I don't find an api like LazyColumn to load and build the items as needed, if I loop through the pager flow, it tries to load to build everything at once. Any adice please?
lazyPagingItems = pager.flow.collectAsLazyPagingItems()
FlowRow(
) {
val items = lazyPagingItems
for (index in 1..items.itemCount-1) {
Text(
text = word,
maxLines = 1
)
}
}
Little late to the party. But it seems you could use LazyVerticalGrid or LazyHorizontalGrid in adaptive mode like below.
LazyVerticalGrid(
columns = GridCells.Adaptive(/* item min size */)
) {
// Items
}

How to create conditional questions in a form for an ios app?

How can I dynamically add additional text fields to a view based on the response to previous question which is a drop down with set list of options.
The issue I am having is with dynamic positioning. For example if I place the field beneath but keep it hidden and show when an option is selected that only works one way. What if I select the other option how can I use the same space to show a different question/text field?
Of course I could overlay all of the options in their positions and show/hide. But is there a better way to build a dynamic form with conditional logic for questions?
Diagram:
If it were me, to make it simple for both myself and the viewer, I would have predefined space. Say, 20% of the view. Then, add questions to the scrollView variably, depending on the situation. The user can then scroll through that view.
let rect = CGRect(x: view.frame.width*0.1, y: someHeightDownInTheView, width: view.frame.width*0.8, height: view.frame.height*0.2)
let scrollView = UIScrollView(frame: rect)
let questions = ["Do I want to be a unicorn"]
if(question2.answer == "A") {
questions.append("Do I want to be a fairy princess")
}
//...Specific options
for question in questions {
let label:UILabel = createQuestion(name: "someName", question: question)
let answer:UITextView = createAnswer(name: "someName")
scrollView.addSubview(label)
scrollView.addSubview(answer)
}
func createQuestion(name: String, question: String) -> UILabel {
//Create a question with a UILabel of some SET SIZE
}
func createAnswer(name: String) -> UITextView {
//Create answer with a UITextView of some SIZE
}
There are various ways to handle variable numbers of fields.
You could create a table view or collection view, and have each cell, or each grouped set of cells, represent a question and its text field.
You could also use a vertical stack view. You can add or remove items from a stack view and it updates make room for/close up empty space as needed.
There are tons of examples of both approaches online. Which is the best fit depends on the details, but if you only ever have at most 2 questions/answers on-screen then maybe a stack view is the way to go.

How to create a ScrollView that can adapt to its children's changing size

[Beta 4] I have a list of cards, each of which can be expanded when the user taps on them to display more information.
However, the containing ScrollView does not expand or contract when the size of the cards changes.
The only way I have found to make this work is to use List — which seems to adjust automatically to the content, but this is not what I want as it introduces other features of the list (most notably the dividers).
Not using a ForEach (i.e. just stacking several Card() yields the same result.
In Beta 3 I found a workaround by making expanded a #Binding, and then have an array of #State for each card, but since Beta 4, this doesn't work anymore as the changing value does not propagate up anymore unless I also have an element in the parent view that binds to this array directly (i.e. also a toggle). It seems that the state of #State only changes now if there are actually elements directly bound to it.
struct ExpandableChildViewTest: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
ForEach(1...10) { _ in
Card()
}
}
}
.padding()
.border(Color.green, width: 1)
}
}
struct Card: View {
#State private var expanded = true
var body: some View {
ZStack {
Rectangle()
.frame(height: expanded ? 350: 100)
.foregroundColor(Color.blue)
.cornerRadius(20)
.padding(.vertical)
.tapAction() {
self.expanded.toggle()
}
}
}
}
I would expect (hope) the containing Scrollview to adjust its size when its children change size, but that doesn't happen. This leads to either content falling out of the Scrollview, or a lot of extra space inside the Scrollview.
The picture shows the extra white space at the top because the second card is collapsed. The outer Scrollview does not recompute itself.
(my reputation does not yet allow to comment, so I'm using this way...)
I have a similar situation: a timeline with different granulations (decade, year, month etc.), and the user shall be able to zoom smoothly, while the scale adopts itself accordingly. The only way I found up to now is to calculate the overall width of the scale for the current magnification, and to hand it over to the view through the model. The embedded view then gets a .frame with that width, and the ScrollView then handles the scrolling correctly.
However, this is far away from being elegant. Because the embedded view actually knows its width, it should be possible somehow (but how?) to make the ScrollView aware of that width. In my environment, the ScrollView (without the explicit .frame) just shows an empty screen, with the explicit .frame everything's fine (but not performant enough).
Btw: Since Beta 5, there are changes to the way how binding is performed. You now set the model in the view with #ObservedObject var model: Model, and you propagate the model's changes using #Published var something: SomeType, this is much easier than before.

Best way to add multiple diagonal connection lines between TableViewCells

I'm creating an app that needs to show a tableview like below image
Similar colored circles are to be matched with a line.
Which view i can add the lines?
Or need to create a new view above tableview? But still my tableview needs to be scrolled.
How can i achieve this?
Update for Bounty
I want to implement the same with incliend lines between neighbouring circles. How to achieve the same?
Demonstration below:
create design like this
Based on your requirement just hide upper line and lower line of circle
You need to create collection view in tableview cell. In collection view you create one cell. Design the same user interface like your design. Show and hide the view with matching of rule. It will not affect tableview scrolling. and with this approach you can also provide scroll in collection view cell. i can provide you coded solution if you able to provide me more information. Thanks
You can use this Third Party LIb
You need to use a combination of collection view and a table view to give support for all devices.
1.Create one collection view cell with following layout
Hide upper and lower lines as per your need
Add collection view in table view cell and managed a number of cells in collection view depending upon the current device width and item's in between spacing.
You can create a vertical label without text, set the background color with black and place it behind the circle in view hierarchy and set a width of the label as per your requirement. Then you can hide unhide the label whenever you want.
P.S.: Make sure to hide your cell separator.
I have created a demo project. You can find it here. I tried to match your requirements. You can update the collection view settings to handle the hide and show of labels.
Let me know if you have any questions.
Hope this help.
Thanks!
To connect any circle with any other circle in the cell above / below, it will be easier and cleaner to create the connection lines dynamically rather than building them into the asset as before. This part is simple. The question now is where to add them.
You could have the connection lines between every two cells be contained in the top or bottom cell of each pair, since views can show content beyond their bounds.
There's a problem with this though, regardless of which cell contains the lines. For example, if the top cell contains them, then as soon as it is scrolled up off screen, the lines will disappear when didEndDisplayingCell is called, even though the bottom cell is still completely on screen. And then scrolling slightly such that cellForRow is called, the lines will suddenly appear again.
If you want to avoid that problem, then here is one approach:
One Approach
Give your table view and cells a clear background color, and have another table view underneath to display a new cell which will contain the connection lines.
So you now have a background TVC, with a back cell, and a foreground TVC with a fore cell. You add these TVC's as children in a parent view controller (of which you can set whatever background color you like), disable user interaction on the background TVC, and peg the background TVC's content offset to the foreground TVC's content offset in an observation block, so they will stay in sync when scrolling. I've done this before; it works well. Use the same row height, and give the background TVC a top inset of half the row height.
We can make the connection lines in the back cell hug the top and bottom edges of the cell. This way circles will be connected at their centre.
Perhaps define a method in your model that calculates what connections there are, and returns them, making that a model concern.
extension Array where Element == MyModel {
/**
A connection is a (Int, Int).
(0, 0) means the 0th circle in element i is connected to the 0th circle in element j
For each pair of elements i, j, there is an array of such connections, called a mesh.
Returns n - 1 meshes.
*/
func getMeshes() -> [[(Int, Int)]] {
// Your code here
}
}
Then in your parent VC, do something like this:
class Parent_VC: UIViewController {
var observation: NSKeyValueObservation!
var b: Background_TVC!
override func viewDidLoad() {
super.viewDidLoad()
let b = Background_TVC(model.getMeshes())
let f = Foreground_TVC(model)
for each in [b, f] {
self.addChild(each)
each.view.frame = self.view.bounds
self.view.addSubview(each.view)
each.didMove(toParent: self)
}
let insets = UIEdgeInsets(top: b.tableView.rowHeight / 2, left: 0, bottom: 0, right: 0)
b.tableView.isUserInteractionEnabled = false
b.tableView.contentInset = insets
self.b = b
self.observation = f.tableView.observe(\.contentOffset, options: [.new]) { (_, change) in
let y = change.newValue!.y
self.b.tableView.contentOffset.y = y // + or - half the row height
}
}
}
Then of course there's your drawing code. You could make it a method of your back cell class (a custom cell), which will take in a mesh data structure and then draw the lines that represent it. Something like this:
class Back_Cell: UITableViewCell {
/**
Returns an image with all the connection lines drawn for the given mesh.
*/
func createMeshImage(for mesh: [(Int, Int)]) -> UIImage {
let canvasSize = self.contentView.bounds.size
// Create a new canvas
UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0)
// Grab that canvas
let canvas = UIGraphicsGetCurrentContext()!
let spacing: CGFloat = 10.0 // whatever the spacing between your circles is
// Draw the lines
for each in mesh {
canvas.move(to: CGPoint(x: CGFloat(each.0) * spacing, y: 0))
canvas.addLine(to: CGPoint(x: CGFloat(each.1) * spacing, y: self.contentView.bounds.height))
}
canvas.setStrokeColor(UIColor.black.cgColor)
canvas.setLineWidth(3)
canvas.strokePath()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
You'd probably want to create a Mesh class and store the images in that model, to avoid redrawing.

Centering node vertically in horizontal ASStackLayout

I'm wonder what is the correct way to center textNode inside vertical ASSStackNode which is inside horizontal ASStackNode without setting any sizes?
I've tried different options with ASCenterLayout / ASRelativeLayout around vertical layout, flewGrow, flexShrink.
Only possible way that I see is static layout with minimum height around vertical layout. But wanna find if there are another way.
Images to better picture:
My layout structure:
What I get when one of text fields is empty:
What I want to see:
If your ASTextNode is empty or nil. Isn't mean node not exist. If you want hide node if it empty, you must write a simple logic for it. For exlude it Spec from calculation. What i use on ADK1.9:
- (ASLayoutSpec *)layoutSpecThatFits: (ASSizeRange)constrainedSize
{
NSMutableArray *arrayOfChildren = [NSMutableArray new];
[arrayOfChildren insertObject:self.titleNode];
if (self.textNode.text.length > 0) {
[arrayOfChildren insertObject:self.textNode];
}
ASStackLayoutSpec *verticalElementCoreStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:8
justifyContent:ASStackLayoutJustifyContentCenter alignItems:ASStackLayoutAlignItemsStretch
children:arrayOfChildren];
return verticalElementCoreStack;
}

Resources