ReactNative: How to measure height of Text Input after programatic change - react-native

React Native has documentation for AutoExpandingTextInput: https://facebook.github.io/react-native/docs/textinput.html
The Problem: When the content of the AutoExpandingTextInput is changed programmatically the height never changes.
For example:
componentWillReceiveProps(props) {
this.setState({
richText: this._addHighlights(props.richText)
});
}
//
<AutoExpandingTextInput ref={component => this._text = component}>
{this.state.richText}
</AutoExpandingTextInput>
Say, for example. the user hits a button that adds a link to the text that wraps to the next line; in this case, the AutoExpandingTextInput never expands, because the height only is measured & changed on the onChange event of the TextInput.
I need some work around to get the content height when no onChange is triggered --- or less ideally, a way to programmatically trigger an onChange to the TextInput.
Are there any solutions????

No need to use the AutoExpandingTextInput plugin any more. The functionality you need is supported (sort of) in react-native now and will resize with a programatic update. Try something like this:
_heightChange(event) {
let height = event.nativeEvent.contentSize.height;
if (height < _minHeight) {
height = _minHeight;
} else if (height > _maxHeight) {
height = _maxHeight;
}
if (height !== this.state.height) {
this.setState({height: height});
}
}
render() {
return (
<TextInput
{...this.props}
multiline={true}
onContentSizeChange={this._heightChange.bind(this)}
/>
)
}

Related

How to disable animations when the user scrolls (react-native-reanimated 2)

Here's a weekly mini calendar, that turns into a monthly mini calendar component.
When it turns from weekly to monthly we have some entering/exiting animations
So far so good.
Problem:
The problem is, that those animations (being entering/exiting animations) also take place while the user is scrolling.
As you can see in the gif, animations play when I scroll horizontally, which isn't what I want, I only want animations when the component changes from weekly to monthly (expands/collapses)
Code:
import Animated, {
FadeInDown,
FadeInUp,
SlideOutUp,
SlideOutDown,
} from 'react-native-reanimated';
const MiniCalendarItem = () => {
let animationEnter;
let animationExit;
if (this.props.itemRepresents === ITEM_REPRESENTS.MONTH) {
if (this.dayIsPartOfCurrentWeek(day)) {
animationEnter = FadeInUp;
} else {
animationEnter = FadeInUp.delay((weekIndex * 150)).duration(350)
}
animationExit = SlideOutDown.duration(400);
} else {
animationEnter = FadeInDown.duration(500);
animationExit = SlideOutUp.duration(400);
}
return (
<Animated.View
entering={animationEnter}
exiting={animationExit}
key={`dayData_${dayProps.id}`}
>
{...}
</Animated.View>
);
};
and here's the parent:
renderItem = () => {
return (
<MiniCalendarItem
animationsEnabled
key={itemKey}
mode={mode}
itemRepresents={visible ? ITEM_REPRESENTS.MONTH : ITEM_REPRESENTS.WEEK}
/>
)
}
}
Essentially the parent is a ScrollView (not a FlatList)
Question:
How can I stop react-native-reanimated#2 from playing any animations and when is it a good time to do that.
I added a animationsEnabled prop, but ideally I'd love to feed it with an Animated.Value(true) object. I'm just not sure how to conditionally disable animations based on that prop, from within the MiniCalendarItem.

Flatlist visible position

I'm trying to render an curved vertical list like this iOS component: https://github.com/makotokw/CocoaWZYCircularTableView
That component (written in Obj-c) iterates the visible cells when laying them out, and sets the frame (i.e. indent) using asin.
I know in React Native I can set the leftMargin style in the renderItem callback, but I can't figure out how to get the on-screen index of the item - all I have is the index into the source data. And also, at that point, I don't think I have access to the absolute position.
Any ideas?
The function you are looking for is
onViewableItemsChanged.
You can use it with viewabilityConfig which provides us with
minimumViewTime,viewAreaCoveragePercentThreshold,waitForInteraction
which can be set accordingly
const VIEWABILITY_CONFIG = {
minimumViewTime: 3000,
viewAreaCoveragePercentThreshold: 100,
waitForInteraction: true,
};
_onViewableItemsChanged = (info: {
changed: Array<{
key: string,
isViewable: boolean,
item: any,
index: ?number,
section?: any,
}>
}
){
//here you can have the index which is visible to you
}
<FlatList
renderItem={this.renderItem}
data={this.state.data}
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
Thanks for both answers.
What I have ended up doing is deriving the visible items using the scroll offset of the list. This is simple because the list items all have the same height.
I do this in the onScroll handler, and at that point I calculate the horizontal offset for each item (and I use leftMargin / rightMargin to render this). It's not perfect, but it does give me an elliptical list.
_handleScroll = (event) => {
const topItemIndex = Math.floor(event.nativeEvent.contentOffset.y / LIST_ITEM_HEIGHT);
const topItemSpare = LIST_ITEM_HEIGHT-(event.nativeEvent.contentOffset.y % LIST_ITEM_HEIGHT);
const positionFromEllipseTop = (forIndex-topItemIndex)*LIST_ITEM_HEIGHT+topItemSpare;
const positionFromOrigin = Math.floor(Math.abs(yRadius - positionFromEllipseTop));
const angle = Math.asin(positionFromOrigin / yRadius);
if (orientation === 'Left') {
marginLeft = 0;
marginRight = ((xRadius * Math.cos(angle)))-LIST_ITEM_HEIGHT;
alignSelf = 'flex-end';
}
else if (orientation === 'Right') {
marginLeft = (xRadius * Math.cos(angle))-LIST_ITEM_HEIGHT;
marginRight = 0;
alignSelf = 'flex-start';
}
}
React-native's FlatList component has a prop called onLayout. You can get the position of the component on screen with this prop.
onLayout
Invoked on mount and layout changes with:
{nativeEvent: { layout: {x, y, width, height}}}
This event is fired immediately once the layout has been calculated,
but the new layout may not yet be reflected on the screen at the time
the event is received, especially if a layout animation is in
progress.

React DnD change div style only when dragging

I am implementing the drag and drop mechanic using react-dnd library, but I find it hard to style my drop targets. I want to show the user which drop target is available to drop on, but using the isOver and canDrop will only style the item that is currently being hovered on.
If I use the !isOver value, all the divs are being styled, without even dragging any of the elements.
How can I style the drop targets only when the dragging of an element happens?
This is my code so far, for a #DropTarget:
import React from 'react';
import {DropTarget} from 'react-dnd';
import {ItemTypes} from './Constants';
const target = {
drop(props, monitor, component){
// console.log("Dropped on", props.id);
},
canDrop(props, monitor, component){
var cardColumn = monitor.getItem().column;
var targetColumn = props.column;
return false; // still testing styling when only an element is being dragged on the page
}
};
#DropTarget(ItemTypes.CARD, target, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver({shallow: true}),
canDrop: monitor.canDrop(),
}))
class CardList extends React.Component{
constructor(props){
super(props);
this.addClass = this.addClass.bind(this);
}
addClass(){
const {isOver, canDrop} = this.props;
if(isOver && canDrop){
return "willDrop"; // green background for .card-list
}
if(isOver && !canDrop){
return "noDrop"; // red background for .card-list
}
if(!isOver && !canDrop){
return ""; // will style all the backgrounds in a color, but not when dragging
}
}
render(){
const {connectDropTarget} = this.props;
return connectDropTarget(
<div class={"card-list col-xl-12 col-lg-12 col-md-12 col-sm-12 col-xs-12 " + this.addClass()} id={this.props.id}>
{this.props.children}
</div>
);
}
}
export default CardList;
Is there a way to get the isDragging value when an element is being dragged on the page, since this is the only possibility to obtain what I want.
Thanks!
Both isOver and canDrop implicitly do the isDragging check, per http://react-dnd.github.io/react-dnd/docs-drop-target-monitor.html - note that they only return true if a drag operation is in progress. Therefore, if you want to style drop targets such that only when something that can be dragged is being dragged, then I think you need another case in your addClass() function to handle that, like this:
addClass(){
const {isOver, canDrop} = this.props;
if(isOver && canDrop){
return "willDrop"; // green background for .card-list
}
if(isOver && !canDrop){
return "noDrop"; // red background for .card-list
}
if(!isOver && canDrop){
return ""; // THIS BLOCK WILL EXECUTE IF SOMETHING IS BEING DRAGGED THAT *COULD* BE DROPPED HERE
}
}
And I don't think you want the !isOver && !canDrop block - this will execute even when nothing is being dragged at all.

How to render absoulute element inside ListView or FlatList?

Here's a fun one i've been poking at for while:
I have a FlatList (same issue with ListView) and I want to render an element INSIDE the internal scrolling container with the following characteristics:
Absolutely Positioned (thus having no effect on position of list elements)
Position XX distance from top (translateY or top)
zIndex (above list elements)
The use case is i'm rendering a day view calendar grid with a horizontal bar at the current time position fixed at X distance from the beginning of the internal scrollview so it appears as the user scrolls pass that position.
So far i've tried wrapping wrapping FlatList/ListView with another ScrollView... also tried rendering this element as the header element which only works while the header/footer are visible (trashed when out of view).
Any and all ideas welcomed. :)
Thanks
Screenshot Below (red bar is what i'm trying to render):
Here's a working demo of what it sounds like you're trying to achieve: https://sketch.expo.io/BkreW1che. You can click "preview" to see it in your browser.
And here's the main code you need to measure the height of the ListView and place the indicator on top of it (visit the link above to see the full source):
handleLayout(event) {
const { y, height } = event.nativeEvent.layout;
// Now we know how tall the ListView is; let's put the indicator in the middle.
this.setState({ indicatorOffset: y + (height / 2) });
}
renderIndicator() {
const { indicatorOffset } = this.state;
// Once we know how tall the ListView is, put the indicator on top.
return indicatorOffset ? (
<View style={[{ position: 'absolute', left: 0, right: 0, top: indicatorOffset }]} />
) : null;
}
render() {
return (
<View style={styles.container}>
<ListView
onLayout={(event) => this.handleLayout(event)}
dataSource={this.state.dataSource}
renderRow={this.renderRow}
/>
{this.renderIndicator()}
</View>
);
}
Edit: I now understand that you want the indicator to scroll along with the list. That's a simple change from above, just add an onScroll listener to the ListView: https://sketch.expo.io/HkEjDy92e
handleScroll(event) {
const { y } = event.nativeEvent.contentOffset;
// Keep the indicator at the same position in the list using this offset.
this.setState({ scrollOffset: y });
},
With this change, the indicator actually seems to lag behind a bit because of the delay in the onScroll callback.
If you want better performance, you might consider rendering the indicator as part of your renderRow method instead. For example, if you know the indicator should appear at 10:30 am, then you would render it right in the middle of your 10am row.

React Native Retrieve Actual Image Sizes

I would like to be able to know the actual size of a network-loaded image that has been passed into <Image /> I have tried using onLayout to work out the size (as taken from here https://github.com/facebook/react-native/issues/858) but that seems to return the sanitised size after it's already been pushed through the layout engine.
I tried looking into onLoadStart, onLoad, onLoadEnd, onProgress to see if there was any other information available but cannot seem to get any of these to fire. I have declared them as follows:
onImageLoadStart: function(e){
console.log("onImageLoadStart");
},
onImageLoad: function(e){
console.log("onImageLoad");
},
onImageLoadEnd: function(e){
console.log("onImageLoadEnd");
},
onImageProgress: function(e){
console.log("onImageProgress");
},
onImageError: function(e){
console.log("onImageError");
},
render: function (e) {
return (
<Image
source={{uri: "http://adomain.com/myimageurl.jpg"}}
style={[this.props.style, this.state.style]}
onLayout={this.onImageLayout}
onLoadStart={(e) => {this.onImageLoadStart(e)}}
onLoad={(e) => {this.onImageLoad(e)}}
onLoadEnd={(e) => {this.onImageLoadEnd(e)}}
onProgress={(e) => {this.onImageProgress(e)}}
onError={(e) => {this.onImageError(e)}} />
);
}
Thanks.
Image component now provides a static method to get the size of the image. For example:
Image.getSize(myUri, (width, height) => {this.setState({width, height})});
You can use resolveAssetSource method from the Image component :
import picture from 'pathToYourPicture';
const {width, height} = Image.resolveAssetSource(picture);
This answer is now out of date. See Bill's answer.
Image.getSize(myUri, (width, height) => { this.setState({ width, height }) });
Old Answer (valid for older builds of react native)
Ok, I got it working. Currently this takes some modification of the React-Native installation as it's not natively supported.
I followed the tips in this thread to enabled me to do this.
https://github.com/facebook/react-native/issues/494
Mainly, alter the RCTNetworkImageView.m file: add the following into setImageURL
void (^loadImageEndHandler)(UIImage *image) = ^(UIImage *image) {
NSDictionary *event = #{
#"target": self.reactTag,
#"size": #{
#"height": #(image.size.height),
#"width": #(image.size.width)
}
};
[_eventDispatcher sendInputEventWithName:#"loaded" body:event];
};
Then edit the line that handles the load completion:
[self.layer removeAnimationForKey:#"contents"];
self.layer.contentsScale = image.scale;
self.layer.contents = (__bridge id)image.CGImage;
loadEndHandler();
replace
loadEndHandler();
with
loadImageEndHandler(image);
Then in React-Native you have access to the size via the native events. data from the onLoaded function - note the documentation currently says the function is onLoad but this is incorrect. The correct functions are as follows for v0.8.0:
onLoadStart
onLoadProgress
onLoaded
onLoadError
onLoadAbort
These can be accessed like so:
onImageLoaded: function(data){
try{
console.log("image width:"+data.nativeEvents.size.width);
console.log("image height:"+data.nativeEvents.size.height);
}catch(e){
//error
}
},
...
render: function(){
return (
<View style={{width:1,height:1,overflow='hidden'}}>
<Image source={{uri: yourImageURL}} resizeMode='contain' onLoaded={this.onImageLoaded} style={{width:5000,height:5000}} />
</View>
);
}
Points to note:
I have set a large image window and set it inside a wrapping element of 1x1px this is because the image must fit inside if you are to retrieve meaningful values.
The resize mode must be 'contain' to enable you to get the correct sizes, otherwise the constrained size will be reported.
The image sizes are scaled proportionately to the scale factor of the device, e.g. a 200*200 image on an iPhone6 (not 6 plus) will be reported as 100*100. I assume that this also means it will be reported as 67*67 on an iPhone6 plus but I have not tested this.
I have not yet got this to work for GIF files which traverse a different path on the Obj-C side of the bridge. I will update this answer once I have done that.
I believe there is a PR going through for this at the moment but until it is included in the core then this change will have to be made to the react-native installation every time you update/re-install.
TypeScript example:
import {Image} from 'react-native';
export interface ISize {
width: number;
height: number;
}
function getImageSize(uri: string): Promise<ISize> {
const success = (resolve: (value?: ISize | PromiseLike<ISize>) => void) => (width: number, height: number) => {
resolve({
width,
height
});
};
const error = (reject: (reason?: any) => void) => (failure: Error) => {
reject(failure);
};
return new Promise<ISize>((resolve, reject) => {
Image.getSize(uri, success(resolve), error(reject));
});
}