package org.lab99.mdt.view; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; import org.lab99.mdt.R; import org.lab99.mdt.drawable.MessengerDrawable; import org.lab99.mdt.drawable.PaperDrawable; /** * The Paper widget implement the basic idea of the Material Design. * <p/> * To implement the shadow and ripple effect of material design, a special wrapper Drawable, * {@link PaperDrawable} is used as the view background. * Any background set through the setBackground() or "android:background" will be set as inner * background of the wrapper. */ public class Paper extends TextView { public Paper(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } public Paper(Context context) { this(context, null, 0); } public Paper(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * Apply materials design on normal Android widget by wrapping the background with {@link PaperDrawable}. * * @param view the given normal android widget * @return The created {@link PaperDrawable} wrapper background will be returned. */ public static PaperDrawable apply(View view) { return apply(view, view); } /** * Apply materials design on normal Android widget by wrapping the background with PaperDrawable. * This function is enable the separation of touch view and ripple effect display view, * so, one can touch a widget, and the ripple is shown on the top larger transparent layer. * * @param touch_view touch view will trigger the state change event; * @param ripple_view the ripple view has to be able to receive 'onTouch' event * @return The created {@link PaperDrawable} wrapper background will be returned. */ public static PaperDrawable apply(View touch_view, View ripple_view) { if (touch_view instanceof Paper) { return ((Paper) touch_view).getPaperBackground(); } Drawable original = touch_view.getBackground(); if (original instanceof PaperDrawable) { // don't swap the background if it's already PaperDrawable already. return (PaperDrawable) original; } else { // create new warp drawable for the old drawable PaperDrawable background = new PaperDrawable(ripple_view.getBackground(), touch_view.getContext()); ViewCompat.setBackground(ripple_view, background); // attach touch tracker ripple_view.setOnTouchListener(background.getTouchTracker()); if (touch_view != ripple_view) { // link the messenger for passing state message to 'background' MessengerDrawable messenger = new MessengerDrawable(touch_view.getBackground(), background); ViewCompat.setBackground(touch_view, messenger); // force ripple_view not 'clickable', so it will not trigger the state change event. ripple_view.setClickable(false); } return background; } } // initializer protected void init(Context context, AttributeSet attrs) { initAttributes(context, attrs); } protected void initAttributes(Context context, AttributeSet attrs) { if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Paper); if (a == null) { return; } try { setDepth(a.getFloat(R.styleable.Paper_depth, getDefaultDepth())); setRippleEnabled(a.getBoolean(R.styleable.Paper_rippleEnabled, isDefaultRippleEnabled())); setRippleOnTouchEnabled(a.getBoolean(R.styleable.Paper_rippleOnTouchEnabled, isDefaultRippleOnTouchEnabled())); } finally { a.recycle(); } } } // Override @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void setRotation(float rotation) { super.setRotation(rotation); getPaperBackground().setRotation(rotation); } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { getPaperBackground().getTouchTracker().onTouch(this, event); return super.onTouchEvent(event); } // Getters & Setters /** * Get the {@link PaperDrawable} background. * * @return the {@link PaperDrawable} object of the background. */ public PaperDrawable getPaperBackground() { Drawable background = super.getBackground(); if (background instanceof PaperDrawable) { return (PaperDrawable) background; } else { // create the PaperDrawable to wrap the original one, if it's not already PaperDrawable. PaperDrawable drawable = new PaperDrawable(background, getContext()); ViewCompat.setBackground(this, drawable); return drawable; } } /** * Check whether the ripple effect is enabled on this view. * * @return Return true if the ripple effect is enabled. */ public boolean isRippleEnabled() { return getPaperBackground().isRippleEnabled(); } /** * Enable or disable the ripple effect of this view. * * @param enabled Set to true if want to enable the ripple effect. */ public void setRippleEnabled(boolean enabled) { getPaperBackground().setRippleEnabled(enabled); postInvalidate(); } /** * Check whether the ripple will be active via touch event. * * @return Return true if the ripple will be active by touch event. */ public boolean isRippleOnTouchEnabled() { return getPaperBackground().isRippleOnTouchEnabled(); } /** * Enable the ripple effect on the touch event. * * @param enabled Set to true if want to enable the ripple effect on the touch event. */ public void setRippleOnTouchEnabled(boolean enabled) { getPaperBackground().setRippleOnTouchEnabled(enabled); } /** * Get the drawable of the user specified background, which is the inner Drawable of the wrapper * {@link PaperDrawable}. * * @return the inner drawable of the background. */ @Override public Drawable getBackground() { return getPaperBackground().getOriginal(); } /** * Set the inner background of the wrapper {@link PaperDrawable}, so the material design effect * will not be affected if the background is changed by the user. * * @param background The new drawable for the background. */ @Override public void setBackground(Drawable background) { if (background instanceof PaperDrawable) { // replace the background of PaperDrawable super.setBackground(background); } else { // replace the inner original PaperDrawable getPaperBackground().setOriginal(background); } } /** * Get the shadow depth, * @return */ public float getDepth() { return getPaperBackground().getDepth(); } /** * Set the shadow depth. * * The depth is a relative depth to the view below, just the value for how the depth will be drawn. * It's not the absolute z-depth. So, if the setDepth(1) on the below view, and setDepth(2) on the * above view, the result shadow depth will still be <b>2</b>, rather than <b>1</b>. * * @param depth The depth value for the shadow. */ public void setDepth(float depth) { getPaperBackground().setDepth(depth); postInvalidate(); } // default value protected float getDefaultDepth() { return 0; } protected boolean isDefaultRippleEnabled() { return true; } protected boolean isDefaultRippleOnTouchEnabled() { return false; } }