Vijay R

Vijay R

1664220170

dynamically change app launcher icon

Check out how to dynamically change app launcher icon in flutter: https://lnkd.in/gPnksNhu

Visit my channel for more awesome flutter contents: https://lnkd.in/gYh4HAfD

 

#flutter #flutterdev #flutterpackage #package #flutterapp #aappdev #mobileapp #developer #ui #ux #uiux #app

dynamically change app launcher icon
harvey h

harvey h

1663914710

Top Handy Tips & Tools To Boost Mobile App Performance

Due to their ever-increasing popularity, #mobileapp has begun taking over large shares of global online traffic. Feature-rich. User-friendly interface. Engaging content. These are great selling points for a #mobile #app.

Although it may not be what convinces people to download your app, #performance can be a huge reason why they continue to engage with it—or simply uninstall it.

Check out the blog to know about the best #tips and tools for boosting #mobileappperformance. Click👇

https://aglowiditsolutions.com/blog/tips-to-boost-mobile-app-performance/

 

Top Handy Tips & Tools To Boost Mobile App Performance

A Compact Native Smooth Scrolling Component Library for Vue 3

Next-gen compact native smooth scrolling component library for Vue 3

No scrollbar reinventing using DOM elements, no weird logic, only native scroll event, a sprinkle of CSS magic and the power of ResizeObserver.

Native means the library doesn't interfere with scroll logic at all. Every scroll feature works as it should be.

DEMO (EXAMPLE SOURCE)

Installation:

pnpm add vue-smoothie
yarn add vue-smoothie
npm i vue-smoothie

Usage:

You should use the component as a container element for your scrollable content. The container has overflow: auto by default.

<script setup>
import { Smoothie } from "vue-smoothie";
</script>

<template>
  <Smoothie class="container">
    <div>
      <p>Test paragraph</p>
    </div>
  </Smoothie>
</template>

<style>
.container {
  /* define height and/or width */
}
</style>

There are two flavors of the component:

  • Smoothie - use this when you only need vertical scroll.
  • OmniSmoothie - use this when you need both vertical and horizontal scroll. In this case prefer using OmniSmoothie component for all scrollable areas even if they're vertical-only to prevent bundling both flavors simulataneously.

weight prop

You can setup how smooth the scrolling is by specifying an optional weight prop: <Smoothie :weight="0.03"> The lower the value the lazier transition

Exposed properties

Both flavors expose an object via ref with properties:

  • el - container DOM element (available in onMounted hook)
  • x and y - current smooth scroll position (x only in Omni)

Common issues

To make root (App) view work with smoothie you have to pass down overflow to the smoothie element. One way of doing so is:

html,
body,
#app,
.container {
  height: 100%;
}

where #app is the element you mount your Vue application on and .container a class applied to root <smoothie> element

Instead of styling #app with padding and etc, better style scroll container

Don't forget about box-sizing: border-box when a container has border and/or padding to accomodate it into its width and height to prevent multiple scrollbars, root-level (<html>) scrollbar overtaking overflow and other issues

Scrollbar appears inside of page not at a side - you need to set width to 100%

Download Details:
 

Author: zeokku
Download Link: Download The Source Code
Official Website: https://github.com/zeokku/vue-smoothie 
 

#vue #vuejs 

 

A Compact Native Smooth Scrolling Component Library for Vue 3
Nat  Grady

Nat Grady

1661596980

STARTapp: R Shiny Transcriptome Analysis Resource Tool

START App

To run this app locally on your machine, download R or RStudio and run the following commands once to set up the environment:


install.packages(c("reshape2","ggplot2","ggthemes","gplots","ggvis","dplyr","tidyr","DT", "readr",
                   "RColorBrewer","pheatmap","shinyBS","plotly","janitor",
                   "markdown","NMF","scales","heatmaply"))
## try http:// if https:// URLs are not supported
if (!requireNamespace("BiocManager", quietly = TRUE))
  install.packages("BiocManager")
BiocManager::install(c("limma","edgeR"))

You may now run the shiny app with just one command in R:

shiny::runGitHub("STARTapp", "jminnier")

Jonathan Nelson, Jiri Sklenar, Anthony Barnes, Jessica Minnier. The Knight Cardiovascular Institute and OHSU-PSU School of Public Health, Oregon Health & Science University, Portland, OR 97239-3098, USA.

We would appreciate reports of any issues with the app via the issues option of Github or by emailing start.app.help-at-gmail.com.

Instructions

Instructions can be found here: https://github.com/jminnier/STARTapp/blob/master/instructions/Instructions.md
 

Code adapted for use in app:

Law, CW, Chen, Y, Shi, W, and Smyth, GK (2014). Voom: precision weights unlock linear model analysis tools for RNA-seq read counts. Genome Biology 15, R29.

Love MI, Huber W, and Anders S (2014). Moderated estimation of fold change and dispersion for RNA-Seq data with DESeq2. Genome Biology, 15, 550.

DOI

DOI

This is the code to run the app described in the manuscript:

Nelson, JW, Sklenar J, Barnes AP, Minnier J. (2016) "The START App: A Web-Based RNAseq Analysis and Visualization Resource." Bioinformatics. doi: 10.1093/bioinformatics/btw624.

The app is hosted on Shinyapps.io here: https://kcvi.shinyapps.io/START/

Download Details:

Author: jminnier
Source Code: https://github.com/jminnier/STARTapp 
License: GPL-3.0 license

#r #app #visualization 

STARTapp: R Shiny Transcriptome Analysis Resource Tool
Mike  Kozey

Mike Kozey

1661472600

Appandup_lint: App & Up SRL Lint Preferences for Dart and Flutter Apps

appandup_lint

App and Up SRL Lint preferences for Dart and Flutter applications

How to use this package

The appandup_lint package doesn't ship any dart source code.

To enable appandup_lint,

Add it to your dev_dependencies

dev_dependencies:
  appandup_lint: ^0.0.5

Include the rules into your analysis_options.yaml

include: package:appandup_lint/recommended.yaml

Use this package as a library

Depend on it

Run this command:

With Dart:

 $ dart pub add appandup_lint

With Flutter:

 $ flutter pub add appandup_lint

This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get):

dependencies:
  appandup_lint: ^0.0.5

Alternatively, your editor might support dart pub get or flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:appandup_lint/appandup_lint.dart';

Download Details:

Author: App-and-Up
Source Code: https://github.com/App-and-Up/appandup_lint 
License: MIT license

#flutter #dart #app 

Appandup_lint: App & Up SRL Lint Preferences for Dart and Flutter Apps
Nat  Grady

Nat Grady

1661343420

Simple & Secure Authentification Mechanism for Single Shiny Apps

shinymanager

Simple and secure authentication mechanism for single 'Shiny' applications. Credentials are stored in an encrypted 'SQLite' database. Password are hashed using 'scrypt' R package. Source code of main application is protected until authentication is successful.

Installation

Install from CRAN with :

install.packages("shinymanager")

Or install development version from GitHub :

remotes::install_github("datastorm-open/shinymanager")

Usage

Secure your Shiny app to control who can access it :

  • secure_app() & auth_ui() (customization)
  • secure_server() & check_credentials()
# define some credentials
credentials <- data.frame(
  user = c("shiny", "shinymanager"), # mandatory
  password = c("azerty", "12345"), # mandatory
  start = c("2019-04-15"), # optinal (all others)
  expire = c(NA, "2019-12-31"),
  admin = c(FALSE, TRUE),
  comment = "Simple and secure authentification mechanism 
  for single ‘Shiny’ applications.",
  stringsAsFactors = FALSE
)

library(shiny)
library(shinymanager)

ui <- fluidPage(
  tags$h2("My secure application"),
  verbatimTextOutput("auth_output")
)

# Wrap your UI with secure_app
ui <- secure_app(ui)


server <- function(input, output, session) {
  
  # call the server part
  # check_credentials returns a function to authenticate users
  res_auth <- secure_server(
    check_credentials = check_credentials(credentials)
  )
  
  output$auth_output <- renderPrint({
    reactiveValuesToList(res_auth)
  })
  
  # your classic server logic
  
}

shinyApp(ui, server)

Starting page of the application will be :

Once logged, the application will be launched and a button added to navigate between the app and the admin panel (SQL credentials only and if user is authorized to access it), and to logout from the application :

Secure database

Store your credentials data in SQL database protected with a symmetric AES encryption from openssl, and password hashing using scrypt :

  • create_db()
# Init DB using credentials data
credentials <- data.frame(
  user = c("shiny", "shinymanager"),
  password = c("azerty", "12345"),
  # password will automatically be hashed
  admin = c(FALSE, TRUE),
  stringsAsFactors = FALSE
)

# you can use keyring package to set database key
library(keyring)
key_set("R-shinymanager-key", "obiwankenobi")

# Init the database
create_db(
  credentials_data = credentials,
  sqlite_path = "path/to/database.sqlite", # will be created
  passphrase = key_get("R-shinymanager-key", "obiwankenobi")
  # passphrase = "passphrase_wihtout_keyring"
)

# Wrap your UI with secure_app, enabled admin mode or not
ui <- secure_app(ui, enable_admin = TRUE)


server <- function(input, output, session) {
  
  # check_credentials directly on sqlite db
  res_auth <- secure_server(
    check_credentials = check_credentials(
        "path/to/database.sqlite",
        passphrase = key_get("R-shinymanager-key", "obiwankenobi")
        # passphrase = "passphrase_wihtout_keyring"
    )
  )
  
  output$auth_output <- renderPrint({
    reactiveValuesToList(res_auth)
  })
  
  # your classic server logic
  ...
}

Admin mode

Using SQL database protected, an admin mode is available to manage access to the application, features included are

  • manage users account : add, modify and delete users
  • ask the user to change his password
  • see logs about application usage

Use your own function ?

You can also use your own authentification function with check_credentials, for example doiing a control to your intern database. check_credentials must be a function with two arguments user & password, returning a least with at least result (TRUE to authorize acces, or FALSE) and user_info (all you want to retrieve from the user in the app) :

require(RPostgreSQL)
library(shiny)
library(shinymanager)
library(DBI)
library(glue)

dbname = "*****"
host = "localhost"
port = *****
user = "*****"
password = "******"

con <- dbConnect(dbDriver("PostgreSQL"), dbname = dbname , host = host, port = port ,
                 user = user, password = password )


DBI::dbWriteTable(con, "my_table", data.frame(
  user = c("test"),
  password = c("123"),
  stringsAsFactors = FALSE
))

# or a config .yml file or others arguments
my_custom_check_creds <- function(dbname, host, port, db_user, db_password) {
  
 # finally one function of user and password
  function(user, password) {
    
    con <- dbConnect(dbDriver("PostgreSQL"), dbname = dbname, 
                     host = host, port = port,
                     user = db_user, password = db_password)
    
    on.exit(dbDisconnect(con))
    
    req <- glue_sql("SELECT * FROM my_table WHERE \"user\" = ({user}) AND \"password\" = ({password})", 
             user = user, password = password, .con = con
    )
    
    req <- dbSendQuery(con, req)
    res <- dbFetch(req)
    if (nrow(res) > 0) {
      list(result = TRUE, user_info = list(user = user, something = 123))
    } else {
      list(result = FALSE)
    }
  }
}

ui <- fluidPage(
  tags$h2("My secure application"),
  verbatimTextOutput("auth_output")
)
ui <- secure_app(ui)


server <- function(input, output, session) {
  res_auth <- secure_server(
    check_credentials = my_custom_check_creds(
      dbname = "******",
      host = "*****",
      port = ****,
      db_user = "*****",
      db_password = "*******"
    )
  )  
  auth_output <- reactive({
    reactiveValuesToList(res_auth)
  })
  
  # access info
  observe({
    print(auth_output())
  })
}

shinyApp(ui, server)

shiny input

Two inputs are created :

observe({
    print(input$shinymanager_where)
    print(input$shinymanager_language)
})

Customization

You can customize the module (css, image, language, ...).

?secure_app
?auth_ui
?set_labels

Flexdasboard

It's possible to use shinymanager authentification on flexdashboard (but not admin console at moment). You can find information on this discussion. But it's not a really secure way because user can overpass the authentification using developper console... Prefer use shiny application with secure_app function.

shinyapps.io

There's no persistent data storage on shinyapps.io, you can read more here : https://docs.rstudio.com/shinyapps.io/Storage.html. So your sqlite database is lost when the instance is closed, and the one you've pushed when deploying the application will be used. You have to use external database.

It's possible to use shinymanager authentification on flexdashboard (but not admin console at moment). You can find information on this discussion. But it's not a really secure way because user can overpass the authentification using developper console... Prefer use shiny application with secure_app function.

Troubleshooting

The application works fine without shinymanager but not you have trouble using shinymanager.

There is a lag between your ui and the server, since shinymanger hides the ui part until authentication is successful. It is therefore possible that some of `ui element`` (input) are not defined and are NULL. In this case, you'll see some warning / error message in your R console.

So we recommend to use in all your reactive/observer functions the req instruction to validate the inputs.

One more global and brutal solution can be :

server <- function(input, output, session) {
  
  auth_out <- secure_server(....)
  
  observe({
    if(is.null(input$shinymanager_where) || (!is.null(input$shinymanager_where) && input$shinymanager_where %in% "application")){
      
      # your server app code
    }
  })
}

But it's better to use req solution. More discussion on https://github.com/datastorm-open/shinymanager/issues/36

HTTP request

shinymanager use http request and sha256 tokens to grant access to the application, like this the source code is protected without having the need to change your UI or server code.

About security

The credentials database is secured with a pass phrase and the openssl package. Hashed password using scrypt. If you have concern about method we use, please fill an issue.

Related work

Package shinyauthr provides a nice shiny module to add an authentication layer to your shiny apps.

Live demo:

You can authenticate with:

  • user: shiny / password: shiny
  • user: shinymanager / password: shinymanager (Admin)

Online documentation : https://datastorm-open.github.io/shinymanager/

News on shinymanager 1.0.400

  • (84) : new FAB button with a position argument
  • (81) : keep request query string. Thanks @erikor
  • (24) : secure_server() : added keep_token arg
  • (54) add spanish. Thanks @EAMI91
  • (98) add german. Thanks @indubio
  • (106) add polish. Thanks @StatisMike
  • (39) : fix use shiny bookmarking
  • Admin mode: new edit multiple users
  • Add full language label using choose_language
  • (66) fix quanteda bad interaction
  • (71) fix logs count for admin user
  • (26) : restrict number of users

Download Details:

Author: Datastorm-open
Source Code: https://github.com/datastorm-open/shinymanager/ 

#r #app #authentification 

Simple & Secure Authentification Mechanism for Single Shiny Apps
高橋  花子

高橋 花子

1660980600

Vue でアニメーション化されたカウントダウン タイマーを作成する方法

タイマー アプリはどこにでもあり、それぞれに独自の外観とデザインがあります。残り時間を示すためにテキストのみを使用するミニマルなデザインを選択する人もいれば、ゆっくりと減少するパイの形を表示したり、定期的にオーディオを再生して残り時間を通知したりすることで、より視覚的にしようとする人もいます.

この記事で作成するタイマーの種類は次のとおりです。

記事用に作成するアニメーション タイマー。

長さ、テキスト、色で残り時間を示しており、非常にわかりやすくなっています。

Vue を使用するので、プロジェクトをセットアップして始めましょう。

タイマーリングの作成

まず、 svg要素を使用してタイマー リングを作成します。これには、タイマー リングの作成に使用するcircle要素が含まれます。svgコンテナの中心に半径 45 ピクセルの円が描画されます。

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

circle要素を要素にラップして、チュートリアルの後半で追加される他の要素とgグループ化できるようにします。circle

基本的な HTML マークアップを作成したので、リングを表示する CSS を追加しましょう。

src/components/AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

AppTimerコンポーネントを登録しApp.vueて表示しましょう。

src/App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

これが私たちが今持っているものです。あくまでもベーシックなリングです。動き続けましょう。

サークルリングです。

タイマーラベルの表示

タイマー リングを作成したら、次に行うことは、残り時間を示すラベルを表示することです。

src/components/AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

現在、AppTimer2 つの小道具があります。elapsedプロップは経過時間を設定するために使用され、プロップlimitは合計時間を指定します。

timeLeft()elapsed変更時に自動的に更新される計算されたプロパティです。

timeLeftString()MM:SS残りのタイマーを示す形式で文字列を返す別の計算されたプロパティです。その値はtimeLeft()変更されるたびに更新されます。

次の CSS を に追加してAppTimer.vue、ラベルのスタイルを設定し、タイマー リングの上に重ねます。

src/components/AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

AppTimerから、作成した小道具を設定しましょうApp.vue

src/App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

これで、ラベルが表示されます。

タイマーにラベルが付きました。

タイマー カウントダウンを有効にする

タイマーは、カウントダウンできなければ意味がありません。この機能を有効にするロジックを追加しましょう。

状態変数 ( timeElapsed) を使用して、これまでに経過した合計時間を秒単位で追跡します。メソッドを使用して、setInterval()この変数を 1000 ミリ秒 (1 秒) ごとに 1 ずつ増やします。メソッドを使用して、すべてのタイマーが経過したら定期的なインクリメントを確実に停止しますclearInterval()

このロジックはすべてstartTimer()メソッドに含まれます。ページが読み込まれた直後にカウントダウンが開始されるようstartTimer()に、フックを呼び出します。mounted()

src/App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

これで、機能的なタイマーができました。

タイマー進行リングの作成

ここで、残り時間を視覚化するためにアニメーション化されるリングを追加する必要があります。このリングに独特の色を付けて、グレーのリングの上に置きます。時間が経過すると、残り時間がなくなるとグレーのリングだけが表示されるまで、グレーのリングがどんどん表示されます。

path要素を使用してリングを作成し、CSS でスタイルを設定します。

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

この青いリングが灰色のリングを覆っています。

青いタイマー リングが灰色のリングに変換されるようになりました。

タイマー進行リングのアニメーション化

リングをアニメートするには、 のstroke-dasharray属性を使用しますpath

stroke-dasharrayさまざまな値でリングがどのように見えるかを次に示します。

青いタイマー リングが異なる stroke-dasharray 値でどのように見えるか。

単一の値に設定stroke-dasharrayすると、同じ長さのダッシュ (青い弧) とギャップ (ダッシュ間のスペース) のパターンが作成されることがわかります。stroke-dasharrayパスの全長を埋めるために、できるだけ多くのダッシュを追加します。

名前が示すように、stroke-dasharray複数の値を取ることもできます。2 つ指定するとどうなるか見てみましょう。

stroke-dasharray に 2 つの値を指定します。

stroke-dasharray:10 30

2 つの値が指定されている場合、最初の値はダッシュの長さを決定し、2 番目の値はギャップの長さを決定します。

この動作を使用して、青いパスに残り時間を視覚化できます。これを行うには、まず、円周の式 ( 2πr)を使用して、青いパスによって作成される円の全長を計算します。

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

したがって、パスの残り時間を表示するには、2 つの値を指定します。最初の値は から始まり283、徐々に減少し0ます。2 番目の値は で一定283です。283はパス全体と同じ長さであるため、これにより、常に 1 つのダッシュと 1 つのギャップのみが存在することが保証されます。

最初の値が変更されると、パスの長さがどのように変化するかを次に示します。

最初の stroke-dasharray 値が変化したときに、青のパスの長さがどのように変化するか。

これをコードに実装しましょう。

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

計算されたプロパティ ( ) を使用して、残り時間が変わるたびに属性strokeDasharrayの新しい値を計算します。stroke-dasharray

v-if残り時間がなくなると、青色のリングの表示を完全に停止していました。

そして、青いリングがラベルに沿ってアニメーション化され、残り時間を示します。

現在、ラベルに合わせて青いリングがアニメーション化されています。

ただし、問題が 1 つあります。よく見ると、タイマーがゼロになると青いリングが突然消えます。

タイマーがゼロになると、この長さで青いパスが突然消えます。

これは、アニメーションの継続時間が 1 秒に設定されているために発生します。残り時間の値がゼロに設定されている場合でも、実際に青いリングがゼロになるまで 1 秒かかります。

これを修正するには、1 秒が経過するたびに (アニメーションとは別に) リングの長さを追加で減らす式を使用できます。これを行うために変更しましょうAppTimer.vue

src/AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

によって削除される前に、青いリングが最後まで縮小されv-ifます。

青いリングは、v-if によって削除される前に最後まで縮小されます。

背景の作成

これでタイマーが完成したので、バックグラウンドで作業する必要があります。で作成してスタイルを設定しますApp.vue

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

背景色を青に設定したので、タイマー リングとタイマー ラベルの色を変更して、それらがまだ表示されるようにする必要があります。

白を使用します。

src/components/AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

背景が表示されます。

背景が見えるようになりました。

背景のアニメーション

背景は素晴らしいですが、静的です。時間の経過とともに高さをアニメーション化するコードを追加しましょう。

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

これで、残り時間の別のインジケーターとして機能するバックグラウンド アニメーションができました。

背景の高さのアニメーション。

特定の時点での背景色の変更

残り時間も色で表示できたらいいなと思います。

thresholds配列を使用して、背景色が変化する特定の時点を定義します。カウントダウン開始時の背景色は青になります。合計時間の 50% でオレンジ色に変わり、合計時間の 20% で赤色に変わります。

src/コンポーネント/App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

reduce()メソッドを使用して、thresholds配列から現​​在の時間の色を取得します。

src/コンポーネント/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

これで完了です。4 つの異なる方法で残り時間をカウントダウンして表示する機能的なタイマーがあります。

このアプリのソース コードを調べる

このミニアプリの完全なソース コードは、GitHub で確認できます。

 ソース: https://codingbeautydev.com/animated-countdown-timer-vue/

#vue 

Vue でアニメーション化されたカウントダウン タイマーを作成する方法
Hoang  Kim

Hoang Kim

1660979100

Cách Tạo Đồng Hồ đếm Ngược Hoạt Hình Với Vue

Ứng dụng hẹn giờ có ở khắp mọi nơi và mỗi ứng dụng đều có giao diện và thiết kế độc đáo. Một số chọn thiết kế tối giản, chỉ sử dụng văn bản để biểu thị thời gian còn lại, trong khi những người khác cố gắng trực quan hơn bằng cách hiển thị hình chiếc bánh giảm dần hoặc thậm chí phát âm thanh đều đặn để thông báo thời gian còn lại.

Đây là loại bộ đếm thời gian mà chúng tôi sẽ xây dựng trong bài viết này:

Bộ đếm thời gian hoạt hình mà chúng tôi sẽ xây dựng cho bài viết.

Nó cho biết thời gian còn lại với độ dài, văn bản và màu sắc, điều này làm cho nó rất rõ ràng.

Chúng tôi sẽ sử dụng Vue, vì vậy hãy thiết lập dự án của bạn và bắt đầu!

Tạo chuông hẹn giờ

Chúng ta sẽ bắt đầu bằng cách tạo chuông hẹn giờ bằng phần tử svg . Nó sẽ chứa một phần tử vòng tròn mà chúng tôi sẽ sử dụng để tạo chuông hẹn giờ. Hình tròn sẽ được vẽ ở tâm của vùng svgchứa, với bán kính 45 pixel.

src / components / AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

Chúng tôi sẽ bọc circlephần tử trong một gphần tử để có thể nhóm circlephần tử đó với các phần tử khác sẽ được thêm vào sau trong hướng dẫn.

Chúng tôi đã tạo đánh dấu HTML cơ bản, bây giờ hãy thêm CSS sẽ hiển thị vòng.

src / components / AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

Hãy đăng ký AppTimerthành phần trong App.vuevà hiển thị nó:

src / App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

Vì vậy, đây là những gì chúng tôi có ngay bây giờ. Chỉ là một chiếc nhẫn cơ bản. Hãy tiếp tục di chuyển.

Một vòng tròn.

Hiển thị nhãn hẹn giờ

Việc tiếp theo cần làm sau khi tạo chuông hẹn giờ là hiển thị nhãn cho biết thời gian còn lại.

src / components / AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

Bây AppTimergiờ có hai đạo cụ. Phần elapsedhỗ trợ sẽ được sử dụng để đặt thời gian đã trôi qua và phần limithỗ trợ sẽ chỉ định tổng thời gian.

timeLeft()là một thuộc tính được tính toán sẽ tự động được cập nhật khi có elapsedthay đổi.

timeLeftString()là một thuộc tính được tính toán khác sẽ trả về một chuỗi ở MM:SSđịnh dạng cho biết bộ hẹn giờ còn lại. Giá trị của nó sẽ được cập nhật bất cứ khi nào timeLeft()thay đổi.

Hãy thêm CSS sau vào AppTimer.vue, CSS này sẽ tạo kiểu cho nhãn và phủ nó lên trên vòng hẹn giờ:

src / components / AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

Hãy đặt các AppTimerđạo cụ mà chúng tôi đã tạo, từ App.vue:

src / App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

Bây giờ chúng ta có thể thấy nhãn.

Bộ hẹn giờ hiện có nhãn.

Bật đếm ngược hẹn giờ

Một bộ đếm thời gian không có tác dụng nếu nó không thể đếm ngược, hãy thêm một số logic để kích hoạt chức năng này.

Chúng tôi sẽ sử dụng một biến trạng thái ( timeElapsed) để theo dõi tổng thời gian đã trôi qua tính bằng giây. Sử dụng setInterval()phương pháp này, chúng tôi sẽ tăng biến này lên 1 sau mỗi 1000 mili giây (1 giây). Chúng tôi cũng sẽ đảm bảo rằng chúng tôi dừng các bước tăng đều đặn khi tất cả bộ đếm thời gian đã trôi qua, bằng cách sử dụng clearInterval()phương pháp này.

Tất cả logic này sẽ được chứa trong startTimer()phương thức. Chúng tôi sẽ gọi startTimer()trong mounted()hook, để đếm ngược bắt đầu ngay sau khi tải trang.

src / App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

Và bây giờ chúng ta có một bộ đếm thời gian chức năng.

Tạo chuông tiến trình hẹn giờ

Bây giờ chúng ta cần thêm một chiếc nhẫn sẽ được làm động để hình dung thời gian còn lại. Chúng tôi sẽ cung cấp cho chiếc nhẫn này một màu sắc đặc biệt và đặt nó trên chiếc nhẫn màu xám. Khi thời gian trôi qua, nó sẽ hoạt hình để hiển thị ngày càng nhiều vòng màu xám cho đến khi chỉ có vòng màu xám hiển thị khi không còn thời gian.

Chúng tôi sẽ tạo vòng bằng cách sử dụng phần tử đường dẫn và tạo kiểu bằng CSS:

src / components / AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

Vì vậy, bây giờ vòng màu xanh này bao phủ vòng màu xám.

Chuông hẹn giờ màu xanh giờ chuyển sang vòng màu xám.

Tạo hoạt ảnh cho chuông tiến trình hẹn giờ

Để tạo hiệu ứng cho chiếc nhẫn, chúng ta sẽ sử dụng thuộc tính stroke-dasharray của path.

Đây là cách chiếc nhẫn sẽ trông như thế nào với các stroke-dasharraygiá trị khác nhau:

Vòng hẹn giờ màu xanh sẽ trông như thế nào với các giá trị đột quỵ-dasharray khác nhau.

Chúng ta có thể thấy rằng việc đặt stroke-dasharraythành một giá trị duy nhất sẽ tạo ra một mẫu dấu gạch ngang (các vòng cung màu xanh lam) và các khoảng trống (khoảng cách giữa các dấu gạch ngang) có cùng độ dài. stroke-dasharraythêm càng nhiều dấu gạch ngang càng tốt để lấp đầy toàn bộ chiều dài của đường dẫn.

Như tên cho thấy, stroke-dasharraycũng có thể nhận nhiều giá trị. Hãy xem điều gì sẽ xảy ra khi chúng tôi chỉ định hai:

Chỉ định hai giá trị cho stroke-dasharray.

stroke-dasharray:10 30

Khi hai giá trị được chỉ định, giá trị đầu tiên sẽ xác định độ dài của dấu gạch ngang và giá trị thứ hai sẽ xác định độ dài của khoảng trống.

Chúng ta có thể sử dụng hành vi này để làm cho đường dẫn màu xanh lam hình dung thời gian còn lại. Để thực hiện việc này, trước tiên, chúng ta hãy tính tổng chiều dài của hình tròn được tạo bởi đường màu xanh lam bằng cách sử dụng công thức chu vi hình tròn ( 2πr):

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

Vì vậy, để hiển thị thời gian còn lại với đường dẫn, chúng tôi sẽ chỉ định hai giá trị, giá trị đầu tiên sẽ bắt đầu tại 283và giảm dần đến 0, trong khi giá trị thứ hai sẽ không đổi tại 283. Điều này đảm bảo rằng chỉ có một dấu gạch ngang và một khoảng trống tại mọi thời điểm, vì 283nó dài bằng toàn bộ đường dẫn.

Đây là cách đường dẫn sẽ thay đổi độ dài khi giá trị đầu tiên thay đổi:

Chiều dài đường dẫn màu xanh lam thay đổi như thế nào khi giá trị đột quỵ-dasharray đầu tiên thay đổi.

Hãy triển khai điều này trong mã của chúng tôi:

src / components / AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Chúng tôi sử dụng thuộc tính computed ( strokeDasharray) để tính giá trị mới cho stroke-dasharraythuộc tính bất cứ khi nào thời gian còn lại thay đổi.

Chúng tôi sử dụng v-ifđể ngừng hiển thị hoàn toàn vòng màu xanh khi không còn thời gian nữa.

Và bây giờ vòng màu xanh lam hoạt ảnh phù hợp với nhãn để chỉ thời gian còn lại.

Vòng màu xanh lam hoạt ảnh phù hợp với nhãn ngay bây giờ.

Tuy nhiên, có một vấn đề: nếu bạn quan sát kỹ, vòng màu xanh lam đột nhiên biến mất khi bộ đếm thời gian về không.

Đường màu xanh lam đột nhiên biến mất ở độ dài này khi bộ đếm thời gian về không.

Điều này xảy ra vì thời lượng hoạt ảnh được đặt thành một giây. Khi giá trị của thời gian còn lại được đặt thành 0, vẫn mất một giây để thực sự chuyển vòng màu xanh lam về 0.

Để khắc phục điều này, chúng ta có thể sử dụng một công thức sẽ làm giảm độ dài của vòng một lượng bổ sung (tách biệt với hoạt ảnh) mỗi khi một giây trôi qua. Hãy sửa đổi AppTimer.vueđể làm điều này:

src / AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Bây giờ vòng màu xanh được giảm xuống cuối trước khi bị loại bỏ bởi v-if:

Vòng màu xanh lam được giảm đến hết trước khi bị loại bỏ bởi v-if.

Tạo nền

Bây giờ chúng ta đã làm xong bộ hẹn giờ, vì vậy chúng ta cần làm việc trên nền. Chúng tôi sẽ tạo và tạo kiểu cho nó trong App.vue:

src / App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

Vì chúng tôi đặt màu nền thành xanh lam, chúng tôi sẽ cần thay đổi màu của chuông hẹn giờ và nhãn hẹn giờ để chúng vẫn hiển thị.

Chúng tôi sẽ sử dụng màu trắng:

src / components / AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

Bây giờ chúng ta có thể thấy nền:

Bây giờ chúng ta có thể nhìn thấy nền.

Hoạt hình nền

Nền rất đẹp, nhưng nó chỉ tĩnh. Hãy thêm một số mã để làm sinh động chiều cao của nó khi thời gian trôi qua.

src / App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

Và bây giờ chúng ta có một hoạt ảnh nền, đóng vai trò như một chỉ báo khác về thời gian còn lại.

Hoạt ảnh chiều cao nền.

Thay đổi màu nền tại một số thời điểm nhất định

Sẽ thật tuyệt nếu chúng ta cũng có thể sử dụng màu sắc để chỉ thời gian còn lại.

Chúng tôi sẽ xác định thời điểm nhất định mà màu nền sẽ thay đổi, bằng cách sử dụng một thresholdsmảng. Màu nền sẽ là màu xanh lam khi bắt đầu đếm ngược. Nó sẽ chuyển sang màu cam ở 50% tổng thời gian và màu đỏ ở 20% tổng thời gian.

src / components / App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

Chúng ta sẽ sử dụng phương thức Reduce () để lấy màu cho thời điểm hiện tại từ thresholdsmảng.

src / components / App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

Và chúng tôi đã hoàn thành! Chúng tôi có một bộ đếm thời gian chức năng, đếm ngược và hiển thị thời gian còn lại theo 4 cách khác nhau.

Khám phá mã nguồn cho ứng dụng này

Bạn có thể xem mã nguồn hoàn chỉnh của ứng dụng nhỏ này tại đây trên GitHub.

 Nguồn: https://codingbeautydev.com/animated-countdown-timer-vue/

#vue 

Cách Tạo Đồng Hồ đếm Ngược Hoạt Hình Với Vue
Anne  de Morel

Anne de Morel

1660977000

Comment Créer Un Compte à Rebours animé Avec Vue

Les applications de minuterie sont partout, et elles ont chacune leur apparence et leur design uniques. Certains optent pour un design minimaliste, n'utilisant que du texte pour indiquer le temps restant, tandis que d'autres essaient d'être plus visuels en affichant une forme de tarte qui diminue lentement ou même en jouant de l'audio à intervalles réguliers pour notifier le temps restant.

Eh bien, voici le type de minuterie que nous allons construire dans cet article :

La minuterie animée que nous allons construire pour l'article.

Il indique le temps restant avec longueur, texte et couleur, ce qui le rend très démonstratif.

Nous utiliserons Vue, alors configurez votre projet et commençons !

Création de l'anneau de la minuterie

Nous allons commencer par créer l'anneau du minuteur à l'aide d'un élément svg . Il contiendra un élément de cercle que nous utiliserons pour créer l'anneau de la minuterie. Le cercle sera dessiné au centre du svgconteneur, avec un rayon de 45 pixels.

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

Nous allons envelopper l' circleélément dans un gélément afin de pouvoir le regrouper circleavec d'autres éléments qui seront ajoutés plus tard dans le didacticiel.

Nous avons créé le balisage HTML de base, ajoutons maintenant le CSS qui affichera l'anneau.

src/components/AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

Enregistrons le AppTimercomposant dans App.vueet affichons-le :

src/App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

C'est donc ce que nous avons en ce moment. Juste une bague basique. Continuons d'avancer.

Une bague circulaire.

Affichage de l'étiquette de la minuterie

La prochaine chose à faire après avoir créé l'anneau de la minuterie est d'afficher l'étiquette qui indique combien de temps il reste.

src/components/AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

Le AppTimera maintenant deux accessoires. L' elapsedaccessoire sera utilisé pour définir combien de temps s'est écoulé, et l' limitaccessoire spécifiera le temps total.

timeLeft()est une propriété calculée qui sera automatiquement mise à jour lors des elapsedmodifications.

timeLeftString()est une autre propriété calculée qui renverra une chaîne au MM:SSformat indiquant le temps restant. Ses valeurs seront mises à jour à chaque timeLeft()changement.

Ajoutons le CSS suivant à AppTimer.vue, qui stylisera l'étiquette et la superposera au-dessus de l'anneau du minuteur :

src/components/AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

Définissons les AppTimeraccessoires que nous avons créés, à partir deApp.vue :

src/App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

Maintenant, nous pouvons voir l'étiquette.

La minuterie a maintenant une étiquette.

Activation du compte à rebours

Un minuteur n'a aucune utilité s'il ne peut pas compter à rebours, ajoutons un peu de logique pour activer cette fonctionnalité.

Nous allons utiliser une variable d'état ( timeElapsed) pour garder une trace du temps total écoulé jusqu'à présent en secondes. En utilisant la setInterval()méthode, nous allons incrémenter cette variable de 1 toutes les 1000 millisecondes (1 seconde). Nous veillerons également à arrêter les incréments réguliers une fois que tout le temps est écoulé, en utilisant la clearInterval()méthode .

Toute cette logique sera contenue dans la startTimer()méthode. Nous appellerons startTimer()le mounted()crochet, de sorte que le compte à rebours commence immédiatement après le chargement de la page.

src/App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

Et maintenant nous avons une minuterie fonctionnelle.

Création de l'anneau de progression du minuteur

Maintenant, nous devons ajouter un anneau qui sera animé pour visualiser le temps restant. Nous allons donner à cet anneau une couleur distinctive et le placer sur l'anneau gris. Au fil du temps, il s'animera pour révéler de plus en plus l'anneau gris jusqu'à ce que seul l'anneau gris soit visible lorsqu'il ne reste plus de temps.

Nous allons créer l'anneau à l'aide d'un élément de chemin et le styliser avec CSS :

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

Alors maintenant, cet anneau bleu recouvre l'anneau gris.

L'anneau bleu de la minuterie converse maintenant avec l'anneau gris.

Animation de l'anneau de progression du minuteur

Pour animer l'anneau, nous allons utiliser l' attribut stroke-dasharray du path.

Voici à quoi ressemblera l'anneau avec différentes stroke-dasharrayvaleurs :

À quoi ressemblera l'anneau bleu de la minuterie avec différentes valeurs de course-dasharray.

Nous pouvons voir que le réglage stroke-dasharraysur une seule valeur crée un motif de tirets (les arcs bleus) et d'espaces (espaces entre les tirets) qui ont la même longueur. stroke-dasharrayajoute autant de tirets que possible pour remplir toute la longueur du chemin.

Comme son nom l'indique, stroke-dasharraypeut également prendre plusieurs valeurs. Voyons ce qui se passe lorsque nous en spécifions deux :

Spécifiez deux valeurs pour stroke-dasharray.

stroke-dasharray:10 30

Lorsque deux valeurs sont spécifiées, la première valeur déterminera la longueur des tirets et la seconde déterminera la longueur des espaces.

Nous pouvons utiliser ce comportement pour que le chemin bleu visualise le temps restant. Pour ce faire, calculons d'abord la longueur totale du cercle formé par le chemin bleu à l'aide de la formule de circonférence du cercle ( 2πr):

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

Donc, pour afficher le temps restant avec le chemin, nous allons spécifier deux valeurs, la première valeur commencera à 283et se réduira progressivement à 0, tandis que la seconde valeur sera constante à 283. Cela garantit qu'il n'y a qu'un seul tiret et un seul espace à tout moment, car 283il est aussi long que le chemin entier.

Voici comment la longueur du chemin changera lorsque la première valeur changera :

Comment la longueur du chemin bleu change lorsque la première valeur stroke-dasharray change.

Implémentons ceci dans notre code :

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Nous utilisons une propriété calculée ( strokeDasharray) pour calculer la nouvelle valeur de l' stroke-dasharrayattribut chaque fois que le temps restant change.

Nous avons l'habitude v-ifd'arrêter de montrer entièrement l'anneau bleu lorsqu'il ne reste plus de temps.

Et maintenant l'anneau bleu s'anime en ligne avec l'étiquette pour indiquer le temps restant.

L'anneau bleu s'anime en ligne avec l'étiquette maintenant.

Il y a cependant un problème : si vous regardez attentivement, l'anneau bleu disparaît soudainement lorsque la minuterie atteint zéro.

Le chemin bleu disparaît soudainement à cette longueur lorsque le chronomètre atteint zéro.

Cela se produit parce que la durée de l'animation est définie sur une seconde. Lorsque la valeur du temps restant est mise à zéro, il faut encore une seconde pour réellement animer l'anneau bleu à zéro.

Pour résoudre ce problème, nous pouvons utiliser une formule qui réduira la longueur d'un anneau d'un montant supplémentaire (distinct de l'animation) à chaque fois qu'une seconde passe. Modifions AppTimer.vuepour faire ceci :

src/AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Maintenant l'anneau bleu est réduit jusqu'au bout avant d'être supprimé par v-if:

L'anneau bleu est réduit jusqu'au bout avant d'être supprimé par v-if.

Création de l'arrière-plan

Nous en avons fini avec la minuterie maintenant, nous devons donc travailler sur le fond. Nous allons le créer et le styliser en App.vue:

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

Étant donné que nous avons défini la couleur d'arrière-plan sur le bleu, nous devrons changer la couleur de l'anneau de la minuterie et de l'étiquette de la minuterie pour qu'ils soient toujours visibles.

Nous allons utiliser du blanc :

src/components/AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

Nous pouvons maintenant voir le fond :

Nous pouvons maintenant voir le fond.

Animer l'arrière-plan

L'arrière-plan est agréable, mais c'est juste statique. Ajoutons du code pour animer sa hauteur au fil du temps.

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

Et maintenant, nous avons une animation d'arrière-plan, qui sert d'autre indicateur du temps restant.

Animation de la hauteur d'arrière-plan.

Modification de la couleur d'arrière-plan à certains moments

Ce serait formidable si nous pouvions également utiliser la couleur pour indiquer le temps restant.

Nous définirons certains moments dans le temps auxquels la couleur d'arrière-plan changera, à l'aide d'un thresholdstableau. La couleur de fond sera bleue au début du compte à rebours. Il passera à l'orange à 50 % du temps total et au rouge à 20 % du temps total.

src/components/App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

Nous utiliserons la méthode reduce() pour obtenir la couleur de l'heure actuelle à partir du thresholdstableau.

src/components/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

Et nous avons terminé ! Nous avons une minuterie fonctionnelle, qui compte à rebours et affiche le temps restant de 4 manières différentes.

Explorez le code source de cette application

Vous pouvez voir le code source complet de cette mini-application ici sur GitHub.

 Source : https://codingbeautydev.com/animated-countdown-timer-vue/

#vue 

Comment Créer Un Compte à Rebours animé Avec Vue
顾 静

顾 静

1660975200

如何使用 Vue 创建动画倒数计时器

计时器应用程序无处不在,它们都有其独特的外观和设计。有些人选择简约设计,仅使用文本来指示剩余时间,而另一些人则尝试通过显示缓慢减小的饼形甚至定期播放音频来通知剩余时间,从而更加直观。

这是我们将在本文中构建的那种计时器:

我们将为本文构建的动画计时器。

它用长度、文字和颜色表示剩余时间,非常具有示范性。

我们将使用 Vue,所以设置您的项目,让我们开始吧!

创建定时器环

我们将从使用svg元素创建计时器环开始。它将包含一个圆形元素,我们将使用它来创建计时器环。圆将绘制在svg容器的中心,半径为 45 像素。

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

我们将把circle元素包装在一个g元素中,这样我们就可以将它circle与稍后将在本教程中添加的其他元素组合在一起。

我们已经创建了基本的 HTML 标记,现在让我们添加将显示环的 CSS。

src/components/AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

让我们在其中注册AppTimer组件App.vue并显示它:

src/App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

这就是我们现在所拥有的。只是一个基本的戒指。让我们继续前进。

一个圆环。

显示定时器标签

创建计时器环后要做的下一件事是显示指示剩余时间的标签。

src/components/AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

现在AppTimer有两个道具。prop将elapsed用于设置经过了多少时间,并且limitprop 将指定总时间。

timeLeft()是一个计算属性,将在elapsed更改时自动更新。

timeLeftString()是另一个计算属性,它将以MM:SS指示剩余计时器的格式返回一个字符串。其值将在更改时更新timeLeft()

让我们将以下 CSS 添加到 中AppTimer.vue,这将为标签设置样式并将其覆盖在计时器环的顶部:

src/components/AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

让我们设置AppTimer我们创建的道具,来自App.vue

src/App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

现在我们可以看到标签了。

计时器现在有一个标签。

启用定时器倒计时

如果计时器不能倒计时,它就没有用,让我们添加一些逻辑来启用此功能。

我们将使用状态变量 ( timeElapsed) 来跟踪到目前为止以秒为单位的总时间。使用该setInterval()方法,我们将每 1000 毫秒(1 秒)将该变量增加 1。我们还将确保使用该clearInterval()方法在所有计时器结束后停止常规增量。

所有这些逻辑都将包含在startTimer()方法中。我们将调用钩子,以便在页面加载后立即开始倒计时startTimer()mounted()

src/App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

现在我们有了一个功能定时器。

创建计时器进度环

现在我们需要添加一个将动画显示剩余时间的环。我们会给这个戒指一个独特的颜色,然后把它放在灰色的戒指上。随着时间的流逝,它将动画显示越来越多的灰色环,直到没有时间剩余时只有灰色环可见。

我们将使用路径元素创建圆环并使用 CSS 设置样式:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

所以现在这个蓝色的环覆盖了灰色的环。

蓝色计时器环现在转换为灰色环。

为计时器进度环设置动画

要为环设置动画,我们将使用path.

以下是戒指在不同stroke-dasharray值下的外观:

蓝色计时器环在不同的 stroke-dasharray 值下的外观。

我们可以看到,设置stroke-dasharray为单个值会创建具有相同长度的短划线(蓝色弧线)和间隙(短划线之间的空间)的模式。stroke-dasharray添加尽可能多的破折号以填充路径的整个长度。

顾名思义,stroke-dasharray也可以取多个值。让我们看看当我们指定两个时会发生什么:

为 stroke-dasharray 指定两个值。

stroke-dasharray10 30

当指定两个值时,第一个值将确定破折号的长度,第二个值将确定间隙的长度。

我们可以使用此行为使蓝色路径可视化剩余时间。为此,首先,让我们使用圆的周长公式 ( 2πr) 计算由蓝色路径构成的圆的总长度:

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

因此,为了显示路径的剩余时间,我们将指定两个值,第一个值将从 开始283并逐渐减少到0,而第二个值将保持不变283。这确保了始终只有一个破折号和一个间隙,因为283它与整个路径一样长。

以下是路径将如何随着第一个值的变化而改变长度:

蓝色路径长度如何随着第一个 stroke-dasharray 值的变化而变化。

让我们在我们的代码中实现它:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

每当剩余时间发生变化时,我们使用计算属性 ( strokeDasharray) 来计算属性的新值。stroke-dasharray

v-if当没有剩余时间时,我们习惯于完全停止显示蓝色环。

现在,蓝色圆环按照标签显示动画,以指示剩余时间。

现在,蓝色环的动画与标签一致。

但是有一个问题:如果你仔细观察,当计时器归零时,蓝色的环会突然消失。

当计时器达到零时,蓝色路径突然消失在这个长度上。

发生这种情况是因为动画持续时间设置为一秒。当剩余时间的值设置为零时,仍然需要一秒钟才能将蓝色环实际设置为零。

为了解决这个问题,我们可以使用一个公式,每经过一秒,就会将环的长度减少一个额外的量(与动画分开)。让我们修改AppTimer.vue来做到这一点:

src/AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

现在蓝色环在被移除之前减少到末端v-if

在被 v-if 删除之前,蓝色的环被缩小到最后。

创建背景

我们现在完成了计时器,所以我们需要在后台工作。我们将创建并设置它的样式App.vue

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

由于我们将背景颜色设置为蓝色,因此我们需要更改计时器环的颜色和计时器标签以使其仍然可见。

我们将使用白色:

src/components/AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

我们现在可以看到背景:

我们现在可以看到背景。

动画背景

背景很好,但它只是静态的。让我们添加一些代码来随着时间的推移对其高度进行动画处理。

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

现在我们有了一个背景动画,它作为剩余时间的另一个指标。

动画背景高度。

在特定时间点更改背景颜色

如果我们也可以使用颜色来指示剩余时间,那就太好了。

我们将使用thresholds数组定义背景颜色发生变化的特定时间点。在倒计时开始时,背景颜色将为蓝色。它将在总时间的 50% 时变为橙色,在总时间的 20% 时变为红色。

src/components/App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

我们将使用reduce()thresholds方法从数组中获取当前时间的颜色。

src/components/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

我们完成了!我们有一个功能计时器,可以倒计时并以 4 种不同方式显示剩余时间。

探索此应用程序的源代码

你可以在 GitHub 上查看这个小应用程序的完整源代码。

 来源:https ://codingbeautydev.com/animated-countdown-timer-vue/ 

#vue 

如何使用 Vue 创建动画倒数计时器
Iara  Simões

Iara Simões

1660973400

Criar Um Temporizador De Contagem Regressiva Animado Com Vue

Os aplicativos de timer estão em toda parte, e cada um tem sua aparência e design exclusivos. Alguns optam por um design minimalista, usando apenas texto para indicar o tempo restante, enquanto outros tentam ser mais visuais exibindo uma forma de torta que diminui lentamente ou até reproduzindo áudio em intervalos regulares para notificar o tempo restante.

Bem, aqui está o tipo de timer que vamos construir neste artigo:

O cronômetro animado que construiremos para o artigo.

Indica o tempo restante com comprimento, texto e cor, o que o torna muito demonstrativo.

Estaremos usando o Vue, então configure seu projeto e vamos começar!

Criando o anel do temporizador

Começaremos criando o anel do temporizador usando um elemento svg . Ele conterá um elemento de círculo que usaremos para criar o anel do temporizador. O círculo será desenhado no centro do svgcontainer, com raio de 45 pixels.

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

Vamos envolver o circleelemento em um gelemento para que possamos agrupar circlecom outros elementos que serão adicionados posteriormente no tutorial.

Criamos a marcação HTML básica, agora vamos adicionar o CSS que exibirá o anel.

src/components/AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

Vamos registrar o AppTimercomponente App.vuee exibi-lo:

src/App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

Então é isso que temos agora. Apenas um anel básico. Vamos continuar em movimento.

Um anel circular.

Exibindo o rótulo do temporizador

A próxima coisa a fazer depois de criar o anel do temporizador é mostrar o rótulo que indica quanto tempo resta.

src/components/AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

O AppTimeragora tem dois adereços. O elapsedprop será usado para definir quanto tempo se passou e o limitprop especificará o tempo total.

timeLeft()é uma propriedade computada que será atualizada automaticamente quando houver elapsedalterações.

timeLeftString()é outra propriedade computada que retornará uma string no MM:SSformato que indica o cronômetro restante. Seus valores serão atualizados sempre que houver timeLeft()alterações.

Vamos adicionar o seguinte CSS a AppTimer.vue, que estilizará o rótulo e o sobreporá ao anel do temporizador:

src/components/AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

Vamos definir os AppTimeradereços que criamos, de App.vue:

src/App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

Agora podemos ver o rótulo.

O temporizador agora tem um rótulo.

Ativando a contagem regressiva do temporizador

Um cronômetro não tem utilidade se não puder contar, vamos adicionar alguma lógica para habilitar essa funcionalidade.

Usaremos uma variável de estado ( timeElapsed) para acompanhar o tempo total decorrido até agora em segundos. Usando o setInterval()método, vamos incrementar esta variável em 1 a cada 1000 milissegundos (1 segundo). Também garantiremos que interrompemos os incrementos regulares assim que todo o cronômetro tiver decorrido, usando o clearInterval()método .

Toda essa lógica estará contida no startTimer()método. Chamaremos startTimer()o mounted()gancho, para que a contagem regressiva comece imediatamente após o carregamento da página.

src/App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

E agora temos um temporizador funcional.

Criando o anel de progresso do temporizador

Agora precisamos adicionar um anel que será animado para visualizar o tempo restante. Daremos a este anel uma cor distinta e o colocaremos no anel cinza. Com o passar do tempo, ele se animará para revelar cada vez mais o anel cinza até que apenas o anel cinza fique visível quando não houver mais tempo.

Vamos criar o anel usando um elemento de caminho e estilizá-lo com CSS:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

Então agora este anel azul cobre o anel cinza.

O anel do temporizador azul agora converte o anel cinza.

Animando o Anel de Progresso do Timer

Para animar o anel, vamos usar o atributo stroke-dasharray do path.

Veja como o anel ficará com stroke-dasharrayvalores diferentes:

Como o anel do temporizador azul ficará com diferentes valores de traço-dasharray.

Podemos ver que definir stroke-dasharrayum valor único cria um padrão de traços (os arcos azuis) e lacunas (espaços entre os traços) que têm o mesmo comprimento. stroke-dasharrayadiciona tantos traços quanto possível para preencher todo o comprimento do caminho.

Como o nome sugere, stroke-dasharraytambém pode assumir vários valores. Vamos ver o que acontece quando especificamos dois:

Especifique dois valores para stroke-dasharray.

stroke-dasharray:10 30

Quando dois valores são especificados, o primeiro valor determinará o comprimento dos traços e o segundo valor determinará o comprimento das lacunas.

Podemos usar esse comportamento para fazer com que o caminho azul visualize o tempo restante. Para fazer isso, primeiro, vamos calcular o comprimento total do círculo feito pelo caminho azul usando a fórmula da circunferência do círculo ( 2πr):

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

Portanto, para exibir o tempo restante com o caminho, especificaremos dois valores, o primeiro valor começará em 283e reduzirá gradualmente para 0, enquanto o segundo valor será constante em 283. Isso garante que haja apenas um traço e uma lacuna em todos os momentos, pois 283é tão longo quanto o caminho inteiro.

Veja como o caminho mudará em comprimento à medida que o primeiro valor for alterado:

Como o comprimento do caminho azul muda conforme o valor do primeiro traço-dasharray muda.

Vamos implementar isso em nosso código:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Usamos uma propriedade computada ( strokeDasharray) para calcular o novo valor para o stroke-dasharrayatributo sempre que o tempo restante mudar.

Costumamos v-ifparar de mostrar o anel azul completamente quando não há mais tempo restante.

E agora o anel azul se anima de acordo com o rótulo para indicar o tempo restante.

O anel azul anima de acordo com o rótulo agora.

Porém, há um problema: se você observar de perto, o anel azul desaparece de repente quando o cronômetro chega a zero.

O caminho azul desaparece repentinamente nesse comprimento quando o cronômetro chega a zero.

Isso acontece porque a duração da animação é definida como um segundo. Quando o valor do tempo restante é definido como zero, ainda leva um segundo para realmente animar o anel azul para zero.

Para corrigir isso, podemos usar uma fórmula que reduzirá o comprimento de um anel em uma quantidade adicional (separada da animação) toda vez que um segundo passar. Vamos modificar AppTimer.vuepara fazer isso:

src/AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Agora o anel azul é reduzido ao final antes de ser removido por v-if:

O anel azul é reduzido até o fim antes de ser removido por v-if.

Criando o plano de fundo

Nós terminamos com o cronômetro agora, então precisamos trabalhar em segundo plano. Vamos criar e estilizá-lo em App.vue:

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

Como definimos a cor de fundo para azul, precisaremos alterar a cor do anel do temporizador e o rótulo do temporizador para que eles ainda fiquem visíveis.

Usaremos o branco:

src/components/AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

Agora podemos ver o fundo:

Agora podemos ver o fundo.

Animando o fundo

O fundo é bom, mas é apenas estático. Vamos adicionar algum código para animar sua altura com o passar do tempo.

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

E agora temos uma animação de fundo, que serve como mais um indicador do tempo restante.

Animando a altura do fundo.

Alterando a cor de fundo em determinados momentos

Seria ótimo se também pudéssemos usar cores para indicar o tempo restante.

Vamos definir certos pontos no tempo em que a cor de fundo mudará, usando um thresholdsarray. A cor de fundo será azul no início da contagem regressiva. Ele mudará para laranja em 50% do tempo total e vermelho em 20% do tempo total.

src/components/App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

Usaremos o método reduce() para obter a cor da hora atual do thresholdsarray.

src/components/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

E terminamos! Temos um temporizador funcional, que faz a contagem regressiva e mostra quanto tempo resta de 4 maneiras diferentes.

Explore o código-fonte para este aplicativo

Você pode ver o código-fonte completo deste mini-aplicativo aqui no GitHub.

 Fonte: https://codingbeautydev.com/animated-countdown-timer-vue/

#vue 

Criar Um Temporizador De Contagem Regressiva Animado Com Vue
Saul  Alaniz

Saul Alaniz

1660971600

Cómo Crear Un Temporizador De Cuenta Atrás animado Con Vue

Las aplicaciones de temporizador están en todas partes y cada una tiene su apariencia y diseño únicos. Algunos optan por un diseño minimalista, usando solo texto para indicar el tiempo restante, mientras que otros intentan ser más visuales mostrando una forma circular que disminuye lentamente o incluso reproduciendo audio a intervalos regulares para notificar el tiempo restante.

Bueno, este es el tipo de temporizador que construiremos en este artículo:

El temporizador animado que construiremos para el artículo.

Indica el tiempo restante con longitud, texto y color, lo que lo hace muy demostrativo.

Usaremos Vue, así que configure su proyecto y ¡comencemos!

Crear el anillo del temporizador

Comenzaremos creando el anillo del temporizador usando un elemento svg . Contendrá un elemento circular que usaremos para crear el anillo del temporizador. El círculo se dibujará en el centro del svgcontenedor, con un radio de 45 píxeles.

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

Envolveremos el circleelemento en un gelemento para que podamos agruparlo circlecon otros elementos que se agregarán más adelante en el tutorial.

Hemos creado el marcado HTML básico, ahora agreguemos CSS que mostrará el anillo.

src/components/AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

Registremos el AppTimercomponente App.vuey mostrémoslo:

src/App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

Así que esto es lo que tenemos ahora. Solo un anillo básico. Sigamos moviéndonos.

Un anillo circular.

Visualización de la etiqueta del temporizador

Lo siguiente que debe hacer después de crear el anillo del temporizador es mostrar la etiqueta que indica cuánto tiempo queda.

src/components/AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

El AppTimerahora tiene dos puntales. La elapsedpropiedad se usará para establecer cuánto tiempo ha transcurrido y la limitpropiedad especificará el tiempo total.

timeLeft()es una propiedad calculada que se actualizará automáticamente cuando elapsedcambie.

timeLeftString()es otra propiedad calculada que devolverá una cadena en el MM:SSformato que indica el tiempo restante. Sus valores se actualizarán cada vez que timeLeft()cambie.

Agreguemos el siguiente CSS a AppTimer.vue, que le dará estilo a la etiqueta y la superpondrá sobre el anillo del temporizador:

src/components/AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

Configuremos los AppTimeraccesorios que creamos, desde App.vue:

src/App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

Ahora podemos ver la etiqueta.

El temporizador ahora tiene una etiqueta.

Habilitación de la cuenta regresiva del temporizador

Un temporizador no sirve de nada si no puede contar hacia atrás, agreguemos algo de lógica para habilitar esta funcionalidad.

Usaremos una variable de estado ( timeElapsed) para realizar un seguimiento del tiempo total transcurrido hasta ahora en segundos. Usando el setInterval()método, incrementaremos esta variable en 1 cada 1000 milisegundos (1 segundo). También nos aseguraremos de detener los incrementos regulares una vez que haya transcurrido todo el tiempo, utilizando el clearInterval()método.

Toda esta lógica estará contenida en el startTimer()método. Llamaremos startTimer()al mounted()gancho, para que la cuenta regresiva comience inmediatamente después de que se cargue la página.

src/App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

Y ahora tenemos un temporizador funcional.

Creación del anillo de progreso del temporizador

Ahora necesitamos agregar un anillo que se animará para visualizar el tiempo restante. Le daremos a este anillo un color distintivo y lo colocaremos sobre el anillo gris. A medida que pasa el tiempo, se animará para revelar más y más del anillo gris hasta que solo se vea el anillo gris cuando no quede tiempo.

Crearemos el anillo usando un elemento de ruta y le daremos estilo con CSS:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

Así que ahora este anillo azul cubre el anillo gris.

El anillo azul del temporizador ahora se convierte en el anillo gris.

Animación del anillo de progreso del temporizador

Para animar el anillo, vamos a usar el atributo stroke-dasharray del path.

Así es como se verá el anillo con diferentes stroke-dasharrayvalores:

Cómo se verá el anillo del temporizador azul con diferentes valores de trazo-raya.

Podemos ver que establecer stroke-dasharrayun solo valor crea un patrón de guiones (los arcos azules) y espacios (espacios entre los guiones) que tienen la misma longitud. stroke-dasharrayagrega tantos guiones como sea posible para llenar toda la longitud de la ruta.

Como sugiere su nombre, stroke-dasharraytambién puede tomar varios valores. Veamos qué sucede cuando especificamos dos:

Especifique dos valores para trazo-raya.

stroke-dasharray:10 30

Cuando se especifican dos valores, el primer valor determinará la longitud de los guiones y el segundo valor determinará la longitud de los espacios.

Podemos usar este comportamiento para hacer que el camino azul visualice el tiempo restante. Para hacer esto, primero, calculemos la longitud total del círculo formado por el camino azul usando la fórmula de la circunferencia del círculo ( 2πr):

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

Entonces, para mostrar el tiempo restante con la ruta, especificaremos dos valores, el primer valor comenzará en 283y se reducirá gradualmente a 0, mientras que el segundo valor será constante en 283. Esto asegura que solo haya un guión y un espacio en todo momento, ya 283que es tan largo como la ruta completa.

Así es como la ruta cambiará en longitud a medida que cambie el primer valor:

Cómo cambia la longitud de la ruta azul a medida que cambia el valor del primer trazo-raya.

Implementemos esto en nuestro código:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Usamos una propiedad calculada ( strokeDasharray) para calcular el nuevo valor del stroke-dasharrayatributo cada vez que cambia el tiempo restante.

Solemos v-ifdejar de mostrar el anillo azul por completo cuando no queda más tiempo.

Y ahora el anillo azul se anima en línea con la etiqueta para indicar el tiempo restante.

El anillo azul se anima en línea con la etiqueta ahora.

Sin embargo, hay un problema: si observa de cerca, el anillo azul desaparece repentinamente cuando el temporizador llega a cero.

El camino azul desaparece repentinamente en esta longitud cuando el temporizador llega a cero.

Esto sucede porque la duración de la animación se establece en un segundo. Cuando el valor del tiempo restante se establece en cero, aún se necesita un segundo para animar el anillo azul a cero.

Para solucionar esto, podemos usar una fórmula que reducirá la longitud de un anillo en una cantidad adicional (separada de la animación) cada vez que pase un segundo. Vamos a modificar AppTimer.vuepara hacer esto:

src/AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Ahora el anillo azul se reduce hasta el final antes de ser eliminado por v-if:

El anillo azul se reduce hasta el final antes de ser eliminado por v-if.

Crear el fondo

Ya hemos terminado con el temporizador, por lo que debemos trabajar en segundo plano. Lo crearemos y le daremos estilo en App.vue:

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

Dado que configuramos el color de fondo en azul, necesitaremos cambiar el color del anillo del temporizador y la etiqueta del temporizador para que sigan siendo visibles.

Usaremos blanco:

src/components/AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

Ahora podemos ver el fondo:

Ahora podemos ver el fondo.

Animando el Fondo

El fondo es agradable, pero es solo estático. Agreguemos algo de código para animar su altura a medida que pasa el tiempo.

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

Y ahora tenemos una animación de fondo, que sirve como otro indicador del tiempo restante.

Animando la altura del fondo.

Cambiar el color de fondo en ciertos puntos en el tiempo

Sería genial si también pudiéramos usar el color para indicar el tiempo restante.

Definiremos ciertos puntos en el tiempo en los que cambiará el color de fondo, usando una thresholdsmatriz. El color de fondo será azul al comienzo de la cuenta regresiva. Cambiará a naranja al 50 % del tiempo total y a rojo al 20 % del tiempo total.

src/components/App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

Usaremos el método reduce() para obtener el color de la hora actual de la thresholdsmatriz.

src/components/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

¡Y hemos terminado! Tenemos un temporizador funcional, que cuenta hacia atrás y muestra cuánto tiempo queda de 4 maneras diferentes.

Explore el código fuente de esta aplicación

Puede ver el código fuente completo de esta miniaplicación aquí en GitHub.

 Fuente: https://codingbeautydev.com/animated-countdown-timer-vue/

#vue 

Cómo Crear Un Temporizador De Cuenta Atrás animado Con Vue
Polly  Connelly

Polly Connelly

1660957200

A Collection Of Small Functions for Creating Reactive, State-based UIs

Kelp

A collection of small functions for creating reactive, state-based UIs.

Kelp is a simpler alternative to React, Vue, and other large frameworks.

Getting Started

There are a few ways to install Kelp.

ES Modules

import {store, component} from './dist/kelp.es.min.js';

Global Script

<script src="./dist/kelp.min.js"></script>

With the global script, you can call API methods on the kelp object, or destructure them into their own variables.

let {store, component} = kelp;

API

kelp.store()

Create a reactive data object.

It accepts an object ({}) or array ([]) as an argument. If no value is provided, it uses an empty object by default.

import {store} from './dist/kelp.es.js';

let data = store({
    greeting: 'Hello',
    name: 'World'
});

This emits a kelp:store event on the document whenever a property is modified. The event.detail property contains the current value of the data.

// Listen for data changes
document.addEventListener('kelp:store', function (event) {
    console.log('The data was updated!');
    console.log(event.detail);
});

// Update the data
data.greeting = 'Hi there';

You can customize the event name by passing in a second argument into the kelp.store() method. It gets added to the end of the kelp:store event with a dash delimiter (-).

let wizards = store([], 'wizards');

// A "kelp:store-wizards" event gets emitted
wizards.push('Merlin');

kelp.render()

Render an HTML template string into the UI.

It accepts the element (or a selector for the element) to render into, and the template to render as a string. Unlike the Element.innerHTML property, this diffs the DOM and sanitizes your HTML to reduce the risk of XSS attacks.

import {render} from './dist/kelp.es.js';

// Create a template
function template () {
    return 'Hello, world!';
}

// Render it into the #app element
render('#app', template());

To reduce the risk of XSS attacks, dangerous properties, including on* events, are removed from the HTML before rendering.

// The onerror event is removed before rendering
render('#app', `<p><img src="x" onerror="alert(1)"></p>`);

If you want to allow on* event listeners, pass in true as an optional third argument.

// Track clicks
let n = 0;

// Log clicks
function log () {
    n++;
    console.log(`Clicked ${n} times.`);
}

// Render a button with an onclick event
render('#app', `<button onclick="log()">Activate Me</button>`, true);

Note: Do NOT do this if your template contains any third-party data. It can expose you to XSS attacks.

kelp.component()

Create a reactive component.

Pass in the element (or selector for the element) to render your template into, and the template to render. The kelp.component() method will render it into the UI, and automatically update the UI whenever your reactive data changes.

import {store, component} from './dist/kelp.es.js';

// Create a reactive store
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// Create a template
function template () {
    return `
        <ul>
            ${todos.map(function (todo) {
                return `<li>${todo}</li>`;
            }).join('')}
        </ul>`;
}

// Create a reactive component
// It automatically renders into the UI
component('#app', template);

// After two seconds, add an item to the todo list
setTimeout(function () {
    todos.push('Take a nap... zzzz');
}, 2000);

The kelp.component() method accepts a third argument, an object of options.

  • events - If true, will allow inline events on the template.
  • stores - An array of custom event names to use for kelp.store() events.
// Allow on* events
component('#app', template, {events: true});

// Use a custom event name
let wizards = store([], 'wizards');
component('#app', template, {stores: ['wizards']});

// Use a custom name AND allow on* events
component('#app', template, {stores: ['wizards'], events: true});

If you assign your component to a variable, you can stop reactive rendering with the kelp.component.stop() method, and start it again with the kelp.component.start() method.

// Create a component
let app = component('#app', template);

// Stop reactive rendering
app.stop();

// Restart reactive rendering
app.start();

Putting it all together

Using the kelp.store() and kelp.component() methods, you can create a reactive, state-based UI that automatically updates when your data changes.

let {store, component} = kelp;

// Create a reactive store
let data = store({
    heading: 'My Todos',
    todos: ['Swim', 'Climb', 'Jump', 'Play'],
    emoji: '👋🎉'
});

// Create a template
function template () {
    let {heading, todos, emoji} = data;
    return `
        <h1>${heading} ${emoji}</h1>
        <ul>
            ${todos.map(function (todo) {
                return `<li id="${todo.toLowerCase().replaceAll(' ', '-')}">${todo}</li>`;
            }).join('')}
        </ul>`;
}

// Create a reactive component
// It automatically renders into the UI
component('#app', template);

// After two seconds, add an item to the todo list
setTimeout(function () {
    data.todos.push('Take a nap... zzzz');
}, 2000);

If you have a more simple UI, you can instead combine the kelp.store() method with the browser-native Element.addEventListener() to manually update your UI as needed.

Cart (<span id="cart-items">0</span>)
import {store} from './dist/kelp.es.js';

// Create a reactive store
let cart = store([], 'cart-updated');

// Update how many cart items are displayed in the UI
document.addEventListener('cart-updated', function () {
    let cartCount = document.querySelector('#cart-items');
    cartCount.textContent = cart.length;
});

// Add an item to the cart
// The UI will automatically be updated
cart.push({
    item: 'T-Shirt',
    size: 'M',
    cost: 29
});

Advanced Techniques

As your project gets bigger, the way you manage components and data may need to grow with it.

Default and state-based HTML attributes

You can use data to conditionally include or change the value of HTML attributes in your template.

To dynamically set checked, selected, and value attributes, prefix them with an @ symbol. Use a falsy value when the item should not be checked or selected.

In the example below, the checkbox is checked when agreeToTOS is true.

// The reactive store
let data = store({
    agreeToTOS: true
});

// The template
function template () {
    return `
        <label>
            <input type="checkbox" @checked="${agreeToTOS}">
        </label>`;
}

// The component
component('#app', template);

You might instead want to use a default value when an element initially renders, but defer to any changes the user makes after that.

You can do that by prefixing your attributes with a # symbol.

In this example, Merlin has the [selected] attribute on it when first rendered, but will defer to whatever changes the user makes when diffing and updating the UI.

function template () {
    return `
        <label for="wizards">Who is the best wizard?</label>
        <select>
            <option>Gandalf</option>
            <option #selected>Merlin</option>
            <option>Ursula</option>
        </select>`;
}

Batch Rendering

With a kelp.component(), multiple reactive data updates are often batched into a single render that happens asynchronously.

// Reactive store
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// Create a component from a template
component('#app', template);

// These three updates would result in a single render
todos.push('Sleep');
todos.push('Wake up');
todos.push('Repeat');

You can detect when an element has been rendered by listening for the kelp:render event.

It's emitted directly on the element that was rendered, and also bubbles if you want to listen for all render events.

document.addEventListener('kelp:render', function (event) {
    console.log(`The #${event.target.id} element has been rendered.`);
});

More efficient diffing with IDs

Unique IDs can help Kelp more effectively handle UI updates.

For example, imagine you have a list of items, and you're rendering them into the UI as an unordered list.

// Reactive store
let todos = store(['Swim', 'Climb', 'Jump', 'Play']);

// The template
function template () {
    return `
        <ul>
            ${todos.map(function (todo) {
                return `<li>${todo}</li>`;
            })}
        </ul>`;
}

// Create a component
component('#app', template);

The resulting HTML would look like this.

<ul>
    <li>Swim</li>
    <li>Climb</li>
    <li>Jump</li>
    <li>Play</li>
</ul>

Next, let's imagine that you remove an item from the middle of your array of todos.

// remove "Climb"
todos.splice(1, 1);

Because of how Kelp diffs the UI, rather than removing the list item (li) with Climb as it's text, it would update the text of Climb to Jump, and the text of Jump to Play, and then remove the last list item from the UI.

For larger and more complex UIs, this can be really inefficient.

You can help Kelp more effectively diff the UI by assigning unique IDs to elements that may change.

// The template
function template () {
    return `
        <ul>
            ${todos.map(function (todo) {
                let id = todo.toLowerCase();
                return `<li id="${id}">${todo}</li>`;
            })}
        </ul>`;
}

Now, the starting HTML looks like this.

<ul>
    <li id="swim">Swim</li>
    <li id="climb">Climb</li>
    <li id="jump">Jump</li>
    <li id="play">Play</li>
</ul>

If you remove Climb from the todos array, Kelp will now remove the #climb element rather than updating all of the other list items (and any content within them).

Lifecycle Events

Kelp emits custom events throughout the lifecycle of a component or reactive store.

  • kelp:store is emitted when a reactive store is modified. The event.detail property contains the data object.
  • kelp:start is emitted on a component element when kelp starts listening for reactive data changes.
  • kelp:stop is emitted on a component element when kelp stops listening for reactive data changes.
  • kelp:render is emitted on a component element when kelp renders a UI update.

Author: cferdinandi
Source code: https://github.com/cferdinandi/kelp

#react #javascript 

A Collection Of Small Functions for Creating Reactive, State-based UIs
Lawson  Wehner

Lawson Wehner

1660909440

Instabug-Flutter: In-app Feedback and Bug Reporting for Flutter Apps

Instabug for Flutter

A Flutter plugin for Instabug.

Integration

Installation

Add Instabug to your pubspec.yaml file.

dependencies:
      instabug_flutter:

Install the package by running the following command.

flutter packages get

Initializing Instabug

To start using Instabug, import it into your Flutter app.

import 'package:instabug_flutter/instabug_flutter.dart';

Initialize the SDK in initState(). This line enables the SDK with the default behavior and sets it to be shown when the device is shaken.

Instabug.start('APP_TOKEN', [InvocationEvent.shake]);

:warning: If you're updating the SDK from versions prior to v11, please check our migration guide.

Crash reporting

Instabug automatically captures every crash of your app and sends relevant details to the crashes page of your dashboard.

⚠️ Crashes will only be reported in release mode and not in debug mode.

Replace void main() => runApp(MyApp()); with the following snippet:

void main() async {
  FlutterError.onError = (FlutterErrorDetails details) {
    Zone.current.handleUncaughtError(details.exception, details.stack);
  };
  runZonedGuarded<Future<void>>(() async {
    runApp(MyApp());
  }, (Object error, StackTrace stackTrace) {
    CrashReporting.reportCrash(error, stackTrace);
  });
}

Repro Steps

Repro Steps list all of the actions an app user took before reporting a bug or crash, grouped by the screens they visited in your app.

To enable this feature, you need to add InstabugNavigatorObserver to the navigatorObservers :

 runApp(MaterialApp(
   navigatorObservers: [InstabugNavigatorObserver()],
 ));

⚠️ Screenshots in repro steps on android is not currently supported.

Network Logging

You can choose to attach all your network requests to the reports being sent to the dashboard. To enable the feature when using the dart:io package HttpClient, please refer to the Instabug Dart IO Http Client repository.

We also support the packages http and dio. For details on how to enable network logging for these external packages, refer to the Instabug Dart Http Adapter and the Instabug Dio Interceptor repositories.

Microphone and Photo Library Usage Description (iOS Only)

Instabug needs access to the microphone and photo library to be able to let users add audio and video attachments. Starting from iOS 10, apps that don’t provide a usage description for those 2 permissions would be rejected when submitted to the App Store.

For your app not to be rejected, you’ll need to add the following 2 keys to your app’s info.plist file with text explaining to the user why those permissions are needed:

  • NSMicrophoneUsageDescription
  • NSPhotoLibraryUsageDescription

If your app doesn’t already access the microphone or photo library, we recommend using a usage description like:

  • "<app name> needs access to the microphone to be able to attach voice notes."
  • "<app name> needs access to your photo library for you to be able to attach images."

The permission alert for accessing the microphone/photo library will NOT appear unless users attempt to attach a voice note/photo while using Instabug.

Available Features

FeatureStatus
Bug Reporting
Crash Reporting
App Performance Monitoring
In-App Replies
In-App Surveys
Feature Requests
  • ✅ Stable
  • ⚙️ Under active development

Use this package as a library

Depend on it

Run this command:

With Flutter:

 $ flutter pub add instabug_flutter

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  instabug_flutter: ^11.0.0

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

Import it

Now in your Dart code, you can use:

import 'package:instabug_flutter/instabug_flutter.dart';

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:instabug_flutter/instabug_flutter.dart';

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    Zone.current.handleUncaughtError(details.exception, details.stack!);
  };

  runZonedGuarded(() => runApp(MyApp()), CrashReporting.reportCrash);

  Instabug.start(
      'ed6f659591566da19b67857e1b9d40ab', [InvocationEvent.floatingButton]);
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
        // This makes the visual density adapt to the platform that you run
        // the app on. For desktop platforms, the controls will be smaller and
        // closer together (more dense) than on mobile platforms.
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  void show() {
    Instabug.show();
  }

  void sendBugReport() {
    BugReporting.show(ReportType.bug, [InvocationOption.emailFieldOptional]);
  }

  void sendFeedback() {
    BugReporting.show(
        ReportType.feedback, [InvocationOption.emailFieldOptional]);
  }

  void askQuestion() {
    BugReporting.show(
        ReportType.question, [InvocationOption.emailFieldOptional]);
  }

  void showNpsSurvey() {
    Surveys.showSurvey('pcV_mE2ttqHxT1iqvBxL0w');
  }

  void showMultipleQuestionSurvey() {
    Surveys.showSurvey('ZAKSlVz98QdPyOx1wIt8BA');
  }

  void showFeatureRequests() {
    FeatureRequests.show();
  }

  void setInvocationEvent(InvocationEvent invocationEvent) {
    BugReporting.setInvocationEvents([invocationEvent]);
  }

  void setPrimaryColor(Color c) {
    Instabug.setPrimaryColor(c);
  }

  void setColorTheme(ColorTheme colorTheme) {
    Instabug.setColorTheme(colorTheme);
  }

  @override
  Widget build(BuildContext context) {
    final buttonStyle = ButtonStyle(
      backgroundColor: MaterialStateProperty.all(Colors.lightBlue),
      foregroundColor: MaterialStateProperty.all(Colors.white),
    );

    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: SingleChildScrollView(
          padding: const EdgeInsets.only(top: 20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Container(
                margin: const EdgeInsets.only(
                    left: 20.0, right: 20.0, bottom: 20.0),
                child: const Text(
                  'Hello Instabug\'s awesome user! The purpose of this application is to show you the different options for customizing the SDK and how easy it is to integrate it to your existing app',
                  textAlign: TextAlign.center,
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: show,
                  style: buttonStyle,
                  child: const Text('Invoke'),
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: sendBugReport,
                  style: buttonStyle,
                  child: const Text('Send Bug Report'),
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: sendFeedback,
                  style: buttonStyle,
                  child: const Text('Send Feedback'),
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: askQuestion,
                  style: buttonStyle,
                  child: const Text('Ask a Question'),
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: showNpsSurvey,
                  style: buttonStyle,
                  child: const Text('Show NPS Survey'),
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: showMultipleQuestionSurvey,
                  style: buttonStyle,
                  child: const Text('Show Multiple Questions Survey'),
                ),
              ),
              Container(
                width: double.infinity,
                margin: const EdgeInsets.only(left: 20.0, right: 20.0),
                // height: double.infinity,
                child: ElevatedButton(
                  onPressed: showFeatureRequests,
                  style: buttonStyle,
                  child: const Text('Show Feature Requests'),
                ),
              ),
              Container(
                alignment: Alignment.centerLeft,
                margin: const EdgeInsets.only(top: 20.0, left: 20.0),
                child: const Text(
                  'Change Invocation Event',
                  textAlign: TextAlign.left,
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ),
              ButtonBar(
                mainAxisSize: MainAxisSize.min,
                alignment: MainAxisAlignment.start,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: () => setInvocationEvent(InvocationEvent.none),
                    style: buttonStyle,
                    child: const Text('none'),
                  ),
                  ElevatedButton(
                    onPressed: () => setInvocationEvent(InvocationEvent.shake),
                    style: buttonStyle,
                    child: const Text('Shake'),
                  ),
                  ElevatedButton(
                    onPressed: () =>
                        setInvocationEvent(InvocationEvent.screenshot),
                    style: buttonStyle,
                    child: const Text('Screenshot'),
                  ),
                ],
              ),
              ButtonBar(
                mainAxisSize: MainAxisSize.min,
                alignment: MainAxisAlignment.start,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: () =>
                        setInvocationEvent(InvocationEvent.floatingButton),
                    style: buttonStyle,
                    child: const Text('Floating Button'),
                  ),
                  ElevatedButton(
                    onPressed: () =>
                        setInvocationEvent(InvocationEvent.twoFingersSwipeLeft),
                    style: buttonStyle,
                    child: const Text('Two Fingers Swipe Left'),
                  ),
                ],
              ),
              Container(
                alignment: Alignment.centerLeft,
                margin: const EdgeInsets.only(left: 20.0),
                child: const Text(
                  'Set Primary Color',
                  textAlign: TextAlign.left,
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ),
              ButtonBar(
                mainAxisSize: MainAxisSize.min,
                alignment: MainAxisAlignment.center,
                children: <Widget>[
                  ButtonTheme(
                    minWidth: 50.0,
                    height: 30.0,
                    child: ElevatedButton(
                      onPressed: () => setPrimaryColor(Colors.red),
                      style: ButtonStyle(
                        backgroundColor: MaterialStateProperty.all(Colors.red),
                        foregroundColor:
                            MaterialStateProperty.all(Colors.white),
                      ),
                      child: null,
                    ),
                  ),
                  ButtonTheme(
                    minWidth: 50.0,
                    height: 30.0,
                    child: ElevatedButton(
                      onPressed: () => setPrimaryColor(Colors.green),
                      style: ButtonStyle(
                        backgroundColor:
                            MaterialStateProperty.all(Colors.green),
                        foregroundColor:
                            MaterialStateProperty.all(Colors.white),
                      ),
                      child: null,
                    ),
                  ),
                  ButtonTheme(
                    minWidth: 50.0,
                    height: 30.0,
                    child: ElevatedButton(
                      onPressed: () => setPrimaryColor(Colors.blue),
                      style: ButtonStyle(
                        backgroundColor: MaterialStateProperty.all(Colors.blue),
                        foregroundColor:
                            MaterialStateProperty.all(Colors.white),
                      ),
                      child: null,
                    ),
                  ),
                  ButtonTheme(
                    minWidth: 50.0,
                    height: 30.0,
                    child: ElevatedButton(
                      onPressed: () => setPrimaryColor(Colors.yellow),
                      style: ButtonStyle(
                        backgroundColor:
                            MaterialStateProperty.all(Colors.yellow),
                        foregroundColor:
                            MaterialStateProperty.all(Colors.white),
                      ),
                      child: null,
                    ),
                  ),
                ],
              ),
              Container(
                alignment: Alignment.centerLeft,
                margin: const EdgeInsets.only(left: 20.0),
                child: const Text(
                  'Color Theme',
                  textAlign: TextAlign.left,
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
              ),
              ButtonBar(
                mainAxisSize: MainAxisSize.max,
                alignment: MainAxisAlignment.center,
                children: <Widget>[
                  ElevatedButton(
                    onPressed: () => setColorTheme(ColorTheme.light),
                    style: ButtonStyle(
                      backgroundColor: MaterialStateProperty.all(Colors.white),
                      foregroundColor:
                          MaterialStateProperty.all(Colors.lightBlue),
                    ),
                    child: const Text('Light'),
                  ),
                  ElevatedButton(
                    onPressed: () => setColorTheme(ColorTheme.dark),
                    style: ButtonStyle(
                      backgroundColor: MaterialStateProperty.all(Colors.black),
                      foregroundColor: MaterialStateProperty.all(Colors.white),
                    ),
                    child: const Text('Dark'),
                  ),
                ],
              ),
            ],
          )), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

Download Details:

Author: Instabug
Source Code: https://github.com/Instabug/Instabug-Flutter 
License: MIT license

#flutter #dart #bug #app 

Instabug-Flutter: In-app Feedback and Bug Reporting for Flutter Apps