Java tutorial
/* * ================================================================================================= * Copyright (C) 2014 Martin Albedinsky * ================================================================================================= * Licensed under the Apache License, Version 2.0 or later (further "License" only). * ------------------------------------------------------------------------------------------------- * You may use this file only in compliance with the License. More details and copy of this License * you may obtain at * * http://www.apache.org/licenses/LICENSE-2.0 * * You can redistribute, modify or publish any part of the code written within this file but as it * is described in the License, the software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES or CONDITIONS OF ANY KIND. * * See the License for the specific language governing permissions and limitations under the License. * ================================================================================================= */ package com.albedinsky.android.support.ui.widget; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import com.albedinsky.android.support.ui.R; import com.albedinsky.android.support.ui.UiConfig; import com.albedinsky.android.support.ui.WidgetSizeAnimator; import com.albedinsky.android.support.ui.graphics.TintOptions; import com.albedinsky.android.support.ui.graphics.drawable.TintDrawable; /** * Extended version of {@link android.widget.FrameLayout}. * * <h3>Tinting</h3> * <b>Tinting of the background is supported by this widget for the versions below LOLLIPOP.</b> * * <h3>Sliding</h3> * This updated view allows updating of its current position along <b>x</b> and <b>y</b> axis by * changing <b>fraction</b> of these properties depends on its current size using the new animation * framework introduced in {@link android.os.Build.VERSION_CODES#HONEYCOMB HONEYCOMB} by * {@link android.animation.ObjectAnimator ObjectAnimator}s API. * <p> * Changing of fraction of X or Y is supported by these two methods: * <ul> * <li>{@link #setFractionX(float)}</li> * <li>{@link #setFractionY(float)}</li> * </ul> * <p> * For example if an instance of this view class needs to be slided to the right by whole width of * such a view, an Xml file with ObjectAnimator will look like this: * <pre> * <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" * android:propertyName="fractionX" * android:valueFrom="0.0" * android:valueTo="1.0" * android:duration="300"/> * </pre> * If this layout class is used as a root of view hierarchy, this can be especially used for fragment * transitions framework, where for {@link android.support.v4.app.FragmentTransaction FragmentTransaction}, * used to change currently visible fragment by a new one, can be specified custom animations which * will change fragments by sliding them horizontally. * * @author Martin Albedinsky */ public class FrameLayoutWidget extends FrameLayout implements Widget { /** * Interface =================================================================================== */ /** * Constants =================================================================================== */ /** * Log TAG. */ // private static final String TAG = "FrameLayoutWidget"; /** * Flag indicating whether the debug output trough log-cat is enabled or not. */ // private static final boolean DEBUG_ENABLED = true; /** * Flag indicating whether the output trough log-cat is enabled or not. */ // private static final boolean LOG_ENABLED = true; /** * Static members ============================================================================== */ /** * Members ===================================================================================== */ /** * This view's dimension. */ private int mWidth, mHeight; /** * Animator used to animate size of this view. */ private WidgetSizeAnimator mSizeAnimator; /** * Set of private flags specific for this widget. */ private int mPrivateFlags = PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION; /** * Data used when tinting components of this view. */ private BackgroundTintInfo mTintInfo; /** * Constructors ================================================================================ */ /** * Same as {@link #FrameLayoutWidget(android.content.Context, android.util.AttributeSet)} without * attributes. */ public FrameLayoutWidget(Context context) { this(context, null); } /** * Same as {@link #FrameLayoutWidget(android.content.Context, android.util.AttributeSet, int)} * with {@code 0} as attribute for default style. */ public FrameLayoutWidget(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * Creates a new instance of FrameLayoutWidget within the given <var>context</var>. * * @param context Context in which will be this view presented. * @param attrs Set of Xml attributes used to configure the new instance of this view. * @param defStyleAttr An attribute which contains a reference to a default style resource for * this view within a theme of the given context. */ public FrameLayoutWidget(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); /** * Process attributes. */ final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Ui_ViewGroup, defStyleAttr, 0); if (typedArray != null) { this.processTintValues(context, typedArray); typedArray.recycle(); } this.applyBackgroundTint(); } /** * Methods ===================================================================================== */ /** * Public -------------------------------------------------------------------------------------- */ /** */ @NonNull @Override public WidgetSizeAnimator animateSize() { return (mSizeAnimator != null) ? mSizeAnimator : (mSizeAnimator = new WidgetSizeAnimator(this)); } /** */ @Override public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(FrameLayoutWidget.class.getName()); } /** */ @Override public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(FrameLayoutWidget.class.getName()); } /** * Getters + Setters --------------------------------------------------------------------------- */ /** */ @Override @SuppressWarnings("deprecation") public void setBackgroundDrawable(Drawable background) { super.setBackgroundDrawable(background); this.applyBackgroundTint(); } /** */ @Override @SuppressLint("NewApi") public void setBackgroundTintList(@Nullable ColorStateList tint) { if (UiConfig.LOLLIPOP) { super.setBackgroundTintList(tint); return; } this.ensureTintInfo(); mTintInfo.backgroundTintList = tint; mTintInfo.hasBackgroundTintList = true; this.applyBackgroundTint(); } /** */ @Nullable @Override @SuppressLint("NewApi") public ColorStateList getBackgroundTintList() { if (UiConfig.LOLLIPOP) { return super.getBackgroundTintList(); } return mTintInfo != null ? mTintInfo.backgroundTintList : null; } /** */ @Override @SuppressLint("NewApi") public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { if (UiConfig.LOLLIPOP) { super.setBackgroundTintMode(tintMode); return; } this.ensureTintInfo(); mTintInfo.backgroundTintMode = tintMode; mTintInfo.hasBackgroundTinMode = true; this.applyBackgroundTint(); } /** */ @Nullable @Override @SuppressLint("NewApi") public PorterDuff.Mode getBackgroundTintMode() { if (UiConfig.LOLLIPOP) { return super.getBackgroundTintMode(); } return mTintInfo != null ? mTintInfo.backgroundTintMode : null; } /** */ @Override public void setFractionX(float fraction) { setX(mWidth > 0 ? (getLeft() + (fraction * mWidth)) : OUT_OF_SCREEN); } /** */ @Override public float getFractionX() { return (mWidth > 0) ? (getLeft() + (getX() / mWidth)) : 0; } /** */ @Override public void setFractionY(float fraction) { setY(mHeight > 0 ? (getTop() + (fraction * mHeight)) : OUT_OF_SCREEN); } /** */ @Override public float getFractionY() { return (mHeight > 0) ? (getTop() + (getY() / mHeight)) : 0; } /** */ @Override public void setPressed(boolean pressed) { final boolean isPressed = isPressed(); super.setPressed(pressed); if (!isPressed && pressed) { onPressed(); } else if (isPressed) { onReleased(); } } /** */ @Override public void setSelected(boolean selected) { if (hasPrivateFlag(PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION)) { setSelectionState(selected); } } /** */ @Override public void setSelectionState(boolean selected) { super.setSelected(selected); } /** */ @Override public void setAllowDefaultSelection(boolean allow) { this.updatePrivateFlags(PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION, allow); } /** */ @Override public boolean allowsDefaultSelection() { return this.hasPrivateFlag(PrivateFlags.PFLAG_ALLOWS_DEFAULT_SELECTION); } /** * Protected ----------------------------------------------------------------------------------- */ /** * Invoked whenever {@link #setPressed(boolean)} is called with {@code true} and this view * isn't in the pressed state yet. */ protected void onPressed() { } /** * Invoked whenever {@link #setPressed(boolean)} is called with {@code false} and this view * is currently in the pressed state. */ protected void onReleased() { } /** */ @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); this.mWidth = w; this.mHeight = h; } /** * Private ------------------------------------------------------------------------------------- */ /** * Ensures that the tint info object is initialized. */ private void ensureTintInfo() { if (mTintInfo == null) { this.mTintInfo = new BackgroundTintInfo(); } } /** * Called from the constructor to process tint values for this view. <b>Note</b>, that for * {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} is this call ignored. * * @param context The context passed to constructor. * @param typedArray TypedArray obtained for styleable attributes specific for this view. */ @SuppressWarnings("All") private void processTintValues(Context context, TypedArray typedArray) { // Do not handle for LOLLIPOP. if (UiConfig.LOLLIPOP) { return; } this.ensureTintInfo(); // Get tint colors. if (typedArray.hasValue(R.styleable.Ui_ViewGroup_uiBackgroundTint)) { mTintInfo.backgroundTintList = typedArray.getColorStateList(R.styleable.Ui_ViewGroup_uiBackgroundTint); } // Get tint modes. mTintInfo.backgroundTintMode = TintManager.parseTintMode( typedArray.getInt(R.styleable.Ui_ViewGroup_uiBackgroundTintMode, 0), mTintInfo.backgroundTintList != null ? PorterDuff.Mode.SRC_IN : null); // If there is no tint mode specified within style/xml do not tint at all. if (mTintInfo.backgroundTintMode == null) { mTintInfo.backgroundTintList = null; } mTintInfo.hasBackgroundTintList = mTintInfo.backgroundTintList != null; mTintInfo.hasBackgroundTinMode = mTintInfo.backgroundTintMode != null; } /** * Applies current background tint from {@link #mTintInfo} to the current background drawable. * <b>Note</b>, that for {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} is this call * ignored. * * @return {@code True} if the tint has been applied or cleared, {@code false} otherwise. */ @SuppressWarnings("deprecation") private boolean applyBackgroundTint() { final Drawable drawable = getBackground(); if (UiConfig.LOLLIPOP || mTintInfo == null || (!mTintInfo.hasBackgroundTintList && !mTintInfo.hasBackgroundTinMode) || drawable == null) { return false; } final TintOptions tintOptions = new TintOptions().tintList(mTintInfo.backgroundTintList) .tintMode(mTintInfo.backgroundTintMode); if (drawable instanceof TintDrawable) { if (!tintOptions.applyable()) { drawable.setCallback(null); drawable.clearColorFilter(); super.setBackgroundDrawable(((TintDrawable) drawable).getDrawable()); } else { ((TintDrawable) drawable).setTintOptions(tintOptions); } return true; } if (!tintOptions.applyable()) { drawable.clearColorFilter(); return true; } final TintDrawable tintDrawable = new TintDrawable(drawable); tintDrawable.setTintOptions(tintOptions); super.setBackgroundDrawable(tintDrawable); tintDrawable.attachCallback(); return true; } /** * Updates the current private flags. * * @param flag Value of the desired flag to add/remove to/from the current private flags. * @param add Boolean flag indicating whether to add or remove the specified <var>flag</var>. */ @SuppressWarnings("unused") private void updatePrivateFlags(int flag, boolean add) { if (add) { this.mPrivateFlags |= flag; } else { this.mPrivateFlags &= ~flag; } } /** * Returns a boolean flag indicating whether the specified <var>flag</var> is contained within * the current private flags or not. * * @param flag Value of the flag to check. * @return {@code True} if the requested flag is contained, {@code false} otherwise. */ @SuppressWarnings("unused") private boolean hasPrivateFlag(int flag) { return (mPrivateFlags & flag) != 0; } /** * Inner classes =============================================================================== */ }