/* * Copyright 2014 Gleb Godonoga. * * 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.andrada.sitracker.ui.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.TouchDelegate; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import com.andrada.sitracker.BuildConfig; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * RelativeLayout base class that provides multiple tappable areas with custom * touch delegates of 8 possible configurations. * <p>Extended touch regions are highlighted in different colors when compiled in debug mode</p> * See {@link TouchDelegateRelativeLayout.ViewConfig} * for more details on possible configs */ public class TouchDelegateRelativeLayout extends RelativeLayout { private static final int TOUCH_ADDITION = 20; private TouchDelegateGroup mTouchDelegateGroup; private int mTouchAddition; private int mPreviousWidth = -1; private int mPreviousHeight = -1; /** * This is a mandatory field * Should contain all the views and their respective configs for delegated touch input */ protected final HashMap<ViewConfig, View> delegatedTouchViews = new HashMap<ViewConfig, View>(); public static final boolean TapRegionHighlighted = false; private static final int[] HIGHLIGHT_COLOR_ARRAY = { Color.argb(50, 255, 0, 0), Color.argb(50, 0, 255, 0), Color.argb(50, 0, 0, 255), Color.argb(50, 0, 255, 255), Color.argb(50, 255, 0, 255), Color.argb(50, 255, 255, 0), }; /** * Static class that provides configuration to * {@link TouchDelegateRelativeLayout} * describing expandable directions of touch delegate views */ public static class ViewConfig { private static final int VIEW_CONFIG_START = 0x0; private static final int VIEW_CONFIG_END = 0x1; public int getHPosition() { return hPosition; } public int getHExpanding() { return hExpanding; } public int getVPosition() { return vPosition; } public int getVExpanding() { return vExpanding; } private int hPosition; private int hExpanding; private int vPosition; private int vExpanding; private ViewConfig(int horizontalPosition, int horizontalExpanding, int verticalPosition, int verticalExpanding) { //Positions can't expand both ways in MATCH_PARENT if ((horizontalExpanding != ViewGroup.LayoutParams.MATCH_PARENT && horizontalExpanding != ViewGroup.LayoutParams.WRAP_CONTENT) || (verticalExpanding != ViewGroup.LayoutParams.MATCH_PARENT && verticalExpanding != ViewGroup.LayoutParams.WRAP_CONTENT)) { throw new IllegalArgumentException( "Expanding parameters can have only android.view.ViewGroup.LayoutParams.MATCH_PARENT " + "or android.view.ViewGroup.LayoutParams.WRAP_CONTENT values"); } if ((horizontalPosition != VIEW_CONFIG_START && horizontalPosition != VIEW_CONFIG_END) || (verticalPosition != VIEW_CONFIG_START && verticalPosition != VIEW_CONFIG_END)) { throw new IllegalArgumentException( "Expanding parameters can have only android.view.ViewGroup.LayoutParams.MATCH_PARENT " + "or android.view.ViewGroup.LayoutParams.WRAP_CONTENT values"); } if (horizontalExpanding == ViewGroup.LayoutParams.MATCH_PARENT && verticalExpanding == ViewGroup.LayoutParams.MATCH_PARENT) { throw new IllegalArgumentException("You cannot configure both expanding directions to MATCH_PARENT"); } this.hPosition = horizontalPosition; this.vPosition = verticalPosition; this.hExpanding = horizontalExpanding; this.vExpanding = verticalExpanding; } /** * ViewConfig for a touch region that takes the whole height of the parent component * and is anchored to the left. It's width is equal to view width + 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig wholeLeft() { return new ViewConfig( ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.WRAP_CONTENT, ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.MATCH_PARENT ); } /** * ViewConfig for a touch region that takes the whole height of the parent component * and is anchored to the right. It's width is equal to view width + 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig wholeRight() { return new ViewConfig( ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.WRAP_CONTENT, ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.MATCH_PARENT ); } /** * ViewConfig for a touch region that takes the whole width of the parent component * and is anchored to the top. It's height is equal to view height + 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig wholeTop() { return new ViewConfig( ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.MATCH_PARENT, ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.WRAP_CONTENT ); } /** * ViewConfig for a touch region that takes the whole width of the parent component * and is anchored to the bottom. It's height is equal to view height + 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig wholeBottom() { return new ViewConfig( ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.MATCH_PARENT, ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.WRAP_CONTENT ); } /** * ViewConfig for a touch region is anchored to the top-left corner of the parent component. * It's width and height is increased by 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig topLeft() { return new ViewConfig( ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.WRAP_CONTENT, ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.WRAP_CONTENT ); } /** * ViewConfig for a touch region is anchored to the top-right corner of the parent component. * It's width and height is increased by 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig topRight() { return new ViewConfig( ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.WRAP_CONTENT, ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.WRAP_CONTENT ); } /** * ViewConfig for a touch region is anchored to the bottom-left corner of the parent component. * It's width and height is increased by 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig bottomLeft() { return new ViewConfig( ViewConfig.VIEW_CONFIG_START, ViewGroup.LayoutParams.WRAP_CONTENT, ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.WRAP_CONTENT ); } /** * ViewConfig for a touch region is anchored to the bottom-right corner of the parent component. * It's width and height is increased by 20dp * * @return ViewConfig object with the specified options */ @NotNull public static ViewConfig bottomRight() { return new ViewConfig( ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.WRAP_CONTENT, ViewConfig.VIEW_CONFIG_END, ViewGroup.LayoutParams.WRAP_CONTENT ); } } private static class TouchDelegateRecord { public final Rect rect; public final int color; public TouchDelegateRecord(Rect _rect, int _color) { rect = _rect; color = _color; } } private final ArrayList<TouchDelegateRecord> mTouchDelegateRecords = new ArrayList<TouchDelegateRecord>(); private final Paint mPaint = new Paint(); public TouchDelegateRelativeLayout(@NotNull Context context) { super(context); init(context); } public TouchDelegateRelativeLayout(@NotNull Context context, AttributeSet attrs) { super(context, attrs); init(context); } public TouchDelegateRelativeLayout(@NotNull Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(@NotNull Context context) { setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); mTouchDelegateGroup = new TouchDelegateGroup(this); if (BuildConfig.DEBUG && TapRegionHighlighted) { mPaint.setStyle(Paint.Style.FILL); } final float density = context.getResources().getDisplayMetrics().density; mTouchAddition = (int) (density * TOUCH_ADDITION + 0.5f); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); final int width = r - l; final int height = b - t; if (width != mPreviousWidth || height != mPreviousHeight) { mPreviousWidth = width; mPreviousHeight = height; mTouchDelegateGroup.clearTouchDelegates(); int j = 0; for (Map.Entry<ViewConfig, View> entry : delegatedTouchViews.entrySet()) { if (j == HIGHLIGHT_COLOR_ARRAY.length) { j = 0; } entry.getValue().setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onDelegatedTouchViewClicked(v); } }); entry.getValue().setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View view, @NotNull MotionEvent motionEvent) { switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: onDelegatedTouchViewDown(view); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_UP: onDelegatedTouchViewCancel(view); break; } return false; } }); addTouchDelegate(computeRectFor(width, height, entry.getValue(), entry.getKey()), HIGHLIGHT_COLOR_ARRAY[j], entry.getValue()); j++; } setTouchDelegate(mTouchDelegateGroup); } } @NotNull private Rect computeRectFor(int parentWidth, int parentHeight, @NotNull View childView, @NotNull ViewConfig config) { int x, y, w, h; if (config.getHExpanding() == ViewGroup.LayoutParams.MATCH_PARENT) { //We can ignore horizontal position.we will use full width of the parent component x = 0; w = parentWidth; if (config.getVPosition() == ViewConfig.VIEW_CONFIG_START) { //Place at the top y = 0; h = childView.getHeight() + mTouchAddition; } else { //Place at the bottom y = parentHeight - childView.getHeight() - mTouchAddition; h = parentHeight; } } else if (config.getVExpanding() == ViewGroup.LayoutParams.MATCH_PARENT) { //We can ignore vertical position, we will use height of the parent component y = 0; h = parentHeight; if (config.getHPosition() == ViewConfig.VIEW_CONFIG_START) { //Place on the left x = 0; w = childView.getWidth() + mTouchAddition; } else { //Place at the right x = parentWidth - childView.getWidth() - mTouchAddition; w = parentWidth; } } else { /** * We are in a position when both expanding are WRAP_CONTENT * That means we need to care just about the view positioning. * We have 4 options here */ if (config.getHPosition() == ViewConfig.VIEW_CONFIG_START) { x = 0; w = childView.getWidth() + mTouchAddition; } else { x = parentWidth - childView.getWidth() - mTouchAddition; w = parentWidth; } if (config.getVPosition() == ViewConfig.VIEW_CONFIG_START) { y = 0; h = childView.getHeight() + mTouchAddition; } else { y = parentHeight - childView.getHeight() - mTouchAddition; h = parentHeight; } } return new Rect(x, y, w, h); } private void addTouchDelegate(Rect rect, int color, @NotNull View delegateView) { mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(rect, delegateView)); mTouchDelegateRecords.add(new TouchDelegateRecord(rect, color)); } @Override protected void dispatchDraw(@NotNull Canvas canvas) { if (BuildConfig.DEBUG && TapRegionHighlighted) { for (TouchDelegateRecord record : mTouchDelegateRecords) { mPaint.setColor(record.color); canvas.drawRect(record.rect, mPaint); } } super.dispatchDraw(canvas); } /** * Callback when a touch delegate view is being clicked * * @param view that is being clicked on */ protected void onDelegatedTouchViewClicked(View view) { } /** * Callback when a touch delegate view is being pressed * * @param view that is being pressed */ protected void onDelegatedTouchViewDown(View view) { } /** * Callback when a touch delegate view receives a cancel event * Use only to reset the state of the view. Is not intended for low level touch manipulation * * @param view that is being pressed */ protected void onDelegatedTouchViewCancel(View view) { } }