Java tutorial
/* * Copyright (C) 2011 The Android Open Source Project * * 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 org.dmfs.android.view; import java.lang.ref.WeakReference; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.graphics.drawable.Drawable; import android.os.Build.VERSION; import android.support.v4.view.PagerAdapter; import android.support.v4.view.PagerTabStrip; import android.util.AttributeSet; import android.view.Gravity; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.ImageView; /** * PagerTitleStrip is a non-interactive indicator of the current, next, and previous pages of a {@link ViewPager}. It is intended to be used as a child view of * a ViewPager widget in your XML layout. Add it as a child of a ViewPager in your layout file and set its android:layout_gravity to TOP or BOTTOM to pin it to * the top or bottom of the ViewPager. The title from each page is supplied by the method {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to the * ViewPager. * * <p> * For an interactive indicator, see {@link PagerTabStrip}. * </p> */ public class DrawablePagerTitleStrip extends ViewGroup implements ViewPager.Decor { private static final String TAG = "PagerTitleStrip"; ViewPager mPager; ImageView[] mImageViews = null; private int mLastKnownCurrentPage = -1; private float mLastKnownPositionOffset = -1; private int mScaledTextSpacing; private int mGravity; private boolean mUpdatingDrawables; private boolean mUpdatingPositions; private final PageListener mPageListener = new PageListener(); private WeakReference<PagerAdapter> mWatchingAdapter; private static final int[] ATTRS = new int[] { android.R.attr.gravity }; private static final int TEXT_SPACING = 16; // dip public DrawablePagerTitleStrip(Context context) { this(context, null); } public DrawablePagerTitleStrip(Context context, AttributeSet attrs) { super(context, attrs); final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); mGravity = a.getInteger(0, Gravity.BOTTOM); a.recycle(); final float density = context.getResources().getDisplayMetrics().density; mScaledTextSpacing = (int) (TEXT_SPACING * density); } /** * Set the required spacing between title segments. * * @param spacingPixels * Spacing between each title displayed in pixels */ public void setTextSpacing(int spacingPixels) { mScaledTextSpacing = spacingPixels; requestLayout(); } /** * @return The required spacing between title segments in pixels */ public int getTextSpacing() { return mScaledTextSpacing; } /** * Set the {@link Gravity} used to position text within the title strip. Only the vertical gravity component is used. * * @param gravity * {@link Gravity} constant for positioning title text */ public void setGravity(int gravity) { mGravity = gravity; requestLayout(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); final ViewParent parent = getParent(); if (!(parent instanceof ViewPager)) { throw new IllegalStateException("PagerTitleStrip must be a direct child of a ViewPager."); } final ViewPager pager = (ViewPager) parent; final PagerAdapter adapter = pager.getAdapter(); pager.setInternalPageChangeListener(mPageListener); pager.setOnAdapterChangeListener(mPageListener); mPager = pager; updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mPager != null) { updateAdapter(mPager.getAdapter(), null); mPager.setInternalPageChangeListener(null); mPager.setOnAdapterChangeListener(null); mPager = null; } } void updateImages(int currentItem, PagerAdapter adapter) { if (mImageViews == null || mImageViews.length == 0 || !(adapter instanceof IDrawableTitlePagerAdapter)) { // better throw an exception? return; } IDrawableTitlePagerAdapter dAdapter = (IDrawableTitlePagerAdapter) adapter; final int itemCount = adapter != null ? adapter.getCount() : 0; mUpdatingDrawables = true; int half = mImageViews.length / 2; for (int i = 0, l = mImageViews.length; i < l; ++i) { mImageViews[i].setImageDrawable( adapter != null && currentItem + (i - half) >= 0 && currentItem + (i - half) < itemCount ? dAdapter.getDrawableTitle(currentItem + (i - half)) : null); } // Measure everything final int width = getWidth() - getPaddingLeft() - getPaddingRight(); final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom(); final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f), MeasureSpec.AT_MOST); final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); for (ImageView iv : mImageViews) { iv.measure(childWidthSpec, childHeightSpec); } mLastKnownCurrentPage = currentItem; if (!mUpdatingPositions) { updateDrawablePositions(currentItem, mLastKnownPositionOffset, false); } mUpdatingDrawables = false; } @Override public void requestLayout() { if (!mUpdatingDrawables) { super.requestLayout(); } } void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) { if (oldAdapter != null) { oldAdapter.unregisterDataSetObserver(mPageListener); mWatchingAdapter = null; } if (newAdapter != null) { if (!(newAdapter instanceof IDrawableTitlePagerAdapter)) { throw new IllegalArgumentException("Adapter must implement IDrawableTitlePagerAdapter"); } newAdapter.registerDataSetObserver(mPageListener); mWatchingAdapter = new WeakReference<PagerAdapter>(newAdapter); Context context = getContext(); // TODO: we should determine the number of images dynamically int newCount = Math.max(1, Math.min(9, newAdapter.getCount())); if (mImageViews == null || mImageViews.length < newCount) { ImageView[] newImages = new ImageView[newCount]; int start = 0; if (mImageViews != null) { System.arraycopy(mImageViews, 0, newImages, 0, mImageViews.length); start = mImageViews.length; } for (int i = start; i < newCount; ++i) { addView(newImages[i] = new ImageView(context)); } mImageViews = newImages; } else if (mImageViews.length > newCount) { ImageView[] newImages = new ImageView[newCount]; System.arraycopy(mImageViews, 0, newImages, 0, newCount); for (int i = newCount; i < mImageViews.length; ++i) { removeView(mImageViews[i]); } mImageViews = newImages; } } else { mImageViews = null; } if (mPager != null) { mLastKnownCurrentPage = -1; mLastKnownPositionOffset = -1; updateImages(mPager.getCurrentItem(), newAdapter); requestLayout(); } } @SuppressLint("NewApi") void updateDrawablePositions(int position, float positionOffset, boolean force) { if (mImageViews == null || mImageViews.length == 0) { return; } if (position != mLastKnownCurrentPage) { updateImages(position, mPager.getAdapter()); } else if (!force && positionOffset == mLastKnownPositionOffset) { return; } mUpdatingPositions = true; final int[] widths = new int[mImageViews.length]; for (int i = 0, l = mImageViews.length; i < l; ++i) { widths[i] = mImageViews[i].getMeasuredWidth(); } final int stripWidth = getWidth(); final int stripHeight = getHeight(); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingRight(); final int paddingTop = getPaddingTop(); final int paddingBottom = getPaddingBottom(); float currOffset = positionOffset + 0.5f; if (currOffset > 1.f) { currOffset -= 1.f; } final int currCenter = stripWidth / 2 - (int) ((widths[widths.length / 2] + mScaledTextSpacing) * (currOffset - 0.5f)); final int currLeft = currCenter - widths[widths.length / 2] / 2; int maxBaseline = 0; for (ImageView iv : mImageViews) { maxBaseline = Math.max(maxBaseline, iv.getBaseline()); } int maxTextHeight = 0; for (ImageView iv : mImageViews) { maxTextHeight = Math.max(maxTextHeight, maxBaseline - iv.getBaseline() + iv.getMeasuredHeight()); } final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK; int left = currLeft; for (int i = mImageViews.length / 2, l = mImageViews.length; i < l; ++i) { int baseline = mImageViews[i].getBaseline(); int top; switch (vgrav) { default: case Gravity.TOP: top = paddingTop + maxBaseline - baseline; break; case Gravity.CENTER_VERTICAL: final int paddedHeight = stripHeight - paddingTop - paddingBottom; final int centeredTop = (paddedHeight - maxTextHeight) / 2; top = centeredTop + maxBaseline - baseline; break; case Gravity.BOTTOM: final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight; top = bottomGravTop + maxBaseline - baseline; break; } int right = left + widths[i]; mImageViews[i].layout(left, top, right, top + mImageViews[i].getMeasuredHeight()); left += widths[i] + mScaledTextSpacing; if (VERSION.SDK_INT >= 14) { if (stripWidth - right < paddingRight) { mImageViews[i].setAlpha( 1 - ((float) Math.abs(Math.min(stripWidth - right - paddingRight, 0))) / widths[i]); } else { mImageViews[i].setAlpha(1f); } } } left = currLeft; for (int i = mImageViews.length / 2 - 1; i >= 0; --i) { int baseline = mImageViews[i].getBaseline(); int top; switch (vgrav) { default: case Gravity.TOP: top = paddingTop + maxBaseline - baseline; break; case Gravity.CENTER_VERTICAL: final int paddedHeight = stripHeight - paddingTop - paddingBottom; final int centeredTop = (paddedHeight - maxTextHeight) / 2; top = centeredTop + maxBaseline - baseline; break; case Gravity.BOTTOM: final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight; top = bottomGravTop + maxBaseline - baseline; break; } left -= widths[i] + mScaledTextSpacing; mImageViews[i].layout(left, top, left + widths[i], top + mImageViews[i].getMeasuredHeight()); if (VERSION.SDK_INT >= 14) { if (left < paddingLeft) { mImageViews[i].setAlpha(1 - ((float) Math.abs(Math.min(left - paddingLeft, 0))) / widths[i]); } else { mImageViews[i].setAlpha(1f); } } } mLastKnownPositionOffset = positionOffset; mUpdatingPositions = false; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("Must measure with an exact width"); } int childHeight = heightSize; int minHeight = getMinHeight(); int padding = 0; padding = getPaddingTop() + getPaddingBottom(); childHeight -= padding; final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f), MeasureSpec.AT_MOST); final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); for (ImageView iv : mImageViews) { iv.measure(childWidthSpec, childHeightSpec); } if (heightMode == MeasureSpec.EXACTLY) { setMeasuredDimension(widthSize, heightSize); } else { int textHeight = mImageViews[mImageViews.length / 2].getMeasuredHeight(); setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding)); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mPager != null) { final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; updateDrawablePositions(mLastKnownCurrentPage, offset, true); } } int getMinHeight() { int minHeight = 0; final Drawable bg = getBackground(); if (bg != null) { minHeight = bg.getIntrinsicHeight(); } return minHeight; } private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener, ViewPager.OnAdapterChangeListener { private int mScrollState; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (positionOffset > 0.5f) { // Consider ourselves to be on the next page when we're 50% of the way there. position++; } updateDrawablePositions(position, positionOffset, false); } @Override public void onPageSelected(int position) { // this seems to cause flickering and it appears to be unnecessary after all // if (mScrollState == ViewPager.SCROLL_STATE_IDLE) // { // // Only update the text here if we're not dragging or settling. // updateImages(mPager.getCurrentItem(), mPager.getAdapter()); // // final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; // updateDrawablePositions(mPager.getCurrentItem(), offset, true); // } } @Override public void onPageScrollStateChanged(int state) { mScrollState = state; } @Override public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) { updateAdapter(oldAdapter, newAdapter); } @Override public void onChanged() { updateImages(mPager.getCurrentItem(), mPager.getAdapter()); final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; updateDrawablePositions(mPager.getCurrentItem(), offset, true); } } }