diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ClippedEventDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ClippedEventDelegate.java new file mode 100644 index 000000000..df81d8fcd --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/ClippedEventDelegate.java @@ -0,0 +1,131 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.utils.ViewUtils; + +import java.util.Deque; + +public class ClippedEventDelegate implements View.OnHoverListener, View.OnTouchListener { + + private View mView; + private Region mRegion; + private boolean mHovered; + private boolean mTouched; + private OnClickListener mClickListener; + + public ClippedEventDelegate(@DrawableRes int res, @NonNull View view) { + mView = view; + mHovered = false; + mTouched = false; + + view.getViewTreeObserver().addOnGlobalLayoutListener( + () -> { + Path path = createPathFromResource(res); + RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + bounds = new RectF(); + path.computeBounds(bounds, true); + mRegion = new Region(); + mRegion.setPath(path, new Region((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom)); + }); + } + + public void setOnClickListener(OnClickListener listener) { + mClickListener = listener; + } + + private Path createPathFromResource(@DrawableRes int res) { + VectorShape shape = new VectorShape(mView.getContext(), res); + shape.onResize(mView.getWidth(), mView.getHeight()); + Deque layers = shape.getLayers(); + VectorShape.Layer layer = layers.getFirst(); + + // TODO Handle state changes and update the Region based on the new current state shape + + return layer.transformedPath; + } + + @Override + public boolean onHover(View v, MotionEvent event) { + if(!mRegion.contains((int)event.getX(),(int) event.getY())) { + if (mHovered) { + mHovered = false; + event.setAction(MotionEvent.ACTION_HOVER_EXIT); + return v.onHoverEvent(event); + } + + return true; + + } else { + if (!mHovered) { + mHovered = true; + event.setAction(MotionEvent.ACTION_HOVER_ENTER); + } + + return v.onHoverEvent(event); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + v.getParent().requestDisallowInterceptTouchEvent(true); + + if(!mRegion.contains((int)event.getX(),(int) event.getY())) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (mTouched) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + } + + return true; + + } else { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + v.setPressed(true); + mTouched = true; + return true; + + case MotionEvent.ACTION_UP: + if (mTouched && ViewUtils.isInsideView(v, (int)event.getRawX(), (int)event.getRawY())) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + return true; + + case MotionEvent.ACTION_MOVE: + return true; + + case MotionEvent.ACTION_CANCEL: + v.setPressed(false); + mTouched = false; + return true; + } + + return false; + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java index f20ac811b..86e1a50bf 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HoneycombButton.java @@ -19,7 +19,6 @@ import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.SystemUtils; -import org.mozilla.vrbrowser.utils.ViewUtils; public class HoneycombButton extends LinearLayout { @@ -33,6 +32,7 @@ public class HoneycombButton extends LinearLayout { private String mSecondaryButtonText; private Drawable mButtonIcon; private boolean mButtonIconHover; + private VectorClippedEventDelegate mEventDelegate; public HoneycombButton(Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.style.honeycombButtonTheme); @@ -109,41 +109,53 @@ private void initialize(Context aContext) { mSecondaryText.setClickable(false); } - setOnHoverListener((view, motionEvent) -> false); + mEventDelegate = new VectorClippedEventDelegate(R.drawable.settings_honeycomb_background, this); + setOnHoverListener(mEventDelegate); + setOnTouchListener(mEventDelegate); } - @Override public void setOnClickListener(@Nullable OnClickListener aListener) { - ViewUtils.setStickyClickListener(this, aListener); + mEventDelegate.setOnClickListener(aListener); } @Override - public void setOnHoverListener(final OnHoverListener l) { - super.setOnHoverListener((view, motionEvent) -> { - switch (motionEvent.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - if (mIcon != null && mText != null) { - if (mButtonIconHover) { - mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.asphalt, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); - } - mText.setTextColor(getContext().getColor(R.color.asphalt)); - mSecondaryText.setTextColor(getContext().getColor(R.color.asphalt)); + public boolean onHoverEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + if (mIcon != null && mText != null) { + if (mButtonIconHover) { + mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.asphalt, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); } - break; - case MotionEvent.ACTION_HOVER_EXIT: - if (mIcon != null && mText != null) { - if (mButtonIconHover) { - mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.fog, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); - } - mText.setTextColor(getContext().getColor(R.color.fog)); - mSecondaryText.setTextColor(getContext().getColor(R.color.fog)); + mText.setTextColor(getContext().getColor(R.color.asphalt)); + mSecondaryText.setTextColor(getContext().getColor(R.color.asphalt)); + } + break; + case MotionEvent.ACTION_HOVER_EXIT: + if (mIcon != null && mText != null) { + if (mButtonIconHover) { + mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.fog, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); } - break; - } + mText.setTextColor(getContext().getColor(R.color.fog)); + mSecondaryText.setTextColor(getContext().getColor(R.color.fog)); + } + break; + } - return l.onHover(view, motionEvent); - }); + if (mEventDelegate.isInside(event)) { + return super.onHoverEvent(event); + + } else { + setHovered(false); + if (mIcon != null && mText != null) { + if (mButtonIconHover) { + mIcon.setColorFilter(new PorterDuffColorFilter(getResources().getColor(R.color.fog, getContext().getTheme()), PorterDuff.Mode.MULTIPLY)); + } + mText.setTextColor(getContext().getColor(R.color.fog)); + mSecondaryText.setTextColor(getContext().getColor(R.color.fog)); + } + return false; + } } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorClippedEventDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorClippedEventDelegate.java new file mode 100644 index 000000000..dcf8777c1 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorClippedEventDelegate.java @@ -0,0 +1,144 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewTreeObserver; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.utils.ViewUtils; + +import java.util.Deque; + +public class VectorClippedEventDelegate implements View.OnHoverListener, View.OnTouchListener { + + private View mView; + private @DrawableRes int mRes; + private Region mRegion; + private boolean mHovered; + private boolean mTouched; + private OnClickListener mClickListener; + + VectorClippedEventDelegate(@DrawableRes int res, @NonNull View view) { + mView = view; + mRes = res; + mHovered = false; + mTouched = false; + + view.getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener); + } + + private ViewTreeObserver.OnGlobalLayoutListener mLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Path path = createPathFromResource(mRes); + RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + bounds = new RectF(); + path.computeBounds(bounds, true); + mRegion = new Region(); + mRegion.setPath(path, new Region((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom)); + + mView.getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener); + } + }; + + public void setOnClickListener(OnClickListener listener) { + mClickListener = listener; + } + + private Path createPathFromResource(@DrawableRes int res) { + VectorShape shape = new VectorShape(mView.getContext(), res); + shape.onResize(mView.getWidth(), mView.getHeight()); + Deque layers = shape.getLayers(); + VectorShape.Layer layer = layers.getFirst(); + + // TODO Handle state changes and update the Region based on the new current state shape + + return layer.transformedPath; + } + + @Override + public boolean onHover(View v, MotionEvent event) { + if(!isInside(event)) { + if (mHovered) { + mHovered = false; + event.setAction(MotionEvent.ACTION_HOVER_EXIT); + return v.onHoverEvent(event); + } + + return false; + + } else { + if (!mHovered) { + mHovered = true; + event.setAction(MotionEvent.ACTION_HOVER_ENTER); + } + + return v.onHoverEvent(event); + } + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + v.getParent().requestDisallowInterceptTouchEvent(true); + + if(!isInside(event)) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + if (mTouched) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + } + + return true; + + } else { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + v.setPressed(true); + mTouched = true; + return true; + + case MotionEvent.ACTION_UP: + if (mTouched && ViewUtils.isInsideView(v, (int)event.getRawX(), (int)event.getRawY())) { + v.requestFocus(); + v.requestFocusFromTouch(); + if (mClickListener != null) { + mClickListener.onClick(v); + } + } + v.setPressed(false); + mTouched = false; + return true; + + case MotionEvent.ACTION_MOVE: + return true; + + case MotionEvent.ACTION_CANCEL: + v.setPressed(false); + mTouched = false; + return true; + } + + return false; + } + } + + public boolean isInside(MotionEvent event) { + return mRegion.contains((int)event.getX(),(int) event.getY()); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorShape.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorShape.java new file mode 100644 index 000000000..93a950af5 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/VectorShape.java @@ -0,0 +1,111 @@ +package org.mozilla.vrbrowser.ui.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.drawable.shapes.Shape; +import android.util.AttributeSet; +import android.util.Xml; + +import androidx.core.graphics.PathParser; + +import org.jetbrains.annotations.NotNull; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +class VectorShape extends Shape { + private static final String TAG_VECTOR = "vector"; + private static final String TAG_PATH = "path"; + + private RectF viewportRect = new RectF(); + private List layers = new ArrayList<>(); + + VectorShape(Context context, int id) { + XmlResourceParser parser = context.getResources().getXml(id); + AttributeSet set = Xml.asAttributeSet(parser); + try { + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if(eventType == XmlPullParser.START_TAG) { + String tagName = parser.getName(); + if (tagName.equals(TAG_VECTOR)) { + int[] attrs = { android.R.attr.viewportWidth, android.R.attr.viewportHeight }; + TypedArray ta = context.obtainStyledAttributes(set, attrs); + viewportRect.set(0, 0, ta.getFloat(0, 0), ta.getFloat(1, 0)); + ta.recycle(); + + } else if (tagName.equals(TAG_PATH)) { + int[] attrs = { android.R.attr.name, android.R.attr.fillColor, android.R.attr.pathData }; + TypedArray ta = context.obtainStyledAttributes(set, attrs); + layers.add(new Layer(ta.getString(2), ta.getColor(1, 0xdeadc0de), ta.getString(0))); + ta.recycle(); + } + } + eventType = parser.next(); + } + + } catch (XmlPullParserException | IOException e) { + e.printStackTrace(); + } + } + + public Deque getLayers() { + Deque outLayers = new LinkedList<>(); + for (Layer layer : layers) { + outLayers.addLast(layer); + } + + return outLayers; + } + + @Override + protected void onResize(float width, float height) { + Matrix matrix = new Matrix(); + Region shapeRegion = new Region(0, 0, (int) width, (int) height); + matrix.setRectToRect(viewportRect, new RectF(0, 0, width, height), Matrix.ScaleToFit.CENTER); + for (Layer layer : layers) { + layer.transform(matrix, shapeRegion); + } + } + + @Override + public void draw(Canvas canvas, Paint paint) { + for (Layer layer : layers) { + canvas.drawPath(layer.transformedPath, layer.paint); + } + } + + class Layer { + Path originalPath; + Path transformedPath = new Path(); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + Region region = new Region(); + String name; + + public Layer(String data, int color, String name) { + originalPath = PathParser.createPathFromPathData(data); + paint.setColor(color); + this.name = name; + } + + public void transform(Matrix matrix, Region clip) { + originalPath.transform(matrix, transformedPath); + region.setPath(transformedPath, clip); + } + + @NotNull + @Override public String toString() { return name; } + } +} diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 913036c8b..4923d8dec 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -94,5 +94,4 @@ -