//////////////////////////////////////////////////////////////////////////////// // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// package spark.skins.mobile.supportClasses { import flash.display.DisplayObject; import flash.geom.Point; import flash.text.TextLineMetrics; import mx.core.ILayoutElement; import mx.core.UITextField; import mx.core.mx_internal; import mx.events.FlexEvent; import spark.components.Group; import spark.components.IconPlacement; import spark.components.ResizeMode; import spark.components.supportClasses.ButtonBase; import spark.components.supportClasses.StyleableTextField; import spark.primitives.BitmapImage; use namespace mx_internal; /* ISSUES: - should we support textAlign (if not, remove extra code) */ /** * ActionScript-based skin for mobile applications. The skin supports * icon and iconPlacement. It uses FXG classes to * implement the vector drawing. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public class ButtonSkinBase extends MobileSkin { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 * */ public function ButtonSkinBase() { super(); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * iconDisplay skin part. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ private var iconChanged:Boolean = false; private var iconInstance:Object; // Can be either DisplayObject or BitmapImage private var iconHolder:Group; // Needed when iconInstance is a BitmapImage private var _icon:Object; // The currently set icon, can be Class, DisplayObject, URL /** * @private * Flag that is set when the currentState changes from enabled to disabled */ private var enabledChanged:Boolean = false; /** * labelDisplay skin part. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ public var labelDisplay:StyleableTextField; /** * If true, then create the iconDisplay using the icon style. * * @default true * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected var useIconStyle:Boolean = true; /** * If true, then the labelDisplay and iconDisplay are centered. * * @default true * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected var useCenterAlignment:Boolean = true; private var _hostComponent:ButtonBase; /** * @copy spark.skins.spark.ApplicationSkin#hostComponent */ public function get hostComponent():ButtonBase { return _hostComponent; } /** * @private */ public function set hostComponent(value:ButtonBase):void { _hostComponent = value; } //-------------------------------------------------------------------------- // // Layout variables // //-------------------------------------------------------------------------- protected var layoutBorderSize:uint; protected var layoutGap:int; /** * Left padding for icon or labelDisplay. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected var layoutPaddingLeft:int; /** * Right padding for icon or labelDisplay. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected var layoutPaddingRight:int; /** * Top padding for icon or labelDisplay. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected var layoutPaddingTop:int; /** * Bottom padding for icon or labelDisplay. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected var layoutPaddingBottom:int; //-------------------------------------------------------------------------- // // Overridden methods // //-------------------------------------------------------------------------- /** * @private */ override public function set currentState(value:String):void { var isDisabled:Boolean = currentState && currentState.indexOf("disabled") >= 0; super.currentState = value; if (isDisabled != currentState.indexOf("disabled") >= 0) { enabledChanged = true; invalidateProperties(); } } /** * @private */ override protected function createChildren():void { labelDisplay = StyleableTextField(createInFontContext(StyleableTextField)); labelDisplay.styleName = this; // update shadow when labelDisplay changes labelDisplay.addEventListener(FlexEvent.VALUE_COMMIT, labelDisplay_valueCommitHandler); addChild(labelDisplay); } /** * @private */ override public function styleChanged(styleProp:String):void { var allStyles:Boolean = !styleProp || styleProp == "styleName"; if (allStyles || styleProp == "iconPlacement") { invalidateSize(); invalidateDisplayList(); } if (useIconStyle && (allStyles || styleProp == "icon")) { iconChanged = true; invalidateProperties(); } if (styleProp == "textShadowAlpha") { invalidateDisplayList(); } super.styleChanged(styleProp); } /** * @private */ override protected function commitProperties():void { super.commitProperties(); if (useIconStyle && iconChanged) { // force enabled update when icon changes enabledChanged = true; iconChanged = false; setIcon(getStyle("icon")); } if (enabledChanged) { commitDisabled(); enabledChanged = false; } } /** * @private */ override protected function measure():void { super.measure(); var labelWidth:Number = 0; var labelHeight:Number = 0; var textDescent:Number = 0; var iconDisplay:DisplayObject = getIconDisplay(); // reset text if it was truncated before. if (hostComponent && labelDisplay.isTruncated) labelDisplay.text = hostComponent.label; // we want to get the label's width and height if we have text or there's // no icon present if (labelDisplay.text != "" || !iconDisplay) { labelWidth = getElementPreferredWidth(labelDisplay); labelHeight = getElementPreferredHeight(labelDisplay); textDescent = labelDisplay.getLineMetrics(0).descent; } var w:Number = layoutPaddingLeft + layoutPaddingRight; var h:Number = 0; var iconWidth:Number = 0; var iconHeight:Number = 0; if (iconDisplay) { iconWidth = getElementPreferredWidth(iconDisplay); iconHeight = getElementPreferredHeight(iconDisplay); } var iconPlacement:String = getStyle("iconPlacement"); // layoutPaddingBottom is from the bottom of the button to the text // baseline or the bottom of the icon. // It must be adjusted when descent grows larger than the padding. var adjustablePaddingBottom:Number = layoutPaddingBottom; if (iconPlacement == IconPlacement.LEFT || iconPlacement == IconPlacement.RIGHT) { w += labelWidth + iconWidth; if (labelWidth && iconWidth) w += layoutGap; var viewHeight:Number = Math.max(labelHeight, iconHeight); h += viewHeight; } else { w += Math.max(labelWidth, iconWidth); h += labelHeight + iconHeight; adjustablePaddingBottom = layoutPaddingBottom; if (labelHeight && iconHeight) { if (iconPlacement == IconPlacement.BOTTOM) { // adjust gap if descent is larger h += Math.max(textDescent, layoutGap); } else { adjustablePaddingBottom = Math.max(layoutPaddingBottom, textDescent); h += layoutGap; } } } h += layoutPaddingTop + adjustablePaddingBottom; // measuredMinHeight for width and height for a square measured minimum size measuredMinWidth = h; measuredMinHeight = h; measuredWidth = w measuredHeight = h; } /** * @private */ override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void { super.layoutContents(unscaledWidth, unscaledHeight); var hasLabel:Boolean = (hostComponent && hostComponent.label != ""); var labelX:Number = 0; var labelY:Number = 0; var labelWidth:Number = 0; var labelHeight:Number = 0; var textWidth:Number = 0; var textHeight:Number = 0; var textDescent:Number = 0; var iconPlacement:String = getStyle("iconPlacement"); var isHorizontal:Boolean = (iconPlacement == IconPlacement.LEFT || iconPlacement == IconPlacement.RIGHT); var iconX:Number = 0; var iconY:Number = 0; var unscaledIconWidth:Number = 0; var unscaledIconHeight:Number = 0; // vertical gap grows when text descent > gap var adjustableGap:Number = 0; // bottom constraint grows when text descent > layoutPaddingBottom var adjustablePaddingBottom:Number = layoutPaddingBottom; // reset text if it was truncated before. if (hostComponent && labelDisplay.isTruncated) labelDisplay.text = hostComponent.label; if (hasLabel) { var metrics:TextLineMetrics = labelDisplay.getLineMetrics(0); textWidth = getElementPreferredWidth(labelDisplay); textHeight = getElementPreferredHeight(labelDisplay); textDescent = metrics.descent; } var iconDisplay:DisplayObject = getIconDisplay(); if (iconDisplay) { unscaledIconWidth = getElementPreferredWidth(iconDisplay); unscaledIconHeight = getElementPreferredHeight(iconDisplay); adjustableGap = (hasLabel) ? layoutGap : 0; } // compute padding bottom based on descent and text position if (iconPlacement == IconPlacement.BOTTOM) { // icon bottom constrained by padding adjustablePaddingBottom = layoutPaddingBottom; } else if (iconPlacement == IconPlacement.TOP) { // adjust padding if descent is larger adjustablePaddingBottom = Math.max(layoutPaddingBottom, textDescent); } var viewWidth:Number = Math.max(unscaledWidth - layoutPaddingLeft - layoutPaddingRight, 0); var viewHeight:Number = Math.max(unscaledHeight - layoutPaddingTop - adjustablePaddingBottom, 0); var iconViewWidth:Number = Math.min(unscaledIconWidth, viewWidth); var iconViewHeight:Number = Math.min(unscaledIconHeight, viewHeight); // snap label to left and right bounds labelWidth = viewWidth; // default label vertical positioning is ascent centered labelHeight = Math.min(viewHeight, textHeight); labelY = (viewHeight - labelHeight) / 2; if (isHorizontal) { // label width constrained by icon width labelWidth = Math.max(Math.min(viewWidth - iconViewWidth - adjustableGap, textWidth), 0); if (useCenterAlignment) labelX = (viewWidth - labelWidth - iconViewWidth - adjustableGap) / 2; else labelX = 0; if (iconPlacement == IconPlacement.LEFT) { iconX = labelX; labelX += iconViewWidth + adjustableGap; } else { iconX = labelX + labelWidth + adjustableGap; } iconY = (viewHeight - iconViewHeight) / 2; } else if (iconViewHeight) { // icon takes precedence over label labelHeight = Math.min(Math.max(viewHeight - iconViewHeight - adjustableGap, 0), textHeight); // adjust gap for descent when text is above icon if (hasLabel && (iconPlacement == IconPlacement.BOTTOM)) adjustableGap = Math.max(adjustableGap, textDescent); if (useCenterAlignment) { // labelWidth already set to viewWidth with textAlign=center labelX = 0; // y-position for vertical center of both icon and label labelY = (viewHeight - labelHeight - iconViewHeight - adjustableGap) / 2; } else { // label horizontal center with textAlign=left labelWidth = Math.min(textWidth, viewWidth); labelX = (viewWidth - labelWidth) / 2; } // horizontally center iconWidth iconX = (viewWidth - iconViewWidth) / 2; var availableIconHeight:Number = viewHeight - labelHeight - adjustableGap; if (iconPlacement == IconPlacement.TOP) { if (useCenterAlignment) { iconY = labelY; labelY = iconY + adjustableGap + iconViewHeight; } else { if (unscaledIconHeight >= availableIconHeight) { // constraint to top iconY = 0; } else { // center icon in available height (above label) including gap // remove padding top since we offset again later iconY = ((availableIconHeight + layoutPaddingTop + adjustableGap - unscaledIconHeight) / 2) - layoutPaddingTop; } labelY = viewHeight - labelHeight; } } else // IconPlacement.BOTTOM { if (useCenterAlignment) { iconY = labelY + labelHeight + adjustableGap; } else { if (unscaledIconHeight >= availableIconHeight) { // constraint to bottom iconY = viewHeight - iconViewHeight; } else { // center icon in available height (below label) including gap iconY = ((availableIconHeight + adjustablePaddingBottom + adjustableGap - unscaledIconHeight) / 2) + labelHeight; } labelY = 0; } } } // adjust labelHeight for vertical clipping at the bottom edge if (isHorizontal && (labelHeight < textHeight)) { // allow gutter to be outside skin bounds // this appears as clipping by the bottom border var labelViewHeight:Number = Math.min(unscaledHeight - layoutPaddingTop - labelY - textDescent + (StyleableTextField.TEXT_HEIGHT_PADDING / 2), textHeight); labelHeight = Math.max(labelViewHeight, labelHeight); } labelX = Math.max(0, Math.round(labelX)) + layoutPaddingLeft; // text looks better a little high as opposed to low, so we use floor instead of round labelY = Math.max(0, Math.floor(labelY)) + layoutPaddingTop; iconX = Math.max(0, Math.round(iconX)) + layoutPaddingLeft; iconY = Math.max(0, Math.round(iconY)) + layoutPaddingTop; setElementSize(labelDisplay, labelWidth, labelHeight); setElementPosition(labelDisplay, labelX, labelY); if (textWidth > labelWidth) labelDisplay.truncateToFit(); if (iconDisplay) { setElementSize(iconDisplay, iconViewWidth, iconViewHeight); setElementPosition(iconDisplay, iconX, iconY); } } //-------------------------------------------------------------------------- // // Class methods // //-------------------------------------------------------------------------- /** * Commit alpha values for the skin when in a disabled state. * * @see mx.core.UIComponent#enabled * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected function commitDisabled():void { alpha = hostComponent.enabled ? 1 : 0.5; } /** * The current skin part that displays the icon. * If the icon is a Class, then the iconDisplay is an instance of that class. * If the icon is a DisplayObject instance, then the iconDisplay is that instance. * If the icon is URL, then iconDisplay is the Group that holds the BitmapImage with that URL. * * @see #setIcon * @see #useIconStyle * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected function getIconDisplay():DisplayObject { return iconHolder ? iconHolder : iconInstance as DisplayObject; } /** * Sets the current icon for the iconDisplay skin part. * The iconDisplay skin part is created/set-up on demand. * * @param icon The current icon. * * @see #getIconDisplay * @see #useIconStyle * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 2.5 * @productversion Flex 4.5 */ protected function setIcon(icon:Object):void { if (_icon == icon) return; _icon = icon; // Clean-up the iconInstance if (iconInstance) { if (iconHolder) iconHolder.removeAllElements(); else this.removeChild(iconInstance as DisplayObject); } iconInstance = null; // Set-up the iconHolder var needsHolder:Boolean = icon && !(icon is Class) && !(icon is DisplayObject); if (needsHolder && !iconHolder) { // layoutContents() will set icon size no larger than it's unscaled size // icon will only scale down when limited by button size iconHolder = new Group(); iconHolder.resizeMode = ResizeMode.SCALE; addChild(iconHolder); } else if (!needsHolder && iconHolder) { this.removeChild(iconHolder); iconHolder = null; } // Set-up the icon if (icon) { if (needsHolder) { iconInstance = new BitmapImage(); iconInstance.source = icon; iconHolder.addElementAt(iconInstance as BitmapImage, 0); } else { if (icon is Class) iconInstance = new (Class(icon))(); else iconInstance = icon; addChild(iconInstance as DisplayObject); } } // explicitly invalidate, since addChild/removeChild don't invalidate for us invalidateSize(); invalidateDisplayList(); } //-------------------------------------------------------------------------- // // Event Handlers // //-------------------------------------------------------------------------- /** * @private */ protected function labelDisplay_valueCommitHandler(event:FlexEvent):void { invalidateSize(); invalidateDisplayList(); } } }