Set a NativeScript Label line height on iOS - nativescript

I have a label with a large font size, and the default line-height is quite loose for my taste. I'd like to reduce it to less than the default.
Providing a line-height value larger than the font-size does increase the line spacing, but a smaller value (or negative value) does not reduce it to be smaller than the default on iOS.
From a GitHub issue, I got this snippet that I updated to work with the latest NS;
import { Label } from "#nativescript/core/ui/label";
export function setIOSLineHeight(label: Label, lineHeight: number){
const iosLabel = label.nativeView;
let attributedString;
if (iosLabel.attributedText) {
attributedString = iosLabel.attributedText;
} else {
attributedString = NSMutableAttributedString.alloc().initWithString(iosLabel.text);
}
let range = new NSRange({ location: 0, length: iosLabel.text.length });
const paragraphStyle = NSMutableParagraphStyle.alloc().init();
paragraphStyle.lineSpacing = 0;
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
attributedString.addAttributeValueRange(NSParagraphStyleAttributeName, paragraphStyle, range);
iosLabel.attributedText = attributedString;
}
However, calling this method in the mounted() lifecycle method does not have any effect for any value of lineHeight - even ones that do have an effect via the CSS property:
<template>
<Page ref="page">
<Label ref="topLine" text="Hello this is a text that flows onto multiple lines" textWrap="true" />
</Page>
</template>
<script>
import { isIOS } from 'tns-core-modules/platform';
import { setIOSLineHeight } from '../label-helper.ts';
export default {
mounted() {
if (isIOS) {
/* Has no effect, regardless of value */
setIOSLineHeight(this.$refs.topLine, 40);
}
}
}
</script>
<style lang="scss" scoped>
Label {
font-size: 60;
/* Does work */
line-height: 100;
/* Does not work */
line-height: 40;
}
</style>
How can I reduce the line height of my Label to a value smaller than the font size?

In NativeScript I have used the following code to handle both IOS and Android for Line Spacing
function labelLineHeight(nsLabel) {
if(isIOS){
var label= nsLabel.ios;
var attributedString;
if(label.atributedText){
attributedString = label.atributedText;
}
else{
attributedString=NSMutableAttributedString.alloc().initWithString(label.text);
}
var paragraphStyle = NSMutableParagraphStyle.alloc().init();
paragraphStyle.lineSpacing = 5;
var range= NSMakeRange(0, label.text.length);
attributedString.addAttributeValueRange(NSParagraphStyleAttributeName, paragraphStyle, range);
label.attributedText = attributedString;
}
if(isAndroid){
var label = nsLabel.android;
//Default spacing is 20% of text size
//setLineSpacing(add,multiplyby);
label.setLineSpacing(12, 1);
}
}
Also follow this thread for more inputs on the line-spacing. You can see the pull request too for reference.

Related

Nativescript-Vue Issue with Panning Elements around

So I am building a photo group creator in Nativescript-vue. I have the pan working and the view is rendering correctly. But my issue is when a user starts to pan the image outside the container it's in, the image goes 'behind' the container. And I need it to render ontop of everything. I have tried 'z-index' with css and still no go.
The idea is, there is a group of photos (un-grouped group). And the user will be able to click-and-drag a photo in the group down to an 'empty' group. And this in turn would create a new group. Or if there were other groups the user would be able to drag an image and add it to an existing group.
Any help or suggestions, thank you in advance!
This is my Vue Component
<template>
<StackLayout orientation="vertical" ref="mainContainer">
</StackLayout>
</template>
<script>
import * as StackLayout from 'tns-core-modules/ui/layouts/stack-layout';
import * as Image from 'tns-core-modules/ui/image';
import _ from 'underscore';
export default {
name: "photo-groupings",
props:{
photoList:{
type: Object,
required: true
}
},
mounted(){
let ungrouped = this.createGroupElement();
let newGroupElement = this.createGroupElement();
console.log(_.keys(this.photoList));
for(let pht of _.keys(this.photoList)){
console.log(pht);
console.log(this.photoList[pht]);
ungrouped.addChild(this.createImageChild(this.photoList[pht]))
}
this.$refs.mainContainer.nativeView.addChild(ungrouped);
this.$refs.mainContainer.nativeView.addChild(newGroupElement)
},
data(){
return {
photoGroups:{},
groupElements:{},
prevDeltaX: 0,
prevDeltaY: 0
}
},
methods:{
createImageChild(src){
let tempImg = new Image.Image();
tempImg.src = src;
tempImg.width = 100;
tempImg.height = 100;
tempImg.stretch = "aspectFill";
tempImg.borderRadius = 10;
tempImg.borderWidth = 2;
tempImg.borderColor = "forestgreen";
tempImg.margin = 5;
tempImg.on('pan', this.handlePan);
return tempImg;
},
createGroupElement(){
let tempGroup = new StackLayout.StackLayout();
tempGroup.orientation = "horizontal";
tempGroup.margin = 5;
tempGroup.borderColor = "black";
tempGroup.borderRadius = 5;
tempGroup.borderWidth = 1;
tempGroup.minHeight = 110;
return tempGroup;
},
handlePan(args){
if (args.state === 1) // down
{
this.prevDeltaX = 0;
this.prevDeltaY = 0;
console.log(args.view.getLocationRelativeTo(args.view.parent));
console.log(args.view.parent.getLocationInWindow());
}
else if (args.state === 2) // panning
{
args.view.translateX += args.deltaX - this.prevDeltaX;
args.view.translateY += args.deltaY - this.prevDeltaY;
this.prevDeltaX = args.deltaX;
this.prevDeltaY = args.deltaY;
}
else if (args.state === 3) // up
{
console.log("Pan release")
}
}
}
}
</script>
<style scoped>
</style>
Example Image of the images rendering 'behind'
The best way here would be creating a ghost element.
You should not move the original image but hide the original image when you detect drag, create a ghost of original image and insert it on the parent layout. When user drops the ghost image, destroy the ghost and move original image to destination.

How can I set the font-family of large titles on iOS?

By attaching an event listener to the loaded-event on Page and getting the NativeView-object I've been able to set the prefersLargeTitle to true:
loaded(event){
const page = event.object;
if (isIOS) {
page.frame.ios.controller.navigationBar.prefersLargeTitles = true;
}
}
This works, but I would like to change the font-family of the large title. How can I do this in Nativescript?
Try adding the code below to your app.js
import {
isIOS
} from "tns-core-modules/platform";
import {
ActionBar
} from "tns-core-modules/ui/action-bar";
import {
Font
} from "tns-core-modules/ui/styling/font";
if (isIOS) {
ActionBar.prototype.originalSetColor = ActionBar.prototype.setColor;
ActionBar.prototype.setColor = function (navBar, color) {
ActionBar.prototype.originalSetColor.call(this, navBar, color);
var newDict = {
[NSFontAttributeName]: Font
.default
.withFontFamily(
"yourFontFamily")
.withFontSize(yourFontSize)
.getUIFont(UIFont
.systemFontOfSize(20)),
};
if (navBar.largeTitleTextAttributes) {
newDict[NSForegroundColorAttributeName] = navBar.largeTitleTextAttributes.valueForKey(NSForegroundColorAttributeName);
}
navBar.largeTitleTextAttributes = newDict;
};
}
You may still set the prefersLargeTitles flags as you are doing already.

Making SVG Responsive in React

I am working on a responsive utility component, to make a few D3 components responsive in react. However I deep SVG knowledge escapes me. I have based my responsive utility on this issue on github. However it isn't quite working, All it does is render the a chart, but not at the width or height passed in but rather at a really small width and height. It also doesn't resize.
import React from 'react';
class Responsive extends React.Component{
constructor () {
super();
this.state = {
size: {
w: 0,
h: 0
}
}
}
componentDidMount () {
window.addEventListener('resize', this.fitToParentSize.bind(this));
this.fitToParentSize();
}
componentWillReceiveProps () {
this.fitToParentSize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.fitToParentSize.bind(this));
}
fitToParentSize () {
let elem = this.findDOMNode(this);
let w = elem.parentNode.offsetWidth;
let h = elem.parentNode.offsetHeight;
let currentSize = this.state.size;
if (w !== currentSize.w || h !== currentSize.h) {
this.setState({
size: {
w: w,
h: h
}
});
}
}
render () {
let {width, height} = this.props;
width = this.state.size.w || 100;
height = this.state.size.h || 100;
var Charts = React.cloneElement(this.props.children, { width, height});
return Charts;
}
};
export default Responsive;
Responsive width={400} height={500}>
<XYAxis data={data3Check}
xDataKey='x'
yDataKey='y'
grid={true}
gridLines={'solid'}>
<AreaChart dataKey='a'/>
<LineChart dataKey='l' pointColor="#ffc952" pointBorderColor='#34314c'/>
</XYAxis>
</Responsive>
disclaimer: I'm the author of vx a low-level react+d3 library full of visualization components.
You could use #vx/responsive or create your own higher-order component based on withParentSize() or withWindowSize() depending on what sizing you want to respond to (I've found most situations require withParentSize()).
The gist is you create a higher-order component that takes in your chart component and it attaches/removes event listeners for when the window resizes with a debounce time of 300ms by default (you can override this with a prop) and stores the dimensions in its state. The new parent dimensions will get passed in as props to your chart as parentWidth, parentHeight or screenWidth, screenHeight and you can set your svg's width and height attributes from there or calculate your chart dimensions based on those values.
Usage:
// MyChart.js
import { withParentSize } from '#vx/responsive';
function MyChart({ parentWidth, parentHeight }) {
return (
<svg width={parentWidth} height={parentHeight}>
{/* stuff */}
</svg>
);
}
export default withParentSize(MyChart);

Make a NativeScript ListView Transparent on iOS

I’m trying to get a NativeScript <ListView> to be transparent on iOS and I’m failing. I found an old thread on the topic at https://groups.google.com/forum/#!topic/nativescript/-MIWcQo-l6k, but when I try the solution it doesn’t work for me. Here’s my complete code:
/* app.css */
Page { background-color: black; }
<!-- main-page.xml -->
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="loaded">
<ListView id="list-view" items="{{ items }}" itemLoading="itemLoading">
<ListView.itemTemplate>
<Label text="{{ name }}" />
</ListView.itemTemplate>
</ListView>
</Page>
// main-page.js
var ios = require("utils/utils");
var Observable = require("data/observable").Observable;
var ObservableArray = require("data/observable-array").ObservableArray;
var page;
var items = new ObservableArray([]);
var pageData = new Observable();
exports.loaded = function(args) {
page = args.object;
page.bindingContext = pageData;
// Toss a few numbers in the list for testing
items.push({ name: "1" });
items.push({ name: "2" });
items.push({ name: "3" });
pageData.set("items", items);
};
exports.itemLoading = function(args) {
var cell = args.ios;
if (cell) {
// Use ios.getter for iOS 9/10 API compatibility
cell.backgroundColor = ios.getter(UIColor.clearColor);
}
}
Any help would be appreciated. Thanks!
Don't forget to set the listview to transparent, seems to have a backgroundcolor itself
ListView{
background-color: transparent;
}
Currently with NativeScript 2.4 the following works
var cell = args.ios;
if (cell) {
cell.selectionStyle = UITableViewCellSelectionStyleNone
}
And if you want to change the selection highlight color here is a simple approach, I have not tested performance but it works okay on an iPhone 6.
import { Color } from 'color';
cell.selectedBackgroundView = UIView.alloc().initWithFrame(CGRectMake(0, 0, 0, 0));
let blue = new Color('#3489db');
cell.selectedBackgroundView.backgroundColor = blue.ios
Not sure if there are better ways to do this, but this is what worked for me with NativeScript 2.4 on iOS to both A) make the ListView background transparent, and B) change the color when an item is tapped:
let lvItemLoading = (args) => {
let cell = args.ios;
if (cell) {
// Make the iOS listview background transparent
cell.backgroundColor = ios.getter(cell, UIColor.clearColor);
// Create new background view for selected state
let bgSelectedView = UIView.alloc().init();
bgSelectedView.backgroundColor = new Color("#777777").ios;
bgSelectedView.layer.masksToBounds = true;
cell.selectedBackgroundView = bgSelectedView;
}
};

Random changing backgrounds WITH fade effect

Aloha Stockoverflow.
In advance, thank you!
I am trying to modify my randomly changing background on my webpage, to add a FADE effect, so the change from 1 background to another is not so sudden and sharp.
I have tried to search through the web endlessly for a solution to my issue, but it all points towards adding a jQuery plugin which I would preferably avoid if it is possible.
My working code is as follows and needs to have added some kind of fadein / fadeout effect.
<script type="text/javascript">
var num;
var temp=0;
var speed=5000; /* this is set for 5 seconds, edit value to suit requirements */
var preloads=[];
/* add any number of images here */
preload(
'images/bg1.jpg',
'images/bg2.jpg',
'images/bg3.jpg',
'images/bg4.jpg',
'images/bg5.jpg'
);
function preload(){
for(var c=0;c<arguments.length;c++) {
preloads[preloads.length]=new Image();
preloads[preloads.length-1].src=arguments[c];
}
}
function rotateImages() {
num=Math.floor(Math.random()*preloads.length);
if(num==temp){
rotateImages();
}
else {
document.body.style.backgroundImage='url('+preloads[num].src+')';
temp=num;
setTimeout(function(){rotateImages()},speed);
}
}
if(window.addEventListener){
window.addEventListener('load',rotateImages,false);
}
else {
if(window.attachEvent){
window.attachEvent('onload',rotateImages);
}
}
</script>
Thank you very much for taking the time to look at it. :)
How to do it without plugins:
Use 2 layers for the background image, position them on top of each other.
Init the page with the first image on the bottom layer, make the top layer invisible (using CSS opacity property, make sure to Google this, different browsers use different approaches).
When fading:
Set the new image for the top layer.
Use a short, looping (frameduration < 40ms) setTimeout to increment the opacity of your top layer to 1. Use increments of 1/(speed/frameduration).
When comletely faded in, set the bottom layer to use the new (now visible) image, and set the top layer to opacity 0.
Like this:
<html>
<head>
<script type="text/javascript">
var num;
var current=0;
var speed=5000; /* this is set for 5 seconds, edit value to suit requirements */
var fps = 25;
var fadeDuration = 1000;
var opacityIncrement = 1/(fadeDuration/(1000/fps));
var preloads=[];
var topLayerOpacity = 0;
var topLayer = document.createElement("div");
var bottomLayer = document.createElement("div");
setOpacity(topLayer, 0);
/* add any number of images here */
preload(
'images/bg1.jpg',
'images/bg2.jpg',
'images/bg3.jpg',
'images/bg4.jpg'
);
function loadComplete(){
//add layers to background div
document.getElementById('backgroundContainer').appendChild(bottomLayer);
document.getElementById('backgroundContainer').appendChild(topLayer);
rotateImages();
}
function preload(){
//preload images
for(var c=0;c<arguments.length;c++) {
preloads[preloads.length]=new Image();
preloads[preloads.length-1].src=arguments[c];
}
}
// selecte new random image from preloads and start fade-in
function rotateImages() {
num=Math.floor(Math.random()*preloads.length);
//don't select current image
if(num==current){
rotateImages();
}
else {
topLayer.style.backgroundImage = 'url('+preloads[num].src+')';
current=num;
//start fade-in
fadeIn();
setTimeout(function(){rotateImages()},speed);
}
}
// fade in topLayer
function fadeIn(){
if (topLayerOpacity < 1){
topLayerOpacity += opacityIncrement;
setOpacity(topLayer, topLayerOpacity);// opacityIncrement);
setTimeout(fadeIn, 1000/fps);
}else{
fadeInComplete();
}
}
//return opacity for element
function getOpacity(el){
alert (el.style.opacity);
return el.style.opacity;
}
//sets opacity on element
function setOpacity(el, val){
el.style.opacity = val;
el.style.filter = 'alpha(opacity=' + val*100 + ')';
}
//called when fadeIn completed
function fadeInComplete(){
bottomLayer.style.backgroundImage = topLayer.style.backgroundImage;
topLayerOpacity = 0;
setOpacity(topLayer, topLayerOpacity);
}
if(window.addEventListener){
window.addEventListener('load',loadComplete,false);
}
else {
if(window.attachEvent){
window.attachEvent('onload',loadComplete);
}
}
</script>
<style type="text/css">
#backgroundContainer{
width:100%;
height:100%;
position:absolute;
/*background-color:green;*/
}
#backgroundContainer div{
width:100%;
height:100%;
position:absolute;
top:0;
}
.page {
width:100%;
text-align:center;
position:absolute;
}
.contents{
width:400px;
margin:0 auto;
background-color:lightblue;
}
</style>
</head>
<body>
<!-- holds background layers -->
<div id="backgroundContainer"></div>
<!-- substitutes for 'body' on this webpage -->
<div class="page">
<!-- contents for your webpage, through css centered within page-div -->
<div class="contents">
<p>Contents</p>
</div>
</div>
</body>
</html>
OR
Use jQuery/mootools/script.aculo.us/...
Best of luck!

Resources