Java tutorial
/* * Copyright (C) 2015 Pedro Vicente Gomez Sanchez. * * 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.github.pedrovgs.nox; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.v4.view.GestureDetectorCompat; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; import com.github.pedrovgs.nox.imageloader.ImageLoader; import com.github.pedrovgs.nox.imageloader.ImageLoaderFactory; import com.github.pedrovgs.nox.shape.Shape; import com.github.pedrovgs.nox.shape.ShapeConfig; import com.github.pedrovgs.nox.shape.ShapeFactory; import java.util.List; import java.util.Observable; import java.util.Observer; /** * Main library component. This custom view receives a List of Nox objects and creates a awesome * panel full of images following different shapes. This new UI component is going to be similar to * the Apple's watch main menu user interface. * * NoxItem objects are going to be used to render the user interface using the resource used * to render inside each element and a view holder. * * @author Pedro Vicente Gomez Sanchez. */ public class NoxView extends View { private NoxConfig noxConfig; private Shape shape; private Scroller scroller; private NoxItemCatalog noxItemCatalog; private Paint paint = new Paint(); private boolean wasInvalidatedBefore; private OnNoxItemClickListener listener = OnNoxItemClickListener.EMPTY; private GestureDetectorCompat gestureDetector; private int defaultShapeKey; private boolean useCircularTransformation; public NoxView(Context context) { super(context); initializeNoxViewConfig(context, null, 0, 0); } public NoxView(Context context, AttributeSet attrs) { super(context, attrs); initializeNoxViewConfig(context, attrs, 0, 0); } public NoxView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initializeNoxViewConfig(context, attrs, defStyleAttr, 0); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public NoxView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initializeNoxViewConfig(context, attrs, defStyleAttr, defStyleRes); } /** * Given a List<NoxItem> instances configured previously gets the associated resource to draw the * view. */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (noxItemCatalog == null) { wasInvalidatedBefore = false; return; } updateShapeOffset(); for (int i = 0; i < noxItemCatalog.size(); i++) { if (shape.isItemInsideView(i)) { loadNoxItem(i); float left = shape.getXForItemAtPosition(i); float top = shape.getYForItemAtPosition(i); drawNoxItem(canvas, i, left, top); } } canvas.restore(); wasInvalidatedBefore = false; } /** * Configures a of List<NoxItem> instances to draw this items. */ public void showNoxItems(final List<NoxItem> noxItems) { this.post(new Runnable() { @Override public void run() { initializeNoxItemCatalog(noxItems); createShape(); initializeScroller(); refreshView(); } }); } /** * Used to notify when the data source has changed and is necessary to re draw the view. */ public void notifyDataSetChanged() { if (noxItemCatalog != null) { noxItemCatalog.recreate(); createShape(); initializeScroller(); refreshView(); } } /** * Changes the Shape used to the one passed as argument. This method will refresh the view. */ public void setShape(Shape shape) { validateShape(shape); this.shape = shape; this.shape.calculate(); initializeScroller(); resetScroll(); } /** * Delegates touch events to the scroller instance initialized previously to implement the scroll * effect. If the scroller does not handle the MotionEvent NoxView will check if any NoxItem has * been clicked to notify a previously configured OnNoxItemClickListener. */ @Override public boolean onTouchEvent(MotionEvent event) { super.onTouchEvent(event); boolean clickCaptured = processTouchEvent(event); boolean scrollCaptured = scroller != null && scroller.onTouchEvent(event); boolean singleTapCaptured = getGestureDetectorCompat().onTouchEvent(event); return clickCaptured || scrollCaptured || singleTapCaptured; } /** * Delegates computeScroll method to the scroller instance to implement the scroll effect. */ @Override public void computeScroll() { super.computeScroll(); if (scroller != null) { scroller.computeScroll(); } } /** * Returns the minimum X position the view can get taking into account the scroll offset. */ public int getMinX() { return scroller.getMinX(); } /** * Returns the maximum X position the view can get taking into account the scroll offset. */ public int getMaxX() { return scroller.getMaxX(); } /** * Returns the minimum Y position the view can get taking into account the scroll offset. */ public int getMinY() { return scroller.getMinY(); } /** * Returns the minimum Y position the view can get taking into account the scroll offset. */ public int getMaxY() { return scroller.getMaxY(); } public int getOverSize() { return scroller.getOverSize(); } /** * Returns the current Shape used in to draw this view. */ public Shape getShape() { return shape; } /** * Configures a OnNoxItemClickListener instance to be notified when a NoxItem instance is * clicked. */ public void setOnNoxItemClickListener(OnNoxItemClickListener listener) { validateListener(listener); this.listener = listener; } /** * Resets the scroll position to the 0,0. */ public void resetScroll() { if (scroller != null) { scroller.reset(); refreshView(); } } /** * Controls visibility changes to pause or resume this custom view and avoid performance * problems. */ @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (changedView != this) { return; } if (visibility == View.VISIBLE) { resume(); } else { pause(); } } /** * Release resources when the view has been detached from the window. */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); release(); } /** * Given a NoxItem position try to load this NoxItem if the view is not performing a fast scroll * after a fling gesture. */ private void loadNoxItem(int position) { if (!scroller.isScrollingFast()) { noxItemCatalog.load(position, useCircularTransformation); } } /** * Resumes NoxItemCatalog and adds a observer to be notified when a NoxItem is ready to be drawn. */ private void resume() { noxItemCatalog.addObserver(catalogObserver); noxItemCatalog.resume(); } /** * Pauses NoxItemCatalog and removes the observer previously configured. */ private void pause() { noxItemCatalog.pause(); noxItemCatalog.deleteObserver(catalogObserver); } /** * Releases NoxItemCatalog and removes the observer previously configured. */ private void release() { noxItemCatalog.release(); noxItemCatalog.deleteObserver(catalogObserver); } /** * Observer implementation used to be notified when a NoxItem has been loaded. */ private Observer catalogObserver = new Observer() { @Override public void update(Observable observable, Object data) { Integer position = (Integer) data; boolean isNoxItemLoadedInsideTheView = shape != null && shape.isItemInsideView(position); if (isNoxItemLoadedInsideTheView) { refreshView(); } } }; /** * Tries to post a invalidate() event if another one was previously posted. */ private void refreshView() { if (!wasInvalidatedBefore) { wasInvalidatedBefore = true; invalidate(); } } private void initializeNoxItemCatalog(List<NoxItem> noxItems) { ImageLoader imageLoader = ImageLoaderFactory.getPicassoImageLoader(getContext()); this.noxItemCatalog = new NoxItemCatalog(noxItems, (int) noxConfig.getNoxItemSize(), imageLoader); this.noxItemCatalog.setDefaultPlaceholder(noxConfig.getPlaceholder()); this.noxItemCatalog.addObserver(catalogObserver); } private void initializeScroller() { scroller = new Scroller(this, shape.getMinX(), shape.getMaxX(), shape.getMinY(), shape.getMaxY(), shape.getOverSize()); } /** * Checks the X and Y scroll offset to update that values into the Shape configured previously. */ private void updateShapeOffset() { int offsetX = scroller.getOffsetX(); int offsetY = scroller.getOffsetY(); shape.setOffset(offsetX, offsetY); } /** * Draws a NoxItem during the onDraw method. */ private void drawNoxItem(Canvas canvas, int position, float left, float top) { if (noxItemCatalog.isBitmapReady(position)) { Bitmap bitmap = noxItemCatalog.getBitmap(position); canvas.drawBitmap(bitmap, left, top, paint); } else if (noxItemCatalog.isDrawableReady(position)) { Drawable drawable = noxItemCatalog.getDrawable(position); drawNoxItemDrawable(canvas, (int) left, (int) top, drawable); } else if (noxItemCatalog.isPlaceholderReady(position)) { Drawable drawable = noxItemCatalog.getPlaceholder(position); drawNoxItemDrawable(canvas, (int) left, (int) top, drawable); } } /** * Draws a NoxItem drawable during the onDraw method given a canvas object and all the * information needed to draw the Drawable passed as parameter. */ private void drawNoxItemDrawable(Canvas canvas, int left, int top, Drawable drawable) { if (drawable != null) { int itemSize = (int) noxConfig.getNoxItemSize(); drawable.setBounds(left, top, left + itemSize, top + itemSize); drawable.draw(canvas); } } /** * Initializes a Shape instance given the NoxView configuration provided programmatically or * using XML styleable attributes. */ private void createShape() { if (shape == null) { float firstItemMargin = noxConfig.getNoxItemMargin(); float firstItemSize = noxConfig.getNoxItemSize(); int viewHeight = getMeasuredHeight(); int viewWidth = getMeasuredWidth(); int numberOfElements = noxItemCatalog.size(); ShapeConfig shapeConfig = new ShapeConfig(numberOfElements, viewWidth, viewHeight, firstItemSize, firstItemMargin); shape = ShapeFactory.getShapeByKey(defaultShapeKey, shapeConfig); } else { shape.setNumberOfElements(noxItemCatalog.size()); } shape.calculate(); } /** * Initializes a NoxConfig instance given the NoxView configuration provided programmatically or * using XML styleable attributes. */ private void initializeNoxViewConfig(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { noxConfig = new NoxConfig(); TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.nox, defStyleAttr, defStyleRes); initializeNoxItemSize(attributes); initializeNoxItemMargin(attributes); initializeNoxItemPlaceholder(attributes); initializeShapeConfig(attributes); initializeTransformationConfig(attributes); attributes.recycle(); } /** * Configures the nox item default size used in NoxConfig, Shape and NoxItemCatalog to draw nox * item instances during the onDraw execution. */ private void initializeNoxItemSize(TypedArray attributes) { float noxItemSizeDefaultValue = getResources().getDimension(R.dimen.default_nox_item_size); float noxItemSize = attributes.getDimension(R.styleable.nox_item_size, noxItemSizeDefaultValue); noxConfig.setNoxItemSize(noxItemSize); } /** * Configures the nox item default margin used in NoxConfig, Shape and NoxItemCatalog to draw nox * item instances during the onDraw execution. */ private void initializeNoxItemMargin(TypedArray attributes) { float noxItemMarginDefaultValue = getResources().getDimension(R.dimen.default_nox_item_margin); float noxItemMargin = attributes.getDimension(R.styleable.nox_item_margin, noxItemMarginDefaultValue); noxConfig.setNoxItemMargin(noxItemMargin); } /** * Configures the placeholder used if there is no another placeholder configured in the NoxItem * instances during the onDraw execution. */ private void initializeNoxItemPlaceholder(TypedArray attributes) { Drawable placeholder = attributes.getDrawable(R.styleable.nox_item_placeholder); if (placeholder == null) { placeholder = getContext().getResources().getDrawable(R.drawable.ic_nox); } noxConfig.setPlaceholder(placeholder); } /** * Configures the Shape used to show the list of NoxItems. */ private void initializeShapeConfig(TypedArray attributes) { defaultShapeKey = attributes.getInteger(R.styleable.nox_shape, ShapeFactory.FIXED_CIRCULAR_SHAPE_KEY); } /** * Configures the visual transformation applied to the NoxItem resources loaded, images * downloaded from the internet and resources loaded from the system. */ private void initializeTransformationConfig(TypedArray attributes) { useCircularTransformation = attributes.getBoolean(R.styleable.nox_use_circular_transformation, true); } private void validateShape(Shape shape) { if (shape == null) { throw new NullPointerException("You can't pass a null Shape instance as argument."); } if (noxItemCatalog != null && shape.getNumberOfElements() != noxItemCatalog.size()) { throw new IllegalArgumentException( "The number of items in the Shape instance passed as argument doesn't match with " + "the current number of NoxItems."); } } private void validateListener(OnNoxItemClickListener listener) { if (listener == null) { throw new NullPointerException( "You can't configure a null instance of OnNoxItemClickListener as NoxView listener."); } } /** * Returns a GestureDetectorCompat lazy instantiated created to handle single tap events and * detect if a NoxItem has been clicked to notify the previously configured listener. */ private GestureDetectorCompat getGestureDetectorCompat() { if (gestureDetector == null) { GestureDetector.OnGestureListener gestureListener = new SimpleOnGestureListener() { @Override public boolean onSingleTapUp(MotionEvent e) { boolean handled = false; int position = shape.getNoxItemHit(e.getX(), e.getY()); if (position >= 0) { handled = true; NoxItem noxItem = noxItemCatalog.getNoxItem(position); listener.onNoxItemClicked(position, noxItem); } return handled; } }; gestureDetector = new GestureDetectorCompat(getContext(), gestureListener); } return gestureDetector; } private boolean processTouchEvent(MotionEvent event) { if (shape == null) { return false; } boolean handled = false; float x = event.getX(); float y = event.getY(); int noxItemHit = shape.getNoxItemHit(x, y); boolean isNoxItemHit = noxItemHit >= 0; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isNoxItemHit) { changeNoxItemStateToPressed(noxItemHit); handled = true; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: for (int i = 0; i < noxItemCatalog.size(); i++) { if (shape.isItemInsideView(i)) { changeNoxItemStateToNotPressed(i); handled = true; } } break; default: } return handled; } private void changeNoxItemStateToPressed(int noxItemPosition) { int[] stateSet = new int[2]; stateSet[0] = android.R.attr.state_pressed; stateSet[1] = android.R.attr.state_enabled; setNoxItemState(noxItemPosition, stateSet); } private void changeNoxItemStateToNotPressed(int noxItemPosition) { int[] stateSet = new int[2]; stateSet[0] = -android.R.attr.state_pressed; stateSet[1] = android.R.attr.state_enabled; setNoxItemState(noxItemPosition, stateSet); } private void setNoxItemState(int noxItemPosition, int[] stateSet) { boolean refreshView = false; if (noxItemCatalog.isDrawableReady(noxItemPosition)) { Drawable drawable = noxItemCatalog.getDrawable(noxItemPosition); drawable.setState(stateSet); refreshView = true; } else if (noxItemCatalog.isPlaceholderReady(noxItemPosition)) { Drawable drawable = noxItemCatalog.getPlaceholder(noxItemPosition); drawable.setState(stateSet); refreshView = true; } if (refreshView) { refreshView(); } } /** * Method created for testing purposes. Configures the Scroller to be used by NoxView. * This method is needed because we don't have access to the view constructor. */ void setScroller(Scroller scroller) { this.scroller = scroller; } /** * Method created for testing purposes. Configures the NoxItemCatalog to be used by NoxView. * This method is needed because we don't have access to the view constructor. */ void setNoxItemCatalog(NoxItemCatalog noxItemCatalog) { this.noxItemCatalog = noxItemCatalog; } /** * Method created for testing purposes. Returns the NoxItemCatalog to be used by NoxView. * This method is needed because we don't have access to the view constructor. */ NoxItemCatalog getNoxItemCatalog() { return noxItemCatalog; } }