I am trying to get a good understanding of how to use MotionLayout in Jetpack Compose.
At this state, I believe to have a basic understanding of how the MotionLayout works, by having a MotionScene (defined in a .json5 file) set to the MotionLayout and then apply a Modifier.layoutId to all the Composables, which should have an effect in the animation.
What I am trying to do, is to have a circle, which should stretch out on the X-axis, left side first (for maybe 2-300 ms) then have the right side follow along awards the left (for 2-300 ms), so that it will be a full circle once again - Just at a different position.
An example of what I wish to have, is an animation similar to the three images here.
Sadly I haven't been able to do that yet. In the Transitions part of the MotionScene, I have played around with scaleX, which made the circle misshaped and stretched out to both sides at the same time. And translationX which ofc, just moves the entire circle instead of just a part of it.
My current implementation looks like this:
Composable
#OptIn(ExperimentalMotionApi::class)
#Preview
#Composable
fun AnimationScreen() {
val context = LocalContext.current
val motionScene = remember {
context.resources.openRawResource(R.raw.motion_scene).readBytes().decodeToString()
}
var progress by remember { mutableStateOf(0f) }
Column {
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = progress,
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.layoutId("row_container")
) { }
Box(
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topEnd = 25.dp,
bottomEnd = 25.dp,
topStart = 25.dp,
bottomStart = 25.dp
)
)
.background(Color.Red)
.layoutId("circle")
)
}
Spacer(Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = {
progress = it
}
)
}
}
motion_scene.json5
{
ConstraintSets: {
start: {
circle: {
width: 40,
height: 40,
start: ['logo_pic', 'start', 0],
end: ['logo_pic', 'end', 0],
top: ['logo_pic', 'top', 0],
bottom: ['logo_pic', 'bottom', 0]
},
},
end: {
circle: {
width: 40,
height: 40,
start: ['logo_pic', 'start', 0],
end: ['logo_pic', 'end', 0],
top: ['logo_pic', 'top', 0],
bottom: ['logo_pic', 'bottom', 0],
},
},
},
Transitions: {
default: {
from: 'start',
to: 'end',
pathMotionArc: 'startHorizontal',
KeyFrames: {
KeyAttributes: [
{
target: ['circle'],
frames: [0, 50, 80, 100],
scaleX: [1, 1, 2, 2],
//translationX: [0, 0, 0, -150]
},
],
KeyPosition: [
{
target: ['circle'],
percentWidth: 20
}
]
}
}
}
}
Hopefully it all comes down to my just being new to this framework, and someone just says, "Easy! You just need to...".
Any suggestions on, how I would make this work?
There is not a simple way to do this with motionlayout
Because the start and end are the same size.
But you can add a middle and play some trick with progress.
It was coded this way to avoid a few bugs in the latest release.
public const val motionSceneStr = """
{
ConstraintSets: {
start: {
circle: {
width: 40,
height: 40,
start: ['parent', 'start', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0]
},
},
middle: {
circle: {
width: 'spread',
height: 40,
start: ['parent', 'start', 0],
end: ['parent', 'end', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0],
},
},
end: {
circle: {
width: 40,
height: 40,
end: ['parent', 'end', 0],
top: ['parent', 'top', 0],
bottom: ['parent', 'bottom', 0],
},
},
},
Transitions: {
part2: { from: 'middle', to: 'end' }
part1: { from: 'middle', to: 'start' }
}
}
""";
#OptIn(ExperimentalMotionApi::class)
#Preview
#Composable
fun AnimationScreen() {
val context = LocalContext.current
val motionScene = remember {motionSceneStr }
var progress by remember { mutableStateOf(0f) }
Column {
MotionLayout(
motionScene = MotionScene(content = motionScene),
progress = if (progress<0.5) 1-progress*2 else progress*2-1,
transitionName = if (progress<0.5f) "part1" else "part2",
modifier = Modifier
.fillMaxWidth()
) {
Row(
modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.layoutId("row_container")
) { }
Box(
modifier = Modifier
.size(100.dp)
.clip(
RoundedCornerShape(
topEnd = 25.dp,
bottomEnd = 25.dp,
topStart = 25.dp,
bottomStart = 25.dp
)
)
.background(Color.Red)
.layoutId("circle")
)
}
Spacer(Modifier.height(32.dp))
Slider(
value = progress,
onValueChange = {
progress = it
}
)
}
}
Related
I'm creating a timeline with Konva and the entire timeline (stage) is draggable on all directions but I have an axis with all the years of the timeline (Konva group) that I want to restrict its movement so that it only moves horizontally.
I can't use dragBoundFunc as it will restrict the movement on all nodes of the timeline.
I tried to change the position of the element using the dragmove event:
stage.on("dragmove", function(evt) {
xaxis.y(0);
});
But the xaxis still moves on all direction while dragging the stage.
I could also use different draggable layers for the axis and the timeline itself, but then when I drag the axis it wouldn't move the timeline and the same if I move the timeline.
As the simplest solution, you can just make sure that the absolute position of your timelime group is the same:
stage.on("dragmove", function(evt) {
// read absolute position
const oldAbs = xaxis.absolutePosition();
// set new absolute position, but make sure x = 0
xaxis.absolutePosition({
x: oldAbs.x,
y: 0
});
});
Here is a slightly more capable version that allows vertical drag of the event layer whilst keeping the time line axis visible for reference. This uses two layers - one to act as the background containing the time line and grid, whilst the second shows the events.
The key technique here is using the dragMove event listener on the draggable event layer to move the background layer in sync horizontally but NOT vertically. Meanwhile the event layer is also constrained with a dragBound function to stop silly UX.
An improvement would be to add clipping to the event layer so that when dragged down it would not obscure the timeline.
var stageWidth = 800,
stageHeight = 300,
timeFrom = 1960,
timeTo = 2060,
timeRange = timeTo - timeFrom,
timeLineWidth = 1000,
timeSteps = 20, // over 100 yrs = 5 year intervals
timeInt = timeRange / timeSteps,
timeLineStep = timeLineWidth / timeSteps,
yearWidth = timeLineWidth / timeRange,
plotHeight = 500,
events = [{
date: 1964,
desc: 'Born',
dist: 10
},
{
date: 1966,
desc: 'England win world cup - still celebrating !',
dist: 20
},
{
date: 1968,
desc: 'Infant school',
dist: 30
},
{
date: 1975,
desc: 'Secondary school',
dist: 50
},
{
date: 1981,
desc: 'Sixth form',
dist: 7
},
{
date: 1983,
desc: 'University',
dist: 30
},
{
date: 1986,
desc: 'Degree, entered IT career',
dist: 50
},
{
date: 1990,
desc: 'Marriage #1',
dist: 0
},
{
date: 1996,
desc: 'Divorce #1',
dist: 0
},
{
date: 1998,
desc: 'Marriage #2 & Son born',
dist: 90
},
{
date: 2000,
desc: 'World did not end',
dist: 20
},
{
date: 2025,
desc: 'Retired ?',
dist: 0
},
{
date: 2044,
desc: 'Enters Duncodin - retirement home for IT workers',
dist: 0
},
{
date: 2054,
desc: 'Star dust',
dist: 0
}
]
function setup() {
// Set up a stage and a shape
stage = new Konva.Stage({
container: 'konva-stage',
width: stageWidth,
height: stageHeight
});
// bgLayer is the background with the grid, timeline and date text.
var bgLayer = new Konva.Layer({
draggable: false
})
stage.add(bgLayer);
for (var i = 0, max = timeSteps; i < max; i = i + 1) {
bgLayer.add(new Konva.Line({
points: [(i * timeLineStep) + 0.5, 0, (i * timeLineStep) + .5, plotHeight],
stroke: 'cyan',
strokeWidth: 1
}))
bgLayer.add(new Konva.Text({
x: (i * timeLineStep) + 4,
y: 260,
text: timeFrom + (timeInt * i),
fontSize: 12,
fontFamily: 'Calibri',
fill: 'magenta',
rotation: 90,
listening: false
}));
}
for (var i = 0, max = plotHeight; i < max; i = i + timeLineStep) {
bgLayer.add(new Konva.Line({
points: [0, i + 0.5, timeLineWidth, i + .5],
stroke: 'cyan',
strokeWidth: 1
}))
}
// add timeline
var timeLine = new Konva.Rect({
x: 0,
y: 245,
height: 1,
width: timeLineWidth,
fill: 'magenta',
listening: false
});
bgLayer.add(timeLine)
// eventLayer contains only the event link line and text.
var eventLayer = new Konva.Layer({
draggable: true,
// the dragBoundFunc returns an object as {x: val, y: val} in which the x is constricted to stop
// the user dragging out of sight, and the y is not allowed to change.
// ! position of bgLayer is moved in x axis in sync with eventLayer via dragMove event
dragBoundFunc: function(pos) {
return {
x: function() {
var retX = pos.x;
if (retX > 20) { // if the left exceeds 20px from left edge of stage
retX = 20;
} else if (retX < (stageWidth - (timeLineWidth + 50))) { // if the right exceeds 50 px from right edge of stage
retX = stageWidth - (timeLineWidth + 50);
}
return retX;
}(),
y: function() {
var retY = pos.y;
if (retY < 0) {
retY = 0;
} else if (retY > 200) {
retY = 200;
}
return retY;
}()
};
}
});
stage.add(eventLayer);
// ! position of bgLayer is moved in x axis in sync with eventLayer via dragMove event of eventLayer.
eventLayer.on('dragmove', function() {
var pos = eventLayer.position();
var bgPos = bgLayer.position();
bgLayer.position({
x: pos.x,
y: bgPos.y
}); // <--- move the bgLayer in sync with the event eventLayer.
stage.draw()
});
for (var i = 0, max = events.length; i < max; i = i + 1) {
var event = events[i];
var link = new Konva.Rect({
x: yearWidth * (event.date - timeFrom),
y: 200 - event.dist,
width: 1,
height: 55 + event.dist,
fill: 'magenta',
listening: false
});
eventLayer.add(link)
var eventLabel = new Konva.Text({
x: yearWidth * (event.date - timeFrom) - 5,
y: 190 - event.dist,
text: event.date + ' - ' + event.desc,
fontSize: 16,
fontFamily: 'Calibri',
fill: 'magenta',
rotation: -90,
listening: false
});
eventLayer.add(eventLabel);
var dragRect = new Konva.Rect({
x: 0,
y: 0,
width: timeLineWidth,
height: 500,
opacity: 0,
fill: 'cyan',
listening: true
});
eventLayer.add(dragRect);
dragRect.moveToTop()
}
stage.draw()
}
var stage, eventLayer;
setup()
.konva-stage {
width: 100%;
height: 100%;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Drag the timeline left & right AND up & down...</p>
<div id="konva-stage"></div>
Just for fun, a stripped down version of my answers showing the ondrag() function without all the timeline frills.
var stage;
function setup() {
// Set up a stage and a shape
stage = new Konva.Stage({
container: 'konva-stage',
width: 600,
height: 300
});
// layer1.
var layer1 = new Konva.Layer({
draggable: false
})
stage.add(layer1);
var ln1 = new Konva.Line({
points: [10, 0, 10, 20, 10, 10, 0, 10, 20, 10],
stroke: 'cyan',
strokeWidth: 4
});
layer1.add(ln1);
var layer2 = new Konva.Layer({
draggable: true,
});
stage.add(layer2);
var ln2 = new Konva.Line({
points: [10, 0, 10, 20, 10, 10, 0, 10, 20, 10],
stroke: 'magenta',
strokeWidth: 4
});
layer2.add(ln2);
// position the crosses on the canvas
ln1.position({
x: 100,
y: 80
});
ln2.position({
x: 100,
y: 40
});
// ! position of layer1 is moved in x axis in sync with layer2 via dragMove event of layer2.
layer2.on('dragmove', function() {
var pos = layer2.position();
var bgPos = layer1.position();
layer1.position({
x: pos.x,
y: bgPos.y
}); // <--- move layer1 in sync with layer2.
stage.draw()
});
stage.draw()
}
setup()
.konva-stage {
width: 100%;
height: 100%;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Drag the upper cross - only one moves vertically whilst the other is contrained in the y-axis. Both move in sync on the x-axis</p>
<div id="konva-stage"></div>
It is not entirely clear what you are asking but I have assumed you want to constrain the drag of the timeline so that it gives good UX. See working snippet below. The majority of the code is setup of the timeline. The important piece is
Include a rect covering the entire timeline that has zero opacity and is listening for mouse events.
Provide the layer with a dragBoundFunc that returns an object as {x: val, y: val} in which the x is constricted to stop the user dragging out of sight horizontally, and the y is not allowed to change. If you think of the rect and the stage as rectangles then the math is not difficult to comprehend. If your timeline is vertical, swap the x & y behaviour.
var stageWidth = 800,
timeFrom = 1960,
timeTo = 2060,
range = timeTo - timeFrom,
timeLineWidth = 1000;
yearWidth = timeLineWidth / range,
events = [{
date: 1964,
desc: 'Born'
},
{
date: 1968,
desc: 'Infant school'
},
{
date: 1975,
desc: 'Secondary school'
},
{
date: 1981,
desc: 'Sixth form'
},
{
date: 1983,
desc: 'University'
},
{
date: 1986,
desc: 'Degree, entered IT career'
},
{
date: 1990,
desc: 'Marriage #1'
},
{
date: 1998,
desc: 'Marriage #2'
},
{
date: 1999,
desc: 'Son born'
},
{
date: 2025,
desc: 'Retired ?'
},
{
date: 2044,
desc: 'Enters Duncodin - retirement home for IT workers'
},
{
date: 2054,
desc: 'Star dust'
}
]
function setup() {
// Set up a stage and a shape
stage = new Konva.Stage({
container: 'konva-stage',
width: stageWidth,
height: 500
});
layer = new Konva.Layer({
draggable: true,
// the dragBoundFunc returns an object as {x: val, y: val} in which the x is constricted to stop
// the user dragging out of sight, and the y is not allowed to change.
dragBoundFunc: function(pos) {
return {
x: function() {
retX = pos.x;
if (retX > 20) {
retX = 20;
} else if (retX < (stageWidth - (timeLineWidth + 50))) {
retX = stageWidth - (timeLineWidth + 50);
}
return retX;
}(),
y: this.absolutePosition().y
};
}
});
stage.add(layer);
// add timeline
var timeLine = new Konva.Rect({
x: 0,
y: 245,
height: 10,
width: timeLineWidth,
fill: 'magenta',
listening: false
});
layer.add(timeLine)
for (var i = 0, max = events.length; i < max; i = i + 1) {
var event = events[i];
var link = new Konva.Rect({
x: yearWidth * (event.date - timeFrom),
y: 200,
width: 5,
height: 55,
fill: 'magenta',
listening: false
});
layer.add(link)
var timeLabel = new Konva.Text({
x: yearWidth * (event.date - timeFrom) + 10,
y: 265,
text: event.date,
fontSize: 16,
fontFamily: 'Calibri',
fill: 'magenta',
rotation: 90,
listening: false
});
layer.add(timeLabel);
var eventLabel = new Konva.Text({
x: yearWidth * (event.date - timeFrom) - 5,
y: 190,
text: event.desc,
fontSize: 16,
fontFamily: 'Calibri',
fill: 'magenta',
rotation: -90,
listening: false
});
layer.add(eventLabel);
var dragRect = new Konva.Rect({
x: 0,
y: 0,
width: timeLineWidth,
height: 500,
opacity: 0,
fill: 'cyan',
listening: true
});
layer.add(dragRect);
dragRect.moveToTop()
}
stage.draw()
}
var stage, layer;
setup()
.konva-stage {
width: 100%;
height: 100%;
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/4.0.13/konva.js"></script>
<p>Drag the timeline...</p>
<div id="konva-stage"></div>
I have series legends as scrollbar as demo-ed in this fiddle
http://jsfiddle.net/BlackLabel/kb4gu5rn/
How can I draw series legend marker ("circle"/"square"/"diamond" etc..) instead of line
var $line = $('<div>')
.css({
width: 16,
position: 'absolute',
left: -20,
top: 8,
borderTop: '2px solid ' + (series.visible ? series.color :
options.itemHiddenStyle.color)
})
.appendTo($legendItem);
Also upon hover in this legendItemList I would like to trigger the mouseOver event on the series.. To highlight the series(or data points) in the chart for easy co-visualization
I added this handler, but it doesnt work
$legendItem.hover(function () {
series.onMouseOver();
});
Thanks for your help
You can use Highcharts.SVGRenderer class to add a specific legend marker symbol.
To highlight the series use setState('hover):
var chart = Highcharts.chart({...},
function(chart) {
var options = chart.options.legend,
legend = chart.legend;
/**
* What happens when the user clicks the legend item
*/
function clickItem(series, $legendItem, symbolLine, symbolSVG) {
series.setVisible();
$legendItem.css(
options[series.visible ? 'itemStyle' : 'itemHiddenStyle']
);
symbolLine.attr({
stroke: series.visible ? series.color : options.itemHiddenStyle.color
});
symbolSVG.attr({
fill: series.visible ? series.color : options.itemHiddenStyle.color
});
}
// Create the legend box
var $legend = $('<div>')
.css({
width: 110,
maxHeight: 210,
padding: 10,
position: 'absolute',
overflow: 'auto',
right: 10,
top: 40,
borderColor: options.borderColor,
borderWidth: options.borderWidth,
borderStyle: 'solid',
borderRadius: options.borderRadius
})
.appendTo(chart.container);
$.each(chart.series, function(i, series) {
// crete the legend item
var $legendItem = $('<div>')
.css({
position: 'relative',
marginLeft: 20
})
.css(options[series.visible ? 'itemStyle' : 'itemHiddenStyle'])
.html(series.name)
.appendTo($legend);
// create the line with each series color
var symbolEl = $('<div>');
var renderer = new Highcharts.Renderer(
symbolEl[0],
16,
10
);
var symbolLine = renderer
.path([
'M',
0,
5,
'L',
16,
5
])
.attr({
'stroke-width': 2,
stroke: series.color
})
.add();
var symbolSVG = renderer.symbol(
series.symbol,
3,
0,
10,
10,
Highcharts.merge(series.options.marker, {
width: 10,
height: 10
})
).attr({
fill: series.color
})
.add();
symbolEl.css({
position: 'absolute',
left: -20,
top: 2
}).appendTo($legendItem);
// click handler
$legendItem.hover(function() {
chart.series.forEach(function(s) {
if (s !== series) {
s.setState('inactive');
} else {
series.setState('hover');
}
});
}, function() {
chart.series.forEach(function(s) {
s.setState('normal');
});
});
$legendItem.click(function() {
clickItem(series, $legendItem, symbolLine, symbolSVG);
});
});
});
Live demo: http://jsfiddle.net/BlackLabel/mfvh9ubq/
API Reference:
https://api.highcharts.com/class-reference/Highcharts.Series#setState
https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#symbol
EDIT: I hate googling for answers and finding some question that never got solved from 10 years ago so I am answering my own question for those that might want to know. In my case, I simply disabled the bounces prop for the scrollview. Since FlatList extends React's ScrollView, setting bounces to false in the animated FlatList component that I created stopped it from bouncing and solved my problem. Have a nice day.
hope you're having a great day. I am trying to animate my header dynamically but for some reason whenever I scroll beyond the beginning or the end of the scrollview, the bounce effect messes with the Animation.(as shown in the gif below)
GIF
Same GIF but higher resolution
As you can see, when I scroll to the top and enable the bounce animation, the header thinks that i am scrolling down as the bounce returns the first element in the list back to the top. How do I fix this? I saw on the web somewhere that adding an interpolator to the animated value would help, though I don't really understand.
Below is my code. Thank You
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
const tempArray = [
...(an array of my data)
]
export default class TempScreen extends React.Component {
static navigationOptions = {
header: null
}
constructor(props) {
super(props)
this.state = {
animatedHeaderValue: new Animated.Value(0),
}
}
render() {
const animatedHeaderHeight = Animated.diffClamp(this.state.animatedHeaderValue, 0, 60)
.interpolate({
inputRange: [0, 70],
outputRange: [70, 0],
})
return ( <
View >
<
Animated.View style = {
{
backgroundColor: 'white',
borderBottomColor: '#DEDEDE',
borderBottomWidth: 1,
padding: 15,
width: Dimensions.get('window').width,
height: animatedHeaderHeight,
}
} >
<
/Animated.View> <
AnimatedFlatList scrollEventThrottle = {
16
}
onScroll = {
Animated.event(
[{
nativeEvent: {
contentOffset: {
y: this.state.animatedHeaderValue
}
}
}]
)
}
data = {
tempArray
}
renderItem = {
({
item
}) =>
<
View style = {
{
flex: 1
}
} >
<
Text style = {
{
fontWeight: 'bold',
fontSize: 30
}
} > {
item.name
} < /Text> <
Text > {
item.year
} < /Text> <
/View>
}
/>
<
/View>
)
}
}
If you want to solve the "bounce" problem only, the problem is that iOS gives to diffClamp negative scrollY values. You need to filter these and ensure scrollY remains >= 0 to avoid diffClamp being affected by overscroll.
const clampedScrollY = scrollY.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
Another nice trick is to use a "cliff" technique, so that the header only disappear after a minimum scrollY position.
Here is code from my app:
const minScroll = 100;
const clampedScrollY = scrollY.interpolate({
inputRange: [minScroll, minScroll + 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
const minusScrollY = Animated.multiply(clampedScrollY, -1);
const translateY = Animated.diffClamp(
minusScrollY,
-AnimatedHeaderHeight,
0,
);
const opacity = translateY.interpolate({
inputRange: [-AnimatedHeaderHeight, 0],
outputRange: [0.4, 1],
extrapolate: 'clamp',
});
clampedScrollY will be:
0 when scrollY=0
0 when scrollY=50
0 when scrollY=100
30 when scrollY=130
170 when scrollY=270
You get the idea. So diffClamp will only be > 0 if scrollY > 100, and increment 1 by 1 after that threshold.
I had the same problem like two hour ago...
You can set Scrollview property bounces=false but if you want a RefreshControl for refreshing the ScrollView content (like in my case), the bounce property has to stay active.
I fixed this following this cool article: https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a.
I'm not an expert of the Animated library, so I post my code:
constructor(props) {
const scrollAnim = new Animated.Value(0);
const offsetAnim = new Animated.Value(0);
this.state = {
scrollAnim,
offsetAnim,
AnimatedViewHeight: 1,
clampedScroll: Animated.diffClamp(
Animated.add(
scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
offsetAnim
),0, 1
)
}
}
render() {
const minScroll = this.state.AnimatedViewHeight;
const navbarTranslate = this.state.clampedScroll.interpolate({
inputRange: [0, minScroll],
outputRange: [0, -minScroll],
extrapolate: 'clamp',
});
return (
<View style={{
flex: 1
}}>
<Animated.View
onLayout={(event) => {
var { height } = event.nativeEvent.layout;
this.setState({
AnimatedViewHeight: height,
clampedScroll: Animated.diffClamp(
Animated.add(
this.state.scrollAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
}),
this.state.offsetAnim
), 0, height)
})
}}
style={[{ transform: [{ translateY: navbarTranslate }] }]}>
<View><Text>THIS IS YOUR HEADER</Text></View>
</Animated.View>
<AnimatedFlatList
// iOS offset for RefreshControl
contentInset={{
top: this.state.AnimatedViewHeight,
}}
contentOffset={{
y: -this.state.AnimatedViewHeight,
}}
scrollEventThrottle={1}
onScroll={
Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scrollAnim } } }],
{ useNativeDriver: true },
)}
data={this.state.data}
keyExtractor={(item, idx) => idx}
ListFooterComponent={this.renderFooter}
renderItem={this.renderItem}
onEndReached={this.handleLoadMore}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
// Android offset for RefreshControl
progressViewOffset={this.state.AnimatedViewHeight}
/>
}
onEndReachedThreshold={0.5} />
</View>
)
}
this.state.AnimatedViewHeight is the height of the header, retrieved by calling onLayout function. Inside of this function I also set a new clampedScroll because I have a new height (In my case, the header doesn't have a fixed size).
After that, in render(), define a variable (navbarTranslate) for control the headerSize based on the scroll position of your Animated Scrollview.
This is an implementation that works for bounces in both directions (at the start and at the end of the list).
const minScroll = 100;
const headerHeight = 65;
const activeRange = 200;
const yOffset = useRef(new Animated.Value(0)).current;
const diffClamp = Animated.diffClamp(
yOffset,
-minScroll,
activeRange + minScroll
);
const translateY = diffClamp.interpolate({
inputRange: [0, activeRange],
outputRange: [0, -headerHeight],
extrapolate: "clamp",
});
The yOffset is passed to the onScroll prop of the ScrollView or Flatlist, e.g.,
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {
y: yOffset,
},
},
},
],
{ useNativeDriver: false }
)}
scrollEventThrottle={10}
bounces={true}
It works as follows:
yOffset is the scrolled distance within the list and can take any value depending on the length of the list.
diffClamp maps the yOffset to a value between -minScroll and activeRange + minScroll. When yOffset is outside of this range, diffclamp essentially shifts this range to the difference outside the range so that it is always active when you change the scroll direction.
translateY is the value with which you want the header to move. The interpolation of translateY only accepts inputs in the range: 0 to activeRange, and thus does not respond to unless the scrolled distance is larger than minScroll (in both directions). The output equals -headerHeight to move the header up, and this output is clamped to keep the header hidden as the scrolling continues.
Note that by changing the activeRange, you can change the speed at which the header moves.
I resolved using this answer https://stackoverflow.com/a/51638296/3639398
import React from 'react';
import {
Animated,
Text,
View,
StyleSheet,
ScrollView,
Dimensions,
RefreshControl,
} from 'react-native';
import Constants from 'expo-constants';
import randomColor from 'randomcolor';
const HEADER_HEIGHT = 44 + Constants.statusBarHeight;
const BOX_SIZE = Dimensions.get('window').width / 2 - 12;
const wait = (timeout: number) => {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
};
function App() {
const [refreshing, setRefreshing] = React.useState(false);
const scrollAnim = new Animated.Value(0);
const minScroll = 100;
const clampedScrollY = scrollAnim.interpolate({
inputRange: [minScroll, minScroll + 1],
outputRange: [0, 1],
extrapolateLeft: 'clamp',
});
const minusScrollY = Animated.multiply(clampedScrollY, -1);
const translateY = Animated.diffClamp(minusScrollY, -HEADER_HEIGHT, 0);
const onRefresh = React.useCallback(() => {
setRefreshing(true);
wait(2000).then(() => {
setRefreshing(false);
});
}, []);
return (
<View style={styles.container}>
<Animated.ScrollView
contentContainerStyle={styles.gallery}
scrollEventThrottle={1}
bounces={true}
showsVerticalScrollIndicator={false}
style={{
zIndex: 0,
height: '100%',
elevation: -1,
}}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollAnim } } }],
{ useNativeDriver: true }
)}
overScrollMode="never"
contentInset={{ top: HEADER_HEIGHT }}
contentOffset={{ y: -HEADER_HEIGHT }}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}>
{Array.from({ length: 20 }, (_, i) => i).map((uri) => (
<View style={[styles.box, { backgroundColor: 'grey' }]} />
))}
</Animated.ScrollView>
<Animated.View style={[styles.header, { transform: [{ translateY }] }]}>
<Text style={styles.title}>Header</Text>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
gallery: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: 4,
},
box: {
height: BOX_SIZE,
width: BOX_SIZE,
margin: 4,
},
header: {
flex: 1,
height: HEADER_HEIGHT,
paddingTop: Constants.statusBarHeight,
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: randomColor(),
},
title: {
fontSize: 16,
},
});
export default App;
checkout on Expo https://snack.expo.io/#raksa/auto-hiding-header
I have a treemap made of
a big container
with an element
with an element
with an element which is itself made of
an element
an element
another big container
Highcharts.chart('container', {
series: [{
type: "treemap",
animation: false,
data: [
// first big category
{
id: 'B',
name: 'B'
},
{
name: 'Anne',
parent: 'B',
value: 4
}, {
name: 'Peter',
parent: 'B',
value: 1
},
// below is a member of forst big category, but with sub-categories
{
name: 'aaa container',
parent: 'B',
id: 'aaa'
}, {
name: 'Anneinaaa',
parent: 'aaa',
value: 1
}, {
name: 'Rickinaaa',
parent: 'aaa',
value: 3
},
// second big category
{
name: 'Susanne',
parent: 'Kiwi',
value: 2,
color: '#9EDE00'
}
]
}],
});
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/treemap.js"></script>
<div id="container"></div>
I would like to highlight the fact that there is a container for Rickinaaa and Anneinaaa, with a description.
Right now Rickinaaa and Anneinaaa are at the same level as Anne and Peter, despite the fact that it is their container (the one I do not know how to show) which should be at their level.
An example from ZingCharts which shows this multilayer presentation:
It is much easier to see the box-in-a-box structure.
Is there a way to achieve this in Highcharts? Specifically, how could I have a visible container labelled aaa container which would encompass the rectangles Rickinaaa and Anneinaaa
It can be done by using custom positioning algorithm for treemap. The process is described here: https://www.highcharts.com/docs/chart-and-series-types/treemap (ADD YOUR OWN ALGORITHM section).
I created simplified algorithm for demo purposes (some values are hardcoded):
// Custom positioning algorithm
function myFunction(parent, children) {
childrenAreas = [];
Highcharts.each(children, function(child) {
// Do some calculations
var newLeaf = null;
if (child.id === 'A') {
newLeaf = {
x: 0,
y: 0,
width: 50,
height: 50
};
} else if (child.id === 'B') {
newLeaf = {
x: 50,
y: 0,
width: 50,
height: 50
};
} else if (child.name === 'Rick') {
newLeaf = {
x: parent.x + 10,
y: parent.y + 10,
width: parent.width - 20,
height: (parent.height - 20) / 2
}
} else if (child.name === 'Peter') {
newLeaf = {
x: parent.x + 10,
y: parent.y + 30,
width: parent.width - 20,
height: (parent.height - 20) / 2
}
} else if (child.name === 'Anne') {
newLeaf = {
x: parent.x + 10,
y: 10,
width: parent.width - 20,
height: parent.height - 20
}
}
if (newLeaf) {
childrenAreas.push(newLeaf);
}
});
return childrenAreas;
};
Highcharts.seriesTypes.treemap.prototype.myCustomAlgorithm = myFunction;
By default Highcharts assigns color fill attribute only for the nodes of the Highest level - all other nodes have it set to none. It can be changed manually via CSS:
.highcharts-internal-node {
fill: #bada55
}
I also did a small change in the core code so that parent levels are drawn underneath the child ones:
(function(H) {
var grep = H.grep,
each = H.each,
seriesTypes = H.seriesTypes;
H.seriesTypes.treemap.prototype.drawPoints = function() {
var series = this,
points = grep(series.points, function(n) {
return n.node.visible;
});
each(points, function(point) {
var groupKey = 'level-group-' + point.node.levelDynamic;
if (!series[groupKey]) {
series[groupKey] = series.chart.renderer.g(groupKey)
.attr({
// CHANGED FROM: zIndex: 1000 - point.node.levelDynamic
zIndex: 1000 + point.node.levelDynamic // #todo Set the zIndex based upon the number of levels, instead of using 1000
})
.add(series.group);
}
point.group = series[groupKey];
});
// Call standard drawPoints
seriesTypes.column.prototype.drawPoints.call(this);
// If drillToNode is allowed, set a point cursor on clickables & add drillId to point
if (series.options.allowDrillToNode) {
each(points, function(point) {
if (point.graphic) {
point.drillId = series.options.interactByLeaf ? series.drillToByLeaf(point) : series.drillToByGroup(point);
}
});
}
}
})(Highcharts);
Live demo: https://jsfiddle.net/BlackLabel/wfed4jeo/
I just check a post regarding making a label with design using renderer.label and plotline animate (Add design to plotLabel Highcharts). My question is, Is there a way to use chart.renderer.path as the moving horizontal gridline instead of using the common plotline ? I am a bit confuse on how to use the renderer.path. Can you help me with it? Really appreciate your help with this.
const plotband = yAxis.addPlotLine({
value: 66,
color: 'red',
width: 2,
id: 'plot-line-1',
/* label: {
text: 66,
align: 'right',
y: newY,
x: 0
}*/
});
const labelOffset = 15
const plotbandLabel = this.renderer.label((66).toFixed(2), chart.plotLeft + chart.plotWidth - 8, yAxis.toPixels(66) - labelOffset, 'rect').css({
color: '#FFFFFF'
}).attr({
align: 'right',
zIndex: 99,
fill: 'rgba(0, 0, 0, 0.75)',
padding: 8
})
.add()
setInterval(function() {
var x = (new Date()).getTime(), // current time
y = Math.round(Math.random() * 100);
series.addPoint([x, y], true, true);
plotLine = yAxis.plotLinesAndBands[0].svgElem;
d = plotLine.d.split(' ');
newY = yAxis.toPixels(y) - d[2];
plotlabel = yAxis.plotLinesAndBands[0].label;
plotbandLabel.animate({
y: yAxis.toPixels(y) - labelOffset
}, {
duration: 400,
step: function() {
this.attr({
text: yAxis.toValue(this.y + labelOffset).toFixed(2)
})
},
complete: function() {
this.attr({
text: y.toFixed(2)
})
}
}),
plotLine.animate({
translateY: newY
}, 400);
Please see this jsfiddle I got from the previous post. http://jsfiddle.net/x8vhp0gr/
Thank you so much
I modified demo provided by you. Now, instead of adding a plot line, path is created.
pathLine = this.renderer.path([
'M',
chart.plotLeft,
initialValue,
'L',
chart.plotWidth + chart.plotLeft,
initialValue
])
.attr({
'stroke-width': 2,
stroke: 'red'
})
.add(svgGroup);
API Reference:
http://api.highcharts.com/highcharts/Renderer.path
Example:
http://jsfiddle.net/a64e5qkq/