Pull to refresh

Как создать круглую Progress Button

Reading time 9 min
Views 45K
Original author: Mary Lou
Урок по реализации круглой кнопки загрузки (далее progress button) by Colin Garven. Будем использовать, описанную by Jake Archibald, технику анимации SVG линий для того, чтобы анимировать progress button и показать пользователю состояния «success and fail».

image

Сегодня мы покажем вам как создать изящную progress button. Это уникальный концепт submit button, предложенный by Colin Garven. Сперва взгляните на это (Demo ), постарайтесь придумать, как реализовать, и просто наслаждайтесь анимацией. Идея состоит в следующем: при первом клике submit button трансформируется в круг, который покажет анимацию загрузки, используя свои границы (далее бордер). Когда анимация закончится, кнопка вернет исходную форму и покажет отметку, указывающую, что подтверждение прошло успешно или нет.

Существует несколько способов реализовать кнопку с таким эффектом. Думая о реализации только посредствам CSS, самой тяжёлой частью кажется круг прогресса. Существует техника реализации с помощью свойства clip. Anders Ingemann написал отличный полный tutorial (на LESS). Но мы будем использовать технику, основанную на SVG, CSS transitions и немного JS. Что касается круга прогресса, отметок success /fail, мы воспользуемся техникой рисования SVG линий, описанную by Jake Archibald.

Стоит отметить, что анимирование SVG может быть проблематичным для некоторых браузеров. Так что воспринимайте этот tutorial только как экспериментальное упражнение.

Поехали!

The Master plan


Если вы внимательно изучили Colin’s Dribbble shot, то должны были заметить, что нам следует позаботиться о нескольких состояниях кнопки. Интересная часть – переход между двумя такими состояниями. Сперва мы должны показать простую кнопку с прозрачным фоном и цветным бордером. При наведении мышки кнопка заполняется цветом бордера, а текст становится белым.

image

При нажатии на кнопку (для того, что бы, например, подтвердить отправку формы), текст должен плавно исчезнуть, ширина кнопки — уменьшится до состояния круга, бордер — стать уже, и должна начаться анимация. Мы будем использовать SVG-круг для анимации, так что нам необходимо убедиться, что получившаяся кнопка такого же размера и в той же позиции, что и SVG-круг, который виден в этот момент. Затем мы рисуем круг, изображающий загрузку подтверждения.

image

Когда подтверждение завершено, а круг полностью нарисован, кнопка должна снова расшириться, нарисовать галочку в случае успешного подтверждения. И закрашиваем кнопку соответственно.

image

В случае неудачи подтверждения, нужно показать состояние ошибки.

image

Давайте создадим нашу разметку со всеми необходимыми элементами.

Разметка


Для разметки нам потребуется главный контейнер, кнопка со спаном(span), содержащим текст, и три SVG:

<!-- progress button -->
<div id="progress-button" class="progress-button">
	<!-- button with text -->
	<button><span>Submit</span></button>

	<!-- svg circle for progress indication -->
	<svg class="progress-circle" width="70" height="70">
		<path d="m35,2.5c17.955803,0 32.5,14.544199 32.5,32.5c0,17.955803 -14.544197,32.5 -32.5,32.5c-17.955803,0 -32.5,-14.544197 -32.5,-32.5c0,-17.955801 14.544197,-32.5 32.5,-32.5z"/>
	</svg>

	<!-- checkmark to show on success -->
	<svg class="checkmark" width="70" height="70">
		<path d="m31.5,46.5l15.3,-23.2"/>
		<path d="m31.5,46.5l-8.5,-7.1"/>
	</svg>

	<!-- cross to show on error -->
	<svg class="cross" width="70" height="70">
		<path d="m35,35l-9.3,-9.3"/>
		<path d="m35,35l9.3,9.3"/>
		<path d="m35,35l-9.3,9.3"/>
		<path d="m35,35l9.3,-9.3"/>
	</svg>

</div><!-- /progress-button -->

Используем Method Draw, ведь проще всего воспользоваться онлайн SVG генератором, чтобы нарисовать галочку и крестик для кнопки. Размеры всех SVG будут 70х70, так как высота нашей кнопки 70рх. Если мы хотим круг с бордером толщиной в 5 единиц нам нужно установить правильный радиус, когда будим рисовать его в графическом редакторе, да так чтобы весь круг с его бордером имел высоту 70рх. Заметьте, что в SVG обводка рисуется симметрично границе объекта. К примеру, обводка толщиной в 2рх увеличит круг радиусом в 10рх к реальным толщине и высоте 20+2 вместо 20+4(ширина бордера дважды), то есть формула 2*r+border. Для нашего случая мы знаем 2*r+5=70, от сюда наш круг должен иметь радиус в 32,5рх. Таким образом выходит: />.

К сожалению, мы не можем использовать только эти базовые размеры, потому что дефолтные параметры вставки у каждого браузера свои и мы не можем контролировать, где «анимация загрузки начнётся». Таким образом, нам придётся конвертировать эту форму в path и использовать его. Вы можете легко осуществить это в Method Draw under Object > Convert to Path.

Для крестика мы будем использовать четыре элемента, которые можно нарисовать со средней точки, и анимировать так же как галочку.

Итак, у нас есть все необходимые элементы. Продумаем порядок действий и начнем стилизацию.

CSS


Сперва необходимо стилизовать контейнер для кнопки:

.progress-button {
	position: relative;
	display: inline-block;
	text-align: center;
}

Укажем нашей кнопке цвета и шрифты. Что бы она соответствовала концепту, установим правильный бордер и шрифт Montserrat:

.progress-button button {
	display: block;
	margin: 0 auto;
	padding: 0;
	width: 250px;
	height: 70px;
	border: 2px solid #1ECD97;
	border-radius: 40px;
	background: transparent;
	color: #1ECD97;
	letter-spacing: 1px;
	font-size: 18px;
	font-family: 'Montserrat', sans-serif;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s, border-width 0.3s, border-color 0.3s;
}

Также ставим transition для всех свойств, которые будут анимироваться (background-color, width etc.).

При наведении курсора мыши меняем цвет фона и цвет текста:

.progress-button button:hover {
	background-color: #1ECD97;
	color: #fff;
}

Уберем все обводки (outline):

.progress-button button:focus {
	outline: none;
}

Все SVG должны быть находиться по центру, все pointer-events отключены:

.progress-button svg {
	position: absolute;
	top: 0;
	left: 50%;
	-webkit-transform: translateX(-50%);
	transform: translateX(-50%);
	pointer-events: none;
}

Формы не должны иметь цвета заливки, так как мы хотим играть только с обводкой. Мы не показываем элементы ни в каких состояниях кнопки, кроме нужного, так что давайте спрячем их, сделав прозрачными:

.progress-button svg path {
	opacity: 0;
	fill: none;
}

Наш загрузочный круг будет создан установкой stroke-width в 5 единиц:

.progress-button svg.progress-circle path {
	stroke: #1ECD97;
	stroke-width: 5;
}

Success/fail индикаторы будут иметь обводку тоньше, и она должна быть белой. Свойству stroke-linecap установим значение round, так они будут красивее. У них установим быстрое изменение прозрачности:

.progress-button svg.checkmark path,
.progress-button svg.cross path {
	stroke: #fff;
	stroke-linecap: round;
	stroke-width: 4;
	-webkit-transition: opacity 0.1s;
	transition: opacity 0.1s;
}

Теперь давайте подрезюмируем и вспомним наш master plan. Нам необходимо было иметь возможность стилизовать три добавленных состояния (помимо дефолтного) кнопки и специальных элементов. Будем использовать классы “loading”, “success” и “error” для их индикации.

Кнопка станет круглой и будет выглядеть в точности как загрузочный круг SVG, когда мы начнем процесс загрузки:
.loading.progress-button button {
	width: 70px; /* make a circle */
	border-width: 5px;
	border-color: #ddd;
	background-color: transparent;
	color: #fff;
}

Помним, что мы уже установили transition, когда задавали стили для кнопки. Текст должен быстро исчезнуть, когда начнется анимация загрузки,…

.loading.progress-button span {
	-webkit-transition: opacity 0.15s;
	transition: opacity 0.15s;
}

… путем установки нулевого значения для прозрачности:

.loading.progress-button span,
.success.progress-button span,
.error.progress-button span {
	opacity: 0; /* keep it hidden in all states */
}

При смене состояний с loading на success/error нам не нужен transition, просто оставим текст скрытым.

Когда мы убираем все классы и возвращаем кнопку к исходному состоянию. Тексту нужно немного больше времени для появления, так что установим другие значения длительности анимации и ее задержки для нормального состояния:

/* Transition for when returning to default state */
.progress-button button span {
	-webkit-transition: opacity 0.3s 0.1s;
	transition: opacity 0.3s 0.1s;
}

При достижении последнего состояния необходимо переопределить transition, так как нам не надо анимировать цвет бордера или широту кнопки:

.success.progress-button button,
.error.progress-button button {
	-webkit-transition: background-color 0.3s, width 0.3s, border-width 0.3s;
	transition: background-color 0.3s, width 0.3s, border-width 0.3s;
}

Зададим цвета для последнего состояния:

.success.progress-button button {
	border-color: #1ECD97;
	background-color: #1ECD97;
}

.error.progress-button button {
	border-color: #FB797E;
	background-color: #FB797E;
}

Когда будет применен необходимый класс, нужно показать нашу SVG и анимировать stroke-dashoffset установкой следующео значения для transition:

.loading.progress-button svg.progress-circle path,
.success.progress-button svg.checkmark path,
.error.progress-button svg.cross path {
	opacity: 1;
	-webkit-transition: stroke-dashoffset 0.3s;
	transition: stroke-dashoffset 0.3s;
}

Добавим easing для анимации широты кнопки:

.elastic.progress-button button {
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1), border-width 0.3s, border-color 0.3s;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1.6), border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.25, 0.25, 0.4, 1.6), border-width 0.3s, border-color 0.3s;
}

.loading.elastic.progress-button button {
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, 0, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
	-webkit-transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, -0.6, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
	transition: background-color 0.3s, color 0.3s, width 0.3s cubic-bezier(0.6, -0.6, 0.75, 0.75), border-width 0.3s, border-color 0.3s;
}

Если вы любите играть с другими easing-функциями – пробуйте Ceaser, CSS Easing Animation Tool by Matthew Lein.

Со стилями определились, творим магию дальше.

JavaScript



Начнем с инициализации некоторых элементов: button – это html-элемент button, progressEl – SVG элемент кольцо progress bar, а successEl, errorEl – SVG-элементы галочка и крестик соответственно:

function UIProgressButton( el, options ) {
	this.el = el;
	this.options = extend( {}, this.options );
	extend( this.options, options );
	this._init();
}

UIProgressButton.prototype._init = function() {
	this.button = this.el.querySelector( 'button' );
	this.progressEl = new SVGEl( this.el.querySelector( 'svg.progress-circle' ) );
	this.successEl = new SVGEl( this.el.querySelector( 'svg.checkmark' ) );
	this.errorEl = new SVGEl( this.el.querySelector( 'svg.cross' ) );
	// init events
	this._initEvents();
	// enable button
	this._enable();
}

Добавили функцию SVGEI, которая будет использоваться для того, то бы предоставить SVG-элементы и их paths. Мы кэшируем path и соответственно длину для каждого. Изначально мы «оттягиваем» все paths, управляя значениями свойств strokeDasharray и strokeDashoffset. Позже мы «втянем» их обратно, когда покажем загрузочный круг, или галочку, или крестик. Эту технику хорошо объясняет Jake Archibald в статье Animated line drawing in SVG. Устанавливаем значение stroke-dasharray равное длине path и оттягиваем его. Установив значение stroke-dashoffset также равное его длине, мы больше не видим его. Когда нам нужно будет показать фигуру — установим offset на 0, имитируя рисование фигуры:
function SVGEl( el ) {
	this.el = el;
	// the path elements
	this.paths = [].slice.call( this.el.querySelectorAll( 'path' ) );
	// we will save both paths and its lengths in arrays
	this.pathsArr = new Array();
	this.lengthsArr = new Array();
	this._init();
}

SVGEl.prototype._init = function() {
	var self = this;
	this.paths.forEach( function( path, i ) {
		self.pathsArr[i] = path;
		path.style.strokeDasharray = self.lengthsArr[i] = path.getTotalLength();
	} );
	// undraw stroke
	this.draw(0);
}

// val in [0,1] : 0 - no stroke is visible, 1 - stroke is visible
SVGEl.prototype.draw = function( val ) {
	for( var i = 0, len = this.pathsArr.length; i < len; ++i ){
		this.pathsArr[ i ].style.strokeDashoffset = this.lengthsArr[ i ] * ( 1 - val );
	}
}

Далее, мы должны инициализировать onclick event для нашей кнопки. Сначала кнопка превратится в круг (с помощью добавления класса loading). После окончания анимации, либо будет вызвана callback функция, либо прогресс установится на 100%. В данный момент кнопка отключается (это событие должно бы быть самым первым, однако такой браузер как firefox, например, не сможет удалить transitionend event):

UIProgressButton.prototype._initEvents = function() {
	var self = this;
	this.button.addEventListener( 'click', function() { self._submit(); } );
}

UIProgressButton.prototype._submit = function() {
	classie.addClass( this.el, 'loading' );
	
	var self = this,
		onEndBtnTransitionFn = function( ev ) {
			if( support.transitions ) {
				this.removeEventListener( transEndEventName, onEndBtnTransitionFn );
			}
			
			this.setAttribute( 'disabled', '' );

			if( typeof self.options.callback === 'function' ) {
				self.options.callback( self );
			}
			else {
				self.setProgress(1);
				self.stop();
			}
		};

	if( support.transitions ) {
		this.button.addEventListener( transEndEventName, onEndBtnTransitionFn );
	}
	else {
		onEndBtnTransitionFn();
	}
}

Как только прогресс достигнет 100%, необходимо обновить заполнение загрузочного круга. Затем нужно показать либо галочку, либо крестик. Через некоторое время (options.statusTime) мы уберём все индикаторы состояния и заново включаем кнопку. Обратите внимание, что все переходы контролируются посредством CSS.
UIProgressButton.prototype.stop = function( status ) {
	var self = this,
		endLoading = function() {
			self.progressEl.draw(0);
			
			if( typeof status === 'number' ) {
				var statusClass = status >= 0 ? 'success' : 'error',
					statusEl = status >=0 ? self.successEl : self.errorEl;

				statusEl.draw( 1 );
				// add respective class to the element
				classie.addClass( self.el, statusClass );
				// after options.statusTime remove status and undraw the respective stroke and enable the button
				setTimeout( function() {
					classie.remove( self.el, statusClass );
					statusEl.draw(0);
					self._enable();
				}, self.options.statusTime );
			}
			else {
				self._enable();
			}

			classie.removeClass( self.el, 'loading' );
		};

	// give it a little time (ideally the same like the transition time) so that the last progress increment animation is still visible.
	setTimeout( endLoading, 300 );
}

Кнопка готова!

«We hope you enjoyed this tutorial and find it useful!»
Tags:
Hubs:
+8
Comments 8
Comments Comments 8

Articles