All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.marvinlabs.widget.floatinglabel.FloatingLabelWidgetBase Maven / Gradle / Ivy

The newest version!
package com.marvinlabs.widget.floatinglabel;

import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.TextView;

import com.marvinlabs.widget.floatinglabel.anim.DefaultLabelAnimator;

/**
 * Created by Vincent Mimoun-Prat @ MarvinLabs, 28/08/2014.
 */
public abstract class FloatingLabelWidgetBase extends FrameLayout {
    private static final String SAVE_STATE_KEY_LABEL = "saveStateLabel";
    private static final String SAVE_STATE_KEY_PARENT = "saveStateParent";
    private static final String SAVE_STATE_KEY_INPUT_WIDGET = "saveStateInputWidget";

    private static final String SAVE_STATE_TAG = "saveStateTag";

    /**
     * When the label is floated
     */
    protected boolean isFloatOnFocusEnabled = true;

    /**
     * true when the view has gone through at least one layout pass
     */
    private boolean isLaidOut = false;

    /**
     * When init is complete, child views can no longer be added
     */
    private boolean initCompleted = false;

    /**
     * Reference to the TextView used as the label
     */
    private TextView floatingLabel;

    /**
     * LabelAnimator that animates the appearance and disappearance of the label TextView
     */
    private LabelAnimator labelAnimator;

    /**
     * Holds saved state if any is waiting to be restored
     */
    private Bundle savedState;

    /**
     * The input widget
     */
    private InputWidgetT inputWidget;

    // =============================================================================================
    // Lifecycle
    // ==

    public FloatingLabelWidgetBase(Context context) {
        this(context, null, 0);
    }

    public FloatingLabelWidgetBase(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FloatingLabelWidgetBase(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs, defStyle);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int childLeft = getPaddingLeft();
        final int childRight = right - left - getPaddingRight();

        int childTop = getPaddingTop();
        final int childBottom = bottom - top - getPaddingBottom();

        layoutChild(floatingLabel, childLeft, childTop, childRight, childBottom);
        layoutChild(getInputWidget(), childLeft, childTop + floatingLabel.getMeasuredHeight(), childRight, childBottom);
    }

    private void layoutChild(View child, int parentLeft, int parentTop, int parentRight, int parentBottom) {
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            final int childTop = parentTop + lp.topMargin;

            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = Gravity.TOP | Gravity.START;
            }

            final int layoutDirection;
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
                layoutDirection = LAYOUT_DIRECTION_LTR;
            } else {
                layoutDirection = getLayoutDirection();
            }

            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);

            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin;
                    break;
                case Gravity.RIGHT:
                    childLeft = parentRight - width - lp.rightMargin;
                    break;
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }

            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Restore any state that's been pending before measuring
        if (savedState != null) {
            Parcelable childState = savedState.getParcelable(SAVE_STATE_KEY_LABEL);
            floatingLabel.onRestoreInstanceState(childState);

            childState = savedState.getParcelable(SAVE_STATE_KEY_INPUT_WIDGET);
            // Because View#onRestoreInstanceState is protected, we got to ask subclasses to do it for us
            restoreInputWidgetState(childState);

            savedState = null;
        }
        measureChild(floatingLabel, widthMeasureSpec, heightMeasureSpec);
        measureChild(getInputWidget(), widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

    protected int measureHeight(int heightMeasureSpec) {
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);

        int result = 0;
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = getInputWidget().getMeasuredHeight() + floatingLabel.getMeasuredHeight();
            result += getPaddingTop() + getPaddingBottom();
            result = Math.max(result, getSuggestedMinimumHeight());

            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    protected int measureWidth(int widthMeasureSpec) {
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);

        int result = 0;
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            result = Math.max(getInputWidget().getMeasuredWidth(), floatingLabel.getMeasuredWidth());
            result = Math.max(result, getSuggestedMinimumWidth());
            result += getPaddingLeft() + getPaddingRight();
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }

        return result;
    }

    // =============================================================================================
    // State
    // ==

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            final Bundle savedState = (Bundle) state;
            if (savedState.getBoolean(SAVE_STATE_TAG, false)) {
                // Save our state for later since children will have theirs restored after this
                // and having more than one FloatLabel in an Activity or Fragment means you have
                // multiple views of the same ID
                this.savedState = savedState;

                restoreAdditionalInstanceState(savedState);

                super.onRestoreInstanceState(savedState.getParcelable(SAVE_STATE_KEY_PARENT));
                return;
            }
        }

        super.onRestoreInstanceState(state);
    }

    /**
     * Give the opportunity to child classes to restore additional state variables they had saved
     *
     * @param savedState The state of the floating label widget
     */
    protected void restoreAdditionalInstanceState(Bundle savedState) {
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        final Parcelable superState = super.onSaveInstanceState();

        final Bundle saveState = new Bundle();
        saveState.putParcelable(SAVE_STATE_KEY_INPUT_WIDGET, saveInputWidgetInstanceState());
        saveState.putParcelable(SAVE_STATE_KEY_LABEL, floatingLabel.onSaveInstanceState());
        saveState.putParcelable(SAVE_STATE_KEY_PARENT, superState);
        saveState.putBoolean(SAVE_STATE_TAG, true);

        putAdditionalInstanceState(saveState);

        return saveState;
    }

    /**
     * Give the opportunity to child classes to save additional state variables
     *
     * @param saveState The state of the floating label widget
     */
    protected void putAdditionalInstanceState(Bundle saveState) {
    }

    /**
     * Restore the saved state of the input widget
     *
     * @param inputWidgetState The state of the input widget
     */
    protected void restoreInputWidgetState(Parcelable inputWidgetState) {
    }

    /**
     * Save the input widget state. Usually you will simply call the widget's onSaveInstanceState
     * method
     *
     * @return The saved input widget state
     */
    protected Parcelable saveInputWidgetInstanceState() {
        return new Bundle();
    }

    // =============================================================================================
    // Floating label
    // ==

    /**
     * Set the inital state for the label
     */
    protected void setInitialWidgetState() {
        setLabelAnchored(true);
    }

    /**
     * Delegate method for the floating label animator
     */
    public boolean isLabelAnchored() {
        return getLabelAnimator().isAnchored();
    }

    /**
     * Delegate method for the floating label animator
     */
    public void anchorLabel() {
        if (!isLaidOut) return;
        getLabelAnimator().anchorLabel(getInputWidget(), getFloatingLabel());
    }

    /**
     * Delegate method for the floating label animator
     */
    public void floatLabel() {
        if (!isLaidOut) return;
        getLabelAnimator().floatLabel(getInputWidget(), getFloatingLabel());
    }

    /**
     * Delegate method for the floating label animator
     */
    public void setLabelAnchored(boolean isAnchored) {
        if (!isLaidOut) return;
        getLabelAnimator().setLabelAnchored(getInputWidget(), getFloatingLabel(), isAnchored);
    }

    /**
     * Specifies a new LabelAnimator to handle calls to show/hide the label
     *
     * @param labelAnimator LabelAnimator to use; null causes use of the default LabelAnimator
     */
    public void setLabelAnimator(LabelAnimator labelAnimator) {
        if (labelAnimator == null) {
            this.labelAnimator = new DefaultLabelAnimator();
        } else {
            if (this.labelAnimator != null) {
                labelAnimator.setLabelAnchored(getInputWidget(), getFloatingLabel(), this.labelAnimator.isAnchored());
            }
            this.labelAnimator = labelAnimator;
        }

        if (isInEditMode()) {
            this.labelAnimator.setLabelAnchored(getInputWidget(), getFloatingLabel(), false);
        }
    }

    /**
     * Get the animator for the label
     *
     * @return
     */
    public LabelAnimator getLabelAnimator() {
        return labelAnimator;
    }

    /**
     * The default animator to use for the label. That method is called in init so that subclasses
     * can provide their own specialized animator if appropriate.
     *
     * @return
     */
    protected LabelAnimator getDefaultLabelAnimator() {
        return new DefaultLabelAnimator();
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelText(int resid) {
        floatingLabel.setText(resid);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelText(CharSequence hint) {
        floatingLabel.setText(hint);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelTypeface(Typeface tf, int style) {
        floatingLabel.setTypeface(tf, style);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelTypeface(Typeface tf) {
        floatingLabel.setTypeface(tf);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelTextColor(ColorStateList colors) {
        floatingLabel.setTextColor(colors);
    }

    public void setLabelTextAppearance(Context context, int resid) {
        floatingLabel.setTextAppearance(context, resid);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelColor(int color) {
        floatingLabel.setTextColor(color);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelAllCaps(boolean allCaps) {
        floatingLabel.setAllCaps(allCaps);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelTextSize(float size) {
        floatingLabel.setTextSize(size);
    }

    /**
     * Delegate method for the floating label TextView
     */
    public void setLabelTextSize(int unit, float size) {
        floatingLabel.setTextSize(unit, size);
    }

    /**
     * Delegate method for the floating label TextView
     */
    protected TextView getFloatingLabel() {
        return floatingLabel;
    }

    /**
     * Shall we float the label we we gain focus
     *
     * @return
     */
    public boolean isFloatOnFocusEnabled() {
        return isFloatOnFocusEnabled;
    }

    /**
     * Shall we float the label we we gain focus
     *
     * @param isFloatOnFocusEnabled
     */
    public void setFloatOnFocusEnabled(boolean isFloatOnFocusEnabled) {
        this.isFloatOnFocusEnabled = isFloatOnFocusEnabled;
    }

    // =============================================================================================
    // ViewGroup overrides
    // ==

    @Override
    public void addView(View child) {
        if (initCompleted) {
            throw new UnsupportedOperationException("You cannot add child views to a FloatLabel");
        } else {
            super.addView(child);
        }
    }

    @Override
    public void addView(View child, int index) {
        if (initCompleted) {
            throw new UnsupportedOperationException("You cannot add child views to a FloatLabel");
        } else {
            super.addView(child, index);
        }
    }

    @Override
    public void addView(View child, int index, android.view.ViewGroup.LayoutParams params) {
        if (initCompleted) {
            throw new UnsupportedOperationException("You cannot add child views to a FloatLabel");
        } else {
            super.addView(child, index, params);
        }
    }

    @Override
    public void addView(View child, int width, int height) {
        if (initCompleted) {
            throw new UnsupportedOperationException("You cannot add child views to a FloatLabel");
        } else {
            super.addView(child, width, height);
        }
    }

    @Override
    public void addView(View child, android.view.ViewGroup.LayoutParams params) {
        if (initCompleted) {
            throw new UnsupportedOperationException("You cannot add child views to a FloatLabel");
        } else {
            super.addView(child, params);
        }
    }

    // =============================================================================================
    // Other methods
    // ==

    /**
     * Specifies the layout to be inflated for the widget content. That layout must at least declare
     * an EditText with id "flw_floating_label" and an input widget of the proper class with id
     * "flw_input_widget"
     *
     * @return The ID of the layout to inflate and attach to this view group
     */
    protected abstract int getDefaultLayoutId();

    /**
     * Get the input widget we are using
     *
     * @return The input widget
     */
    public InputWidgetT getInputWidget() {
        return inputWidget;
    }

    /**
     * Returns the saved state of this widget
     *
     * @return
     */
    protected Bundle getSavedState() {
        return savedState;
    }

    /**
     * Initialise the widget: read attributes, inflate layout and set the basic properties
     *
     * @param context
     * @param attrs
     * @param defStyle
     */
    protected void init(Context context, AttributeSet attrs, int defStyle) {
        // Load custom attributes
        final int layoutId;
        final CharSequence floatLabelText;
        final int floatLabelTextAppearance;
        final int floatLabelTextColor;
        final float floatLabelTextSize;

        if (attrs == null) {
            layoutId = getDefaultLayoutId();
            isFloatOnFocusEnabled = true;
            floatLabelText = null;
            floatLabelTextAppearance = -1;
            floatLabelTextColor = 0x66000000;
            floatLabelTextSize = getResources().getDimensionPixelSize(R.dimen.flw_defaultLabelTextSize);
        } else {
            final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingLabelWidgetBase, defStyle, 0);

            layoutId = a.getResourceId(R.styleable.FloatingLabelWidgetBase_android_layout, getDefaultLayoutId());
            isFloatOnFocusEnabled = a.getBoolean(R.styleable.FloatingLabelWidgetBase_flw_floatOnFocus, true);
            floatLabelText = a.getText(R.styleable.FloatingLabelWidgetBase_flw_labelText);
            floatLabelTextColor = a.getColor(R.styleable.FloatingLabelWidgetBase_flw_labelTextColor, 0x66000000);
            floatLabelTextAppearance = a.getResourceId(R.styleable.FloatingLabelWidgetBase_flw_labelTextAppearance, -1);
            floatLabelTextSize = a.getDimension(R.styleable.FloatingLabelWidgetBase_flw_labelTextSize, getResources().getDimensionPixelSize(R.dimen.flw_defaultLabelTextSize));

            a.recycle();
        }

        inflateWidgetLayout(context, layoutId);

        getFloatingLabel().setFocusableInTouchMode(false);
        getFloatingLabel().setFocusable(false);

        setLabelAnimator(getDefaultLabelAnimator());
        setLabelText(floatLabelText);
        if (floatLabelTextAppearance != -1) {
            setLabelTextAppearance(getContext(), floatLabelTextAppearance);
        }
        setLabelColor(floatLabelTextColor);
        setLabelTextSize(TypedValue.COMPLEX_UNIT_PX, floatLabelTextSize);

        afterLayoutInflated(context, attrs, defStyle);

        isLaidOut = false;
        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                isLaidOut = true;
                setInitialWidgetState();
                if (Build.VERSION.SDK_INT >= 16) {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                } else {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }
            }
        });

        // Mark init as complete to prevent accidentally breaking the view by
        // adding children
        initCompleted = true;
        onInitCompleted();
    }

    /**
     * Can be overriden to do something after we are done with init
     */
    protected void onInitCompleted() {
    }

    /**
     * Can be overriden to do something after the layout inflation
     */
    protected void afterLayoutInflated(Context context, AttributeSet attrs, int defStyle) {
    }

    /**
     * Inflate the widget layout and make sure we have everything in there
     *
     * @param context  The context
     * @param layoutId The id of the layout to inflate
     */
    private void inflateWidgetLayout(Context context, int layoutId) {
        inflate(context, layoutId, this);

        floatingLabel = (TextView) findViewById(R.id.flw_floating_label);
        if (floatingLabel == null) {
            throw new RuntimeException("Your layout must have a TextView whose ID is @id/flw_floating_label");
        }

        View iw = findViewById(R.id.flw_input_widget);
        if (iw == null) {
            throw new RuntimeException("Your layout must have an input widget whose ID is @id/flw_input_widget");
        }
        try {
            inputWidget = (InputWidgetT) iw;
        } catch (ClassCastException e) {
            throw new RuntimeException("The input widget is not of the expected type");
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy