Onsen UI for Vueで、リストのスライダーメニューを実装しました。
最近のUIで左スライドで削除というアイテムの削除が流行っています。
本記事では、リストのスライダーメニューで削除要素の表示、
クリック後、リストのアイテムの削除。の実装をご紹介します。
もしかしたら私のリサーチ不足で、スライダーメニューで削除のライブラリなりがあったかも・・しれませんが、一先ずgsapを使用して作成しました。
完成例
下のGIF画像のように作成しました。
OnsenUIなだけあって、ネイティブアプリのようなUIが簡単に作成できます。
要件
・左スライドで削除要素の表示。
・左スライド時点からドラッグ終了時の差が最大値の場合、削除要素は開く。
・左スライド時点からドラッグ終了値の差が最大値以外の場合、削除要素を閉じる。
・他要素をクリックした場合、スライドされたメニューは閉じる。
・削除要素をクリックした場合、リストのアイテムを削除する。
上のような要件に基づき実装しました。
では、ソースを記していきます。
ソース
slider.vue
<template>
<v-ons-page>
<v-ons-list>
<v-ons-list-item
class="swipeListWrap"
lock-on-drag
v-for="item in items"
:key="item.id"
>
<v-ons-row
class="swipeList row px-0 mg-auto"
:style="{ left: 0 - animatedNumber(item.id) + 10 + '%' }"
>
<v-ons-col
class="item swipeListTarget"
:data-order-id="item.id"
width="70%"
>
<v-ons-row
class="row px-0 mg-auto swipeListTarget"
:data-order-id="item.id"
>
<v-ons-col
class="col-12 swipeListTarget"
:data-order-id="item.id"
>
{{ item.name }}
</v-ons-col>
</v-ons-row>
</v-ons-col>
<v-ons-col
class="itemDeleteWrap"
width="30%"
@click="deleteFnc(item)"
>
<div class="itemDelete">
<span class="itemDeleteText">削除</span>
</v-ons-col>
</v-ons-row>
</v-ons-list-item>
</v-ons-list>
</v-ons-page>
</template>
<script>
import gsap from "gsap";
export default {
data() {
return {
number: 0,
tweenedNumber: 0,
dragstartX: 0,
initialPosition: 30,
targetOrderId: 0,
animateLocked: false,
movementPosition: 0,
items: [
{
id: 0,
name: "おれんじ",
},
{
id: 1,
name: "いちご",
},
{
id: 2,
name: "ぶどう",
},
],
};
},
mounted: function () {
let _this = this;
this.$ons
.GestureDetector(this.$el)
.on("dragstart drag dragend touch", (e) => {
if (e.target.matches(".swipeListTarget") && !_this.animateLocked) {
_this.targetOrderId = e.target.getAttribute("data-order-id");
var range = _this.dragstartX - e.gesture.center.pageX;
if (e.type == "dragstart") {
_this.dragstartX = e.gesture.center.pageX;
} else if (e.type == "dragend") {
_this.number = range < 0 + 80 ? 0 : _this.initialPosition;
} else if (e.type == "drag") {
_this.number =
range < 0
? 0
: range && _this.initialPosition <= range
? _this.initialPosition
: range;
}
} else {
_this.number = 0;
}
});
},
methods: {
deleteFnc: function (item) {
//削除処理
this.items = Object.values(this.items).filter((el) => {
return el.id != item.id;
}, 0);
this.animateLocked = false;
},
},
computed: {
animatedNumber: function () {
return function (uid) {
this.movementPosition = this.tweenedNumber.toFixed(0);
if (uid != this.targetOrderId) {
return 0;
} else if (this.movementPosition == this.initialPosition) {
this.animateLocked = true;
return this.movementPosition;
}
if (this.movementPosition == 0) {
this.animateLocked = false;
}
return this.movementPosition;
};
},
},
watch: {
number: function (newValue) {
gsap.to(this.$data, {
duration: 0.4,
tweenedNumber: newValue,
});
},
},
};
</script>
<style scoped>
.swipeListWrap {
position: relative;
height: 50%;
min-height: 70px;
width: 130%;
}
.swipeList {
position: absolute;
width: 100%;
height: 100%;
align-items: center;
}
.item {
position: relative;
display: flex;
align-items: center;
height: 100%;
}
.itemDeleteWrap {
height: 100%;
}
.itemDelete {
display: block;
height: 100%;
background: red;
width: 100%;
}
.itemDeleteText {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background: red;
color: white;
width: 100%;
}
</style>
・number
スライドメニューの右端を示しています。
どれぐらいメニューが開いているか座標が返ります。・tweenedNumber
numberの値が変化するとアニメーションが実行され、
tweenedNumberが変化します。
tweenedNumberはアニメーション後の値を代入するので、
numberを遅れて代入するという認識です。
リスト内でdragを開始したポイントを記録します。
・initialPosition
削除要素を表示した際の移動範囲です。
・targetOrderId
イベントのある要素のカスタムデータを取得、代入します。
data-order-idにより、ページのIDを代入しています。
・animateLocked
連続してアニメーション実行のイベントが発生した場合、それらを受け付けるかどうかを判定するフラグです。
・movementPosition
移動した距離です。
mounted: function ()
{
let _this = this;
this.$ons.GestureDetector(this.$el)
.on("dragstart drag dragend touch", (e) =>
{
if (e.target.matches(".swipeListTarget") && !_this.animateLocked)
{ _this.targetOrderId = e.target.getAttribute("data-order-id");
var range = _this.dragstartX - e.gesture.center.pageX;
if (e.type == "dragstart")
{ _this.dragstartX = e.gesture.center.pageX; }
else if (e.type == "dragend")
{ _this.number = range < 0 + 80 ? 0 : _this.initialPosition; }
else if (e.type == "drag")
{ _this.number = range < 0 ? 0 :
range && _this.initialPosition <= range
? _this.initialPosition : range; }
} else { _this.number = 0;
}
}
); },
dragstart drag dragend touchのイベントが発生した場合に実行されるコードです。
タッチしたポイントからドラッグ終了をrangeとして、
それぞれのポイントに応じ処理を変更しています。
computed: { animatedNumber: function ()
{
return function (uid)
{
this.movementPosition = this.tweenedNumber.toFixed(0);
if (uid != this.targetOrderId) {
return 0;
} else if (this.movementPosition == this.initialPosition)
{ this.animateLocked = true; return this.movementPosition;
} if (this.movementPosition == 0)
{ this.animateLocked = false;
} return this.movementPosition; }; },
それぞれのリストアイテムで持っているanimatedNumberにおける処理です。
イベントの発生した要素とそれ以外の要素で分けて、スライドした要素のみスライドするように設定してあります。
また、前回イベント時のスライドの状態から処理を分けています。
おわりに
今回は、スライダーメニューで削除要素の表示及び削除を実装しました。
簡単に実装できるのでは?と思っていましたが、所々で躓いてしまいました。
例えば、複数リストアイテムに対して
computed内で行ってる処理が共通化されているため、変数の管理をするため、
変数の状態管理で苦労しました。
アイテムAをスライドメニューを開いているときにアイテムBのスライドメニューに触れると、アイテムAは閉じ、アイテムBが開いてしまうという事象です。しかもアニメーションが適用されませんでした。
改めて自ら作成してみると、大手SNSのUIは最適化されているなと思いました。
ストレスのなく操作ができますよね。
ただ、個人的にはInstagramよりTwitterの方が洗練されていると感じます。
スクロールの手の動きって上下ではなく、斜めです。
人間工学に基づいたTwitterは、操作ミスが少ないです。
一方でInstagramは不意に画面スワイプや画面をクローズしてしまいます。
こういう小さな違いが多くのユーザを獲得するか否か。
サービス内容と同様に重要なポイントですね。
以上、ありがとうございました。






コメント