Я думаю, что реализовал его, и я надеюсь, что вы сможете использовать этот код и предоставить обратную связь.
Шаблон компонента состоит из 3 частей:
индикатор для выбора пользователя , он получает видимость (непрозрачность) всякий раз, когда пользователь перетаскивает свой выбор в направлении да / нет
фактическая стопка карт
кнопки, которые пользователь может используйте для выбора в качестве альтернативы
перетаскивание
Код шаблона + s css код выглядит следующим образом:
// HTML:
<div class="tinder" [hidden]="!cards.length">
<div class="tinder--status">
<div [style.opacity]="crossVisible? '1':'0'">
<svg width="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M405 136.798L375.202 107 256 226.202 136.798 107 107 136.798 226.202 256 107 375.202 136.798 405 256 285.798 375.202 405 405 375.202 285.798 256z"
fill="#CDD6DD" />
</svg>
</div>
<div [style.opacity]="heartVisible? '1':'0'">
<svg width="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M349.6 64c-36.4 0-70.7 16.7-93.6 43.9C233.1 80.7 198.8 64 162.4 64 97.9 64 48 114.2 48 179.1c0 79.5 70.7 143.3 177.8 241.7L256 448l30.2-27.2C393.3 322.4 464 258.6 464 179.1 464 114.2 414.1 64 349.6 64zm-80.8 329.3l-4.2 3.9-8.6 7.8-8.6-7.8-4.2-3.9c-50.4-46.3-94-86.3-122.7-122-28-34.7-40.4-63.1-40.4-92.2 0-22.9 8.4-43.9 23.7-59.3 15.2-15.4 36-23.8 58.6-23.8 26.1 0 52 12.2 69.1 32.5l24.5 29.1 24.5-29.1c17.1-20.4 43-32.5 69.1-32.5 22.6 0 43.4 8.4 58.7 23.8 15.3 15.4 23.7 36.5 23.7 59.3 0 29-12.5 57.5-40.4 92.2-28.8 35.7-72.3 75.7-122.8 122z"
fill="#FFACE4" />
</svg>
</div>
</div>
<div class="tinder--cards" (pan)="handlePan($event)" (panend)="handlePanEnd($event)">
<div #tinderCard class="tinder--card" (transitionend)="handleShift()" *ngFor="let card of cards; let i = index"
[ngStyle]="{ zIndex: cards.length - i, transform: 'scale(' + (20 - i) / 20 + ') translateY(-' + 20 * i + 'px)' }">
<img #tinderCardImage [src]="card.img" (load)="tinderCardImage.style.opacity = 1">
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
</div>
</div>
<div class="tinder--buttons">
<button (click)="userClickedButton($event, false)">
<svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M405 136.798L375.202 107 256 226.202 136.798 107 107 136.798 226.202 256 107 375.202 136.798 405 256 285.798 375.202 405 405 375.202 285.798 256z"
fill="#CDD6DD" />
</svg>
</button>
<button (click)="userClickedButton($event, true)">
<svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M349.6 64c-36.4 0-70.7 16.7-93.6 43.9C233.1 80.7 198.8 64 162.4 64 97.9 64 48 114.2 48 179.1c0 79.5 70.7 143.3 177.8 241.7L256 448l30.2-27.2C393.3 322.4 464 258.6 464 179.1 464 114.2 414.1 64 349.6 64zm-80.8 329.3l-4.2 3.9-8.6 7.8-8.6-7.8-4.2-3.9c-50.4-46.3-94-86.3-122.7-122-28-34.7-40.4-63.1-40.4-92.2 0-22.9 8.4-43.9 23.7-59.3 15.2-15.4 36-23.8 58.6-23.8 26.1 0 52 12.2 69.1 32.5l24.5 29.1 24.5-29.1c17.1-20.4 43-32.5 69.1-32.5 22.6 0 43.4 8.4 58.7 23.8 15.3 15.4 23.7 36.5 23.7 59.3 0 29-12.5 57.5-40.4 92.2-28.8 35.7-72.3 75.7-122.8 122z"
fill="#FFACE4" />
</svg>
</button>
</div>
</div>
// S CSS:
.tinder {
width: 100%;
height: 100%;
overflow: hidden;
background-color: rgba(0,0,0,0.4);
position: absolute;
left: 0;
top: 0;
}
.tinder--status {
position: absolute;
top: 50%;
margin-top: -30px;
z-index: 2;
width: 100%;
text-align: center;
pointer-events: none;
}
.tinder--status > div {
transition: all 0.3s ease-in-out;
}
.tinder--status svg {
transition: all 0.3s ease-in-out;
position: absolute;
width: 100px;
margin-left: -50px;
}
.tinder--cards {
text-align: center;
display: flex;
flex-direction: column;
position: fixed;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.tinder--card {
display: inline-block;
width: 260px;
height: 70%;
background: #FFFFFF;
padding-bottom: 40px;
border-radius: 8px;
overflow: hidden;
position: absolute;
will-change: transform;
transition: all 0.3s ease-in-out;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.moving.tinder--card {
transition: none;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.tinder--card img {
max-width: 100%;
max-height: 75%;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.tinder--card h3 {
margin-top: 16px;
font-size: 24px;
padding: 0 16px;
pointer-events: none;
}
.tinder--card p {
margin-top: 24px;
font-size: 16px;
padding: 0 16px;
pointer-events: none;
}
.tinder--buttons {
position: absolute;
flex: 0 0 100px;
text-align: center;
bottom: 20px;
left: 0;
right: 0;
}
.tinder--buttons button {
border-radius: 50%;
line-height: 50px;
width: 50px;
height: 50px;
border: 0;
background: #FFFFFF;
display: inline-block;
padding-top: 10px;
margin: 0 12px;
}
.tinder--buttons button:focus {
outline: 0;
}
Некоторые примечания:
- функции шаблона * ngFor, которая копирует каждую карту и помещает ее в стек
- мы используем молоток. js 'события панорамирования и жеста панорамирования, чтобы справиться с перетаскиванием
- мы ожидаем события конца перехода, чтобы фактически удалить карты из стека
// TS:
import { Component, Input, ViewChildren, QueryList, ElementRef, EventEmitter, Output, Renderer2 } from '@angular/core';
@Component({
selector: 'tinder-ui',
templateUrl: 'tinder-ui.component.html',
styleUrls: ['tinder-ui.component.scss'],
})
export class TinderUIComponent {
@Input('cards') cards: Array<{
img: string,
title: string,
description: string
}>;
@ViewChildren('tinderCard') tinderCards: QueryList<ElementRef>;
tinderCardsArray: Array<ElementRef>;
@Output() choiceMade = new EventEmitter();
moveOutWidth: number;
shiftRequired: boolean;
transitionInProgress: boolean;
heartVisible: boolean;
crossVisible: boolean;
constructor(private renderer: Renderer2) {
}
userClickedButton(event, heart) {
event.preventDefault();
if (!this.cards.length) return false;
if (heart) {
this.tinderCardsArray[0].nativeElement.style.transform = 'translate(' + this.moveOutWidth + 'px, -100px) rotate(-30deg)';
this.toggleChoiceIndicator(false,true);
this.emitChoice(heart, this.cards[0]);
} else {
this.tinderCardsArray[0].nativeElement.style.transform = 'translate(-' + this.moveOutWidth + 'px, -100px) rotate(30deg)';
this.toggleChoiceIndicator(true,false);
this.emitChoice(heart, this.cards[0]);
};
this.shiftRequired = true;
this.transitionInProgress = true;
};
handlePan(event) {
if (event.deltaX === 0 || (event.center.x === 0 && event.center.y === 0) || !this.cards.length) return;
if (this.transitionInProgress) {
this.handleShift();
}
this.renderer.addClass(this.tinderCardsArray[0].nativeElement, 'moving');
if (event.deltaX > 0) { this.toggleChoiceIndicator(false,true) }
if (event.deltaX < 0) { this.toggleChoiceIndicator(true,false) }
let xMulti = event.deltaX * 0.03;
let yMulti = event.deltaY / 80;
let rotate = xMulti * yMulti;
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + event.deltaX + 'px, ' + event.deltaY + 'px) rotate(' + rotate + 'deg)');
this.shiftRequired = true;
};
handlePanEnd(event) {
this.toggleChoiceIndicator(false,false);
if (!this.cards.length) return;
this.renderer.removeClass(this.tinderCardsArray[0].nativeElement, 'moving');
let keep = Math.abs(event.deltaX) < 80 || Math.abs(event.velocityX) < 0.5;
if (keep) {
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', '');
this.shiftRequired = false;
} else {
let endX = Math.max(Math.abs(event.velocityX) * this.moveOutWidth, this.moveOutWidth);
let toX = event.deltaX > 0 ? endX : -endX;
let endY = Math.abs(event.velocityY) * this.moveOutWidth;
let toY = event.deltaY > 0 ? endY : -endY;
let xMulti = event.deltaX * 0.03;
let yMulti = event.deltaY / 80;
let rotate = xMulti * yMulti;
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + toX + 'px, ' + (toY + event.deltaY) + 'px) rotate(' + rotate + 'deg)');
this.shiftRequired = true;
this.emitChoice(!!(event.deltaX > 0), this.cards[0]);
}
this.transitionInProgress = true;
};
toggleChoiceIndicator(cross, heart) {
this.crossVisible = cross;
this.heartVisible = heart;
};
handleShift() {
this.transitionInProgress = false;
this.toggleChoiceIndicator(false,false)
if (this.shiftRequired) {
this.shiftRequired = false;
this.cards.shift();
};
};
emitChoice(heart, card) {
this.choiceMade.emit({
choice: heart,
payload: card
})
};
ngAfterViewInit() {
this.moveOutWidth = document.documentElement.clientWidth * 1.5;
this.tinderCardsArray = this.tinderCards.toArray();
this.tinderCards.changes.subscribe(()=>{
this.tinderCardsArray = this.tinderCards.toArray();
})
};
}
Примечание: часть:
- @ Ввод используется для получения л список карт
- мы используем @Output для выдачи пользователем выбора
- мы используем @ViewChildren для отслеживания стека карт
- карта только фактически удалена (массив .shift ()) когда переход завершен (событие окончания перехода)
- сам компонент скрыт, если в стеке нет карточек, идея состоит в том, что мы показываем этот компонент из компонента страницы, например, ссылка на массив, который может содержать карточки
Надеюсь, это хороший пример того, как такой компонент может быть реализован
Редактор URL: https://stackblitz.com/edit/ionic-4-template-bks4dd
Демо: https://ionic-4-template-bks4dd.stackblitz.io
Артикул: Средний пост