I'm trying to write a calendar like custom view as described in this blog post, which is also kind of like an Excel spreadsheet. Headers on the top and column counts on the left. Those are stickied to the top and left
https://danielrampelt.com/blog/jetpack-compose-custom-schedule-layout-part-1/
Unlike this blog post, which adds a horizontal / vertical scroll modifier on the content which keeps the headers in place works, I need to be able to drag my content in all directions.
It looked like I needed to use a pointInput with a detectDragGestures, but when I use that, it drags like i want, the headers are pinned to the top and move left/right and the column counts are pinned to the left and move up and down
But the schedule content scrolls over the columns/headers as i drag them upwards or left.
I need the dragged content to go behind the header/column indicators.
My layout is like this
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Column(
modifier = modifier.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX = (offsetX + dragAmount.x).coerceIn((-1 * unitWidth.toPx() * unitList.count()), 0f)
offsetY = (offsetY + dragAmount.y).coerceIn(-1 * hourHeight.toPx() * 24, 0f)
}
}
) {
Header(modifier = Modifier.offset { IntOffset(offsetX.roundToInt(), 0) } )
Row(modifier = Modifier.weight(1f)) {
SideBar( modifier = Modifier.offset { IntOffset(0, offsetY.roundToInt())})
Schedule( modifier = Modifier.offset { IntOffset(offsetX.roundToInt() + unitWidth.roundToPx(), offsetY.roundToInt()) }.weight(1f)) } )
)
}
How do I enable dragging in any direction, and not overlap the other views? I want the content to be hidden / vanish behind the headers as it goes into them.
Just set the z-index of the headers and stuff to something like 5 and that of the draggable to a lower value. It will automatically be clipped upon appropriate position attainment
Related
I have to create a reusable component that tries to achieve this goal: I have a column that can have content that's larger than the screen height. On the bottom of the screen we have panel with gradient background that can contain button or something else (it's basically a slot in the component). This bottom panel have to be always visible on the screen, and in case of the column being bigger than screen - bottom panel have to be on the top of this column. Gradient background does a nice UX effect so user knows what is going on. It looks like that:
I have that solved, but here's the challenge. The column content have to be scrollable to be on top of the bottom panel when scrolled to the end. Current solution I have is to add a spacer on the bottom of this column. This spacer have the calculated height of the bottom parent. And here's the issue - right now we have calculation done in onSizeChanged which basically results in additional frame needed for the spacer to have correct size.
We did not observe any negative impact of that performance or UX wise. The spacer height calculation never does anything that user can see, but I still want to solve that properly.
AFAIK this can be done using custom Layout, but that seems a little bit excessive for what I want to achieve. Is there another way to do this properly?
Current solution:
#Composable
fun FloatingPanelColumn(
modifier: Modifier = Modifier,
contentModifier: Modifier = Modifier,
contentHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
bottomPanelContent: #Composable ColumnScope.() -> Unit,
content: #Composable ColumnScope.() -> Unit
) {
val scrollState = rememberScrollState()
var contentSize by remember {
mutableStateOf(1)
}
Box(modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = scrollState)
.then(contentModifier),
horizontalAlignment = contentHorizontalAlignment,
) {
content()
val contentSizeInDp = with(LocalDensity.current) { contentSize.toDp() }
Spacer(modifier = Modifier.height(contentSizeInDp))
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.onSizeChanged {
contentSize = it.height
}
.wrapContentHeight()
.align(Alignment.BottomStart)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0x00FAFCFF),
Color(0xFFF6F9FB),
)
)
),
content = bottomPanelContent
)
}
}
The best way to depend on an other view size during layout is using SubcomposeLayout:
SubcomposeLayout { constraints ->
// subcompose the view you need to measure first
val bottomPanel = subcompose("bottomPanel") {
Column(
// ...
)
}[0].measure(constraints)
// use calculated value in next views layout, like bottomPanel.height
val mainList = subcompose("mainList") {
LazyColumn(
contentPadding = PaddingValues(bottom = bottomPanel.height.toDp())
) {
// ...
}
}[0].measure(constraints)
layout(mainList.width, mainList.height) {
mainList.place(0, 0)
bottomPanel.place(
(mainList.width - bottomPanel.width) / 2,
mainList.height - bottomPanel.height
)
}
}
Here on the right , I have a list of items in a composable , Every item is inside a row , All the items are inside a column
All the children of the are getting clipped to fit the screen which I don't want , I want these items to render completely even if outside of screen since I have a zoomable container above them
As you can see how text in the text field is all in one line vertically rather than expanding the width , This is the problem
Code :
Row(
modifier = modifier.zIndex(3f),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
SimpleNodesList(
modifier = Modifier.padding(8.dp),
parentNode = state.center,
nodes = state.center.left,
renderRight = false,
)
SimpleNode(node = state.center, parentNode = null)
SimpleNodesList(
modifier = Modifier.padding(8.dp),
parentNode = state.center,
nodes = state.center.right,
renderLeft = false
)
}
Simple Nodes List is a column of rows , I have one column on left and one on the right , If the width of the left column increases , right row get's clipped by the screen
Using this modifier does the job for the row , In my case I also needed this layout modifier , wrapContentSize(unbounded = true) was working but children weren't clickable for some reason outside the bounds of the zoom container !
I also had to create a modifier zoomable rather than use a zoomable box , so the zoomable touch events would be dispatched on the this composable rather than the parent !
modifier = Modifier
.layout { measurable, constraints ->
val r =
measurable.measure(constraints = constraints.copy(maxWidth = Constraints.Infinity))
layout(r.measuredWidth, r.measuredHeight, placementBlock = {
r.placeWithLayer(0, 0, 0f) {
}
})
}
.wrapContentSize(unbounded = true)
If you are using hard-coded width for the text, applying Modifier.wrapContentSize() on every container might do the job
Use SimpleFlowRow in place of Row. It will fix the clipping issue.
I'm trying to write a calendar/excel like screen where you can diagonally scroll the content and the column/row headers stay in place.
It's using a custom layout with placeables, where the layout is the whole drawable space, and then the events are drawn within the box where they are needed.
For example
Column 0 starts at X=0 Y=0, and each column is some width. So column 1 draws at 0, column 2 at 20, column 3 at 40 etc.
It all works correctly until the content size becomes larger than the visible width of the screen, the content that starts at position 0 starts getting drawn off screen, such as at -20. Almost like it's centering the content...
Here's some pics of whats happening when the size fits and then doesn't fit
I think it might have something to do with alignment lines in the custom layout but if it does i can't figure out what it wants me to do... I've tried alignments on the column set to start as well to no avail.
Here's a bare bones example of the problem
#Composable
fun ScheduleTest(columnList: List<String>) {
val headerWidth = 150
Column {
HeaderTest(columnList, headerWidth)
}
}
#Composable
fun HeaderTest(
columnList: List<String>,
headerWidth: Int,
headerContent: #Composable (title: String) -> Unit = { title ->
Text(title)
},
) {
val numberOfHeaders = columnList.count()
Layout(
content = {
columnList.forEach { header ->
Box {
headerContent(header)
}
}
}
) { measureables, constraints ->
val height = 45.dp.roundToPx()
val totalWidth = headerWidth * numberOfHeaders
val placeWithHeader = measureables.map { measurable ->
val placeable = measurable.measure(
constraints.copy(
minWidth = headerWidth,
maxWidth = headerWidth,
minHeight = height,
maxHeight = height,
)
)
placeable
}
layout(width = totalWidth, height = height) {
placeWithHeader.forEachIndexed { index, placeable ->
val offset = index * headerWidth
placeable.place(offset, 0)
}
}
}
}
I was helped with this on another forum but they didn't answer it here.
I wasn't constraining the main layouts width and height
val height = 45.dp.roundToPx()
val totalWidth = headerWidth * numberOfHeaders
the contract of layout is that the width/height passed to layout must
be in range of the constraints, if you violate that, your measurable
gets clamped to the constraints and your placement is centered in the
clamped space where your parent placed you
I needed to add constraints to it
val height = 45.dp.roundToPx().coerceIn(constraints.minHeight, constraints.maxHeight)
val totalWidth = headerWidth * numberOfHeaders.coerceIn(constraints.minWidth, constraints.maxWidth)
UPDATE: This is an iOS 10 issue. This still works as before in iOS 9.
This is ...interesting.
I just converted my "teaching project" (a "toy" app) to Swift 3.
It has been working for a couple of years under Swift 1.2.
All of a sudden, my UIScrollView is not scrolling, even when I set the contentSize way past its lower boundary.
Here's the relevant code (the displayTags routine is called with an array of images that are displayed centered and slightly vertically offset, leading to a vertical chain):
/*******************************************************************************************/
/**
\brief Displays the tags in the scroll view.
\param inTagImageArray the array of tag images to be displayed.
*/
func displayTags ( inTagImageArray:[UIImage] )
{
self.tagDisplayView!.bounds = self.tagDisplayScroller!.bounds
if ( inTagImageArray.count > 0 ) // We need to have images to display
{
var offset:CGFloat = 0.0 // This will be the vertical offset for each tag.
for tag in inTagImageArray
{
self.displayTag ( inTag: tag, inOffset: &offset )
}
}
}
/*******************************************************************************************/
/**
\brief Displays a single tag in the scroll view.
\param inTag a UIImage of the tag to be displayed.
\param inOffset the vertical offset (from the top of the display view) of the tag to be drawn.
*/
func displayTag ( inTag:UIImage, inOffset:inout CGFloat )
{
let imageView:UIImageView = UIImageView ( image:inTag )
var containerRect:CGRect = self.tagDisplayView!.frame // See what we have to work with.
containerRect.origin = CGPoint.zero
let targetRect:CGRect = CGRect ( x: (containerRect.size.width - inTag.size.width) / 2.0, y: inOffset, width: inTag.size.width, height: inTag.size.height )
imageView.frame = targetRect
containerRect.size.height = max ( (targetRect.origin.y + targetRect.size.height), (containerRect.origin.y + containerRect.size.height) )
self.tagDisplayView!.frame = containerRect
self.tagDisplayView!.addSubview ( imageView )
self.tagDisplayScroller!.contentSize = containerRect.size
print ( "Tag Container Rect: \(containerRect)" )
print ( " Tag ScrollView Bounds: \(self.tagDisplayScroller!.bounds)" )
inOffset = inOffset + (inTag.size.height * 0.31)
}
Note that the scrollView's contentSize is expanded each time a tag is added. I checked (see the print statements), and the value seems to be correct.
The project itself is completely open-source.
This is where this issue manifests (I have other bugs, but I'll get around to fixing them after I nail this one).
I'm sure that I am doing something obvious and boneheaded (usually the case).
Anyone have any ideas?
it will work when you will set contentSize on main thread and put this code in - (void)viewDidLayoutSubviews
- (void)viewDidLayoutSubviews
{
dispatch_async (dispatch_get_main_queue(), ^
{
[self.scrollview setContentSize:CGSizeMake(0, 2100)];
});
}
contentSize should be change in main thread in swift.
DispatchQueue.main.async {
self.scrollView.contentSize = CGSize(width: 2000, height: 2000)
}
This worked for me.
OK. I solved it.
I remove every single auto layout constraint for the internal (scrolled) view at build time.
I assume that iOS 10 is finally honoring the contract by forcing the top of the scrolled view to attach to the top of the scroller, even though the user wants to move it.
I think this is rather easy achieved, but I couldn't find out how to- and couldn't find much documentary about it.
I hate those 'scroll to top' buttons that appear after you already scrolled just 300px. Like I'm that lazy to scroll to top on myself. Therefor I would like to have a scroll to top button that only appears when you reached the bottom of the page (minus 100vh (100% viewport height).
Let's take in account the button is called .scrollTopButton and it's CSS is opacity: 0 and it's position: fixed on default.
How would I make the button appear when you reached the bottom of the page, minus 100vh and scroll along?
I was thinking of comparing the body height minus 100vh with (window).scrollTop().
var vH = $(window).height(),
bodyMinus100vh = ($('body').height() - vH);
if (bodyMinus100VH < $(window).scrollTop) {
$('.scrollTopButton').toggle();
};
Fixed it myself. Quite easy, honestly.
$(window).scroll(function () {
var vH = $(window).height(),
bodyHeight = ($(document).height() - (vH * 2)),
// When you open a page, you already see the website as big
// as your own screen (viewport). Therefor you need to reduce
// the page by two times the viewport
scrolledPX = $(window).scrollTop();
if (scrolledPX > bodyHeight) {
$('.scrollTopButton').css('opacity', '1');
} else {
$('.scrollTopButton').css('opacity', '0')
};
});