Java tutorial
/* * The MIT License (MIT) * Copyright 2012 Steve Guidetti * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software?), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS?, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.ultramegatech.ey.widget; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.widget.EdgeEffectCompat; import android.support.v4.widget.ExploreByTouchHelper; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.Scroller; import com.ultramegatech.ey.R; import com.ultramegatech.ey.provider.Element; import com.ultramegatech.ey.util.ElementUtils; import com.ultramegatech.ey.util.PreferenceUtils; import java.util.ArrayList; import java.util.List; /** * Zoomable, color coded View of the Periodic Table of the Elements. Renders a list of * PeriodicTableBlock objects in the standard Periodic Table layout. Also implements a custom * PeriodicTableListener that passes the selected PeriodicTableBlock object. * * @author Steve Guidetti */ @SuppressWarnings("unused") public class PeriodicTableView extends View { /** * The amount to zoom in or out for programmatic zooms */ private static final float ZOOM_STEP = 0.5f; /** * The maximum zoom level */ private static final float MAX_ZOOM = 8f; /** * Color value for the selected block indicator */ private static final int COLOR_SELECTED = 0x9900d4ff; /** * Color value for the text within the blocks */ private static final int COLOR_BLOCK_FOREGROUND = 0xff000000; /** * Default color values */ private static final int COLOR_DEFAULT_FOREGROUND = 0xff000000; private static final int COLOR_DEFAULT_BACKGROUND = 0xffffffff; /** * Callback interface for events. */ public interface PeriodicTableListener { /** * Called when a block is clicked. * * @param item The selected block */ void onItemClick(@NonNull PeriodicTableBlock item); /** * Called when a zoom operation has completed. * * @param periodicTableView The PeriodicTableView */ void onZoomEnd(@NonNull PeriodicTableView periodicTableView); } /** * The list of blocks to render */ @NonNull private final List<PeriodicTableBlock> mPeriodicTableBlocks = new ArrayList<>(); /** * Callback for item clicks */ @Nullable private PeriodicTableListener mPeriodicTableListener; /** * Color legend */ @NonNull private final PeriodicTableLegend mLegend; /** * Title string */ @NonNull private CharSequence mTitle; /** * The current block size */ private int mBlockSize; /** * Amount of space around the table */ private int mPadding; /** * Number of rows and columns in the table */ private int mNumRows; private int mNumCols; /** * Paint for the table background */ @NonNull private final Paint mBgPaint = new Paint(); /** * Paint for block backgrounds */ @NonNull private final Paint mBlockPaint = new Paint(); /** * Paint for row and column headers */ @NonNull private final Paint mHeaderPaint; /** * Paint for the table title */ @NonNull private final Paint mTitlePaint; /** * Paint for symbols */ @NonNull private final Paint mSymbolPaint; /** * Paint for atomic numbers */ @NonNull private final Paint mNumberPaint; /** * Paint for the text below the symbol */ @NonNull private final Paint mSmallTextPaint; /** * Paint for the selection indicator */ @NonNull private final Paint mSelectedPaint; /** * Rectangle for many purposes */ @NonNull private final Rect mRect = new Rect(); /** * The currently selected block */ @Nullable private PeriodicTableBlock mBlockSelected; /** * The area for drawing the content */ @NonNull private final Rect mContentRect = new Rect(); /** * The offset of the content within the content area */ @NonNull private final Point mContentOffset = new Point(); /** * The initial area for relative scale operations */ @NonNull private final Rect mScaleRect = new Rect(); /** * The focal point of the current scale operation */ @NonNull private final PointF mScaleFocalPoint = new PointF(); /** * Touch gesture detectors */ @NonNull private final ScaleGestureDetector mScaleGestureDetector; @NonNull private final GestureDetector mGestureDetector; /** * Handler for animating programmatic scaling */ @NonNull private final Zoomer mZoomer; /** * The current zoom level */ private float mCurrentZoom = 1f; /** * Handler for programmatic scrolling and flings */ @NonNull private final Scroller mScroller; /** * Edge effects to provide visual indicators that an edge has been reached */ @NonNull private final EdgeEffectCompat mEdgeEffectTop; @NonNull private final EdgeEffectCompat mEdgeEffectBottom; @NonNull private final EdgeEffectCompat mEdgeEffectLeft; @NonNull private final EdgeEffectCompat mEdgeEffectRight; /** * The accessibility delegate for this View */ @Nullable private AccessibilityDelegate mAccessibilityDelegate; public PeriodicTableView(Context context) { this(context, null, 0); } public PeriodicTableView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PeriodicTableView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mSelectedPaint = new Paint(); mSelectedPaint.setAntiAlias(true); mSelectedPaint.setStyle(Paint.Style.STROKE); mSelectedPaint.setStrokeJoin(Paint.Join.ROUND); mSelectedPaint.setColor(COLOR_SELECTED); mNumberPaint = new Paint(); mNumberPaint.setAntiAlias(true); mNumberPaint.setColor(COLOR_BLOCK_FOREGROUND); mSymbolPaint = new Paint(mNumberPaint); mSymbolPaint.setTextAlign(Paint.Align.CENTER); mTitlePaint = new Paint(mSymbolPaint); mHeaderPaint = new Paint(mSymbolPaint); mSmallTextPaint = new Paint(mSymbolPaint); mNumberPaint.setSubpixelText(true); mSmallTextPaint.setSubpixelText(true); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PeriodicTableView, defStyle, 0); mTitle = a.getText(R.styleable.PeriodicTableView_title); setFgColor(a.getColor(R.styleable.PeriodicTableView_fgColor, COLOR_DEFAULT_FOREGROUND)); setBgColor(a.getColor(R.styleable.PeriodicTableView_bgColor, COLOR_DEFAULT_BACKGROUND)); a.recycle(); mLegend = new PeriodicTableLegend(context); mScaleGestureDetector = new ScaleGestureDetector(context, getOnScaleGestureListener()); mGestureDetector = new GestureDetector(context, getOnGestureListener()); mZoomer = new Zoomer(context); mScroller = new Scroller(context); mEdgeEffectLeft = new EdgeEffectCompat(context); mEdgeEffectTop = new EdgeEffectCompat(context); mEdgeEffectRight = new EdgeEffectCompat(context); mEdgeEffectBottom = new EdgeEffectCompat(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { mAccessibilityDelegate = new AccessibilityDelegate(this); ViewCompat.setAccessibilityDelegate(this, mAccessibilityDelegate); } } /** * Create the listener for the ScaleGestureDetector. * * @return The OnScaleGestureListener */ @NonNull private ScaleGestureDetector.OnScaleGestureListener getOnScaleGestureListener() { //noinspection MethodDoesntCallSuperMethod return new ScaleGestureDetector.SimpleOnScaleGestureListener() { /** * The initial span of the scale gesture */ private float mStartSpan; @Override public boolean onScaleBegin(ScaleGestureDetector detector) { clearSelection(); mScaleRect.set(mContentRect); mStartSpan = detector.getCurrentSpan(); return true; } @Override public boolean onScale(ScaleGestureDetector detector) { mScaleFocalPoint.set(detector.getFocusX() / getWidth(), detector.getFocusY() / getHeight()); setZoom(mCurrentZoom + mCurrentZoom * (detector.getCurrentSpan() - detector.getPreviousSpan()) / mStartSpan); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { if (mPeriodicTableListener != null) { mPeriodicTableListener.onZoomEnd(PeriodicTableView.this); } } }; } /** * Create the listener for the GestureDetector. * * @return The OnGestureListener */ @NonNull private GestureDetector.OnGestureListener getOnGestureListener() { //noinspection MethodDoesntCallSuperMethod return new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { clearEdgeEffects(); mScroller.forceFinished(true); mBlockSelected = null; for (PeriodicTableBlock block : mPeriodicTableBlocks) { findBlockPosition(block); if (mRect.contains((int) e.getX(), (int) e.getY())) { mBlockSelected = block; break; } } ViewCompat.postInvalidateOnAnimation(PeriodicTableView.this); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { if (mPeriodicTableListener != null && mBlockSelected != null) { mPeriodicTableListener.onItemClick(mBlockSelected); } clearSelection(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { clearSelection(); int offsetX = (int) -distanceX; int offsetY = (int) -distanceY; if (offsetX > 0) { offsetX = Math.min(offsetX, -mContentRect.left); } else if (offsetX < 0) { offsetX = Math.max(offsetX, -Math.max(0, mContentRect.right - getWidth())); } if (offsetY > 0) { offsetY = Math.min(offsetY, -mContentRect.top); } else if (offsetY < 0) { offsetY = Math.max(offsetY, -Math.max(0, mContentRect.bottom - getHeight())); } mContentRect.offset(offsetX, offsetY); if (mContentRect.height() > getHeight()) { if (distanceY < 0 && mContentRect.top == 0) { mEdgeEffectTop.onPull(-distanceY / getHeight(), e2.getX() / getWidth()); } else if (distanceY > 0 && mContentRect.bottom == getHeight()) { mEdgeEffectBottom.onPull(-distanceY / getHeight(), 1f - (e2.getX() / getWidth())); } } if (mContentRect.width() > getWidth()) { if (distanceX < 0 && mContentRect.left == 0) { mEdgeEffectLeft.onPull(-distanceX / getWidth(), 1f - (e2.getY() / getHeight())); } else if (distanceX > 0 && mContentRect.right == getWidth()) { mEdgeEffectRight.onPull(-distanceX / getWidth(), e2.getY() / getHeight()); } } if (mAccessibilityDelegate != null) { mAccessibilityDelegate.invalidateRoot(); } return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { clearSelection(); clearEdgeEffects(); mScroller.forceFinished(true); mScroller.fling(mContentRect.left, mContentRect.top, (int) velocityX, (int) velocityY, mContentRect.left - (mContentRect.right - getWidth()), 0, mContentRect.top - (mContentRect.bottom - getHeight()), 0); return true; } @Override public void onLongPress(MotionEvent e) { clearSelection(); } }; } /** * Set the foreground color. This is the color of all text outside of the blocks and legend. * * @param color The color value */ public void setFgColor(int color) { mTitlePaint.setColor(color); mHeaderPaint.setColor(color); ViewCompat.postInvalidateOnAnimation(this); } /** * Get the current foreground color. This is the color of all text outside of the blocks and * legend. * * @return The color value */ public int getFgColor() { return mTitlePaint.getColor(); } /** * Set the background color. * * @param color The color value */ public void setBgColor(int color) { mBgPaint.setColor(color); ViewCompat.postInvalidateOnAnimation(this); } /** * Get the current background color. * * @return The color value */ public int getBgColor() { return mBgPaint.getColor(); } /** * Set the list of blocks to be rendered. This method also determines the row and column of * each block and sets the colors using the legend. * * @param blocks The list of blocks */ public void setBlocks(@NonNull List<PeriodicTableBlock> blocks) { mPeriodicTableBlocks.clear(); mPeriodicTableBlocks.addAll(blocks); int numRows = 0; int numCols = 0; for (PeriodicTableBlock block : mPeriodicTableBlocks) { if (block.element.period > numRows) { numRows = block.element.period; } if (block.element.group > numCols) { numCols = block.element.group; } if (block.element.group == 0) { if (block.element.period == 6) { block.row = 8; block.col = block.element.number - 54; } else if (block.element.period == 7) { block.row = 9; block.col = block.element.number - 86; } } else { block.row = block.element.period; block.col = block.element.group; } block.color = ElementUtils.getElementColor(block.element); } numRows += 2; mNumRows = numRows; mNumCols = numCols; measureCanvas(); if (mAccessibilityDelegate != null) { mAccessibilityDelegate.loadLabels(); mAccessibilityDelegate.invalidateRoot(); } ViewCompat.postInvalidateOnAnimation(this); } public void invalidateLegend() { mLegend.invalidate(getContext()); ViewCompat.postInvalidateOnAnimation(this); } /** * Set the PeriodicTableListener. * * @param listener The PeriodicTableListener */ public void setPeriodicTableListener(@Nullable PeriodicTableListener listener) { mPeriodicTableListener = listener; } /** * Get the PeriodicTableListener. * * @return The PeriodicTableListener */ @Nullable public PeriodicTableListener getPeriodicTableListener() { return mPeriodicTableListener; } /** * Set the title from a string resource. * * @param resId Resource ID */ public void setTitle(int resId) { setTitle(getResources().getText(resId)); } /** * Set the title. * * @param title The title */ public void setTitle(@NonNull CharSequence title) { mTitle = title; ViewCompat.postInvalidateOnAnimation(this); } /** * Get the title. * * @return The title */ @NonNull public CharSequence getTitle() { return mTitle; } /** * Clear the selected block. */ public void clearSelection() { mBlockSelected = null; ViewCompat.postInvalidateOnAnimation(this); } /** * Check whether the table can be zoomed in. * * @return Whether the table can be zoomed in */ public boolean canZoomIn() { return mCurrentZoom < MAX_ZOOM; } /** * Check whether the table can be zoomed out. * * @return Whether the table can be zoomed out */ public boolean canZoomOut() { return mCurrentZoom > 1f; } /** * Zoom to a specified zoom level. * * @param zoomLevel The target zoom level */ public void zoomTo(float zoomLevel) { mZoomer.forceFinished(); mScaleRect.set(mContentRect); mScaleFocalPoint.set(0.5f, 0.5f); mZoomer.startZoom(mCurrentZoom, zoomLevel); ViewCompat.postInvalidateOnAnimation(this); } /** * Zoom in one step. */ public void zoomIn() { zoomTo(mCurrentZoom + mCurrentZoom * ZOOM_STEP); } /** * Zoom out one step. */ public void zoomOut() { zoomTo(mCurrentZoom - mCurrentZoom * ZOOM_STEP); } /** * Determine if a block is within the visible region. This is used to avoid useless drawing * operations. * * @param rect The block boundaries * @return True if the block is visible */ private boolean isBlockVisible(@NonNull Rect rect) { return rect.intersects(0, 0, getWidth(), getHeight()); } /** * Calculate the position of the specified block and store it in the shared rectangle. * * @param block The block */ private void findBlockPosition(@NonNull PeriodicTableBlock block) { mRect.right = (block.col * mBlockSize + mContentRect.left + mContentOffset.x + mPadding) - 1; mRect.bottom = (block.row * mBlockSize + mContentRect.top + mContentOffset.y + mPadding) - 1; mRect.left = mRect.right - mBlockSize + 1; mRect.top = mRect.bottom - mBlockSize + 1; final int number = block.element.number; if ((number > 56 && number < 72) || (number > 88 && number < 104)) { mRect.top += mPadding / 2; mRect.bottom += mPadding / 2; } } /** * Draw the headers and placeholders on the supplied Canvas. * * @param canvas The Canvas */ private void writeHeaders(@NonNull Canvas canvas) { mHeaderPaint.setTextSize(mBlockSize / 4); for (int i = 1; i <= mNumCols; i++) { canvas.drawText(String.valueOf(i), mBlockSize * i + mContentRect.left + mContentOffset.x, mPadding / 2 + mContentRect.top + mContentOffset.y, mHeaderPaint); } for (int i = 1; i <= mNumRows - 2; i++) { canvas.drawText(String.valueOf(i), mPadding / 2 + mContentRect.left + mContentOffset.x, mBlockSize * i + mContentRect.top + mContentOffset.y, mHeaderPaint); } canvas.drawText("57-71", mBlockSize * 3 + mContentRect.left + mContentOffset.x, mBlockSize * 6 + mContentRect.top + mContentOffset.y + mHeaderPaint.getTextSize() / 2, mHeaderPaint); canvas.drawText("89-103", mBlockSize * 3 + mContentRect.left + mContentOffset.x, mBlockSize * 7 + mContentRect.top + mContentOffset.y + mHeaderPaint.getTextSize() / 2, mHeaderPaint); } /** * Draw the title on the supplied Canvas. * * @param canvas The Canvas */ private void writeTitle(@NonNull Canvas canvas) { canvas.drawText(mTitle, 0, mTitle.length(), mBlockSize * mNumCols / 2 + mContentRect.left + mContentOffset.x, mBlockSize + mContentRect.top + mContentOffset.y, mTitlePaint); } /** * Draw the edge effects to the supplied Canvas. * * @param canvas The Canvas */ private void drawEdgeEffects(@NonNull Canvas canvas) { boolean invalidate = false; if (!mEdgeEffectTop.isFinished()) { mEdgeEffectTop.draw(canvas); invalidate = true; } if (!mEdgeEffectBottom.isFinished()) { canvas.save(); canvas.rotate(180, getWidth() / 2, getHeight() / 2); mEdgeEffectBottom.draw(canvas); canvas.restore(); invalidate = true; } if (!mEdgeEffectLeft.isFinished()) { canvas.save(); canvas.translate(0, getHeight()); canvas.rotate(-90); mEdgeEffectLeft.draw(canvas); canvas.restore(); invalidate = true; } if (!mEdgeEffectRight.isFinished()) { canvas.save(); canvas.translate(getWidth(), 0); canvas.rotate(90, 0, 0); mEdgeEffectRight.draw(canvas); canvas.restore(); invalidate = true; } if (invalidate) { ViewCompat.postInvalidateOnAnimation(this); } } /** * Deactivate and release the edge effects. */ private void clearEdgeEffects() { mEdgeEffectTop.onRelease(); mEdgeEffectBottom.onRelease(); mEdgeEffectLeft.onRelease(); mEdgeEffectRight.onRelease(); } /** * Ensure that the content area fills the viewport. */ private void fillViewport() { if (mContentRect.left > 0) { mContentRect.right -= mContentRect.left; mContentRect.left = 0; } else if (mContentRect.right < getWidth()) { mContentRect.left += getWidth() - mContentRect.right; mContentRect.right = getWidth(); } if (mContentRect.top > 0) { mContentRect.bottom -= mContentRect.top; mContentRect.top = 0; } else if (mContentRect.bottom < getHeight()) { mContentRect.top += getHeight() - mContentRect.bottom; mContentRect.bottom = getHeight(); } } /** * Trim the content area to the specified size. * * @param width The actual width of the content * @param height The actual height of the content */ private void trimCanvas(int width, int height) { if (mContentRect.width() > width || mContentRect.height() > height) { final int deltaWidth = Math.max(0, Math.min(mContentRect.width() - getWidth(), mContentRect.width() - width)); final int deltaHeight = Math.max(0, Math.min(mContentRect.height() - getHeight(), mContentRect.height() - height)); final float focusX = (getWidth() / 2f - mContentRect.width()) / mContentRect.width(); final float focusY = (getHeight() / 2f - mContentRect.top) / mContentRect.height(); mContentRect.top += deltaHeight * focusY; mContentRect.bottom -= deltaHeight * (1f - focusY); mContentRect.left += deltaWidth * focusX; mContentRect.right -= deltaWidth * (1f - focusX); } } /** * Measure the content area and determine the block size, padding, and text size. */ private void measureCanvas() { final int blockWidth = (int) (mContentRect.width() / (mNumCols + 0.5)); final int blockHeight = mContentRect.height() / (mNumRows + 1); mBlockSize = Math.min(blockWidth, blockHeight); mPadding = mBlockSize / 2; final int realWidth = mBlockSize * mNumCols + mBlockSize; final int realHeight = mBlockSize * mNumRows + mBlockSize; trimCanvas(realWidth, realHeight); mContentOffset.set(Math.max(0, (mContentRect.width() - realWidth) / 2), Math.max(0, (mContentRect.height() - realHeight) / 2)); fillViewport(); mTitlePaint.setTextSize(mBlockSize / 2); mSymbolPaint.setTextSize(mBlockSize / 2); mNumberPaint.setTextSize(mBlockSize / 4); mSmallTextPaint.setTextSize(mBlockSize / 5); } /** * Set the current zoom level. * * @param zoomLevel The target zoom level */ private void setZoom(float zoomLevel) { zoomLevel = Math.max(1f, Math.min(MAX_ZOOM, zoomLevel)); if (zoomLevel != mCurrentZoom) { final int deltaWidth = (int) (zoomLevel * getWidth()) - mScaleRect.width(); final int deltaHeight = (int) (zoomLevel * getHeight()) - mScaleRect.height(); final float focusX = (mScaleFocalPoint.x * getWidth() - mScaleRect.left) / mScaleRect.width(); final float focusY = (mScaleFocalPoint.y * getHeight() - mScaleRect.top) / mScaleRect.height(); mContentRect.set(mScaleRect.left - (int) (deltaWidth * focusX), mScaleRect.top - (int) (deltaHeight * focusY), mScaleRect.right + (int) (deltaWidth * (1f - focusX)), mScaleRect.bottom + (int) (deltaHeight * (1f - focusY))); mCurrentZoom = zoomLevel; measureCanvas(); ViewCompat.postInvalidateOnAnimation(this); } } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { boolean ret = mScaleGestureDetector.onTouchEvent(event); ret = mGestureDetector.onTouchEvent(event) || ret; return ret || super.onTouchEvent(event); } @Override protected boolean dispatchHoverEvent(MotionEvent event) { return (mAccessibilityDelegate != null && mAccessibilityDelegate.dispatchHoverEvent(event)) || super.dispatchHoverEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { return (mAccessibilityDelegate != null && mAccessibilityDelegate.dispatchKeyEvent(event)) || super.dispatchKeyEvent(event); } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (mAccessibilityDelegate != null) { mAccessibilityDelegate.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (mContentRect.width() < w) { mContentRect.left = 0; mContentRect.right = w; } if (mContentRect.height() < h) { mContentRect.top = 0; mContentRect.bottom = h; } mEdgeEffectTop.setSize(w, h); mEdgeEffectBottom.setSize(w, h); mEdgeEffectLeft.setSize(h, w); mEdgeEffectRight.setSize(h, w); measureCanvas(); ViewCompat.postInvalidateOnAnimation(this); } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { mContentRect.offsetTo(mScroller.getCurrX(), mScroller.getCurrY()); ViewCompat.postInvalidateOnAnimation(this); } if (mZoomer.computeZoom()) { setZoom(mZoomer.getCurrZoom()); if (mPeriodicTableListener != null && mZoomer.isFinished()) { mPeriodicTableListener.onZoomEnd(this); } ViewCompat.postInvalidateOnAnimation(this); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(0, 0, getRight(), getBottom(), mBgPaint); mRect.top = (int) (mBlockSize * 1.3) + mContentRect.top + mContentOffset.y; mRect.left = mBlockSize * 3 + mContentRect.left + mContentOffset.x; mRect.bottom = mRect.top + mBlockSize * 2; mRect.right = mRect.left + mBlockSize * 9; mLegend.drawLegend(canvas, mRect); writeHeaders(canvas); writeTitle(canvas); for (PeriodicTableBlock block : mPeriodicTableBlocks) { findBlockPosition(block); if (!isBlockVisible(mRect)) { continue; } mBlockPaint.setColor(block.color); canvas.drawRect(mRect, mBlockPaint); canvas.drawText(block.element.symbol, mRect.left + mBlockSize / 2, mRect.bottom - (int) (mBlockSize / 2.8), mSymbolPaint); canvas.drawText(String.valueOf(block.element.number), mRect.left + mBlockSize / 20, mRect.top + mNumberPaint.getTextSize(), mNumberPaint); canvas.drawText(block.subtext, mRect.left + mBlockSize / 2, mRect.bottom - mBlockSize / 20, mSmallTextPaint); } if (mBlockSelected != null) { mSelectedPaint.setStrokeWidth(mBlockSize / 10); findBlockPosition(mBlockSelected); canvas.drawRect(mRect, mSelectedPaint); } drawEdgeEffects(canvas); } /** * The ExploreByTouchHelper implementation to provide accessibility. */ private class AccessibilityDelegate extends ExploreByTouchHelper { /** * The description string for unknown values */ @NonNull private final String mUnknownString; /** * The label for subtext descriptions */ private String mSubtextLabel; /** * The label for category descriptions */ private String mCatLabel; /** * The list of category names */ private String[] mCatNames; @RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) AccessibilityDelegate(View host) { super(host); mUnknownString = getResources().getString(R.string.unknown); loadLabels(); } /** * Load the labels used for descriptions. */ void loadLabels() { final Resources res = getResources(); switch (PreferenceUtils.getPrefSubtextValue()) { case PreferenceUtils.SUBTEXT_DENSITY: mSubtextLabel = res.getString(R.string.labelDensity); break; case PreferenceUtils.SUBTEXT_MELT: mSubtextLabel = res.getString(R.string.labelMelt); break; case PreferenceUtils.SUBTEXT_BOIL: mSubtextLabel = res.getString(R.string.labelBoil); break; case PreferenceUtils.SUBTEXT_HEAT: mSubtextLabel = res.getString(R.string.labelHeat); break; case PreferenceUtils.SUBTEXT_NEGATIVITY: mSubtextLabel = res.getString(R.string.labelNegativity); break; case PreferenceUtils.SUBTEXT_ABUNDANCE: mSubtextLabel = res.getString(R.string.labelAbundance); break; default: mSubtextLabel = res.getString(R.string.labelWeight); } if (PreferenceUtils.COLOR_BLOCK.equals(PreferenceUtils.getPrefElementColors())) { mCatLabel = res.getStringArray(R.array.elementColorNames)[1]; } else { if (mCatNames == null) { mCatNames = res.getStringArray(R.array.ptCategories); } mCatLabel = res.getStringArray(R.array.elementColorNames)[0]; } } @Override protected int getVirtualViewAt(float x, float y) { for (PeriodicTableBlock block : mPeriodicTableBlocks) { findBlockPosition(block); if (mRect.contains((int) x, (int) y)) { return block.element.number - 1; } } return INVALID_ID; } @Override protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { for (PeriodicTableBlock block : mPeriodicTableBlocks) { virtualViewIds.add(block.element.number - 1); } } @Override protected void onPopulateNodeForVirtualView(int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) { final PeriodicTableBlock block = mPeriodicTableBlocks.get(virtualViewId); findBlockPosition(block); node.setBoundsInParent(new Rect(mRect)); node.setText(getDescription(block)); node.setClickable(true); } /** * Get the description for a block. * * @param block The PeriodicTableBlock * @return The description string */ private String getDescription(@NonNull PeriodicTableBlock block) { final Element element = block.element; final Resources res = getResources(); final String symbol = element.symbol.toUpperCase(); final String name = res.getString(ElementUtils.getElementName(element.number)); final String subtext; switch (PreferenceUtils.getPrefSubtextValue()) { case PreferenceUtils.SUBTEXT_WEIGHT: subtext = element.unstable ? String.valueOf((int) element.weight) : block.subtext; break; case PreferenceUtils.SUBTEXT_DENSITY: subtext = element.density == null ? mUnknownString : block.subtext; break; case PreferenceUtils.SUBTEXT_MELT: subtext = element.melt == null ? mUnknownString : block.subtext; break; case PreferenceUtils.SUBTEXT_BOIL: subtext = element.boil == null ? mUnknownString : block.subtext; break; case PreferenceUtils.SUBTEXT_HEAT: subtext = element.heat == null ? mUnknownString : block.subtext; break; case PreferenceUtils.SUBTEXT_NEGATIVITY: subtext = element.negativity == null ? mUnknownString : block.subtext; break; case PreferenceUtils.SUBTEXT_ABUNDANCE: subtext = element.abundance == null ? mUnknownString : block.subtext; break; default: subtext = mUnknownString; } final String cat; if (PreferenceUtils.COLOR_BLOCK.equals(PreferenceUtils.getPrefElementColors())) { cat = String.valueOf(element.block); } else { cat = mCatNames[element.category]; } return res.getString(R.string.descTableBlock, element.number, symbol, name, mSubtextLabel, subtext, mCatLabel, cat); } @Override protected boolean onPerformActionForVirtualView(int virtualViewId, int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfoCompat.ACTION_CLICK: if (mPeriodicTableListener != null) { mPeriodicTableListener.onItemClick(mPeriodicTableBlocks.get(virtualViewId)); } return true; } return false; } } }