Пишем круглый прогресс-бар.
Запись от ZackMercury размещена 20.03.2015 в 22:47
Обновил(-а) Zebestov 24.03.2015 в 02:01 (Исправление ошибки в сказке.)
Обновил(-а) Zebestov 24.03.2015 в 02:01 (Исправление ошибки в сказке.)
Ну,
Сам класс:
package com.zackmercury.tools { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Graphics; import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; import flash.geom.Rectangle; /** * Запрещаю продавать, разрешаю делиться и выкладывать в интернет(разумеется, с указанием автора и источника) * Разрешаю совершенствовать. * * @author ZackMercury */ public class CircularBar extends Bitmap { private var _percent:Number; private var _width:int; private var _height:int; private var _shape:Shape; private var _canvas:Graphics; private var _step:Number; private var _color:uint; private var _thickness:int; private var scale:Number; private var halfWidth:int; private var halfHeight:int; private var halfWidthWithoutThickness:int; private var halfHeightWidthoutThickness:int; /** * * @param width The width of the circular bar. * @param height The height of the circular bar. * @param thickness Thickness of line that fills the bar. * @param color Color of line that fills the bar. * @param step Step is a step of a loops that drawing a circle(smaller == smoother). * @param value Value is a default value of the bar. */ public function CircularBar(width:int = 100, height:int = 100, thickness:int = 30, color:uint = 0x66FF66, step:Number = 0.2, value:Number = 0.7) { super(new BitmapData(width, height, true, 0x00000000), "auto", true); _width = width; _height = height; _percent = value; _step = step; _shape = new Shape(); _canvas = _shape.graphics; _color = color; if(thickness >= 0 && thickness <= int(width / 2)) _thickness = thickness; else _thickness = int(width / 2); halfWidth = int(_width / 2); halfHeight = int(_width / 2); halfWidthWithoutThickness = int((_width - 2 * _thickness) / 2); halfHeightWidthoutThickness = int((_width - 2 * _thickness) / 2); scale = _height / _width; update(); } private function update():void { _canvas.clear(); _canvas.beginFill(_color); var endAngle:Number = _percent * 6.283185307179586; _canvas.moveTo(halfWidth + Math.cos(endAngle) * halfWidth, halfHeight + Math.sin(endAngle) * halfHeight); for (var angle1:Number = endAngle; angle1 > 0; angle1 -= _step) { _canvas.lineTo(halfWidth + Math.cos(angle1) * halfWidth, halfHeight + Math.sin(angle1) * halfHeight); } _canvas.lineTo(halfWidth + Math.cos(0) * halfWidth, halfHeight + Math.sin(0) * halfHeight); for (var angle2:Number = 0; angle2 < endAngle; angle2 += _step) { _canvas.lineTo(halfWidth + Math.cos(angle2) * halfWidthWithoutThickness, halfHeight + Math.sin(angle2) * halfHeightWidthoutThickness); } _canvas.lineTo(halfWidth + Math.cos(endAngle) * halfWidthWithoutThickness, halfHeight + Math.sin(endAngle) * halfHeightWidthoutThickness); _canvas.endFill(); _shape.scaleY = scale; bitmapData.fillRect(new Rectangle(0, 0, bitmapData.width, bitmapData.height), 0x00000000); bitmapData.draw(_shape, _shape.transform.matrix); dispatchEvent(new Event(Event.CHANGE)); } public function set value(value:Number):void { if (value < 0 || value > 1) return; _percent = value; update(); } public function get value():Number { return _percent; } } }
Параметры, идущие в конструкторе документированы с помощью ASDoc
Демка:
Код демки:
package { import com.zackmercury.bloodrain.App; import com.zackmercury.tools.CircularBar; import flash.display.Sprite; import flash.events.Event; import flash.text.TextField; import flash.utils.getTimer; public class Main extends Sprite { private var _bar:CircularBar; private var _bar2:CircularBar; private var _bar3:CircularBar; public function Main() { super(); stage ? init() : addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { if (hasEventListener(Event.ADDED_TO_STAGE)) removeEventListener(Event.ADDED_TO_STAGE, init); //entry point _bar = new CircularBar(200, 200, 50, 0x11CC22, 0.1); addChild(_bar); var _tf:TextField = new TextField(); _tf.text = "width = 200, height = 200, thickness = 50, color = 0x11CC22, step = 0.1"; _tf.multiline = true; _tf.wordWrap = true; _tf.y = _bar.height; _tf.x = (_bar.width - _tf.width) / 2; addChild(_tf); _bar2 = new CircularBar(150, 80, 70, 0xCCCC00, 0.1); addChild(_bar2); _bar2.x = _bar.x + _bar.width + 20; var _tf2:TextField = new TextField(); _tf2.text = "width = 150, height = 80, thickness = 70, color = 0xCCCC00, 0.1"; _tf2.multiline = true; _tf2.wordWrap = true; _tf2.y = _bar2.height; _tf2.x = _bar2.x + (_bar2.width - _tf2.width) / 2; addChild(_tf2); _bar3 = new CircularBar(100, 100, 50, 0xCC2222, 0.2); addChild(_bar3); _bar3.x = _bar2.x + _bar2.width + 20; var _tf3:TextField = new TextField(); _tf3.text = "width = 100, height = 100, thickness = 50, color = 0xCC2222, 0.2"; _tf3.multiline = true; _tf3.wordWrap = true; _tf3.y = _bar3.height; _tf3.x = _bar3.x + (_bar3.width - _tf3.width) / 2; addChild(_tf3); addEventListener(Event.ENTER_FRAME, update); } private function update(e:Event = null):void { var value:Number = (1 + Math.sin(getTimer() / 2000)) / 2; _bar.value = value; _bar2.value = value; _bar3.value = value; } } }
Жил-был прогресс-бар. Но был он прямоугольным и некрасивым.
Был он таким много лет, пока люди не додумались скрутить его в трубочку.
Вот так вот просто взять и скрутить.
Ладно, давайте вспомним, что такое круг и как его нарисовать средствами флеш?
Вы тоже об этом подумали? Graphics.drawCircle()? Вы почти у цели.
Ладно, как же нарисовать круг с помощью lineTo?
Как нам описать круг в пространстве по точкам?
В этом нам поможет замечательный цикл for и число 6.283185307179586, что приблизительно равно PI*2.
var width, height:int; width = height = 100; var shape:Shape = new Shape(); var _canvas = shape.graphics; with(_canvas) { beginFill(0xFF00FF); moveTo( width, height / 2 ); //именно в этой точке начинается отсчёт градусов for(var angle:Number = 0; angle <= 6.283185307179586; angle += 0.1) { lineTo(width / 2 + Math.cos(angle) * width / 2, height / 2 + Math.sin(angle) * height / 2); } }
Но для отображения прогресса, нам не нужен целый круг, верно? Нам нужен лишь его кусок.
Для этого мы делаем вот так:
var width, height:int; width = height = 100; var percent:Number = 0.75; var shape:Shape = new Shape(); var _canvas = shape.graphics; with(_canvas) { beginFill(0xFF00FF); moveTo( width/2, height / 2 ); //начинаем из центра, абы выглядело как нам надо for(var angle:Number = 0; angle <= percent * 6.283185307179586; angle += 0.1) { lineTo(width / 2 + Math.cos(angle) * width / 2, height / 2 + Math.sin(angle) * height / 2); } }
Но нам-то нужна полоска!
Что-ж, выходит, для этого нам нужно нарисовать внешнюю её границу, срезы и внутреннюю границу.
Внешняя и внутренняя границы имеют формы круга, а значит, делать их нужно циклами, проходящими от нуля до 2Пи и обратно.
Пишем вот так:
var width, height:int; width = height = 100; var percent:Number = 0.75; var shape:Shape = new Shape(); var _canvas = shape.graphics; with(_canvas) { beginFill(0xFF00FF); moveTo(width / 2 + Math.cos(0) * width / 2, height / 2 + Math.sin(0) * height / 2); //двигаем курсор в нулевую позицию уже не руками for(var angle:Number = 0; angle <= percent * 6.283185307179586; angle += 0.1) { lineTo(width / 2 + Math.cos(angle) * width / 2, height / 2 + Math.sin(angle) * height / 2); } lineTo(width / 2 + Math.cos(percent * 6.283185307179586) * width / 2, height / 2 + Math.sin(percent * 6.283185307179586) * height / 2); // Вручную делаем скос, ибо цикл сам не дойдёт до него. for(var angle:Number = percent * 6.283185307179586; angle > 0 ; angle -= 0.1) { lineTo(width / 2 + Math.cos(angle) * width / 3, height / 2 + Math.sin(angle) * height / 3); } lineTo(width / 2 + Math.cos(0) * width / 3, height / 2 + Math.sin(0) * height / 3); // Вручную делаем скос, ибо цикл сам не дойдёт до него. }
Вуаля!
Круговые прогресс-бары рулят.
И в конец добавлю про то, что класс хоть и использует шейп, сам не является векторной графикой, он, как крем для производительности, наследует Bitmap и обновляет картинку с векторного шейпа только если его трогать за value, рисуя её в битмапдату наследуемого Bitmap.
Всего комментариев 28
Комментарии
21.03.2015 16:27 | |
Ещё классно, если можно было бы передать для этого прогресс бар какой нибудь визуальный объект, на который автоматически наложится такая циркулярная маска.
|
21.03.2015 19:56 | |
samana, да, я тоже об этом думал. Но это уже сверх логики самого прогресс-бара, и кому надо пусть расширяют до SkinnedCircularBar, и реализуют и заднюю подкладку, и маску самого прогресса(например, с скруглёнными краями начала и конца).
Правда так то лучше будет сделать некоторые поля protected. Кстати, думал насчёт программного скругления краёв, абы выглядело красиво, но с реализацией возникли сложности. Хотя примерно я представляю, как должно быть: 1) Делаем цикл 0.5 PI косинус синус умножаем на половину толщины бара.(начинаем с верху, т.е. цикл с 1.5PI до 2PI) 2) Вычитаем потраченный угол на скругление из процента, дважды(на конце полоски также), тут операции с шириной и высотой, а также толщиной. 3) Ведём ведём сколько надо проценту, из которого уже вычтены затраты на скругление. 4) А вот тут немного застопорился, ведь угол уже не под прямым углом то... Хорошо бы и первый угол тоже реализовать не вручную(ставя в верх или вправо) а в зависимости от положения на кругу, т.е. процента, т.е. текущего градуса. И далее, возникает ещё одна проблемка по этому алгоритму, когда процент 0, к примеру, полукруг всё равно будет рисоваться, таким образом нужно как-то резать и сам полукруг. |
|
Обновил(-а) ZackMercury 21.03.2015 в 20:15
|
21.03.2015 20:41 | |
21.03.2015 21:41 | |
Ага, действительно. Сделаю вот как: ширина пусть задаёт масштаб для шейпа и высоты, и ширины, далее перед отрисовкой устанавливаем шейпу высоту какую надо.
|
23.03.2015 20:38 | |
Если знать о полярных координатах, то второй цикл не нужен.
И еще: Цитата:
Демка:
[Вложение #470 - не найдено] |
24.03.2015 20:13 | |
Вот теперь демку видно! Красиво, молодец!
|
25.03.2015 16:37 | |
Если заменить lineTo на curveTo можно кардинально сократить число проходов по циклу.
https://github.com/alatarus/gauges/b.../DonutSlice.as |
26.03.2015 05:06 | |
Спорить не стану, это личное предпочтение, которое, возможно, найдет отклик. Мне лишь кажется логичным всегда знать, каков внешний и внутренний радиус кольца.
|
26.03.2015 14:07 | |
Цитата:
Я просто думал, curveTo - это обёртка над lineTo.
|
27.03.2015 11:21 | |
ZackMercury, думаю, да.
moveTo(prev_position_outer), curveTo(new_position_outer), moveTo(prev_position_inner), curveTo(new_position_inner), |
28.03.2015 20:36 | |
dimarik, при таком раскладе заливка исчезает.
Даже при рисовании обычного круга со смещением курсора в его же координаты. var shp:Shape = new Shape(); var last:Point = new Point(); with(shp.graphics) { beginFill(0x00FFFF); lineStyle(2, 0); last.x = Math.cos(0)*50; last.y = Math.sin(0)*50; for(var ang:Number = 0; ang < 6.28; ang += 0.1) { moveTo(last.x, last.y); last.x = Math.cos(ang)*50; last.y = Math.sin(ang)*50; lineTo(last.x, last.y); } moveTo(last.x, last.y); lineTo(Math.cos(0)*50, Math.sin(0)*50); } addChild(shp); shp.x = shp.y = 50; var shp:Shape = new Shape(); with(shp.graphics) { beginFill(0xFFFF00); lineStyle(2, 0); moveTo(0, 0); lineTo(100, 100); moveTo(100,100); lineTo(0, 100); moveTo(0, 100); lineTo(0, 0); endFill(); } addChild(shp); Ну да ладно, пока что попробую оптимизировать с помощью 2-ух вещей - curveTo и потом ещё drawCircle + вырезание кусков. |
|
Обновил(-а) ZackMercury 30.03.2015 в 01:50
|
30.03.2015 20:57 | |
ZackMercury, если рисовать по нарисованному, оно стирается. Сделай так, чтобы новая заливка образовывала новый контур.
|
31.03.2015 10:42 | |
31.03.2015 16:53 | |
dimarik, так бы и написали, что сделать не одной заливкой а на каждый степ по заливке, а то загадками говорите
Сейчас проведу тест по скорости. |
|
Обновил(-а) ZackMercury 31.03.2015 в 18:27
|
31.03.2015 18:27 | |
UPD: Результаты вполне ожидаемые. (Тестил в релизной, по клику, и стендалон плеер и браузер - одинаково почти)
Вариант с двумя циклами: 1768; Вариант с полярными координатами: 7370; Код теста package com.zackmercury { import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.display.Graphics; import flash.geom.Point; import flash.text.TextField; import flash.utils.getTimer; /** * ... * @author ZackMercury */ public class Main extends Sprite { private var _tf:TextField = new TextField(); private var shp2:Shape; private var shp:Shape; private var rOuter:int = 50; private var rInner:int = 30; private var step:Number = Math.PI / 16; public function Main() { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } public function init(e:Event = null):void { if (hasEventListener(Event.ADDED_TO_STAGE)) removeEventListener(Event.ADDED_TO_STAGE, init); //entry var btn:MyButton = new MyButton("Test"); btn.onClick = beginTest; addChild(btn); addChild(_tf); _tf.text = "Results here."; _tf.x = _tf.y = 100; shp = new Shape(); shp2 = new Shape(); addChild(shp); addChild(shp2); shp.x = 200; shp2.x = 400; shp.y = shp2.y = rOuter; } private function beginTest(e:MouseEvent):void { var time1:int = getTimer(); for (var i:int = 0; i < 100000; i++) { var g:Graphics = shp.graphics; g.clear(); var percent = (1 + Math.sin(i / 1000)) / 2; var reqAngle:Number = percent * 6.283185307179586; g.beginFill(0xFFAA00); g.moveTo(Math.cos(0) * rOuter, Math.sin(0) * rOuter); for (var ang:Number = 0; ang < reqAngle; ang += step) { g.lineTo(Math.cos(ang) * rOuter, Math.sin(ang) * rOuter); } g.lineTo(Math.cos(reqAngle) * rOuter, Math.sin(reqAngle) * rOuter); for (var ang:Number = reqAngle; ang > 0; ang -= step) { g.lineTo(Math.cos(ang) * rInner, Math.sin(ang) * rInner); } g.lineTo(Math.cos(0) * rInner, Math.sin(0) * rInner); } var time2:int = getTimer(); for (var i:int = 0; i < 100000; i++) { var g:Graphics = shp2.graphics; g.clear(); var percent = (1 + Math.sin(i / 1000)) / 2; var reqAngle:Number = percent * 6.283185307179586; var p1:Point; var p2:Point; var ang:Number; for (ang = 0; ang < reqAngle; ang += step) { g.beginFill(0xF08801); p1 = Point.polar(rInner, ang); g.moveTo(p1.x, p1.y); p2 = Point.polar(rOuter, ang); g.lineTo(p2.x, p2.y); p2 = Point.polar(rOuter, ang + step); g.lineTo(p2.x, p2.y); p2 = Point.polar(rInner, ang + step); g.lineTo(p2.x, p2.y); g.lineTo(p1.x, p1.y); g.endFill(); } } var time3:int = getTimer(); _tf.text = "1: " + (time2 - time1) + "; 2: " + (time3 - time2) + ";"; } } } package com.zackmercury { import flash.display.Graphics; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.ColorTransform; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.text.TextFormat; import flash.text.TextFormatAlign; /** * ... * @author ZackMercury */ public class MyButton extends Sprite { private static const NORMAL:ColorTransform = new ColorTransform(1, 1, 1, 1); private static const PRESSED:ColorTransform = new ColorTransform(0.7, 0.7, 0.7); private static const OVER:ColorTransform = new ColorTransform(1.1, 1.1, 1.1); private var _caption:String; private var _buttonText:TextField; private var _holded:Boolean = false; private var _callback:Function; public function MyButton(caption:String) { super.mouseChildren = false; drawButton(); this.caption = caption; addEventListener(MouseEvent.MOUSE_DOWN, mDown); addEventListener(MouseEvent.MOUSE_UP, mUp); addEventListener(MouseEvent.MOUSE_OVER, mOver); addEventListener(MouseEvent.MOUSE_OUT, mOut); } public function set onClick(onClick:Function):void { _callback = onClick; } public function mDown(e:MouseEvent = null):void { this.transform.colorTransform = PRESSED; _holded = true; } public function mUp(e:MouseEvent = null):void { this.transform.colorTransform = OVER; if (_holded) _callback(e); _holded = false; } public function mOver(e:MouseEvent = null):void { if (!_holded) this.transform.colorTransform = OVER; } public function mOut(e:MouseEvent = null):void { if (!_holded) this.transform.colorTransform = NORMAL; else { _holded = false; this.transform.colorTransform = NORMAL; } } public function set caption(caption:String):void { _caption = caption; if (!_buttonText) { _buttonText = new TextField(); _buttonText.selectable = false; _buttonText.defaultTextFormat = new TextFormat("Consolas", 10, 0xFFFFFF, false, false, false, null, null, TextFormatAlign.CENTER); _buttonText.autoSize = TextFieldAutoSize.CENTER; addChild(_buttonText); } _buttonText.text = _caption; } private function drawButton():void { var g:Graphics = this.graphics; g.beginFill(0x22AA22); g.drawRect(0, 0, 100, 20); g.endFill(); } public function get caption():String { return _caption; } } } |
08.04.2015 17:46 | |
Я и не подозревал, что нынче о такой фигне блоги пишут
Лет 7 назад написано на curveTo. От построения отрезками я, разумеется, отказался из сображений производительности. http://188.226.221.96/SOUNDSTAGE/drawSectors.swf |
08.04.2015 19:20 | |
Мне вот градусы для восприятия как-то вот в разы удобнее
Код - декомпильнул чтоли? |
20.04.2015 17:11 | |
Последние записи от ZackMercury
- Вывод формулы для бесконечного цикла. (11.01.2019)
- Как заменить цикл на формулу. (10.01.2019)
- Конечные и бесконечные суммы, Ч. 1 (08.01.2019)
- Как легко запомнить тригонометрические функции (07.01.2019)
- Движение по треугольнику, квадрату, пентагону, хексагону, ... (05.01.2019)