雕虫小艺:Slider控件的制作(Flash)
Slider是常用控件。Adobe官方提供的控件中,fl库所带的控件不好用,便有兄弟《自制Flash Slider滚动条》。Flex库中也提供有Slider控件,但是Flex控件太重量级,动不动就是一二百K的尺寸,很多场合没法用。并且,Flex的Slider也不容易定制,比如,它的highlight track比track要低1像素,这在很多情况下是不合适的。
本文修改MinimalComps项目的Slider控件代码,提供一个小巧的Slider控件解决方案,本方案具有以下优势:
(1)小巧。不依赖于fl控件或Flex库,使用本控件不会让程序尺寸增加多少;
(2)容易定制。很容易定制 highlight track、track和thumb部分;
(3)可数据绑定。
MinimalComps项目提供了简单的Slider控件,该控件源代码为:
* Slider.as
* Keith Peters
* version 0.9.1
*
* Abstract base slider class for HSlider and VSlider.
*
* Copyright (c) 2010 Keith Peters
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.bit101.components
{
import flash.display.DisplayObjectContainer;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.geom.Rectangle;
public class Slider extends Component
{
protected var _handle:Sprite;
private var _back:Sprite;
private var _backClick:Boolean = true;
protected var _value:Number = 0;
protected var _max:Number = 100;
protected var _min:Number = 0;
protected var _orientation:String;
protected var _tick:Number = 1;
public static const HORIZONTAL:String = "horizontal";
public static const VERTICAL:String = "vertical";
/**
* Constructor
* @param orientation Whether the slider will be horizontal or vertical.
* @param parent The parent DisplayObjectContainer on which to add this Slider.
* @param xpos The x position to place this component.
* @param ypos The y position to place this component.
* @param defaultHandler The event handling function to handle the default event for this component (change in this case).
*/
public function Slider(orientation:String = Slider.HORIZONTAL, parent:DisplayObjectContainer = null, xpos:Number = 0, ypos:Number = 0, defaultHandler:Function = null)
{
_orientation = orientation;
super(parent, xpos, ypos);
if(defaultHandler != null)
{
addEventListener(Event.CHANGE, defaultHandler);
}
}
/**
* Initializes the component.
*/
override protected function init():void
{
super.init();
if(_orientation == HORIZONTAL)
{
setSize(100, 10);
}
else
{
setSize(10, 100);
}
}
/**
* Creates and adds the child display objects of this component.
*/
override protected function addChildren():void
{
_back = new Sprite();
_back.filters = [getShadow(2, true)];
addChild(_back);
_handle = new Sprite();
_handle.filters = [getShadow(1)];
_handle.addEventListener(MouseEvent.MOUSE_DOWN, onDrag);
_handle.buttonMode = true;
_handle.useHandCursor = true;
addChild(_handle);
}
/**
* Draws the back of the slider.
*/
protected function drawBack():void
{
_back.graphics.clear();
_back.graphics.beginFill(Style.BACKGROUND);
_back.graphics.drawRect(0, 0, _width, _height);
_back.graphics.endFill();
if(_backClick)
{
_back.addEventListener(MouseEvent.MOUSE_DOWN, onBackClick);
}
else
{
_back.removeEventListener(MouseEvent.MOUSE_DOWN, onBackClick);
}
}
/**
* Draws the handle of the slider.
*/
protected function drawHandle():void
{
_handle.graphics.clear();
_handle.graphics.beginFill(Style.BUTTON_FACE);
if(_orientation == HORIZONTAL)
{
_handle.graphics.drawRect(1, 1, _height - 2, _height - 2);
}
else
{
_handle.graphics.drawRect(1, 1, _width - 2, _width - 2);
}
_handle.graphics.endFill();
positionHandle();
}
/**
* Adjusts value to be within minimum and maximum.
*/
protected function correctValue():void
{
if(_max > _min)
{
_value = Math.min(_value, _max);
_value = Math.max(_value, _min);
}
else
{
_value = Math.max(_value, _max);
_value = Math.min(_value, _min);
}
}
/**
* Adjusts position of handle when value, maximum or minimum have changed.
* TODO: Should also be called when slider is resized.
*/
protected function positionHandle():void
{
var range:Number;
if(_orientation == HORIZONTAL)
{
range = _width - _height;
_handle.x = (_value - _min) / (_max - _min) * range;
}
else
{
range = _height - _width;
_handle.y = _height - _width - (_value - _min) / (_max - _min) * range;
}
}
///////////////////////////////////
// public methods
///////////////////////////////////
/**
* Draws the visual ui of the component.
*/
override public function draw():void
{
super.draw();
drawBack();
drawHandle();
}
/**
* Convenience method to set the three main parameters in one shot.
* @param min The minimum value of the slider.
* @param max The maximum value of the slider.
* @param value The value of the slider.
*/
public function setSliderParams(min:Number, max:Number, value:Number):void
{
this.minimum = min;
this.maximum = max;
this.value = value;
}
///////////////////////////////////
// event handlers
///////////////////////////////////
/**
* Handler called when user clicks the background of the slider, causing the handle to move to that point. Only active if backClick is true.
* @param event The MouseEvent passed by the system.
*/
protected function onBackClick(event:MouseEvent):void
{
if(_orientation == HORIZONTAL)
{
_handle.x = mouseX - _height / 2;
_handle.x = Math.max(_handle.x, 0);
_handle.x = Math.min(_handle.x, _width - _height);
_value = _handle.x / (width - _height) * (_max - _min) + _min;
}
else
{
_handle.y = mouseY - _width / 2;
_handle.y = Math.max(_handle.y, 0);
_handle.y = Math.min(_handle.y, _height - _width);
_value = (_height - _width - _handle.y) / (height - _width) * (_max - _min) + _min;
}
dispatchEvent(new Event(Event.CHANGE));
}
/**
* Internal mouseDown handler. Starts dragging the handle.
* @param event The MouseEvent passed by the system.
*/
protected function onDrag(event:MouseEvent):void
{
stage.addEventListener(MouseEvent.MOUSE_UP, onDrop);
stage.addEventListener(MouseEvent.MOUSE_MOVE, onSlide);
if(_orientation == HORIZONTAL)
{
_handle.startDrag(false, new Rectangle(0, 0, _width - _height, 0));
}
else
{
_handle.startDrag(false, new Rectangle(0, 0, 0, _height - _width));
}
}
/**
* Internal mouseUp handler. Stops dragging the handle.
* @param event The MouseEvent passed by the system.
*/
protected function onDrop(event:MouseEvent):void
{
stage.removeEventListener(MouseEvent.MOUSE_UP, onDrop);
stage.removeEventListener(MouseEvent.MOUSE_MOVE, onSlide);
stopDrag();
}
/**
* Internal mouseMove handler for when the handle is being moved.
* @param event The MouseEvent passed by the system.
*/
protected function onSlide(event:MouseEvent):void
{
var oldValue:Number = _value;
if(_orientation == HORIZONTAL)
{
_value = _handle.x / (width - _height) * (_max - _min) + _min;
}
else
{
_value = (_height - _width - _handle.y) / (height - _width) * (_max - _min) + _min;
}
if(_value != oldValue)
{
dispatchEvent(new Event(Event.CHANGE));
}
}
///////////////////////////////////
// getter/setters
///////////////////////////////////
/**
* Sets / gets whether or not a click on the background of the slider will move the handler to that position.
*/
public function set backClick(b:Boolean):void
{
_backClick = b;
invalidate();
}
public function get backClick():Boolean
{
return _backClick;
}
/**
* Sets / gets the current value of this slider.
*/
public function set value(v:Number):void
{
_value = v;
correctValue();
positionHandle();
// This has the potential to set up an infinite loop if two sliders are listening to each other's change
// events to sync to each other. Tentitively trying it out...
dispatchEvent(new Event(Event.CHANGE));
}
public function get value():Number
{
return Math.round(_value / _tick) * _tick;
}
/**
* Gets / sets the maximum value of this slider.
*/
public function set maximum(m:Number):void
{
_max = m;
correctValue();
positionHandle();
}
public function get maximum():Number
{
return _max;
}
/**
* Gets / sets the minimum value of this slider.
*/
public function set minimum(m:Number):void
{
_min = m;
correctValue();
positionHandle();
}
public function get minimum():Number
{
return _min;
}
/**
* Gets / sets the tick value of this slider. This round the value to the nearest multiple of this number.
*/
public function set tick(t:Number):void
{
_tick = t;
}
public function get tick():Number
{
return _tick;
}
}
}
然而,MinimalComps的Slider控件的外观(http://www.minimalcomps.com/?page_id=5)很简单,见下图。
由于外观直接写死了,没办法换皮肤,因此,难以用于生产环境。
* Slider.as
* Keith Peters
* version 0.9.10
*
* Abstract base slider class for HSlider and VSlider.
*
* Copyright (c) 2011 Keith Peters
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* version 1.0
* By. xiaotie@geblab.com
* Changes: [2012-02-11] make it skinable
*/
package geb.controls
{
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.geom.Rectangle;
import geb.common.BaseComponent;
[Event(name="change", type="flash.events.Event")]
public class Slider extends BaseComponent
{
[Bindable]
public var thumb:Sprite;
[Bindable]
public var track:Sprite;
[Bindable]
public var trackHighlight:Sprite;
[Bindable]
public var orientation:String = "horizontal";
public var trackClickable:Boolean = true;
[Bindable]
public var ignoreThumbSize:Boolean = false;
protected var _value:Number = 0;
protected var _max:Number = 100;
protected var _min:Number = 0;
protected var _tick:Number = 0.01;
public static const HORIZONTAL:String = "horizontal";
public static const VERTICAL:String = "vertical";
/**
* Initializes the component.
*/
override protected function init():void
{
super.init();
if(orientation == HORIZONTAL)
{
setSize(width, height);
}
else
{
setSize(width, height);
}
}
/**
* Creates and adds the child display objects of this component.
*/
override protected function addChildren():void
{
super.addChildren();
}
/**
* Adjusts value to be within minimum and maximum.
*/
protected function correctValue():void
{
if(_max > _min)
{
_value = Math.min(_value, _max);
_value = Math.max(_value, _min);
}
else
{
_value = Math.max(_value, _max);
_value = Math.min(_value, _min);
}
}
private function get thumbSize():Number
{
if(thumb == null || ignoreThumbSize == true) return 0;
else if(orientation == HORIZONTAL)
{
return thumb.width;
}
else
{
return thumb.height;
}
}
/**
* Adjusts position of handle when value, maximum or minimum have changed.
* TODO: Should also be called when slider is resized.
*/
protected function updateDislpay():void
{
var range:Number;
var highlightRange:Number;
var pos:Number;
var ts:Number = thumbSize;
if(orientation == HORIZONTAL)
{
range = _width - ts;
pos = (_value - _min) / (_max - _min) * range;
highlightRange = pos + ts * 0.5;
if(thumb!=null)
{
thumb.x = pos - (ignoreThumbSize ? thumb.width * 0.5 : 0);
thumb.y = 0.5 * (this.height - thumb.height);
}
if(trackHighlight != null)
{
trackHighlight.width = pos + 0.5 * ts;
trackHighlight.height = Math.min(height,trackHighlight.height);
trackHighlight.y = Math.max(0, 0.5 * (height - trackHighlight.height));
if(trackHighlight is BaseComponent)
{
BaseComponent(trackHighlight).invalidate(false);
}
}
if(track != null)
{
track.width = width;
track.height = Math.min(height,track.height);
track.y = Math.max(0, 0.5 * (height - track.height));
if(track is BaseComponent)
{
BaseComponent(track).invalidate(false);
}
}
}
else
{
range = _height - ts;
pos = height - ts - (_value - _min) / (_max - _min) * range;
highlightRange = pos + ts * 0.5;
if(thumb != null)
{
thumb.x = 0.5 * (this.width - thumb.width);
thumb.y = pos - (ignoreThumbSize ? thumb.height * 0.5 : 0);
}
if(trackHighlight != null)
{
trackHighlight.width = Math.min(width,trackHighlight.width);
trackHighlight.y = pos + 0.5 * ts;
trackHighlight.height = height - trackHighlight.y;
trackHighlight.x = Math.max(0, 0.5 * (width - trackHighlight.width));
if(trackHighlight is BaseComponent)
{
BaseComponent(trackHighlight).invalidate(false);
}
}
if(track != null)
{
track.height = height;
track.width = Math.min(width,track.width);
track.x = Math.max(0, 0.5 * (width - track.width));
if(track is BaseComponent)
{
BaseComponent(track).invalidate(false);
}
}
}
if(track != null && this.contains(track) == false)
{
addChild(track);
if(trackClickable)
{
track.useHandCursor = true;
track.buttonMode = true;
track.addEventListener(MouseEvent.MOUSE_DOWN, onBackClick);
}
else
{
track.removeEventListener(MouseEvent.MOUSE_DOWN, onBackClick);
}
}
if(trackHighlight != null && this.contains(trackHighlight) == false)
{
addChild(trackHighlight);
if(trackClickable)
{
trackHighlight.useHandCursor = true;
trackHighlight.buttonMode = true;
trackHighlight.addEventListener(MouseEvent.MOUSE_DOWN, onBackClick);
}
else
{
trackHighlight.removeEventListener(MouseEvent.MOUSE_DOWN, onBackClick);
}
}
if(thumb != null && this.contains(thumb) == false)
{
thumb.addEventListener(MouseEvent.MOUSE_DOWN, onDrag);
thumb.buttonMode = true;
thumb.useHandCursor = true;
addChild(thumb);
}
}
///////////////////////////////////
// public methods
///////////////////////////////////
/**
* Draws the visual ui of the component.
*/
override public function draw():void
{
super.draw();
updateDislpay();
}
/**
* Convenience method to set the three main parameters in one shot.
* @param min The minimum value of the slider.
* @param max The maximum value of the slider.
* @param value The value of the slider.
*/
public function setSliderParams(min:Number, max:Number, value:Number):void
{
this.minimum = min;
this.maximum = max;
this.value = value;
}
///////////////////////////////////
// event handlers
///////////////////////////////////
/**
* Handler called when user clicks the background of the slider, causing the handle to move to that point. Only active if backClick is true.
* @param event The MouseEvent passed by the system.
*/
protected function onBackClick(event:MouseEvent):void
{
var ts:Number = thumbSize;
var pos:Number;
if(orientation == HORIZONTAL)
{
pos = mouseX - ( ignoreThumbSize ? 0 : thumb.width * 0.5);
pos = Math.max(pos, 0);
pos = Math.min(pos, _width - ts);
value = pos / (width - ts) * (_max - _min) + _min;
}
else
{
pos = mouseY - ( ignoreThumbSize ? 0 : thumb.height * 0.5);
pos = Math.max(pos, 0);
pos = Math.min(pos, _height - ts);
value = (_height - ts - pos) / (height - ts) * (_max - _min) + _min;
}
dispatchEvent(new Event(Event.CHANGE));
}
/**
* Internal mouseDown handler. Starts dragging the handle.
* @param event The MouseEvent passed by the system.
*/
protected function onDrag(event:MouseEvent):void
{
stage.addEventListener(MouseEvent.MOUSE_UP, onDrop);
stage.addEventListener(MouseEvent.MOUSE_MOVE, onSlide);
if(orientation == HORIZONTAL)
{
var xStart:Number = ignoreThumbSize? - thumb.width * 0.5 : 0;
thumb.startDrag(false, new Rectangle(xStart, thumb.y, _width - thumbSize, 0));
}
else
{
var yStart:Number = ignoreThumbSize? - thumb.height * 0.5 : 0;
thumb.startDrag(false, new Rectangle(thumb.x, yStart, 0, _height - thumbSize));
}
}
/**
* Internal mouseUp handler. Stops dragging the handle.
* @param event The MouseEvent passed by the system.
*/
protected function onDrop(event:MouseEvent):void
{
stage.removeEventListener(MouseEvent.MOUSE_UP, onDrop);
stage.removeEventListener(MouseEvent.MOUSE_MOVE, onSlide);
stopDrag();
}
/**
* Internal mouseMove handler for when the handle is being moved.
* @param event The MouseEvent passed by the system.
*/
protected function onSlide(event:MouseEvent):void
{
var oldValue:Number = _value;
var pos:Number;
if(orientation == HORIZONTAL)
{
pos = ignoreThumbSize ? (thumb.x + thumb.width * 0.5) : thumb.x;
value = pos / (width - thumbSize) * (_max - _min) + _min;
}
else
{
pos = ignoreThumbSize ? (thumb.y + thumb.height * 0.5) : thumb.y;
value = (height - thumbSize - pos) / (height - thumbSize) * (_max - _min) + _min;
}
if(value != oldValue)
{
dispatchEvent(new Event(Event.CHANGE));
}
}
///////////////////////////////////
// getter/setters
///////////////////////////////////
/**
* Sets / gets whether or not a click on the background of the slider will move the handler to that position.
*/
public function set backClick(b:Boolean):void
{
trackClickable = b;
invalidate();
}
public function get backClick():Boolean
{
return trackClickable;
}
/**
* Sets / gets the current value of this slider.
*/
[Bindable]
public function set value(v:Number):void
{
_value = v;
correctValue();
updateDislpay();
}
[Bindable]
public function get value():Number
{
return Math.round(_value / _tick) * _tick;
}
/**
* Gets the value of the slider without rounding it per the tick value.
*/
public function get rawValue():Number
{
return _value;
}
[Bindable]
/**
* Gets / sets the maximum value of this slider.
*/
public function set maximum(m:Number):void
{
_max = m;
correctValue();
updateDislpay();
}
public function get maximum():Number
{
return _max;
}
[Bindable]
/**
* Gets / sets the minimum value of this slider.
*/
public function set minimum(m:Number):void
{
_min = m;
correctValue();
updateDislpay();
}
public function get minimum():Number
{
return _min;
}
/**
* Gets / sets the tick value of this slider. This round the value to the nearest multiple of this number.
*/
public function set tick(t:Number):void
{
_tick = t;
}
public function get tick():Number
{
return _tick;
}
}
}
BaseComponent类见我的其它博文:《只学一点点:我的技术学习策略 》(http://www.cnblogs.com/xiaotie/archive/2011/12/10/2283384.html)和《使用铁哥SmartFlash快速开发方案:66行代码搞定抽奖程序!》(http://www.cnblogs.com/xiaotie/archive/2011/06/15/2081386.html)。
下面演示Slider控件的使用:
<common:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:local="*"
xmlns:common="geb.common.*"
xmlns:gc="geb.controls.*"
xmlns:shapes="geb.shapes.*"
xmlns:containers="geb.containers.*"
fillMode="false"
width="300" height="300"
>
<fx:Declarations>
<!-- 定义slider1外观的三个形状 -->
<shapes:Rectangle id="trackHighlight" color="0x125894" height="5" />
<shapes:Rectangle id="track" color="0x343434" height="5" />
<shapes:Ellipse id="thumb" color="0x999999" width="10" height="10" />
<!-- 定义slider3外观的三个形状 -->
<shapes:Rectangle id="trackHighlight2" color="0x125894" width="5" />
<shapes:Rectangle id="track2" color="0x343434" width="5" />
<shapes:Ellipse id="thumb2" color="0x999999" width="10" height="10" />
<!-- 定义slider2外观的三个图片 -->
<gc:Image id="thumbImage" height="10" width="10" source="@Embed(source='assets/sliderAssets/thumbSkin.png')" />
<gc:Image id="trackHighlightImage" height="4" width="10" source="@Embed(source='assets/sliderAssets/trackHighlightSkin.png')" />
<gc:Image id="trackImage" height="4" width="10" source="@Embed(source='assets/sliderAssets/trackSkin.png')" />
</fx:Declarations>
<shapes:Rectangle width="{width}" height="{height}" color="0xEEEEEE" />
<gc:Slider id="slider1" x="50" y="50" width="70" height="20"
trackHighlight="{trackHighlight}"
track="{track}"
thumb="{thumb}" ignoreThumbSize="false">
</gc:Slider>
<gc:Label x="130" y="50" autoSize="true" text="{slider1.value}" />
<gc:Slider id="slider2" x="50" y="100" width="70" height="20"
trackHighlight="{trackHighlightImage}" value="20" maximum="50"
track="{trackImage}"
thumb="{thumbImage}" ignoreThumbSize="true">
</gc:Slider>
<gc:Label x="130" y="100" autoSize="true" text="{slider2.value}" />
<gc:Slider id="slider3" x="50" y="150" width="10" height="70"
trackHighlight="{trackHighlight2}" orientation="vertical"
track="{track2}"
thumb="{thumb2}" ignoreThumbSize="true">
</gc:Slider>
</common:Application>
在线演示:
从上到下,可以看到三个Slider:
第一个Slider的皮肤由三个形状组成,track是黑色矩形,highlightTrack是蓝色矩形,thumb是圆。highlightTrack和track都规定了高度,这个高度,就是显示出来的highlightTrack和track高度,如果想让highlightTrack和track变粗,改变它的高度即可。Thumb规定了长和宽,如果修改thumb的大小,改变它的长宽即可,当然,如果长宽不等,则就是椭圆而非圆形了。
第二个Slider是由三个图片组成的,以应对复杂精致的皮肤,所使用的三个图片如下:
使用不同的图片,便是不同的Slider外观。
第二个Slider和第一个Slider还有个不同,就是ignoreThumbSize属性为true。第一个Slider的thumb是圆形,把thumb拖到两端可以发现,thumb遮盖不住两端(如果是方形的thumb则没这个问题),因此,引入了ignoreThumbSize字段。ignoreThumbSize字段默认为false,当ignoreThumbSize为true时,以thumb的中线为定位基准,这样便可避免当thumb是圆形时移动到两端,盖不住track两端的情况。
第三个Slider的orientation设为vertical,因此,便是竖着的Slider。
Slider可供数据绑定,比如:
<gc:Label x="130" y="50" autoSize="true" text="{slider1.value}" />
通过这样简单的代码便可使slider1的值改变时,对应的Lable的文本也发生变化。
是不是既小巧、又易用又灵活呢?