Criei um component com estilo .SCSS porem ele não aplica o estilo.
precisar ser especificamente .css o aquivo de estilo?
(English translation)
I created a component with .SCSS style but it does not apply the style. need to be specifically .css the style file?
{
...,
"devDependencies": {
"css-loader": "^3.4.2",
"nativescript-dev-webpack": "~1.5.1",
"node-sass": "^4.13.1",
"sass-loader": "^8.0.2",
"typescript": "~3.5.3",
"webpack": "^4.42.1"
},
...
}
.container-search {
margin: 8;
padding: 0 5;
border-radius: 30;
background-color: white;
textField {
border-width: 1;
border-color: transparent;
font-size: 14;
margin: 0;
color: #333;
}
button {
width: 35;
height: 35;
padding: 0;
margin: 0;
color: #999;
border-radius: 100%;
background-color: #fff;
font-size: 16;
font-weight: 700;
}
}
<StackLayout class="container-search" loaded="onLoaded">
<GridLayout rows="auto" columns="auto,*,auto" >
<Button row="0" col="0" id="searchButton" text="" class="las" tap="{{ onSearchSubmit }}" />
<TextField row="0" col="1" id="searchField" hint="Pesquise.." autocorrect="false" returnKeyType="search" returnPress="{{ onSearchSubmit }}" />
<Button row="0" col="2" id="searchClear" text="" class="las" tap="{{ onSearchClear }}" visibility="hidden" />
</GridLayout>
</StackLayout>
import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout/stack-layout";
import { TextField } from "tns-core-modules/ui/text-field/text-field";
import { Button } from "tns-core-modules/ui/button/button";
import { Internationalization } from "~/pages/#shared/utilities/internationalization";
export class SearchBar {
public static shared: SearchBar;
private static searchField: TextField;
private static searchButton: Button;
private static searchClear: Button;
public static onLoaded(args: any) {
const innerComponent = args.object as StackLayout;
const bindingContext = {...innerComponent.bindingContext};
SearchBar.searchField = innerComponent.getViewById("searchField") as TextField;
SearchBar.searchButton = innerComponent.getViewById("searchButton") as Button;
SearchBar.searchClear = innerComponent.getViewById("searchClear") as Button;
SearchBar.searchField.on("textChange", SearchBar.onTextChangeST);
bindingContext.internationalization = Internationalization.singleton().getData();
bindingContext.onSearchSubmit = SearchBar.onSearchSubmit;
bindingContext.onSearchClear = SearchBar.onSearchClear;
innerComponent.bindingContext = bindingContext;
SearchBar.shared = SearchBar;
}
public static get onSearchSubmit(): (args: any) => void {
return SearchBar.onSearchSubmitST;
}
public static get onSearchClear(): (args: any) => void {
return SearchBar.onSearchClearST;
}
// Auxiliaries Methods
private static onTextChangeST(args: any) {
const text = SearchBar.searchField.text;
const bindingContext = args.object.page.bindingContext;
if(text != ""){
SearchBar.searchClear.visibility = "visible";
} else {
SearchBar.searchClear.visibility = "collapse";
bindingContext.search("");
}
}
private static onSearchSubmitST(args: any) {
const text = SearchBar.searchField.text;
const bindingContext = args.object.page.bindingContext;
if(args.object.id == "searchButton") {
setTimeout(() => {
SearchBar.searchField.dismissSoftInput();
}, 100);
}
const result = bindingContext.search(text);
if(result == 0) { ()=>{} }
}
private static onSearchClearST(args: any) {
const bindingContext = args.object.page.bindingContext;
SearchBar.searchField.text = "";
bindingContext.searchClear();
}
}
export const onLoaded = SearchBar.onLoaded;
Not necessary .css I think You need to include your file in the webpack this is a link to show you an exemple :How to inciude a local html file in a webview in nativescript main-page.js?
Second option is :Migrate the project to the latest version wiht tns migrate if it is not done .
Third option : You need to compile the .scss files with :
npm install --save-dev node-sass sass-loader
I am letting you this link : https://www.tjvantoll.com/2019/08/30/nativescript-sass/
In the case you have not seen this you may try this link
https://www.tjvantoll.com/2019/08/30/nativescript-sass/
Related
I have a Tooltip that serves as a date filter. It has two different date inputs one is for from date and other is for to date, there are also two buttons one is to clear filters and other is to apply them. When I clear filter tooltip is closed, but I also want to close tooltip without clearing dates just by clicking outside of tooltip area. I have made it work but when I press on a date and open options to select date and press on that open popper, tooltip closes because it detects pooper as outside click.
My question is how to prevent outside click when selecting dates?
This is dates component that sits inside tooltip:
import { EtsButton } from '#components/utils/ets/buttons/ets-button/ets-button';
import { CustomDatePicker } from '#components/utils/ets/inputs/custom-input-date/custom-date-picker';
import {
mergeDateFiltersData,
setDateFiltersData
} from '#store/modules/modals-and-forms/actions';
import { useDispatch, useSelector } from '#utilsFn/hooks/use-selector';
import { useTranslation } from 'next-i18next';
import React from 'react';
import styled from 'styled-components';
export const DateFilters = (props: NTableColumnsFilter.IProps) => {
const { t } = useTranslation();
const fromDate = useSelector(
(s) => s?.modalsAndForms?.dateFiltersData?.fromDate
);
const toDate = useSelector((s) => s?.modalsAndForms?.dateFiltersData?.toDate);
const dispatch = useDispatch();
return (
<TableColumnsFilterMenu>
<div className="item">
<p className="label">{t('page-sales-analysis:::main::Date from')}</p>
<CustomDatePicker
value={fromDate || null}
onChange={(val) => {
dispatch(
mergeDateFiltersData({
fromDate: val
})
);
}}
/>
</div>
<div className="item">
<p className="label">{t('page-sales-analysis:::main::Date until')}</p>
<CustomDatePicker
value={toDate || null}
onChange={(val) => {
dispatch(
mergeDateFiltersData({
toDate: val
})
);
}}
/>
</div>
<div className="action-buttons">
<EtsButton
button={{
onClick: () => {
dispatch(
setDateFiltersData({
openModalType: null,
fromDate: null,
toDate: null
})
);
props.setFromDateHandler(null);
props.setToDateHandler(null);
props.setShowMenuHandler(false);
}
}}
height="Height33"
padding="Padding7x15"
color="grey"
background="whiteFFF"
fontStyle="bold14"
>
{t('page-sales-analysis:::main::Clear the filters')}
</EtsButton>
<EtsButton
button={{
onClick: () => {
dispatch(
mergeDateFiltersData({
openModalType: null
})
);
props.setFromDateHandler(fromDate || null);
props.setToDateHandler(toDate || null);
props.setShowMenuHandler(false);
}
}}
height="Height33"
padding="Padding7x15"
color="white"
background="redC20"
fontStyle="bold14"
>
{t('page-sales-analysis:::main::Filter')}
</EtsButton>
</div>
</TableColumnsFilterMenu>
);
};
export namespace NTableColumnsFilter {
export interface IProps {
setFromDateHandler: (val: Date | null) => void;
setToDateHandler: (val: Date | null) => void;
setShowMenuHandler: (val: boolean) => void;
}
}
const TableColumnsFilterMenu = styled.div`
border-radius: 7px;
background-color: #ffffff;
box-shadow: 0px 3px 6px #00000029;
padding: 16px 21px 20px 21px;
.label {
font: normal normal 600 14px/20px 'Open Sans';
color: #000000;
}
.item {
display: flex;
flex-direction: column;
}
.action-buttons {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 20px;
button:first-child {
margin-right: 10px;
}
button {
height: 33px;
}
}
`;
this is date filters tooltip component
import Tooltip, { TooltipProps } from '#mui/material/Tooltip';
import { SVGIconCalendar } from '#styles/global-icons/icons/svg-icon-calendar';
import { useOutsideClick } from '#utilsFn/hooks/use-outside-click';
import { useSelector } from '#utilsFn/hooks/use-selector';
import React from 'react';
import styled from 'styled-components';
import { DateFilters } from './components/date-filters';
interface ITooltipProps extends TooltipProps {
mobile: boolean | number;
}
const MenuTooltip = styled(({ className, ...props }: ITooltipProps) => (
<Tooltip
{...props}
classes={{ popper: className }}
componentsProps={{
tooltip: {
sx: {
backgroundColor: 'transparent',
padding: '0',
margin: '0',
minWidth: props?.mobile ? 200 : 419
}
}
}}
/>
))(() => ({}));
export const MenuDateTooltipFilter = (props: NTableColumnsFilter.IProps) => {
const isMobile = useSelector((s) => s.device.isMobile);
const refMenu = React.useRef<HTMLDivElement>(null);
const [showMenu, setShowMenu] = React.useState<boolean>(false);
const setShowMenuHandler = (val: boolean) => {
setShowMenu(val);
};
const handlerCloseMenu = () => {
setShowMenu(() => false);
};
useOutsideClick(refMenu, handlerCloseMenu);
return (
<ContainerTableColumnsFilter>
<MenuTooltip
mobile={isMobile ? 1 : 0}
placement="bottom-end"
open={showMenu}
title={
<div ref={refMenu}>
<DateFilters
setShowMenuHandler={setShowMenuHandler}
setFromDateHandler={props.setFromDateHandler}
setToDateHandler={props.setToDateHandler}
/>
</div>
}
>
<button
type="button"
className="icon-button"
onClick={() => {
setShowMenu(() => true);
}}
>
<SVGIconCalendar />
</button>
</MenuTooltip>
</ContainerTableColumnsFilter>
);
};
export namespace NTableColumnsFilter {
export interface IProps {
setFromDateHandler: (val: Date | null) => void;
setToDateHandler: (val: Date | null) => void;
}
}
const ContainerTableColumnsFilter = styled.div`
width: 50px;
height: 50px;
.icon-button {
cursor: pointer;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background-color: #ffffff;
:hover {
svg {
path {
fill: #c20b0b;
}
}
}
}
`;
Date picker itself is a customized mui datepicker
Update:
I have achieved my desired result by using onOpen and onClose event existing in on date picker and disabling outside click function when popper is open ,but each time i select date popper flickers for a second before closing
onOpen and OnClose
onOpen={() => {
props.setIsDatePickerOpensHandler(true);
}}
onClose={() => {
props.setIsDatePickerOpensHandler(false);
}}
type of the handler
setIsDatePickerOpensHandler: (val: boolean) => void;
state and handler function in menu-date-tooltip-filter.tsx
const [isDatePickerOpen, setIsDatePickerOpen] =
React.useState<boolean>(false);
const setIsDatePickerOpensHandler = (val: boolean) => {
setIsDatePickerOpen(val);
};
if date picker is not open allow the use of outside Click function
useOutsideClick(refMenu, () => {
if (!isDatePickerOpen) {
setShowMenu(() => false);
}
});
It looks like after selecting date popper, it gets rerendered several times before closing
My updated question is how to solve this flickering bug?
I am not able to add new elements to webchat using activitymiddleware since the update to 4.14. As an example I added some code I use for typing indicators. As soon as I do that, the grouping of my avatar does not work anymore.
Without activitymiddleware and showAvatarInGroup: 'sender' -> normal behavior. Avatar is shown once per group of messages as well as the timestamp.
With activitymiddleware and showAvatarInGroup: 'sender' -> typing indicators are rendered as expected, timestamps are shown (but not grouped) and the Avatar is not shown at all.
With activitymiddleware and showAvatarInGroup : 'group' -> typing indicators are rendered as expected and the Avatar as well as the timestamps are shown. But not grouped.
Botcode (Node)
The bot sends an event to ask the webchat client to render an element. In this case a typing indicator but it could also be a new inputform.
await context.sendActivity({ name: 'typingIndicator', type: 'event' });
The webchat code (React) has an activitymiddleware to render this typing indicator as soon as the event is in:
import React, { useEffect, useMemo } from 'react';
import ReactWebChat, { createDirectLine } from 'botframework-webchat';
import TypingIndicator from './TypingIndicator';
const WebChat = ({ className, onFetchToken, store, token }) => {
const directLine = useMemo(() => createDirectLine({ token }), [token]);
const activityMiddleware = () => next => ({ activity, nextVisibleActivity, ...otherArgs }) => {
const { name, type } = activity;
// first remove all existing typing indicators
let elements = document.getElementsByClassName('typing-indicator');
for (let i = 0; i < elements.length; i++) {
elements[i].style.display = 'none'
}
// if we reveive a typing event, render a typing Indicator
if (type === 'event' && name === 'typingIndicator') {
return () => <TypingIndicator activity={activity} nextVisibleActivity={nextVisibleActivity} />;
} else {
return next({ activity, nextVisibleActivity, ...otherArgs });
}
}
const styleOptions = {
botAvatarInitials: 'Max',
showAvatarInGroup: 'sender', // group works ok
botAvatarImage: `${process.env.REACT_APP_AVATAR_URL}`,
}
useEffect(() => {
onFetchToken();
}, [onFetchToken]);
return token ? (
<ReactWebChat className={`${className || ''} web-chat`} directLine={directLine} activityMiddleware={activityMiddleware} store={store} styleOptions={styleOptions}/>
) : (
<div className={`${className || ''} connect-spinner`}>
<div className="content">
<div className="icon">
<span className="ms-Icon ms-Icon--Robot" />
</div>
<p>Connecting.</p>
</div>
</div>
);
};
export default WebChat;
The typing indicator
import './TypingIndicator.css';
import React from 'react';
const {
hooks: { useRenderActivityStatus }
} = window.WebChat;
const TypingIndicator = ({ activity, nextVisibleActivity }) => {
const renderActivityStatus = useRenderActivityStatus({ activity, nextVisibleActivity });
return (
<div>
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
{renderActivityStatus()}
</div>
);
};
export default TypingIndicator
And its styling
.typing-indicator {
background-color: transparent;
height: 35px;
width: 60px!important;
border-radius: 20px;
padding:10px;
margin-left: 65px;
}
.typing-indicator span {
line-height: 35px;
display:inline-block;
vertical-align: middle;
height: 10px;
width: 10px;
margin: 0 1px;
background-color: #9E9EA1;
border-radius: 50%!important;
opacity: 0.4;
animation: bounce 0.7s linear infinite;
}
.typing-indicator span:nth-child(1)
{
animation-delay: 0.1s;
}
.typing-indicator span:nth-child(2)
{
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3)
{
animation-delay: 0.1s;
}
#keyframes bounce {
30% { transform: translateY(-4px); }
60% { transform: translateY(0px); }
80% { transform: translateY(4px); }
100% { transform: translateY(0px); opacity: 0.5; }
}
"dependencies": {
"botframework-webchat": "^4.14.0",
"react": "16.12.0",
"react-dom": "16.12.0",
"react-scripts": "^3.4.1",
},
The problem was caused by the order in which I sent activities from the bot to webchat. I used to send:
typing-indicator event
message event
typing-indicator event
message event
The activity middleware would see the typing indicator event and replace the default render ('void') with a new rendering (my typing indicator).
My assumption is that the next event (the message) will be rendered without an avatar because webchat is already under the assumption that an avatar has been rendered.
To bypass this behavior my solution was to send
message event
typing-indicator event
message event
From a Ux perspective not such a bad idea and the problem does not occur
Does the Listview work with Nativescript 7 with Angular I can still see it need tns-core modules?
I get a lot of errors on the nativescript-ui-listview ever since the upgrade.
In NS7 you have to use #nativescript/core instead of tns-core which is kept for the retro compatibility.
Meaning you have to replace all tns-core occurrences with the #nativescript/core.
The ListView works.
I got the RadListView to render but the swiping is not docking and the buttons not disappearing after the swipe. Here the versions I am working with whilst following some tutorial:
"dependencies": {
"#angular/animations": "~11.0.0",
"#angular/common": "~11.0.0",
"#angular/compiler": "~11.0.0",
"#angular/core": "~11.0.0",
"#angular/forms": "~11.0.0",
"#angular/platform-browser": "~11.0.0",
"#angular/platform-browser-dynamic": "~11.0.0",
"#angular/router": "~11.0.0",
"#fortawesome/fontawesome-free": "^5.15.1",
"#nativescript/angular": "~11.0.0",
"#nativescript/core": "~7.1.0",
"#nativescript/theme": "~3.0.0",
"nativescript-toasty": "^3.0.0-alpha.2",
"nativescript-ui-listview": "^9.0.4",
"nativescript-ui-sidedrawer": "^9.0.3",
"reflect-metadata": "~0.1.12",
"rxjs": "^6.6.0",
"zone.js": "~0.11.1"
}
Like Robertino mentioned you will have to import from #nativescript/core rather than tns-core-modules. Check out the component file:
import { Component, OnInit, Inject, ViewChild } from '#angular/core';
import { FavoriteService } from '../services/favorite.service';
import { Dish } from '../shared/dish';
import { ListViewEventData, RadListView, SwipeActionsEventData } from 'nativescript-ui-listview';
import { RadListViewComponent } from 'nativescript-ui-listview/angular';
import { ObservableArray } from '#nativescript/core/data/observable-array';
import { View } from '#nativescript/core/ui/core/view';
import { confirm } from "#nativescript/core/ui";
import { ToastDuration, ToastPosition, Toasty } from 'nativescript-toasty';
#Component({
selector: 'app-favorites',
moduleId: module.id,
templateUrl: './favorites.component.html',
styleUrls: ['./favorites.component.css']
})
export class FavoritesComponent implements OnInit {
favorites: ObservableArray<Dish>;
errMess: string;
#ViewChild('myListView') listViewComponent: RadListViewComponent;
constructor(private favoriteservice: FavoriteService,
#Inject('baseURL') private baseURL) {
}
ngOnInit() {
this.favoriteservice.getFavorites()
.subscribe(favorites => this.favorites = new ObservableArray(favorites),
errmess => this.errMess = errmess);
}
deleteFavorite(id: number) {
console.log('delete', id);
let options = {
title: "Confirm Delete",
message: 'Do you want to delete Dish '+ id,
okButtonText: "Yes",
cancelButtonText: "No",
neutralButtonText: "Cancel"
};
confirm(options).then((result: boolean) => {
if(result) {
this.favorites = null;
this.favoriteservice.deleteFavorite(id)
.subscribe(favorites => {
const toast = new Toasty({
text:"Deleted Dish "+ id,
duration: ToastDuration.SHORT,
position: ToastPosition.BOTTOM
});
toast.show();
this.favorites = new ObservableArray(favorites);
},
errmess => this.errMess = errmess);
}
else {
console.log('Delete cancelled');
}
});
}
public onCellSwiping(args: ListViewEventData) {
var swipeLimits = args.data.swipeLimits;
var currentItemView = args.object;
var currentView;
if(args.data.x > 200) {
}
else if (args.data.x < -200) {
}
}
public onSwipeCellStarted(args: SwipeActionsEventData) {
const swipeLimits = args.data.swipeLimits;
const swipeView = args['object'];
const leftItem = swipeView.getViewById<View>('mark-view');
const rightItem = swipeView.getViewById<View>('delete-view');
swipeLimits.left = leftItem.getMeasuredWidth();
swipeLimits.right = rightItem.getMeasuredWidth();
swipeLimits.threshold = leftItem.getMeasuredWidth()/2;
}
public onSwipeCellFinished(args: ListViewEventData) {
}
public onLeftSwipeClick(args: ListViewEventData) {
console.log('Left swipe click');
this.listViewComponent.listView.notifySwipeToExecuteFinished();
}
public onRightSwipeClick(args: ListViewEventData) {
this.deleteFavorite(args.object.bindingContext.id);
this.listViewComponent.listView.notifySwipeToExecuteFinished();
}
}
Here is the template:
<ActionBar title="My Favorites" class="action-bar">
</ActionBar>
<StackLayout class="page">
<RadListView #myListView [items]="favorites" *ngIf="favorites"
selectionBehavior="none" (itemSwipeProgressEnded)="onSwipeCellFinished($event)"
(itemSwipeProgressStarted)="onSwipeCellStarted($event)"
(itemSwipeProgressChanged)="onCellSwiping($event)"
swipeActions="true">
<ListViewLinearLayout tkListViewLayout scrollDirection="vertical"
itemInsertAnimation="Default" itemDeleteAnimation="Default">
</ListViewLinearLayout>
<ng-template tkListItemTemplate let-item="item">
<StackLayout orientation="horizontal" class="listItemStackLayout">
<Image row="0" col="0" rowSpan="2" height="60" width="60"
[src]="baseURL + item.image" class="thumb p-16"></Image>
<GridLayout rows="auto, *" columns="*">
<Label row="0" col="0" [text]="item.name" class="labelName"></Label>
<Label row="1" col="0" [text]="item.description" class="labelText" textWrap="true"></Label>
</GridLayout>
</StackLayout>
</ng-template>
<GridLayout *tkListItemSwipeTemplate columns="auto, * , auto" class="listItemSwipeGridLayout">
<StackLayout id="mark-view" class="markViewStackLayout" col="0"
(tap)="onLeftSwipeClick($event)">
<Label text="" class="swipetemplateLabel fas"
verticalAlignment="center" horizontalAlignment="center"></Label>
</StackLayout>
<StackLayout id="delete-view" class="deleteViewStackLayout" col="2"
(tap)="onRightSwipeClick($event)">
<Label text="" class="swipetemplateLabel fas"
verticalAlignment="center" horizontalAlignment="center"></Label>
</StackLayout>
</GridLayout>
</RadListView>
<ActivityIndicator busy="true" *ngIf="!(favorites || errMess)" width="50"
height="50" class="activity-indicator"></ActivityIndicator>
<Label *ngIf="errMess" [text]="'Error: ' + errMess"></Label>
</StackLayout>
And the css class:
.listItemStackLayout {
background-color: white;
padding: 10;
}
.labelName {
font-size: 20;
font-weight: bold;
margin-bottom: 8;
margin-left: 16;
}
.labelTitle {
font-size: 14;
font-weight: bold;
}
.labelText {
font-size: 12;
margin-left: 16;
}
.markViewStackLayout {
background-color: blue;
padding: 16;
}
.deleteViewStackLayout {
background-color: red;
padding: 16;
}
.listItemSwipeGridLayout {
background-color: white;
}
.swipetemplateLabel {
size: 20;
font-size: 28;
color: white;
}
There are a few lines of code you may have to remove for things to work on your end.
I would like to know if it's possible to have UIScrollView with fade effect with nativescript please ?
For example : https://medium.com/#luisfmachado/uiscrollview-with-fade-effect-246e332e8b24
I read the documentation https://nativescript-vue.org/en/docs/elements/components/scroll-view/, but I don't found this information.
I would like this result for example :
Do you have an idea please ? Thank you
I have no idea how can I put the native code in my component
<template>
<ScrollView class="scroll" orientation="vertical" row="1" ref="scrollView">
<StackLayout marginLeft="10" marginRight="10" class="container-verses">
<StackLayout horizontalAlignment="center">
<Label textWrap="true" textAlignment="center" text="hello" color="#FFFFFF" fontSize="20"/>
...
<Label textWrap="true" textAlignment="center" text="hello" color="#FFFFFF" fontSize="20"/>
</StackLayout>
</StackLayout>
</ScrollView>
</template>
<script>
export default {
name : 'FadeScrollView',
computed: {},
methods : {
//
}
};
</script>
<style lang='scss' scoped>
</style>
Here is how you translate Swift into NativeScript
import { isIOS } from "#nativescript/core/platform";
import { ScrollView } from "#nativescript/core/ui/scroll-view";
let FadeScrollViewImpl;
if (isIOS) {
FadeScrollViewImpl = UIScrollView.extend({
fadePercentage: 0.2,
gradientLayer: CAGradientLayer.new(),
transparentColor: UIColor.clearColor.CGColor,
opaqueColor: UIColor.blackColor.CGColor,
topOpacity: () => {
const scrollViewHeight = this.frame.size.height;
const scrollContentSizeHeight = this.contentSize.height;
const scrollOffset = this.contentOffset.y;
const alpha = (scrollViewHeight >= scrollContentSizeHeight || scrollOffset <= 0) ? 1 : 0;
return UIColor.alloc().initWithWhiteAlpha(0, alpha).CGColor;
},
bottomOpacity: () => {
const scrollViewHeight = this.frame.size.height;
const scrollContentSizeHeight = this.contentSize.height;
const scrollOffset = this.contentOffset.y;
const alpha = (scrollViewHeight >= scrollContentSizeHeight || scrollOffset + scrollViewHeight >= scrollContentSizeHeight) ? 1 : 0
return UIColor.alloc().initWithWhiteAlpha(0, alpha).CGColor;
},
layoutSubviews() {
super.layoutSubviews()
this.delegate = this;
const maskLayer = CALayer.new();
maskLayer.frame = this.bounds;
this.gradientLayer.frame = CGRectMake(this.bounds.origin.x, 0, this.bounds.size.width, this.bounds.size.height);
this.gradientLayer.colors = [this.topOpacity, this.opaqueColor, this.opaqueColor, this.bottomOpacity];
this.gradientLayer.locations = [0, NSNumber.alloc().initWithFloat(this.fadePercentage), NSNumber.alloc().initWithFloat(1 - this.fadePercentage), 1];
maskLayer.addSublayer(this.gradientLayer);
this.layer.mask = maskLayer
},
scrollViewDidScroll(scrollView) {
this.gradientLayer.colors = [topOpacity, opaqueColor, opaqueColor, bottomOpacity];
}
});
}
export class FadeScrollView extends ScrollView {
createNativeView() {
if (isIOS) {
return FadeScrollViewImpl.new();
} else {
return super.createNativeView();
}
}
attachNative() {
if (!isIOS) {
super.attachNative();
}
}
}
Then you just have to register the element to start using it in template
Vue.registerElement('FadeScrollView', () => require('./fade-scrollView').FadeScrollView)
Playground Sample
I would like to get an advice on how to go around implementing a feature seen in many apps like Whatsapp or Facebook, where you have a list and a header which is not visible always, but is gradually shown when the user begins to scroll upwards from any place within a list.
In whatsapp and facebook the upward scrolling gesture causes the Search bar to slowly appear at the top of the screen, while the list itself is not scrolling until the search bar appears at full (at least this is the android implementation).
I need an advice on how to implement this using Nativescript angular with Telerik RadListView (android + ios). As far as I know, putting a ListView inside a ScrollView generally is not advised by telerik.
Thanks!
Edit: I learned it is called a parallax effect, and found examples of it in native android, however, not in nativescript with ListView (did find an example with ScrollView and regular StackLayout, not with a ListView inside).
You can check the available "Implementing a Parallax Scroll Effect" sample in the 'Samples' section of the official NativeScript marketplace website that shows how to implement just that effect. Just go to Market.nativescript.org and search for 'parallax'. Also there is a plugin that provides such functionality but I am not sure of its quality.
Here is an example of a scrollable parallax effect RadListView implemented using Angular (no ScrollView needed). It also provides an example of sticking the list header at the top.
Please me know if it works for you.
Component template:
<GridLayout class="page">
<RadListView (scrolled)="onScroll($event)" [items]="dataItems" itemReorder="true"
(itemReordered)="onItemReordered($event)">
<ListViewGridLayout tkListViewLayout scrollDirection="Vertical" spanCount="1" ios:itemHeight="150"
dynamicItemSize="false"></ListViewGridLayout>
<ng-template tkListItemTemplate let-item="item">
<StackLayout orientation="vertical">
<!-- list item content goes here -->
</StackLayout>
</ng-template>
<ng-template tkListViewHeader>
<StackLayout>
<GridLayout #fixedHeaderContainer class="fixed-header-container">
<label text="Fixed Content" verticalAlignment="center"></label>
</GridLayout>
<StackLayout class="list-header-container">
<StackLayout #listHeaderContainer>
<label text="List Title"></label>
</StackLayout>
</StackLayout>
</StackLayout>
</ng-template>
</RadListView>
<GridLayout verticalAlignment="top" [height]="dockContainerHeight" [opacity]="dockContainerOpacity">
<FlexboxLayout justifyContent="flex-start" alignItems="center" class="docked-label-wrapper">
<button class="fas" text=""></button>
<StackLayout flexGrow="1" height="100%" [opacity]="dockContentOpacity" orientation="horizontal">
<label text="List Title"></label>
</StackLayout>
</FlexboxLayout>
</GridLayout>
Component scss:
.fixed-header-container {
height: 200;
padding: 0 16;
background-color: green;
label {
font-size: 30;
font-weight: 700;
color: $white;
}
}
.list-header-container {
margin-top: -12;
border-radius: 12 12 0 0;
background-color: #ffffff;
label {
margin: 16 0;
font-size: 22;
color: black;
vertical-align: center;
}
.smaller-label {
font-size: 12;
color: #909090;
}
}
RadListView {
height: 100%;
background-color: #ffffff;
}
.docked-label-wrapper {
margin: 0 0 10;
background-color: #ffffff;
.fas {
margin: 0;
font-size: 18;
}
label {
font-size: 18;
color: black;
vertical-align: center;
}
}
Component ts:
import { Component, ElementRef, OnInit, ViewChild } from '#angular/core';
import { ListViewEventData, ListViewScrollEventData } from 'nativescript-ui-listview';
import { DataItem, DataItemService } from './data-items.service';
export const DOCK_HEADER_HEIGHT = 58;
#Component({
moduleId: module.id,
selector: 'app-comp-name',
templateUrl: './comp-name.component.html',
styleUrls: ['./comp-name.component.scss']
})
export class CompNameComponent implements OnInit {
dataItems: DataItem[];
dockContainerHeight = DOCK_HEADER_HEIGHT;
dockContainerOpacity = 0;
dockContentOpacity = 0;
#ViewChild('fixedHeaderContainer', { static: false })
fixedHeaderContainerRef: ElementRef;
#ViewChild('listHeaderContainer')
listHeaderContainerRef: ElementRef;
constructor(private _dataItemService: DataItemService) {}
ngOnInit(): void {
this.dataItems = this._dataItemService.getDataItems();
}
onItemReordered(args: ListViewEventData) {
console.log('Item reordered. Old index: ' + args.index + ' ' + 'new index: ' + args.data.targetIndex);
}
onScroll(args: ListViewScrollEventData) {
if (!this.fixedHeaderContainerRef) {
return;
}
const offset = args.scrollOffset < 0 ? 0 : args.scrollOffset;
const fixedHeaderHeight = this.fixedHeaderContainerRef.nativeElement.getActualSize().height;
this.applyFixedHeaderTransition(offset);
this.applyTitleTransition(offset, fixedHeaderHeight);
this.applyDockHeaderTransition(offset, fixedHeaderHeight);
}
private applyFixedHeaderTransition(scrollOffset: number) {
this.fixedHeaderContainerRef.nativeElement.translateY = scrollOffset;
}
private applyTitleTransition(scrollOffset: number, fixedHeaderHeight: number) {
const maxHeightChange = fixedHeaderHeight - DOCK_HEADER_HEIGHT;
const titleElement = this.listHeaderContainerRef.nativeElement;
if (maxHeightChange < scrollOffset) {
titleElement.translateX = -(scrollOffset - maxHeightChange) / 1.2;
titleElement.translateY = -(scrollOffset - maxHeightChange) * 2;
titleElement.scaleX = 1 - (scrollOffset - maxHeightChange) / fixedHeaderHeight;
titleElement.scaleY = 1 - (scrollOffset - maxHeightChange) / fixedHeaderHeight;
} else {
titleElement.translateX = 0;
titleElement.translateY = 0;
titleElement.scaleX = 1;
titleElement.scaleY = 1;
}
}
private applyDockHeaderTransition(scrollOffset: number, fixedHeaderHeight: number) {
const maxHeightChange = fixedHeaderHeight - DOCK_HEADER_HEIGHT;
const containerOpacity = 1 - scrollOffset / maxHeightChange;
this.dockContainerOpacity = containerOpacity <= 0 ? 1 : 0;
this.dockContentOpacity = (scrollOffset - (fixedHeaderHeight - DOCK_HEADER_HEIGHT)) / DOCK_HEADER_HEIGHT - 0.2;
}
}