org.appcelerator.titanium.view.TiUIView.java Source code

Java tutorial

Introduction

Here is the source code for org.appcelerator.titanium.view.TiUIView.java

Source

/**
 * Appcelerator Titanium Mobile
 * Copyright (c) 2009-2012 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Apache Public License
 * Please see the LICENSE included with this distribution for details.
 */
package org.appcelerator.titanium.view;

import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.KrollPropertyChange;
import org.appcelerator.kroll.KrollProxy;
import org.appcelerator.kroll.KrollProxyListener;
import org.appcelerator.kroll.common.Log;
import org.appcelerator.kroll.common.TiMessenger;
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.TiDimension;
import org.appcelerator.titanium.proxy.TiViewProxy;
import org.appcelerator.titanium.util.TiAnimationBuilder;
import org.appcelerator.titanium.util.TiAnimationBuilder.TiMatrixAnimation;
import org.appcelerator.titanium.util.TiConvert;
import org.appcelerator.titanium.util.TiUIHelper;
import org.appcelerator.titanium.view.TiCompositeLayout.LayoutParams;
import org.appcelerator.titanium.view.TiGradientDrawable.GradientType;

import android.app.Activity;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.Pair;
import android.util.SparseArray;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener;
import android.view.View.OnLongClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;

/**
 * This class is for Titanium View implementations, that correspond with TiViewProxy. 
 * A TiUIView is responsible for creating and maintaining a native Android View instance.
 */
public abstract class TiUIView implements KrollProxyListener, OnFocusChangeListener {
    private static final boolean HONEYCOMB_OR_GREATER = (Build.VERSION.SDK_INT >= 11);
    private static final int LAYER_TYPE_SOFTWARE = 1;
    private static final String TAG = "TiUIView";

    private static AtomicInteger idGenerator;

    // When distinguishing twofingertap and pinch events, minimum motion (in pixels) 
    // to qualify as a scale event. 
    private static final float SCALE_THRESHOLD = 6.0f;

    public static final int SOFT_KEYBOARD_DEFAULT_ON_FOCUS = 0;
    public static final int SOFT_KEYBOARD_HIDE_ON_FOCUS = 1;
    public static final int SOFT_KEYBOARD_SHOW_ON_FOCUS = 2;

    protected View nativeView; // Native View object

    protected TiViewProxy proxy;
    protected TiViewProxy parent;
    protected ArrayList<TiUIView> children = new ArrayList<TiUIView>();

    protected LayoutParams layoutParams;
    protected TiAnimationBuilder animBuilder;
    protected TiBackgroundDrawable background;

    // Since Android doesn't have a property to check to indicate
    // the current animated x/y scale (from a scale animation), we track it here
    // so if another scale animation is done we can gleen the fromX and fromY values
    // rather than starting the next animation always from scale 1.0f (i.e., normal scale).
    // This gives us parity with iPhone for scale animations that use the 2-argument variant
    // of Ti2DMatrix.scale().
    private Pair<Float, Float> animatedScaleValues = Pair.create(Float.valueOf(1f), Float.valueOf(1f)); // default = full size (1f)

    // Same for rotation animation and for alpha animation.
    private float animatedRotationDegrees = 0f; // i.e., no rotation.
    private float animatedAlpha = Float.MIN_VALUE; // i.e., no animated alpha.

    private KrollDict lastUpEvent = new KrollDict(2);
    // In the case of heavy-weight windows, the "nativeView" is null,
    // so this holds a reference to the view which is used for touching,
    // i.e., the view passed to registerForTouch.
    private WeakReference<View> touchView = null;

    private Method mSetLayerTypeMethod = null; // Honeycomb, for turning off hw acceleration.

    private boolean zIndexChanged = false;
    private TiBorderWrapperView borderView;
    // For twofingertap detection
    private boolean didScale = false;

    //to maintain sync visibility between borderview and view. Default is visible
    private int visibility = View.VISIBLE;

    /**
     * Constructs a TiUIView object with the associated proxy.
     * @param proxy the associated proxy.
     * @module.api
     */
    public TiUIView(TiViewProxy proxy) {
        if (idGenerator == null) {
            idGenerator = new AtomicInteger(0);
        }

        this.proxy = proxy;
        this.layoutParams = new TiCompositeLayout.LayoutParams();
    }

    /**
     * Adds a child view into the ViewGroup.
     * @param child the view to be added.
     */
    public void add(TiUIView child) {
        if (child != null) {
            View cv = child.getOuterView();
            if (cv != null) {
                View nv = getNativeView();
                if (nv instanceof ViewGroup) {
                    if (cv.getParent() == null) {
                        ((ViewGroup) nv).addView(cv, child.getLayoutParams());
                    }
                    children.add(child);
                    child.parent = proxy;
                }
            }
        }
    }

    /**
     * Removes the child view from the ViewGroup, if child exists.
     * @param child the view to be removed.
     */
    public void remove(TiUIView child) {
        if (child != null) {
            View cv = child.getOuterView();
            if (cv != null) {
                View nv = getNativeView();
                if (nv instanceof ViewGroup) {
                    ((ViewGroup) nv).removeView(cv);
                    children.remove(child);
                    child.parent = null;
                }
            }
        }
    }

    /**
     * @return list of views added.
     */
    public List<TiUIView> getChildren() {
        return children;
    }

    /**
     * @return the view proxy.
     * @module.api
     */
    public TiViewProxy getProxy() {
        return proxy;
    }

    /**
     * Sets the view proxy.
     * @param proxy the proxy to set.
     * @module.api
     */
    public void setProxy(TiViewProxy proxy) {
        this.proxy = proxy;
    }

    public TiViewProxy getParent() {
        return parent;
    }

    public void setParent(TiViewProxy parent) {
        this.parent = parent;
    }

    /**
     * @return the view's layout params.
     * @module.api
     */
    public LayoutParams getLayoutParams() {
        return layoutParams;
    }

    /**
     * @return the Android native view.
     * @module.api
     */
    public View getNativeView() {
        return nativeView;
    }

    /**
     * Sets the nativeView to view.
     * @param view the view to set
     * @module.api
     */
    protected void setNativeView(View view) {
        if (view.getId() == View.NO_ID) {
            view.setId(idGenerator.incrementAndGet());
        }
        this.nativeView = view;
        boolean clickable = true;

        if (proxy.hasProperty(TiC.PROPERTY_TOUCH_ENABLED)) {
            clickable = TiConvert.toBoolean(proxy.getProperty(TiC.PROPERTY_TOUCH_ENABLED));
        }
        doSetClickable(nativeView, clickable);
        nativeView.setOnFocusChangeListener(this);

        applyAccessibilityProperties();
    }

    protected void setLayoutParams(LayoutParams layoutParams) {
        this.layoutParams = layoutParams;
    }

    /**
     * Animates the view if there are pending animations.
     */
    public void animate() {
        if (nativeView == null) {
            return;
        }

        // Pre-honeycomb, if one animation clobbers another you get a problem whereby the background of the
        // animated view's parent (or the grandparent) bleeds through.  It seems to improve if you cancel and clear
        // the older animation.  So here we cancel and clear, then re-queue the desired animation.
        if (Build.VERSION.SDK_INT < TiC.API_LEVEL_HONEYCOMB) {
            Animation currentAnimation = nativeView.getAnimation();
            if (currentAnimation != null && currentAnimation.hasStarted() && !currentAnimation.hasEnded()) {
                // Cancel existing animation and
                // re-queue desired animation.
                currentAnimation.cancel();
                nativeView.clearAnimation();
                proxy.handlePendingAnimation(true);
                return;
            }

        }

        TiAnimationBuilder builder = proxy.getPendingAnimation();
        if (builder == null) {
            return;
        }

        proxy.clearAnimation(builder);
        AnimationSet as = builder.render(proxy, nativeView);

        // If a view is "visible" but not currently seen (such as because it's covered or
        // its position is currently set to be fully outside its parent's region),
        // then Android might not animate it immediately because by default it animates
        // "on first frame" and apparently "first frame" won't happen right away if the
        // view has no visible rectangle on screen.  In that case invalidate its parent, which will
        // kick off the pending animation.
        boolean invalidateParent = false;
        ViewParent viewParent = nativeView.getParent();

        if (this.visibility == View.VISIBLE && viewParent instanceof View) {
            int width = nativeView.getWidth();
            int height = nativeView.getHeight();

            if (width == 0 || height == 0) {
                // Could be animating from nothing to something
                invalidateParent = true;
            } else {
                Rect r = new Rect(0, 0, width, height);
                Point p = new Point(0, 0);
                invalidateParent = !(viewParent.getChildVisibleRect(nativeView, r, p));
            }
        }

        Log.d(TAG, "starting animation: " + as, Log.DEBUG_MODE);
        nativeView.startAnimation(as);

        if (invalidateParent) {
            ((View) viewParent).postInvalidate();
        }
    }

    public void listenerAdded(String type, int count, KrollProxy proxy) {
    }

    public void listenerRemoved(String type, int count, KrollProxy proxy) {
    }

    private boolean hasImage(KrollDict d) {
        return d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_IMAGE)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_SELECTED_IMAGE)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_FOCUSED_IMAGE)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_DISABLED_IMAGE);
    }

    private boolean hasRepeat(KrollDict d) {
        return d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_REPEAT);
    }

    private boolean hasGradient(KrollDict d) {
        return d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_GRADIENT);
    }

    private boolean hasBorder(KrollDict d) {
        return d.containsKeyAndNotNull(TiC.PROPERTY_BORDER_COLOR)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BORDER_RADIUS)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BORDER_WIDTH);
    }

    private boolean hasColorState(KrollDict d) {
        return d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR)
                || d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR);
    }

    protected void applyTransform(Ti2DMatrix matrix) {
        layoutParams.optionTransform = matrix;
        if (animBuilder == null) {
            animBuilder = new TiAnimationBuilder();
        }
        if (nativeView != null) {
            if (matrix != null) {
                TiMatrixAnimation matrixAnimation = animBuilder.createMatrixAnimation(matrix);
                matrixAnimation.interpolate = false;
                matrixAnimation.setDuration(1);
                matrixAnimation.setFillAfter(true);
                nativeView.startAnimation(matrixAnimation);
            } else {
                nativeView.clearAnimation();
            }
        }
    }

    public void forceLayoutNativeView(boolean imformParent) {
        layoutNativeView(imformParent);
    }

    protected void layoutNativeView() {
        if (!this.proxy.isLayoutStarted()) {
            layoutNativeView(false);
        }
    }

    protected void layoutNativeView(boolean informParent) {
        if (nativeView != null) {
            Animation a = nativeView.getAnimation();
            if (a != null && a instanceof TiMatrixAnimation) {
                TiMatrixAnimation matrixAnimation = (TiMatrixAnimation) a;
                matrixAnimation.invalidateWithMatrix(nativeView);
            }
            if (informParent) {
                if (parent != null) {
                    TiUIView uiv = parent.peekView();
                    if (uiv != null) {
                        View v = uiv.getNativeView();
                        if (v instanceof TiCompositeLayout) {
                            ((TiCompositeLayout) v).resort();
                        }
                    }
                }
            }
            nativeView.requestLayout();
        }
    }

    public boolean iszIndexChanged() {
        return zIndexChanged;
    }

    public void setzIndexChanged(boolean zIndexChanged) {
        this.zIndexChanged = zIndexChanged;
    }

    public void propertyChanged(String key, Object oldValue, Object newValue, KrollProxy proxy) {
        if (key.equals(TiC.PROPERTY_LEFT)) {
            resetPostAnimationValues();
            if (newValue != null) {
                layoutParams.optionLeft = TiConvert.toTiDimension(TiConvert.toString(newValue),
                        TiDimension.TYPE_LEFT);
            } else {
                layoutParams.optionLeft = null;
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_TOP)) {
            resetPostAnimationValues();
            if (newValue != null) {
                layoutParams.optionTop = TiConvert.toTiDimension(TiConvert.toString(newValue),
                        TiDimension.TYPE_TOP);
            } else {
                layoutParams.optionTop = null;
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_CENTER)) {
            resetPostAnimationValues();
            TiConvert.updateLayoutCenter(newValue, layoutParams);
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_RIGHT)) {
            resetPostAnimationValues();
            if (newValue != null) {
                layoutParams.optionRight = TiConvert.toTiDimension(TiConvert.toString(newValue),
                        TiDimension.TYPE_RIGHT);
            } else {
                layoutParams.optionRight = null;
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_BOTTOM)) {
            resetPostAnimationValues();
            if (newValue != null) {
                layoutParams.optionBottom = TiConvert.toTiDimension(TiConvert.toString(newValue),
                        TiDimension.TYPE_BOTTOM);
            } else {
                layoutParams.optionBottom = null;
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_SIZE)) {
            if (newValue instanceof HashMap) {
                @SuppressWarnings("unchecked")
                HashMap<String, Object> d = (HashMap<String, Object>) newValue;
                propertyChanged(TiC.PROPERTY_WIDTH, oldValue, d.get(TiC.PROPERTY_WIDTH), proxy);
                propertyChanged(TiC.PROPERTY_HEIGHT, oldValue, d.get(TiC.PROPERTY_HEIGHT), proxy);
            } else if (newValue != null) {
                Log.w(TAG, "Unsupported property type (" + (newValue.getClass().getSimpleName()) + ") for key: "
                        + key + ". Must be an object/dictionary");
            }
        } else if (key.equals(TiC.PROPERTY_HEIGHT)) {
            resetPostAnimationValues();
            if (newValue != null) {
                layoutParams.optionHeight = null;
                layoutParams.sizeOrFillHeightEnabled = true;
                if (newValue.equals(TiC.LAYOUT_SIZE)) {
                    layoutParams.autoFillsHeight = false;
                } else if (newValue.equals(TiC.LAYOUT_FILL)) {
                    layoutParams.autoFillsHeight = true;
                } else if (!newValue.equals(TiC.SIZE_AUTO)) {
                    layoutParams.optionHeight = TiConvert.toTiDimension(TiConvert.toString(newValue),
                            TiDimension.TYPE_HEIGHT);
                    layoutParams.sizeOrFillHeightEnabled = false;
                }
            } else {
                layoutParams.optionHeight = null;
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_HORIZONTAL_WRAP)) {
            if (nativeView instanceof TiCompositeLayout) {
                ((TiCompositeLayout) nativeView).setEnableHorizontalWrap(TiConvert.toBoolean(newValue));
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_WIDTH)) {
            resetPostAnimationValues();
            if (newValue != null) {
                layoutParams.optionWidth = null;
                layoutParams.sizeOrFillWidthEnabled = true;
                if (newValue.equals(TiC.LAYOUT_SIZE)) {
                    layoutParams.autoFillsWidth = false;
                } else if (newValue.equals(TiC.LAYOUT_FILL)) {
                    layoutParams.autoFillsWidth = true;
                } else if (!newValue.equals(TiC.SIZE_AUTO)) {
                    layoutParams.optionWidth = TiConvert.toTiDimension(TiConvert.toString(newValue),
                            TiDimension.TYPE_WIDTH);
                    layoutParams.sizeOrFillWidthEnabled = false;
                }
            } else {
                layoutParams.optionWidth = null;
            }
            layoutNativeView();
        } else if (key.equals(TiC.PROPERTY_ZINDEX)) {
            if (newValue != null) {
                layoutParams.optionZIndex = TiConvert.toInt(newValue);
            } else {
                layoutParams.optionZIndex = 0;
            }
            if (!this.proxy.isLayoutStarted()) {
                layoutNativeView(true);
            } else {
                setzIndexChanged(true);
            }
        } else if (key.equals(TiC.PROPERTY_FOCUSABLE) && newValue != null) {
            registerForKeyPress(nativeView, TiConvert.toBoolean(newValue));
        } else if (key.equals(TiC.PROPERTY_TOUCH_ENABLED)) {
            doSetClickable(TiConvert.toBoolean(newValue));
        } else if (key.equals(TiC.PROPERTY_VISIBLE)) {
            this.setVisibility(TiConvert.toBoolean(newValue) ? View.VISIBLE : View.INVISIBLE);
        } else if (key.equals(TiC.PROPERTY_ENABLED)) {
            nativeView.setEnabled(TiConvert.toBoolean(newValue));
        } else if (key.startsWith(TiC.PROPERTY_BACKGROUND_PADDING)) {
            Log.i(TAG, key + " not yet implemented.");
        } else if (key.equals(TiC.PROPERTY_OPACITY) || key.startsWith(TiC.PROPERTY_BACKGROUND_PREFIX)
                || key.startsWith(TiC.PROPERTY_BORDER_PREFIX)) {
            // Update first before querying.
            proxy.setProperty(key, newValue);

            KrollDict d = proxy.getProperties();

            boolean hasImage = hasImage(d);
            boolean hasRepeat = hasRepeat(d);
            boolean hasColorState = hasColorState(d);
            boolean hasBorder = hasBorder(d);
            boolean hasGradient = hasGradient(d);
            boolean nativeViewNull = (nativeView == null);

            boolean requiresCustomBackground = hasImage || hasRepeat || hasColorState || hasBorder || hasGradient;

            if (!requiresCustomBackground) {
                if (background != null) {
                    background.releaseDelegate();
                    background.setCallback(null);
                    background = null;
                }

                if (d.containsKeyAndNotNull(TiC.PROPERTY_BACKGROUND_COLOR)) {
                    Integer bgColor = TiConvert.toColor(d, TiC.PROPERTY_BACKGROUND_COLOR);
                    if (!nativeViewNull) {
                        nativeView.setBackgroundColor(bgColor);
                        nativeView.postInvalidate();
                    }
                } else {
                    if (key.equals(TiC.PROPERTY_OPACITY)) {
                        setOpacity(TiConvert.toFloat(newValue, 1f));
                    }
                    if (!nativeViewNull) {
                        nativeView.setBackgroundDrawable(null);
                        nativeView.postInvalidate();
                    }
                }
            } else {
                boolean newBackground = background == null;
                if (newBackground) {
                    background = new TiBackgroundDrawable();
                }

                Integer bgColor = null;

                if (!hasColorState && !hasGradient) {
                    if (d.get(TiC.PROPERTY_BACKGROUND_COLOR) != null) {
                        bgColor = TiConvert.toColor(d, TiC.PROPERTY_BACKGROUND_COLOR);
                        if (newBackground || (key.equals(TiC.PROPERTY_OPACITY)
                                || key.equals(TiC.PROPERTY_BACKGROUND_COLOR))) {
                            background.setBackgroundColor(bgColor);
                        }
                    }
                }

                if (hasImage || hasRepeat || hasColorState || hasGradient) {
                    if (newBackground || key.equals(TiC.PROPERTY_OPACITY)
                            || key.startsWith(TiC.PROPERTY_BACKGROUND_PREFIX)) {
                        handleBackgroundImage(d);
                    }
                }

                if (hasBorder) {
                    if (borderView == null && parent != null) {
                        // Since we have to create a new border wrapper view, we need to remove this view, and re-add it.
                        // This will ensure the border wrapper view is added correctly.
                        TiUIView parentView = parent.getOrCreateView();
                        parentView.remove(this);
                        initializeBorder(d, bgColor);
                        parentView.add(this);
                    } else if (key.startsWith(TiC.PROPERTY_BORDER_PREFIX)) {
                        handleBorderProperty(key, newValue);
                    }
                }

                applyCustomBackground();

                if (key.equals(TiC.PROPERTY_OPACITY)) {
                    setOpacity(TiConvert.toFloat(newValue, 1f));
                }

            }
            if (!nativeViewNull) {
                nativeView.postInvalidate();
            }
        } else if (key.equals(TiC.PROPERTY_SOFT_KEYBOARD_ON_FOCUS)) {
            Log.w(TAG, "Focus state changed to " + TiConvert.toString(newValue)
                    + " not honored until next focus event.", Log.DEBUG_MODE);
        } else if (key.equals(TiC.PROPERTY_TRANSFORM)) {
            if (nativeView != null) {
                applyTransform((Ti2DMatrix) newValue);
            }
        } else if (key.equals(TiC.PROPERTY_KEEP_SCREEN_ON)) {
            if (nativeView != null) {
                nativeView.setKeepScreenOn(TiConvert.toBoolean(newValue));
            }

        } else if (key.indexOf("accessibility") == 0 && !key.equals(TiC.PROPERTY_ACCESSIBILITY_HIDDEN)) {
            applyContentDescription();

        } else if (key.equals(TiC.PROPERTY_ACCESSIBILITY_HIDDEN)) {
            applyAccessibilityHidden(newValue);

        } else {
            Log.d(TAG, "Unhandled property key: " + key, Log.DEBUG_MODE);
        }
    }

    public void processProperties(KrollDict d) {
        boolean nativeViewNull = false;
        if (nativeView == null) {
            nativeViewNull = true;
            Log.d(TAG, "Nativeview is null", Log.DEBUG_MODE);
        }
        if (d.containsKey(TiC.PROPERTY_LAYOUT)) {
            String layout = TiConvert.toString(d, TiC.PROPERTY_LAYOUT);
            if (nativeView instanceof TiCompositeLayout) {
                ((TiCompositeLayout) nativeView).setLayoutArrangement(layout);
            }
        }
        if (TiConvert.fillLayout(d, layoutParams) && !nativeViewNull) {
            nativeView.requestLayout();
        }

        if (d.containsKey(TiC.PROPERTY_HORIZONTAL_WRAP)) {
            if (nativeView instanceof TiCompositeLayout) {
                ((TiCompositeLayout) nativeView)
                        .setEnableHorizontalWrap(TiConvert.toBoolean(d, TiC.PROPERTY_HORIZONTAL_WRAP));
            }
        }

        Integer bgColor = null;

        // Default background processing.
        // Prefer image to color.
        if (hasImage(d) || hasColorState(d) || hasGradient(d)) {
            handleBackgroundImage(d);

        } else if (d.containsKey(TiC.PROPERTY_BACKGROUND_COLOR) && !nativeViewNull) {
            bgColor = TiConvert.toColor(d, TiC.PROPERTY_BACKGROUND_COLOR);

            // Set the background color on the view directly only
            // if there is no border. If a border is present we must
            // use the TiBackgroundDrawable.
            if (hasBorder(d)) {
                if (background == null) {
                    applyCustomBackground(false);
                }
                background.setBackgroundColor(bgColor);

            } else {
                nativeView.setBackgroundColor(bgColor);
            }
        }

        if (d.containsKey(TiC.PROPERTY_VISIBLE) && !nativeViewNull) {
            this.setVisibility(TiConvert.toBoolean(d, TiC.PROPERTY_VISIBLE) ? View.VISIBLE : View.INVISIBLE);
        }
        if (d.containsKey(TiC.PROPERTY_ENABLED) && !nativeViewNull) {
            nativeView.setEnabled(TiConvert.toBoolean(d, TiC.PROPERTY_ENABLED));
        }

        initializeBorder(d, bgColor);

        if (d.containsKey(TiC.PROPERTY_OPACITY) && !nativeViewNull) {
            setOpacity(TiConvert.toFloat(d, TiC.PROPERTY_OPACITY, 1f));

        }

        if (d.containsKey(TiC.PROPERTY_TRANSFORM)) {
            Ti2DMatrix matrix = (Ti2DMatrix) d.get(TiC.PROPERTY_TRANSFORM);
            if (matrix != null) {
                applyTransform(matrix);
            }
        }

        if (d.containsKey(TiC.PROPERTY_KEEP_SCREEN_ON) && !nativeViewNull) {
            nativeView.setKeepScreenOn(TiConvert.toBoolean(d, TiC.PROPERTY_KEEP_SCREEN_ON));

        }

        if (d.containsKey(TiC.PROPERTY_ACCESSIBILITY_HINT) || d.containsKey(TiC.PROPERTY_ACCESSIBILITY_LABEL)
                || d.containsKey(TiC.PROPERTY_ACCESSIBILITY_VALUE)
                || d.containsKey(TiC.PROPERTY_ACCESSIBILITY_HIDDEN)) {
            applyAccessibilityProperties();
        }
    }

    // TODO dead code? @Override
    public void propertiesChanged(List<KrollPropertyChange> changes, KrollProxy proxy) {
        for (KrollPropertyChange change : changes) {
            propertyChanged(change.getName(), change.getOldValue(), change.getNewValue(), proxy);
        }
    }

    private void applyCustomBackground() {
        applyCustomBackground(true);
    }

    private void applyCustomBackground(boolean reuseCurrentDrawable) {
        if (nativeView != null) {
            if (background == null) {
                background = new TiBackgroundDrawable();

                Drawable currentDrawable = nativeView.getBackground();
                if (currentDrawable != null) {
                    if (reuseCurrentDrawable) {
                        background.setBackgroundDrawable(currentDrawable);

                    } else {
                        nativeView.setBackgroundDrawable(null);
                        currentDrawable.setCallback(null);
                        if (currentDrawable instanceof TiBackgroundDrawable) {
                            ((TiBackgroundDrawable) currentDrawable).releaseDelegate();
                        }
                    }
                }
            }
            nativeView.setBackgroundDrawable(background);
        }
    }

    public void onFocusChange(final View v, boolean hasFocus) {
        if (hasFocus) {
            TiMessenger.postOnMain(new Runnable() {
                public void run() {
                    TiUIHelper.requestSoftInputChange(proxy, v);
                }
            });
            proxy.fireEvent(TiC.EVENT_FOCUS, getFocusEventObject(hasFocus));
        } else {
            TiMessenger.postOnMain(new Runnable() {
                public void run() {
                    TiUIHelper.showSoftKeyboard(v, false);
                }
            });
            proxy.fireEvent(TiC.EVENT_BLUR, getFocusEventObject(hasFocus));
        }
    }

    protected KrollDict getFocusEventObject(boolean hasFocus) {
        return null;
    }

    protected InputMethodManager getIMM() {
        InputMethodManager imm = null;
        imm = (InputMethodManager) TiApplication.getInstance().getSystemService(Context.INPUT_METHOD_SERVICE);
        return imm;
    }

    /**
     * Focuses the view.
     */
    public void focus() {
        if (nativeView != null) {
            nativeView.requestFocus();
        }
    }

    /**
     * Blurs the view.
     */
    public void blur() {
        if (nativeView != null) {
            nativeView.clearFocus();
        }
    }

    public void release() {
        Log.d(TAG, "Releasing: " + this, Log.DEBUG_MODE);
        View nv = getNativeView();
        if (nv != null) {
            if (nv instanceof ViewGroup) {
                ViewGroup vg = (ViewGroup) nv;
                Log.d(TAG, "Group has: " + vg.getChildCount(), Log.DEBUG_MODE);
                if (!(vg instanceof AdapterView<?>)) {
                    vg.removeAllViews();
                }
            }
            Drawable d = nv.getBackground();
            if (d != null) {
                nv.setBackgroundDrawable(null);
                d.setCallback(null);
                if (d instanceof TiBackgroundDrawable) {
                    ((TiBackgroundDrawable) d).releaseDelegate();
                }
                d = null;
            }
            nativeView = null;
            borderView = null;
            if (proxy != null) {
                proxy.setModelListener(null);
            }
        }
    }

    private void setVisibility(int visibility) {
        this.visibility = visibility;
        if (borderView != null) {
            borderView.setVisibility(this.visibility);
        }
        if (nativeView != null) {
            nativeView.setVisibility(this.visibility);
        }
    }

    /**
     * Shows the view, changing the view's visibility to View.VISIBLE.
     */
    public void show() {
        this.setVisibility(View.VISIBLE);
        if (borderView == null && nativeView == null) {
            Log.w(TAG, "Attempt to show null native control", Log.DEBUG_MODE);
        }
    }

    /**
     * Hides the view, changing the view's visibility to View.INVISIBLE.
     */
    public void hide() {
        this.setVisibility(View.INVISIBLE);
        if (borderView == null && nativeView == null) {
            Log.w(TAG, "Attempt to hide null native control", Log.DEBUG_MODE);
        }
    }

    private String resolveImageUrl(String path) {
        return path.length() > 0 ? proxy.resolveUrl(null, path) : null;
    }

    private void handleBackgroundImage(KrollDict d) {
        String bg = d.getString(TiC.PROPERTY_BACKGROUND_IMAGE);
        String bgSelected = d.optString(TiC.PROPERTY_BACKGROUND_SELECTED_IMAGE, bg);
        String bgFocused = d.optString(TiC.PROPERTY_BACKGROUND_FOCUSED_IMAGE, bg);
        String bgDisabled = d.optString(TiC.PROPERTY_BACKGROUND_DISABLED_IMAGE, bg);

        String bgColor = d.getString(TiC.PROPERTY_BACKGROUND_COLOR);
        String bgSelectedColor = d.optString(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR, bgColor);
        String bgFocusedColor = d.optString(TiC.PROPERTY_BACKGROUND_FOCUSED_COLOR, bgColor);
        String bgDisabledColor = d.optString(TiC.PROPERTY_BACKGROUND_DISABLED_COLOR, bgColor);

        if (bg != null) {
            bg = resolveImageUrl(bg);
        }
        if (bgSelected != null) {
            bgSelected = resolveImageUrl(bgSelected);
        }
        if (bgFocused != null) {
            bgFocused = resolveImageUrl(bgFocused);
        }
        if (bgDisabled != null) {
            bgDisabled = resolveImageUrl(bgDisabled);
        }

        TiGradientDrawable gradientDrawable = null;
        KrollDict gradientProperties = d.getKrollDict(TiC.PROPERTY_BACKGROUND_GRADIENT);
        if (gradientProperties != null) {
            try {
                gradientDrawable = new TiGradientDrawable(nativeView, gradientProperties);
                if (gradientDrawable.getGradientType() == GradientType.RADIAL_GRADIENT) {
                    // TODO: Remove this once we support radial gradients.
                    Log.w(TAG, "Android does not support radial gradients.");
                    gradientDrawable = null;
                }
            } catch (IllegalArgumentException e) {
                gradientDrawable = null;
            }
        }

        if (bg != null || bgSelected != null || bgFocused != null || bgDisabled != null || bgColor != null
                || bgSelectedColor != null || bgFocusedColor != null || bgDisabledColor != null
                || gradientDrawable != null) {
            if (background == null) {
                applyCustomBackground(false);
            }

            Drawable bgDrawable = TiUIHelper.buildBackgroundDrawable(bg,
                    d.getBoolean(TiC.PROPERTY_BACKGROUND_REPEAT), bgColor, bgSelected, bgSelectedColor, bgDisabled,
                    bgDisabledColor, bgFocused, bgFocusedColor, gradientDrawable);

            background.setBackgroundDrawable(bgDrawable);
        }
    }

    private void initializeBorder(KrollDict d, Integer bgColor) {
        if (hasBorder(d)) {

            if (nativeView != null) {

                if (borderView == null) {
                    Activity currentActivity = proxy.getActivity();
                    if (currentActivity == null) {
                        currentActivity = TiApplication.getAppCurrentActivity();
                    }
                    borderView = new TiBorderWrapperView(currentActivity);
                    // Create new layout params for the child view since we just want the
                    // wrapper to control the layout
                    LayoutParams params = new LayoutParams();
                    params.height = android.widget.FrameLayout.LayoutParams.MATCH_PARENT;
                    params.width = android.widget.FrameLayout.LayoutParams.MATCH_PARENT;
                    borderView.addView(nativeView, params);
                    borderView.setVisibility(this.visibility);
                }

                if (d.containsKey(TiC.PROPERTY_BORDER_RADIUS)) {
                    float radius = TiConvert.toFloat(d, TiC.PROPERTY_BORDER_RADIUS, 0f);
                    if (radius > 0f && HONEYCOMB_OR_GREATER) {
                        disableHWAcceleration();
                    }
                    borderView.setRadius(radius);
                }
                if (d.containsKey(TiC.PROPERTY_BORDER_COLOR) || d.containsKey(TiC.PROPERTY_BORDER_WIDTH)) {
                    if (d.containsKey(TiC.PROPERTY_BORDER_COLOR)) {
                        borderView.setColor(TiConvert.toColor(d, TiC.PROPERTY_BORDER_COLOR));
                    } else {
                        if (bgColor != null) {
                            borderView.setColor(bgColor);
                        }
                    }
                    if (d.containsKey(TiC.PROPERTY_BORDER_WIDTH)) {
                        borderView.setBorderWidth(TiConvert.toFloat(d, TiC.PROPERTY_BORDER_WIDTH, 0f));
                    }
                }
            }
        }
    }

    private void handleBorderProperty(String property, Object value) {
        if (TiC.PROPERTY_BORDER_COLOR.equals(property)) {
            borderView.setColor(TiConvert.toColor(value.toString()));
        } else if (TiC.PROPERTY_BORDER_RADIUS.equals(property)) {
            float radius = TiConvert.toFloat(value, 0f);
            if (radius > 0f && HONEYCOMB_OR_GREATER) {
                disableHWAcceleration();
            }
            borderView.setRadius(radius);
        } else if (TiC.PROPERTY_BORDER_WIDTH.equals(property)) {
            borderView.setBorderWidth(TiConvert.toFloat(value, 0f));
        }
    }

    private static SparseArray<String> motionEvents = new SparseArray<String>();
    static {
        motionEvents.put(MotionEvent.ACTION_DOWN, TiC.EVENT_TOUCH_START);
        motionEvents.put(MotionEvent.ACTION_UP, TiC.EVENT_TOUCH_END);
        motionEvents.put(MotionEvent.ACTION_MOVE, TiC.EVENT_TOUCH_MOVE);
        motionEvents.put(MotionEvent.ACTION_CANCEL, TiC.EVENT_TOUCH_CANCEL);
    }

    protected KrollDict dictFromEvent(MotionEvent e) {
        KrollDict data = new KrollDict();
        data.put(TiC.EVENT_PROPERTY_X, (double) e.getX());
        data.put(TiC.EVENT_PROPERTY_Y, (double) e.getY());
        data.put(TiC.EVENT_PROPERTY_SOURCE, proxy);
        return data;
    }

    private KrollDict dictFromEvent(KrollDict dictToCopy) {
        KrollDict data = new KrollDict();
        if (dictToCopy.containsKey(TiC.EVENT_PROPERTY_X)) {
            data.put(TiC.EVENT_PROPERTY_X, dictToCopy.get(TiC.EVENT_PROPERTY_X));
        } else {
            data.put(TiC.EVENT_PROPERTY_X, (double) 0);
        }
        if (dictToCopy.containsKey(TiC.EVENT_PROPERTY_Y)) {
            data.put(TiC.EVENT_PROPERTY_Y, dictToCopy.get(TiC.EVENT_PROPERTY_Y));
        } else {
            data.put(TiC.EVENT_PROPERTY_Y, (double) 0);
        }
        data.put(TiC.EVENT_PROPERTY_SOURCE, proxy);
        return data;
    }

    protected boolean allowRegisterForTouch() {
        return true;
    }

    /**
     * @module.api
     */
    protected boolean allowRegisterForKeyPress() {
        return true;
    }

    public View getOuterView() {
        return borderView == null ? nativeView : borderView;
    }

    public void registerForTouch() {
        if (allowRegisterForTouch()) {
            registerForTouch(getNativeView());
        }
    }

    protected void registerTouchEvents(final View touchable) {

        touchView = new WeakReference<View>(touchable);

        final ScaleGestureDetector scaleDetector = new ScaleGestureDetector(touchable.getContext(),
                new SimpleOnScaleGestureListener() {
                    // protect from divide by zero errors
                    long minTimeDelta = 1;
                    float minStartSpan = 1.0f;
                    float startSpan;

                    @Override
                    public boolean onScale(ScaleGestureDetector sgd) {
                        if (proxy.hierarchyHasListener(TiC.EVENT_PINCH)) {
                            float timeDelta = sgd.getTimeDelta() == 0 ? minTimeDelta : sgd.getTimeDelta();

                            // Suppress scale events (and allow for possible two-finger tap events)
                            // until we've moved at least a few pixels. Without this check, two-finger 
                            // taps are very hard to register on some older devices.
                            if (!didScale) {
                                if (Math.abs(sgd.getCurrentSpan() - startSpan) > SCALE_THRESHOLD) {
                                    didScale = true;
                                }
                            }

                            if (didScale) {
                                KrollDict data = new KrollDict();
                                data.put(TiC.EVENT_PROPERTY_SCALE, sgd.getCurrentSpan() / startSpan);
                                data.put(TiC.EVENT_PROPERTY_VELOCITY,
                                        (sgd.getScaleFactor() - 1.0f) / timeDelta * 1000);
                                data.put(TiC.EVENT_PROPERTY_SOURCE, proxy);

                                return proxy.fireEvent(TiC.EVENT_PINCH, data);
                            }
                        }
                        return false;
                    }

                    @Override
                    public boolean onScaleBegin(ScaleGestureDetector sgd) {
                        startSpan = sgd.getCurrentSpan() == 0 ? minStartSpan : sgd.getCurrentSpan();
                        return true;
                    }
                });

        final GestureDetector detector = new GestureDetector(touchable.getContext(), new SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                if (proxy.hierarchyHasListener(TiC.EVENT_DOUBLE_TAP)
                        || proxy.hierarchyHasListener(TiC.EVENT_DOUBLE_CLICK)) {
                    boolean handledTap = proxy.fireEvent(TiC.EVENT_DOUBLE_TAP, dictFromEvent(e));
                    boolean handledClick = proxy.fireEvent(TiC.EVENT_DOUBLE_CLICK, dictFromEvent(e));
                    return handledTap || handledClick;
                }
                return false;
            }

            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                Log.d(TAG, "TAP, TAP, TAP on " + proxy, Log.DEBUG_MODE);
                if (proxy.hierarchyHasListener(TiC.EVENT_SINGLE_TAP)) {
                    return proxy.fireEvent(TiC.EVENT_SINGLE_TAP, dictFromEvent(e));
                    // Moved click handling to the onTouch listener, because a single tap is not the
                    // same as a click. A single tap is a quick tap only, whereas clicks can be held
                    // before lifting.
                    // boolean handledClick = proxy.fireEvent(TiC.EVENT_CLICK, dictFromEvent(event));
                    // Note: this return value is irrelevant in our case. We "want" to use it
                    // in onTouch below, when we call detector.onTouchEvent(event); But, in fact,
                    // onSingleTapConfirmed is *not* called in the course of onTouchEvent. It's
                    // called via Handler in GestureDetector. <-- See its Java source.
                    // return handledTap;// || handledClick;
                }
                return false;
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                Log.d(TAG, "SWIPE on " + proxy, Log.DEBUG_MODE);
                if (proxy.hierarchyHasListener(TiC.EVENT_SWIPE)) {
                    KrollDict data = dictFromEvent(e2);
                    if (Math.abs(velocityX) > Math.abs(velocityY)) {
                        data.put(TiC.EVENT_PROPERTY_DIRECTION, velocityX > 0 ? "right" : "left");
                    } else {
                        data.put(TiC.EVENT_PROPERTY_DIRECTION, velocityY > 0 ? "down" : "up");
                    }
                    return proxy.fireEvent(TiC.EVENT_SWIPE, data);
                }
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                Log.d(TAG, "LONGPRESS on " + proxy, Log.DEBUG_MODE);

                if (proxy.hierarchyHasListener(TiC.EVENT_LONGPRESS)) {
                    proxy.fireEvent(TiC.EVENT_LONGPRESS, dictFromEvent(e));
                }
            }
        });

        touchable.setOnTouchListener(new OnTouchListener() {
            int pointersDown = 0;

            public boolean onTouch(View view, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    lastUpEvent.put(TiC.EVENT_PROPERTY_X, (double) event.getX());
                    lastUpEvent.put(TiC.EVENT_PROPERTY_Y, (double) event.getY());
                }

                scaleDetector.onTouchEvent(event);
                if (scaleDetector.isInProgress()) {
                    pointersDown = 0;
                    return true;
                }

                boolean handled = detector.onTouchEvent(event);
                if (handled) {
                    pointersDown = 0;
                    return true;
                }

                if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
                    if (didScale) {
                        didScale = false;
                        pointersDown = 0;
                    } else {
                        pointersDown++;
                    }
                } else if (event.getAction() == MotionEvent.ACTION_UP) {
                    if (pointersDown == 1) {
                        proxy.fireEvent(TiC.EVENT_TWOFINGERTAP, dictFromEvent(event));
                        pointersDown = 0;
                        return true;
                    }
                    pointersDown = 0;
                }

                String motionEvent = motionEvents.get(event.getAction());
                if (motionEvent != null) {
                    if (proxy.hierarchyHasListener(motionEvent)) {
                        proxy.fireEvent(motionEvent, dictFromEvent(event));
                    }
                }

                // Inside View.java, dispatchTouchEvent() does not call onTouchEvent() if this listener returns true. As
                // a result, click and other motion events do not occur on the native Android side. To prevent this, we
                // always return false and let Android generate click and other motion events.
                return false;
            }
        });

    }

    protected void registerForTouch(final View touchable) {
        if (touchable == null) {
            return;
        }

        registerTouchEvents(touchable);

        // Previously, we used the single tap handling above to fire our click event.  It doesn't
        // work: a single tap is not the same as a click.  A click can be held for a while before
        // lifting the finger; a single-tap is only generated from a quick tap (which will also cause
        // a click.)  We wanted to do it in single-tap handling presumably because the singletap
        // listener gets a MotionEvent, which gives us the information we want to provide to our
        // users in our click event, whereas Android's standard OnClickListener does _not_ contain
        // that info.  However, an "up" seems to always occur before the click listener gets invoked,
        // so we store the last up event's x,y coordinates (see onTouch above) and use them here.
        // Note: AdapterView throws an exception if you try to put a click listener on it.
        doSetClickable(touchable);
    }

    public void registerForKeyPress() {
        if (allowRegisterForKeyPress()) {
            registerForKeyPress(getNativeView());
        }
    }

    protected void registerForKeyPress(final View v) {
        if (v == null) {
            return;
        }

        Object focusable = proxy.getProperty(TiC.PROPERTY_FOCUSABLE);
        if (focusable != null) {
            registerForKeyPress(v, TiConvert.toBoolean(focusable));
        }
    }

    protected void registerForKeyPress(final View v, boolean focusable) {
        if (v == null) {
            return;
        }

        v.setFocusable(focusable);

        // The listener for the "keypressed" event is only triggered when the view has focus. So we only register the
        // "keypressed" event when the view is focusable.
        if (focusable) {
            registerForKeyPressEvents(v);
        } else {
            v.setOnKeyListener(null);
        }
    }

    /**
     * Registers a callback to be invoked when a hardware key is pressed in this view.
     *
     * @param v The view to have the key listener to attach to.
     */
    protected void registerForKeyPressEvents(final View v) {
        if (v == null) {
            return;
        }

        v.setOnKeyListener(new OnKeyListener() {
            public boolean onKey(View view, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_UP) {
                    KrollDict data = new KrollDict();
                    data.put(TiC.EVENT_PROPERTY_KEYCODE, keyCode);
                    proxy.fireEvent(TiC.EVENT_KEY_PRESSED, data);

                    switch (keyCode) {
                    case KeyEvent.KEYCODE_ENTER:
                    case KeyEvent.KEYCODE_DPAD_CENTER:
                        if (proxy.hasListeners(TiC.EVENT_CLICK)) {
                            proxy.fireEvent(TiC.EVENT_CLICK, null);
                            return true;
                        }
                    }
                }
                return false;
            }
        });
    }

    /**
     * Sets the nativeView's opacity.
     * @param opacity the opacity to set.
     */
    public void setOpacity(float opacity) {
        if (opacity < 0 || opacity > 1) {
            Log.w(TAG, "Ignoring invalid value for opacity: " + opacity);
            return;
        }
        if (borderView != null) {
            borderView.setBorderAlpha(Math.round(opacity * 255));
        }
        setOpacity(nativeView, opacity);
    }

    /**
     * Sets the view's opacity.
     * @param view the view object.
     * @param opacity the opacity to set.
     */
    protected void setOpacity(View view, float opacity) {
        if (view != null) {
            TiUIHelper.setDrawableOpacity(view.getBackground(), opacity);
            if (opacity == 1) {
                clearOpacity(view);
            }
            view.invalidate();
        }
    }

    public void clearOpacity(View view) {
        Drawable d = view.getBackground();
        if (d != null) {
            d.clearColorFilter();
        }
    }

    public KrollDict toImage() {
        return TiUIHelper.viewToImage(proxy.getProperties(), getNativeView());
    }

    private View getTouchView() {
        if (nativeView != null) {
            return nativeView;
        } else {
            if (touchView != null) {
                return touchView.get();
            }
        }
        return null;
    }

    private void doSetClickable(View view, boolean clickable) {
        if (view == null) {
            return;
        }
        if (!clickable) {
            view.setOnClickListener(null); // This will set clickable to true in the view, so make sure it stays here so the next line turns it off.
            view.setClickable(false);
            view.setOnLongClickListener(null);
            view.setLongClickable(false);
        } else if (!(view instanceof AdapterView)) {
            // n.b.: AdapterView throws if click listener set.
            // n.b.: setting onclicklistener automatically sets clickable to true.
            setOnClickListener(view);
            setOnLongClickListener(view);
        }
    }

    private void doSetClickable(boolean clickable) {
        doSetClickable(getTouchView(), clickable);
    }

    /*
     * Used just to setup the click listener if applicable.
     */
    private void doSetClickable(View view) {
        if (view == null) {
            return;
        }
        doSetClickable(view, view.isClickable());
    }

    /**
     * Can be overriden by inheriting views for special click handling.  For example,
     * the Facebook module's login button view needs special click handling.
     */
    protected void setOnClickListener(View view) {
        view.setOnClickListener(new OnClickListener() {
            public void onClick(View view) {
                proxy.fireEvent(TiC.EVENT_CLICK, dictFromEvent(lastUpEvent));
            }
        });
    }

    protected void setOnLongClickListener(View view) {
        view.setOnLongClickListener(new OnLongClickListener() {
            public boolean onLongClick(View view) {
                return proxy.fireEvent(TiC.EVENT_LONGCLICK, null);
            }
        });
    }

    private void disableHWAcceleration() {
        if (nativeView == null) {
            return;
        }
        Log.d(TAG, "Disabling hardware acceleration for instance of " + nativeView.getClass().getSimpleName(),
                Log.DEBUG_MODE);
        if (mSetLayerTypeMethod == null) {
            try {
                Class<? extends View> c = nativeView.getClass();
                mSetLayerTypeMethod = c.getMethod("setLayerType", int.class, Paint.class);
            } catch (SecurityException e) {
                Log.e(TAG, "SecurityException trying to get View.setLayerType to disable hardware acceleration.", e,
                        Log.DEBUG_MODE);
            } catch (NoSuchMethodException e) {
                Log.e(TAG,
                        "NoSuchMethodException trying to get View.setLayerType to disable hardware acceleration.",
                        e, Log.DEBUG_MODE);
            }
        }

        if (mSetLayerTypeMethod == null) {
            return;
        }
        try {
            mSetLayerTypeMethod.invoke(nativeView, LAYER_TYPE_SOFTWARE, null);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, e.getMessage(), e);
        } catch (IllegalAccessException e) {
            Log.e(TAG, e.getMessage(), e);
        } catch (InvocationTargetException e) {
            Log.e(TAG, e.getMessage(), e);
        }
    }

    /**
     * Retrieve the saved animated scale values, which we store here since Android provides no property
     * for looking them up.
     */
    public Pair<Float, Float> getAnimatedScaleValues() {
        return animatedScaleValues;
    }

    /**
     * Store the animated x and y scale values (i.e., the scale after an animation)
     * since Android provides no property for looking them up.
     */
    public void setAnimatedScaleValues(Pair<Float, Float> newValues) {
        animatedScaleValues = newValues;
    }

    /**
     * Set the animated rotation degrees, since Android provides no property for looking it up.
     */
    public void setAnimatedRotationDegrees(float degrees) {
        animatedRotationDegrees = degrees;
    }

    /**
     * Retrieve the animated rotation degrees, which we store here since Android provides no property
     * for looking it up.
     */
    public float getAnimatedRotationDegrees() {
        return animatedRotationDegrees;
    }

    /**
     * Set the animated alpha values, since Android provides no property for looking it up.
     */
    public void setAnimatedAlpha(float alpha) {
        animatedAlpha = alpha;
    }

    /**
     * Retrieve the animated alpha value, which we store here since Android provides no property
     * for looking it up.
     */
    public float getAnimatedAlpha() {
        return animatedAlpha;
    }

    /**
     * "Forget" the values we save after scale and rotation and alpha animations.
     */
    private void resetPostAnimationValues() {
        animatedRotationDegrees = 0f; // i.e., no rotation.
        animatedScaleValues = Pair.create(Float.valueOf(1f), Float.valueOf(1f)); // 1 means no scaling
        animatedAlpha = Float.MIN_VALUE; // we use min val to signal no val.
    }

    private void applyContentDescription() {
        if (proxy == null || nativeView == null) {
            return;
        }
        String contentDescription = composeContentDescription();
        if (contentDescription != null) {
            nativeView.setContentDescription(contentDescription);
        }
    }

    /**
     * Our view proxy supports three properties to match iOS regarding
     * the text that is read aloud (or otherwise communicated) by the
     * assistive technology: accessibilityLabel, accessibilityHint
     * and accessibilityValue.
     *
     * We combine these to create the single Android property contentDescription.
     * (e.g., View.setContentDescription(...));
     */
    protected String composeContentDescription() {
        if (proxy == null) {
            return null;
        }

        final String punctuationPattern = "^.*\\p{Punct}\\s*$";
        StringBuilder buffer = new StringBuilder();

        KrollDict properties = proxy.getProperties();
        String label, hint, value;
        label = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_LABEL));
        hint = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_HINT));
        value = TiConvert.toString(properties.get(TiC.PROPERTY_ACCESSIBILITY_VALUE));

        if (!TextUtils.isEmpty(label)) {
            buffer.append(label);
            if (!label.matches(punctuationPattern)) {
                buffer.append(".");
            }
        }

        if (!TextUtils.isEmpty(value)) {
            if (buffer.length() > 0) {
                buffer.append(" ");
            }
            buffer.append(value);
            if (!value.matches(punctuationPattern)) {
                buffer.append(".");
            }
        }

        if (!TextUtils.isEmpty(hint)) {
            if (buffer.length() > 0) {
                buffer.append(" ");
            }
            buffer.append(hint);
            if (!hint.matches(punctuationPattern)) {
                buffer.append(".");
            }
        }

        return buffer.toString();
    }

    private void applyAccessibilityProperties() {
        if (nativeView != null) {
            applyContentDescription();
            applyAccessibilityHidden();
        }

    }

    private void applyAccessibilityHidden() {
        if (nativeView == null || proxy == null) {
            return;
        }

        applyAccessibilityHidden(proxy.getProperty(TiC.PROPERTY_ACCESSIBILITY_HIDDEN));
    }

    private void applyAccessibilityHidden(Object hiddenPropertyValue) {
        if (nativeView == null) {
            return;
        }

        int importanceMode = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;

        if (hiddenPropertyValue != null && TiConvert.toBoolean(hiddenPropertyValue)) {
            importanceMode = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO;
        }

        ViewCompat.setImportantForAccessibility(nativeView, importanceMode);
    }

}