Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d5154ec2 authored by Alan Viverette's avatar Alan Viverette
Browse files

Add prototype for borderless touch feedback drawable

Change-Id: I6366855b1fb838aa077bc6bdb62adc2134c51dca
parent 0e29ad02
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -14983,7 +14983,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     * @param displayList Valid display list for the background drawable
     */
    private void setBackgroundDisplayListProperties(DisplayList displayList) {
        displayList.setProjectBackwards((mPrivateFlags3 & PFLAG3_PROJECT_BACKGROUND) != 0);
        displayList.setTranslationX(mScrollX);
        displayList.setTranslationY(mScrollY);
    }
@@ -15014,6 +15013,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        // Set up drawable properties that are view-independent.
        displayList.setLeftTopRightBottom(bounds.left, bounds.top, bounds.right, bounds.bottom);
        displayList.setProjectBackwards(drawable.isProjected());
        displayList.setClipToBounds(false);
        return displayList;
    }
@@ -15367,7 +15367,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                mBackgroundDisplayList.clear();
            }
            final Rect dirty = drawable.getBounds();
            final Rect dirty = drawable.getDirtyBounds();
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
+25 −0
Original line number Diff line number Diff line
@@ -227,6 +227,20 @@ public abstract class Drawable {
        return mBounds;
    }

    /**
     * Return the drawable's dirty bounds Rect. Note: for efficiency, the
     * returned object may be the same object stored in the drawable (though
     * this is not guaranteed).
     * <p>
     * By default, this returns the full drawable bounds. Custom drawables may
     * override this method to perform more precise invalidation.
     *
     * @hide
     */
    public Rect getDirtyBounds() {
        return getBounds();
    }

    /**
     * Set a mask of the configuration parameters for which this drawable
     * may change, requiring that it be re-created.
@@ -507,6 +521,15 @@ public abstract class Drawable {
     */
    public void clearHotspots() {}

    /**
     * Whether this drawable requests projection.
     *
     * @hide
     */
    public boolean isProjected() {
        return false;
    }

    /**
     * Indicates whether this view will change its appearance based on state.
     * Clients can use this to determine whether it is necessary to calculate
@@ -962,6 +985,8 @@ public abstract class Drawable {
            drawable = new TransitionDrawable();
        } else if (name.equals("reveal")) {
            drawable = new RevealDrawable();
        } else if (name.equals("touch-feedback")) {
            drawable = new TouchFeedbackDrawable();
        } else if (name.equals("color")) {
            drawable = new ColorDrawable();
        } else if (name.equals("shape")) {
+9 −0
Original line number Diff line number Diff line
@@ -251,4 +251,13 @@ class Ripple {

        return false;
    }

    public void getBounds(Rect bounds) {
        final int x = (int) mX;
        final int y = (int) mY;
        final int dX = Math.max(x, mBounds.right - x);
        final int dY = Math.max(x, mBounds.bottom - y);
        final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY));
        bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius);
    }
}
+371 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics.drawable;

import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Xfermode;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.SparseArray;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;

/**
 * An extension of LayerDrawable that is intended to react to touch hotspots
 * and reveal the second layer atop the first.
 * <p>
 * It can be defined in an XML file with the <code>&lt;reveal&gt;</code> element.
 * Each Drawable in the transition is defined in a nested <code>&lt;item&gt;</code>.
 * For more information, see the guide to <a href="{@docRoot}
 * guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
 *
 * @attr ref android.R.styleable#LayerDrawableItem_left
 * @attr ref android.R.styleable#LayerDrawableItem_top
 * @attr ref android.R.styleable#LayerDrawableItem_right
 * @attr ref android.R.styleable#LayerDrawableItem_bottom
 * @attr ref android.R.styleable#LayerDrawableItem_drawable
 * @attr ref android.R.styleable#LayerDrawableItem_id
 * @hide
 */
public class TouchFeedbackDrawable extends Drawable {
    private final Rect mTempRect = new Rect();
    private final Rect mPaddingRect = new Rect();

    /** Current drawing bounds, used to compute dirty region. */
    private final Rect mDrawingBounds = new Rect();

    /** Current dirty bounds, union of current and previous drawing bounds. */
    private final Rect mDirtyBounds = new Rect();

    private final TouchFeedbackState mState;

    /** Lazily-created map of touch hotspot IDs to ripples. */
    private SparseArray<Ripple> mTouchedRipples;

    /** Lazily-created list of actively animating ripples. */
    private ArrayList<Ripple> mActiveRipples;

    /** Lazily-created runnable for scheduling invalidation. */
    private Runnable mAnimationRunnable;

    /** Paint used to control appearance of ripples. */
    private Paint mRipplePaint;

    /** Target density of the display into which ripples are drawn. */
    private int mTargetDensity;

    /** Whether the animation runnable has been posted. */
    private boolean mAnimating;

    TouchFeedbackDrawable() {
        this(new TouchFeedbackState(null), null);
    }

    TouchFeedbackDrawable(TouchFeedbackState state, Resources res) {
        if (res != null) {
            mTargetDensity = res.getDisplayMetrics().densityDpi;
        } else if (state != null) {
            mTargetDensity = state.mTargetDensity;
        }

        mState = state;
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        // Not supported.
    }

    @Override
    public void setAlpha(int alpha) {
        // Not supported.
    }

    @Override
    public int getOpacity() {
        return mActiveRipples != null && !mActiveRipples.isEmpty() ?
                PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
    }

    @Override
    public boolean onStateChange(int[] stateSet) {
        final ColorStateList stateList = mState.mColorStateList;
        if (stateList != null && mRipplePaint != null) {
            final int newColor = stateList.getColorForState(stateSet, 0);
            final int oldColor = mRipplePaint.getColor();
            if (oldColor != newColor) {
                mRipplePaint.setColor(newColor);
                invalidateSelf();
                return true;
            }
        }

        return false;
    }

    /**
     * @hide
     */
    @Override
    public boolean isProjected() {
        return true;
    }

    @Override
    public boolean isStateful() {
        return mState.mColorStateList != null && mState.mColorStateList.isStateful();
    }

    /**
     * Set the density at which this drawable will be rendered.
     *
     * @param density The density scale for this drawable.
     */
    public void setTargetDensity(int density) {
        if (mTargetDensity != density) {
            mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
            // TODO: Update density in ripples?
            invalidateSelf();
        }
    }

    @Override
    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
            throws XmlPullParserException, IOException {
        super.inflate(r, parser, attrs);

        final TypedArray a = r.obtainAttributes(attrs,
                com.android.internal.R.styleable.ColorDrawable);
        mState.mColorStateList = a.getColorStateList(
                com.android.internal.R.styleable.ColorDrawable_color);
        a.recycle();

        mState.mXfermode = null; //new PorterDuffXfermode(Mode.SRC_ATOP);
        mState.mProjected = false;
    }

    /**
     * @hide until hotspot APIs are finalized
     */
    @Override
    public boolean supportsHotspots() {
        return true;
    }

    /**
     * @hide until hotspot APIs are finalized
     */
    @Override
    public void setHotspot(int id, float x, float y) {
        if (mTouchedRipples == null) {
            mTouchedRipples = new SparseArray<Ripple>();
            mActiveRipples = new ArrayList<Ripple>();
        }

        final Ripple ripple = mTouchedRipples.get(id);
        if (ripple == null) {
            final Rect padding = mPaddingRect;
            getPadding(padding);

            final Rect bounds = getBounds();
            final Ripple newRipple = new Ripple(bounds, padding, bounds.exactCenterX(),
                    bounds.exactCenterY(), mTargetDensity);
            newRipple.enter();

            mActiveRipples.add(newRipple);
            mTouchedRipples.put(id, newRipple);
        } else {
            //ripple.move(x, y);
        }

        scheduleAnimation();
    }

    /**
     * @hide until hotspot APIs are finalized
     */
    @Override
    public void removeHotspot(int id) {
        if (mTouchedRipples == null) {
            return;
        }

        final Ripple ripple = mTouchedRipples.get(id);
        if (ripple != null) {
            ripple.exit();

            mTouchedRipples.remove(id);
            scheduleAnimation();
        }
    }

    /**
     * @hide until hotspot APIs are finalized
     */
    @Override
    public void clearHotspots() {
        if (mTouchedRipples == null) {
            return;
        }

        final int n = mTouchedRipples.size();
        for (int i = 0; i < n; i++) {
            final Ripple ripple = mTouchedRipples.valueAt(i);
            ripple.exit();
        }

        if (n > 0) {
            mTouchedRipples.clear();
            scheduleAnimation();
        }
    }

    /**
     * Schedules the next animation, if necessary.
     */
    private void scheduleAnimation() {
        if (mActiveRipples == null || mActiveRipples.isEmpty()) {
            mAnimating = false;
        } else if (!mAnimating) {
            mAnimating = true;

            if (mAnimationRunnable == null) {
                mAnimationRunnable = new Runnable() {
                    @Override
                    public void run() {
                        mAnimating = false;
                        scheduleAnimation();
                        invalidateSelf();
                    }
                };
            }

            scheduleSelf(mAnimationRunnable, SystemClock.uptimeMillis() + 1000 / 60);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        final ArrayList<Ripple> activeRipples = mActiveRipples;
        if (activeRipples == null || activeRipples.isEmpty()) {
            // Nothing to draw, we're done here.
            return;
        }

        final ColorStateList stateList = mState.mColorStateList;
        if (stateList == null) {
            // No color, we're done here.
            return;
        }

        final int color = stateList.getColorForState(getState(), Color.TRANSPARENT);
        if (color == Color.TRANSPARENT) {
            // No color, we're done here.
            return;
        }

        if (mRipplePaint == null) {
            mRipplePaint = new Paint();
            mRipplePaint.setAntiAlias(true);
        }

        mRipplePaint.setXfermode(mState.mXfermode);
        mRipplePaint.setColor(color);

        final int restoreCount = canvas.save();

        // Draw ripples directly onto the canvas.
        int n = activeRipples.size();
        for (int i = 0; i < n; i++) {
            final Ripple ripple = activeRipples.get(i);
            if (!ripple.active()) {
                activeRipples.remove(i);
                i--;
                n--;
            } else {
                ripple.draw(canvas, mRipplePaint);
            }
        }

        canvas.restoreToCount(restoreCount);
    }

    @Override
    public Rect getDirtyBounds() {
        final Rect dirtyBounds = mDirtyBounds;
        final Rect drawingBounds = mDrawingBounds;
        dirtyBounds.set(drawingBounds);
        drawingBounds.setEmpty();

        final Rect rippleBounds = mTempRect;
        final ArrayList<Ripple> activeRipples = mActiveRipples;
        if (activeRipples != null) {
           final int N = activeRipples.size();
           for (int i = 0; i < N; i++) {
               activeRipples.get(i).getBounds(rippleBounds);
               drawingBounds.union(rippleBounds);
           }
        }

        dirtyBounds.union(drawingBounds);
        return dirtyBounds;
    }

    private static class TouchFeedbackState extends ConstantState {
        private ColorStateList mColorStateList;
        private Xfermode mXfermode;
        private int mTargetDensity;
        private boolean mProjected;

        public TouchFeedbackState(TouchFeedbackState orig) {
            if (orig != null) {
                mColorStateList = orig.mColorStateList;
                mXfermode = orig.mXfermode;
                mTargetDensity = orig.mTargetDensity;
                mProjected = orig.mProjected;
            }
        }

        @Override
        public int getChangingConfigurations() {
            return 0;
        }

        @Override
        public Drawable newDrawable() {
            return newDrawable(null);
        }

        @Override
        public Drawable newDrawable(Resources res) {
            return new TouchFeedbackDrawable(this, res);
        }
    }
}