blob: 6cd180041443d3765ad01f0d160f9c1b1e51bb12 [file] [log] [blame]
// Copyright 2018 The Feed Authors.
//
// 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 com.google.android.libraries.feed.piet;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.support.v4.view.ViewCompat;
import android.view.Gravity;
import android.view.View;
import com.google.android.libraries.feed.piet.host.ActionHandler;
import com.google.android.libraries.feed.piet.host.ActionHandler.ActionType;
import com.google.search.now.ui.piet.ActionsProto.Actions;
import com.google.search.now.ui.piet.ActionsProto.VisibilityAction;
import com.google.search.now.ui.piet.PietProto.Frame;
import com.google.search.now.ui.piet.StylesProto.GravityHorizontal;
import com.google.search.now.ui.piet.StylesProto.GravityVertical;
import java.util.Set;
/** Utility class, providing useful methods to interact with Views. */
public class ViewUtils {
private static final String TAG = "ViewUtils";
// TODO: Remove this method; it is only used by
// ElementAdapter.getDeprecatedElementGravity
@Deprecated
static int pietGravityToGravity(
GravityHorizontal gravityHorizontal, GravityVertical gravityVertical) {
return gravityHorizontalToGravity(gravityHorizontal)
| gravityVerticalToGravity(gravityVertical);
}
// TODO: Remove this method; it is only used by
// ElementAdapter.getDeprecatedElementGravity
@Deprecated
@SuppressWarnings("UnnecessaryDefaultInEnumSwitch")
static int gravityHorizontalToGravity(GravityHorizontal gravityHorizontal) {
switch (gravityHorizontal) {
case GRAVITY_START:
return Gravity.START;
case GRAVITY_CENTER:
return Gravity.CENTER_HORIZONTAL;
case GRAVITY_END:
return Gravity.END;
case GRAVITY_HORIZONTAL_UNSPECIFIED:
default:
return Gravity.START;
}
}
// TODO: Remove this method; it is only used by
// ElementAdapter.getDeprecatedElementGravity
@Deprecated
@SuppressWarnings("UnnecessaryDefaultInEnumSwitch")
static int gravityVerticalToGravity(GravityVertical gravityVertical) {
switch (gravityVertical) {
case GRAVITY_TOP:
return Gravity.TOP;
case GRAVITY_MIDDLE:
return Gravity.CENTER_VERTICAL;
case GRAVITY_BOTTOM:
return Gravity.BOTTOM;
case GRAVITY_VERTICAL_UNSPECIFIED:
default:
return Gravity.NO_GRAVITY;
}
}
/**
* Attaches the onClick action from actions to the view, executed by the handler. In Android M+, a
* RippleDrawable is added to the foreground of the view, so that a ripple animation happens on
* each click.
*/
static void setOnClickActions(Actions actions, View view, FrameContext frameContext) {
ActionHandler handler = frameContext.getActionHandler();
if (actions.hasOnLongClickAction()) {
view.setOnLongClickListener(
v -> {
handler.handleAction(
actions.getOnLongClickAction(),
ActionType.LONG_CLICK,
frameContext.getFrame(),
view,
null);
return true;
});
} else {
clearOnLongClickActions(view);
}
if (actions.hasOnClickAction()) {
view.setOnClickListener(
v -> {
handler.handleAction(
actions.getOnClickAction(), ActionType.CLICK, frameContext.getFrame(), view, null);
});
} else {
clearOnClickActions(view);
}
// TODO: Implement alternative support for older versions
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
if (actions.hasOnClickAction() || actions.hasOnLongClickAction()) {
// CAUTION: View.setForeground() is only available in L+
view.setForeground(view.getContext().getDrawable(R.drawable.piet_clickable_ripple));
} else {
view.setForeground(null);
}
}
}
static void clearOnLongClickActions(View view) {
view.setOnLongClickListener(null);
view.setLongClickable(false);
}
/** Sets clickability to false. */
static void clearOnClickActions(View view) {
if (view.hasOnClickListeners()) {
view.setOnClickListener(null);
}
view.setClickable(false);
}
/**
* Check if this view is visible, trigger actions accordingly, and update set of active actions.
*
* <p>Actions are added to activeActions when they trigger, and removed when the condition that
* caused them to trigger is no longer true. (Ex. a view action will be removed when the view goes
* off screen)
*
* @param view this adapter's view
* @param viewport the visible viewport
* @param actions this element's actions, which might be triggered
* @param actionHandler host-provided handler to execute actions
* @param frame the parent frame
* @param activeActions mutable set of currently-triggered actions; this will get updated by this
* method as new actions are triggered and old actions are reset.
*/
static void maybeTriggerViewActions(
View view,
View viewport,
Actions actions,
ActionHandler actionHandler,
Frame frame,
Set<VisibilityAction> activeActions) {
if (actions.getOnViewActionsCount() == 0 && actions.getOnHideActionsCount() == 0) {
return;
}
// For invisible views, short-cut triggering of hide/show actions.
if (view.getVisibility() != View.VISIBLE || !ViewCompat.isAttachedToWindow(view)) {
triggerHideActions(view, actions, actionHandler, frame, activeActions);
return;
}
// Figure out overlap of viewport and view, and trigger based on proportion overlap.
Rect viewRect = getViewRectOnScreen(view);
Rect viewportRect = getViewRectOnScreen(viewport);
if (viewportRect.intersect(viewRect)) {
int viewArea = viewRect.height() * viewRect.width();
int visibleArea = viewportRect.height() * viewportRect.width();
float proportionVisible = ((float) visibleArea) / viewArea;
for (VisibilityAction visibilityAction : actions.getOnViewActionsList()) {
if (proportionVisible >= visibilityAction.getProportionVisible()) {
if (activeActions.add(visibilityAction)) {
actionHandler.handleAction(
visibilityAction.getAction(), ActionType.VIEW, frame, view, null);
}
} else {
activeActions.remove(visibilityAction);
}
}
for (VisibilityAction visibilityAction : actions.getOnHideActionsList()) {
if (proportionVisible < visibilityAction.getProportionVisible()) {
if (activeActions.add(visibilityAction)) {
actionHandler.handleAction(
visibilityAction.getAction(), ActionType.VIEW, frame, view, null);
}
} else {
activeActions.remove(visibilityAction);
}
}
}
}
static void triggerHideActions(
View view,
Actions actions,
ActionHandler actionHandler,
Frame frame,
Set<VisibilityAction> activeActions) {
activeActions.removeAll(actions.getOnViewActionsList());
for (VisibilityAction visibilityAction : actions.getOnHideActionsList()) {
if (activeActions.add(visibilityAction)) {
actionHandler.handleAction(
visibilityAction.getAction(), ActionType.VIEW, frame, view, null);
}
}
}
/**
* Replaces all non-transparent pixels of a {@link Drawable} with overlayColor and returns the new
* {@link Drawable}; If overlayColor is null, returns the original {@link Drawable}.
*/
static Drawable applyOverlayColor(Drawable drawable, /*@Nullable*/ Integer overlayColor) {
if (overlayColor == null) {
return drawable;
}
Drawable drawableWithOverlay = drawable.mutate();
drawableWithOverlay.setColorFilter(overlayColor, Mode.SRC_IN);
return drawableWithOverlay;
}
private static Rect getViewRectOnScreen(View view) {
int[] viewLocation = new int[2];
view.getLocationOnScreen(viewLocation);
return new Rect(
viewLocation[0],
viewLocation[1],
viewLocation[0] + view.getWidth(),
viewLocation[1] + view.getHeight());
}
/** Private constructor to prevent instantiation. */
private ViewUtils() {}
}