diff --git a/AutoViewPager/.gitignore b/AutoViewPager/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3bd8ae1faeeb7a8a26e5f86ae6191facfd82b878 --- /dev/null +++ b/AutoViewPager/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +.gradle/ +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea +.idea/ +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/AutoViewPager/build.gradle b/AutoViewPager/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..d98773d482a1fe880892a1625b8267b8deb35345 --- /dev/null +++ b/AutoViewPager/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.android.library' +} + +android { + namespace 'com.ggz.autoviewpager' + compileSdk 31 + + defaultConfig { + minSdk 24 + targetSdk 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.google.android.material:material:1.4.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/AutoViewPager/consumer-rules.pro b/AutoViewPager/consumer-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/AutoViewPager/proguard-rules.pro b/AutoViewPager/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /dev/null +++ b/AutoViewPager/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/AutoViewPager/src/androidTest/java/com/ggz/autoviewpager/ExampleInstrumentedTest.java b/AutoViewPager/src/androidTest/java/com/ggz/autoviewpager/ExampleInstrumentedTest.java new file mode 100644 index 0000000000000000000000000000000000000000..587b3995c6bf335bef3aab551507fd3e350467f4 --- /dev/null +++ b/AutoViewPager/src/androidTest/java/com/ggz/autoviewpager/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.ggz.autoviewpager; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.ggz.autoviewpager.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/AutoViewPager/src/main/AndroidManifest.xml b/AutoViewPager/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..a5918e68abcdde7f61ccae4f0ad4885b764573fd --- /dev/null +++ b/AutoViewPager/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/AutoViewPager/src/main/java/com/ggz/autoviewpager/AutoViewPager.java b/AutoViewPager/src/main/java/com/ggz/autoviewpager/AutoViewPager.java new file mode 100644 index 0000000000000000000000000000000000000000..31093bf3643e2ca4f257854d30a40a8f3c14d43e --- /dev/null +++ b/AutoViewPager/src/main/java/com/ggz/autoviewpager/AutoViewPager.java @@ -0,0 +1,469 @@ +package com.ggz.autoviewpager; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.Scroller; + +import androidx.viewpager.widget.ViewPager; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicInteger; + +/******************************************************************************* + * FileName: AutoViewPager + *

+ * Description: 可自动播放的ViewPager + *

+ * Author: juhg + *

+ * Version: v1.0 + *

+ * Date: 16/8/1 + *

+ * Copyright: all rights reserved by Freeman. + *******************************************************************************/ +public class AutoViewPager extends RelativeLayout implements MyViewPager.OnPageChangeListener, Runnable { + + private int mDefaultPager; + private int mItemCount; + private int mIndicatorSize; + private int mIndicatorLayoutMargin; + private int mIndicatorGap; + private int mAutoSwitchTime; + private int mIndicatorGravity; + private int mIndicatorNormalIcon; + private int mIndicatorSelectedIcon; + private int mPagerMargin; + private int mPagerWidth; + private int mPagerPaddingLeft; + private int mPagerPaddingRight; + private int mTransformer; + private float mDefaultAlpha; + private float mDefaultScale; + private boolean mIsAutoPlay; + private boolean mLoopPlay; + private boolean mShowIndicator; + + private LinearLayout indicatorLayout; + private MyViewPager viewPager; + private Context context; + private ImageView[] indicatorIcons; + private OnItemView itemView; + private Handler handler = new Handler(); + + public final static int INDICATOR_GRAVITY_TOP = 1; + public final static int INDICATOR_GRAVITY_BOTTOM = 2; + + public final static int TRANSFORM_TYPE_NONE = 0; + public final static int TRANSFORM_TYPE_FADE = 1; + public final static int TRANSFORM_TYPE_SCALE = 2; + public final static int TRANSFORM_TYPE_ROTATE = 3; + public final static int TRANSFORM_TYPE_FLIP = 4; + public final static int TRANSFORM_TYPE_SQUARE = 5; + public final static int TRANSFORM_TYPE_TURNTABLE = 6; + public final static int TRANSFORM_TYPE_CASCADING = 7; + + public AutoViewPager(Context context) { + this(context, null); + } + + public AutoViewPager(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public AutoViewPager(Context context, AttributeSet attrs, int style) { + super(context, attrs, style); + this.context = context; + TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.AutoViewPager); + mIndicatorSize = (int)array.getDimension(R.styleable.AutoViewPager_indicatorIconSize, 0); + mIndicatorLayoutMargin = (int) array.getDimension(R.styleable.AutoViewPager_indicatorLayoutMargin, 10); + mIndicatorGap = (int) array.getDimension(R.styleable.AutoViewPager_indicatorGap, 10); + mIndicatorNormalIcon = array.getResourceId(R.styleable.AutoViewPager_indicatorNormalIcon, 0); + mIndicatorSelectedIcon = array.getResourceId(R.styleable.AutoViewPager_indicatorSelectedIcon, 0); + mAutoSwitchTime = array.getInteger(R.styleable.AutoViewPager_autoSwitchTime, 4000); + mIsAutoPlay = array.getBoolean(R.styleable.AutoViewPager_isAutoPlay, false); + mLoopPlay = array.getBoolean(R.styleable.AutoViewPager_loopPlay, false); + mShowIndicator = array.getBoolean(R.styleable.AutoViewPager_showIndicator, false); + mIndicatorGravity = array.getInt(R.styleable.AutoViewPager_indicatorGravity, INDICATOR_GRAVITY_BOTTOM); + mPagerMargin = array.getDimensionPixelOffset(R.styleable.AutoViewPager_pagerMargin, 0); + mPagerPaddingLeft = array.getDimensionPixelOffset(R.styleable.AutoViewPager_pagerPaddingLeft, 0); + mPagerPaddingRight = array.getDimensionPixelOffset(R.styleable.AutoViewPager_pagerPaddingRight, 0); + mPagerWidth = array.getDimensionPixelOffset(R.styleable.AutoViewPager_pagerWidth, 0); + mTransformer = array.getInt(R.styleable.AutoViewPager_transformer, TRANSFORM_TYPE_NONE); + array.recycle(); + init(); + } + + private void init() { + viewPager = new MyViewPager(context); + viewPager.setId(generateId()); + viewPager.setClipToPadding(false); + viewPager.setPageMargin(mPagerMargin); + viewPager.setPadding(mPagerPaddingLeft, 0, mPagerPaddingRight, 0); + viewPager.addOnPageChangeListener(this); + setPagerTransformer(); + addView(viewPager); + } + + private void setPagerTransformer() { + MyViewPager.PageTransformer pageTransformer = null; + switch (mTransformer) { + case TRANSFORM_TYPE_FADE: + pageTransformer = PageTransformerFactory.fadeTransformer; + break; + case TRANSFORM_TYPE_SCALE: + pageTransformer = PageTransformerFactory.getScaleTransformer(viewPager, mPagerPaddingLeft, mDefaultScale, mDefaultAlpha); + break; + case TRANSFORM_TYPE_ROTATE: + pageTransformer = PageTransformerFactory.rotateTransformer; + break; + case TRANSFORM_TYPE_FLIP: + pageTransformer = PageTransformerFactory.flipTransformer; + break; + case TRANSFORM_TYPE_SQUARE: + pageTransformer = PageTransformerFactory.squareTransformer; + break; + case TRANSFORM_TYPE_TURNTABLE: + pageTransformer = PageTransformerFactory.turntableTransformer; + break; + case TRANSFORM_TYPE_CASCADING: + pageTransformer = PageTransformerFactory.cascadingTransformer; + break; + default: + break; + } + + if (pageTransformer != null) { + viewPager.setPageTransformer(true, pageTransformer); + } + } + + public int getCurrentItem() { + return viewPager.getCurrentItem() % mItemCount; + } + + public void setCurrentItem(int index) { + mDefaultPager = index; + } + + public void setIndicatorIconSize(int size) { + this.mIndicatorSize = size; + } + + public void setIndicatorLayoutMargin(int margin) { + this.mIndicatorLayoutMargin = margin; + } + + public void setIndicatorGap(int gap) { + this.mIndicatorGap = gap; + } + + public void setmIndicatorNormalIcon(int resId) { + mIndicatorNormalIcon = resId; + } + + public void setmIndicatorSelectedIcon(int resId) { + mIndicatorNormalIcon = resId; + } + + public void setSwitchTime(int switchTime) { + this.mAutoSwitchTime = switchTime; + } + + public void setAutoPlay(boolean isAutoPlay) { + this.mIsAutoPlay = isAutoPlay; + } + + public void setLoopPlay(boolean isLoopPlay) { + this.mLoopPlay = isLoopPlay; + } + + public void setShowIndicator(boolean showIndicator) { + this.mShowIndicator = showIndicator; + } + + public void setPagerTransformer(MyViewPager.PageTransformer transformer) { + viewPager.setPageTransformer(true, transformer); + } + + public void setPagerTransformer(int transformer) { + this.mTransformer = transformer; + setPagerTransformer(); + } + + public void setDefaultScale(float defaultScale) { + mDefaultScale = defaultScale; + setPagerTransformer(); + } + + public void setDefaultAlpha(float defaultAlpha) { + mDefaultAlpha = defaultAlpha; + setPagerTransformer(); + } + + public void setVelocityLimit(int velocityLimit) { + viewPager.setVelocityLimit(velocityLimit); + } + + public void setPagerPaddingLeft(int pagerPaddingLeft) { + this.mPagerPaddingLeft = pagerPaddingLeft; + viewPager.setPadding(mPagerPaddingLeft, 0, mPagerPaddingRight, 0); + } + + public void setmPagerPaddingRight(int pagerPaddingRight) { + this.mPagerPaddingRight = pagerPaddingRight; + viewPager.setPadding(mPagerPaddingLeft, 0, mPagerPaddingRight, 0); + } + + public void setPagerPadding(int pagerPadding) { + this.mPagerPaddingLeft = pagerPadding; + this.mPagerPaddingRight = pagerPadding; + viewPager.setPadding(mPagerPaddingLeft, 0, mPagerPaddingRight, 0); + } + + public int getPagerPaddingLeft() { + return mPagerPaddingLeft; + } + + public int getPagerPaddingRight() { + return mPagerPaddingRight; + } + + + public void setPagerMargin(int margin) { + viewPager.setPageMargin(margin); + } + + public int getPagerMargin() { + return viewPager.getPageMargin(); + } + + public void setPagerWidth(int pagerWidth) { + mPagerWidth = pagerWidth; + } + + public int getPagerWidth() { + return mPagerWidth; + } + + + @Override + public void run() { + if (mIsAutoPlay) { + int index = viewPager.getCurrentItem(); + index++; + viewPager.setCurrentItem(index); + } + } + + private void addIndicatorLayout() { + indicatorLayout = new LinearLayout(context); + indicatorLayout.setGravity(Gravity.CENTER_HORIZONTAL); + indicatorLayout.setOrientation(LinearLayout.HORIZONTAL); + indicatorIcons = new ImageView[mItemCount]; + int gap = mIndicatorGap / 2; + for (int i = 0; i < mItemCount; i++) { + indicatorIcons[i] = new ImageView(context); + LinearLayout.LayoutParams params; + if (mIndicatorSize > 0) { + params = new LinearLayout.LayoutParams(mIndicatorSize, mIndicatorSize); + } else { + params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + params.leftMargin = params.rightMargin = gap; + if (mDefaultPager == i) { + indicatorIcons[i].setImageResource(mIndicatorSelectedIcon); + } else { + indicatorIcons[i].setImageResource(mIndicatorNormalIcon); + } + indicatorLayout.addView(indicatorIcons[i], params); + } + + LayoutParams params = new LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + if (mIndicatorGravity == INDICATOR_GRAVITY_TOP) { + params.topMargin = mIndicatorLayoutMargin; + params.addRule(ALIGN_TOP, viewPager.getId()); + } else { + params.bottomMargin = mIndicatorLayoutMargin; + params.addRule(ALIGN_BOTTOM, viewPager.getId()); + } + addView(indicatorLayout, params); + } + + public void setAdapter(int count) { + if (count <= 0) { + return; + } + + this.mItemCount = count; + viewPager.setAdapter(new MyPagerAdapter()); + viewPager.setOffscreenPageLimit(mItemCount); + viewPager.setCurrentItem(mDefaultPager); + if (mShowIndicator) { + addIndicatorLayout(); + } + + if (mIsAutoPlay) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + int index = viewPager.getCurrentItem(); + index++; + viewPager.setCurrentItem(index); + } + }, mAutoSwitchTime); + } + } + + public void setAdapter(int count, int limit) { + setAdapter(count); + viewPager.setOffscreenPageLimit(limit); + } + + /** + * 为View生成唯一的Id + * @return + */ + private int generateId() { + for (;;) { + AtomicInteger sNextGeneratedId = new AtomicInteger(1); + final int result = sNextGeneratedId.get(); + // aapt-generated IDs have the high byte nonzero; clamp to the range under that. + int newValue = result + 1; + if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0. + if (sNextGeneratedId.compareAndSet(result, newValue)) { + return result; + } + } + } + + private void resetIndicatorIcon(int position) { + for (int i = 0; i < mItemCount; i++) { + if (position == i) { + indicatorIcons[i].setImageResource(mIndicatorSelectedIcon); + } else { + indicatorIcons[i].setImageResource(mIndicatorNormalIcon); + } + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(final int position) { + if (mShowIndicator) { + resetIndicatorIcon(position % mItemCount); + } + if (mIsAutoPlay || mLoopPlay) { + handler.removeCallbacks(this); + handler.postDelayed(this, mAutoSwitchTime); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + + } + + /** + * 设置切换过程的时间 + * @param duration + */ + public void setDuration(int duration) { + try { + Field field = ViewPager.class.getDeclaredField("mScroller"); + field.setAccessible(true); + MyScroller scroller = new MyScroller(viewPager.getContext(), new LinearInterpolator()); + field.set(viewPager, scroller); + scroller.setDuration(duration); + } catch (Exception e) { + e.printStackTrace(System.out); + } + } + + public void setItemView(OnItemView itemView) { + this.itemView = itemView; + } + + public interface OnItemView { + View getView(ViewGroup container, int position); + } + + public class MyPagerAdapter extends CustomPagerAdapter { + + @Override + public float getPageWidth(int position) { + if (mPagerWidth != 0) { + return mPagerWidth * 1.0f / Resources.getSystem().getDisplayMetrics().widthPixels; + } + + return super.getPageWidth(position); + } + + @Override + public int getCount() { + if (mIsAutoPlay || mLoopPlay) { + return Integer.MAX_VALUE; + } else { + return mItemCount; + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view.equals(object); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + return itemView.getView(container, position % mItemCount); + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + container.removeView((View) object); + } + } + + public static class MyScroller extends Scroller { + + private int mDuration; + + public MyScroller(Context context) { + super(context); + } + + public MyScroller(Context context, Interpolator interpolator) { + super(context, interpolator); + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy) { + this.startScroll(startX, startY, dx, dy, mDuration); + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + super.startScroll(startX, startY, dx, dy, mDuration); + } + + public void setDuration(int duration) { + this.mDuration = duration; + } + } +} diff --git a/AutoViewPager/src/main/java/com/ggz/autoviewpager/CustomPagerAdapter.java b/AutoViewPager/src/main/java/com/ggz/autoviewpager/CustomPagerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..f5c8a9532382dfbe31df029b52364e668517452b --- /dev/null +++ b/AutoViewPager/src/main/java/com/ggz/autoviewpager/CustomPagerAdapter.java @@ -0,0 +1,278 @@ +package com.ggz.autoviewpager; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +/******************************************************************************* + * Description: + * + * Author: Freeman + * + * Date: 2018/5/18 + * + * Copyright: all rights reserved by Freeman. + *******************************************************************************/ + +public abstract class CustomPagerAdapter { + private final DataSetObservable mObservable = new DataSetObservable(); + private DataSetObserver mViewPagerObserver; + + public static final int POSITION_UNCHANGED = -1; + public static final int POSITION_NONE = -2; + + /** + * Return the number of views available. + */ + public abstract int getCount(); + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void startUpdate(ViewGroup container) { + startUpdate((View) container); + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + */ + public Object instantiateItem(ViewGroup container, int position) { + return instantiateItem((View) container, position); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void destroyItem(ViewGroup container, int position, Object object) { + destroyItem((View) container, position, object); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void setPrimaryItem(ViewGroup container, int position, Object object) { + setPrimaryItem((View) container, position, object); + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void finishUpdate(ViewGroup container) { + finishUpdate((View) container); + } + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #startUpdate(ViewGroup)} + */ + @Deprecated + public void startUpdate(View container) { + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + * + * @deprecated Use {@link #instantiateItem(ViewGroup, int)} + */ + @Deprecated + public Object instantiateItem(View container, int position) { + throw new UnsupportedOperationException( + "Required method instantiateItem was not overridden"); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(View)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #destroyItem(ViewGroup, int, Object)} + */ + @Deprecated + public void destroyItem(View container, int position, Object object) { + throw new UnsupportedOperationException("Required method destroyItem was not overridden"); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)} + */ + @Deprecated + public void setPrimaryItem(View container, int position, Object object) { + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #finishUpdate(ViewGroup)} + */ + @Deprecated + public void finishUpdate(View container) { + } + + /** + * Determines whether a page View is associated with a specific key object + * as returned by {@link #instantiateItem(ViewGroup, int)}. This method is + * required for a PagerAdapter to function properly. + * + * @param view Page View to check for association with object + * @param object Object to check for association with view + * @return true if view is associated with the key object object + */ + public abstract boolean isViewFromObject(View view, Object object); + + /** + * Save any instance state associated with this adapter and its pages that should be + * restored if the current UI state needs to be reconstructed. + * + * @return Saved state for this adapter + */ + public Parcelable saveState() { + return null; + } + + /** + * Restore any instance state associated with this adapter and its pages + * that was previously saved by {@link #saveState()}. + * + * @param state State previously saved by a call to {@link #saveState()} + * @param loader A ClassLoader that should be used to instantiate any restored objects + */ + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Called when the host view is attempting to determine if an item's position + * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given + * item has not changed or {@link #POSITION_NONE} if the item is no longer present + * in the adapter. + * + *

The default implementation assumes that items will never + * change position and always returns {@link #POSITION_UNCHANGED}. + * + * @param object Object representing an item, previously returned by a call to + * {@link #instantiateItem(View, int)}. + * @return object's new position index from [0, {@link #getCount()}), + * {@link #POSITION_UNCHANGED} if the object's position has not changed, + * or {@link #POSITION_NONE} if the item is no longer present. + */ + public int getItemPosition(Object object) { + return POSITION_UNCHANGED; + } + + /** + * This method should be called by the application if the data backing this adapter has changed + * and associated views should update. + */ + public void notifyDataSetChanged() { + synchronized (this) { + if (mViewPagerObserver != null) { + mViewPagerObserver.onChanged(); + } + } + mObservable.notifyChanged(); + } + + /** + * Register an observer to receive callbacks related to the adapter's data changing. + * + * @param observer The {@link DataSetObserver} which will receive callbacks. + */ + public void registerDataSetObserver(DataSetObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer from callbacks related to the adapter's data changing. + * + * @param observer The {@link DataSetObserver} which will be unregistered. + */ + public void unregisterDataSetObserver(DataSetObserver observer) { + mObservable.unregisterObserver(observer); + } + + void setViewPagerObserver(DataSetObserver observer) { + synchronized (this) { + mViewPagerObserver = observer; + } + } + + /** + * This method may be called by the ViewPager to obtain a title string + * to describe the specified page. This method may return null + * indicating no title for this page. The default implementation returns + * null. + * + * @param position The position of the title requested + * @return A title for the requested page + */ + public CharSequence getPageTitle(int position) { + return null; + } + + /** + * Returns the proportional width of a given page as a percentage of the + * ViewPager's measured width from (0.f-1.f] + * + * @param position The position of the page requested + * @return Proportional width for the given page position + */ + public float getPageWidth(int position) { + return 1.f; + } +} \ No newline at end of file diff --git a/AutoViewPager/src/main/java/com/ggz/autoviewpager/LoadMoreViewPager.java b/AutoViewPager/src/main/java/com/ggz/autoviewpager/LoadMoreViewPager.java new file mode 100644 index 0000000000000000000000000000000000000000..2a7e77725d1c9ea0da9b85152958add0f5ab74ef --- /dev/null +++ b/AutoViewPager/src/main/java/com/ggz/autoviewpager/LoadMoreViewPager.java @@ -0,0 +1,105 @@ +package com.ggz.autoviewpager; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.widget.Scroller; + +import androidx.viewpager.widget.ViewPager; + +/******************************************************************************* + * Description: 实现ViewPager左拉加载更多的功能 + * + * Author: Freeman + * + * Date: 2018/5/3 + * + * Copyright: all rights reserved by Freeman. + *******************************************************************************/ + +public class LoadMoreViewPager extends ViewPager { + + private int miniVelocity; + private int itemWidth; + private int scrollRange; + private VelocityTracker velocityTracker; + private Scroller scroller; + + public LoadMoreViewPager(Context context) { + this(context, null); + } + + public LoadMoreViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + scroller = new Scroller(context); + velocityTracker = VelocityTracker.obtain(); + ViewConfiguration viewConfiguration = ViewConfiguration.get(context); + miniVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); + } + + @Override + protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { + Log.i("out", "deltax==" + deltaX + " scrollX=" + scrollX + " scrollRangeX=" + scrollRangeX + " maxOverScrollX=" + maxOverScrollX); + return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, 300, scrollRangeY, 300, maxOverScrollY, true); + } + +// @Override +// public void computeScroll() { +// super.computeScroll(); +// if (!scroller.isFinished() && scroller.computeScrollOffset()) { +// Log.i("out", "currentX==" + scroller.getCurrY()); +// scrollTo(-scroller.getCurrX(), 0); +// +// invalidate(); +// } else { +// if (itemWidth != 0) { +// int index = Math.round(scroller.getCurrX() / itemWidth); +// scrollTo(index * itemWidth, 0); +// } +// } +// } + + private int getMaxX() { + if (scrollRange == 0) { + int count; + if (getAdapter() != null && (count = getAdapter().getCount()) > 0) { + Log.i("out", "pageWidth=" + getAdapter().getPageWidth(0) * getMeasuredWidth() + " pageMargin=" + getPageMargin()); + itemWidth = (int) (getAdapter().getPageWidth(0) + getPageMargin()); + scrollRange = (int) (count * getAdapter().getPageWidth(0) * getMeasuredWidth() + + (count - 1) * getPageMargin()); + } + } + + return scrollRange; + } + +// @Override +// public boolean onTouchEvent(MotionEvent ev) { +// velocityTracker.addMovement(ev); +// if (ev.getActionMasked() == MotionEvent.ACTION_UP) { +// velocityTracker.computeCurrentVelocity(1000); +// int velocityX = (int) velocityTracker.getXVelocity(); +// Log.i("out", "velocityX=" + velocityX + " miniVelocity=" + miniVelocity + " maxX=" + getMaxX()); +// if (velocityX > miniVelocity) { +// scroller.fling(getScrollX(), getScrollY(), velocityX, 0, 0, getMaxX(), 0, 0); +// } +// return false; +// } +// +// return super.onTouchEvent(ev); +// } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + velocityTracker.recycle(); + velocityTracker = null; + } +} diff --git a/AutoViewPager/src/main/java/com/ggz/autoviewpager/MyViewPager.java b/AutoViewPager/src/main/java/com/ggz/autoviewpager/MyViewPager.java new file mode 100644 index 0000000000000000000000000000000000000000..f5c304652cd1edfc7ee981cb60e4aa6212b9aed5 --- /dev/null +++ b/AutoViewPager/src/main/java/com/ggz/autoviewpager/MyViewPager.java @@ -0,0 +1,3139 @@ +package com.ggz.autoviewpager; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.AbsSavedState; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.WindowInsets; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.Scroller; + +import androidx.annotation.CallSuper; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.viewpager.widget.PagerAdapter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/******************************************************************************* + * Description: + * + * Author: Freeman + * + * Date: 2018/5/18 + * + * Copyright: all rights reserved by Freeman. + *******************************************************************************/ + +public class MyViewPager extends ViewGroup { + private static final String TAG = "ViewPager"; + private static final boolean DEBUG = false; + + private static final boolean USE_CACHE = false; + + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + + private static final int MIN_FLING_VELOCITY = 400; // dips + + static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator COMPARATOR = new Comparator(){ + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList mItems = new ArrayList(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + CustomPagerAdapter mAdapter; + int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + + private Scroller mScroller; + private boolean mIsScrollStarted; + private int mVelocityLimit; + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffect mLeftEdge; + private EdgeEffect mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private List mOnPageChangeListeners; + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private List mAdapterChangeListeners; + private PageTransformer mPageTransformer; + private int mPageTransformerLayerType; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + @Override + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see MyViewPager#SCROLL_STATE_IDLE + * @see MyViewPager#SCROLL_STATE_DRAGGING + * @see MyViewPager#SCROLL_STATE_SETTLING + */ + void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + *

As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a ViewPager on earlier platform versions will + * be ignored.

+ */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + void transformPage(View page, float position); + } + + /** + * Callback interface for responding to adapter changes. + */ + public interface OnAdapterChangeListener { + /** + * Called when the adapter for the given view pager has changed. + * + * @param viewPager ViewPager where the adapter change has happened + * @param oldAdapter the previously set adapter + * @param newAdapter the newly set adapter + */ + void onAdapterChanged(@NonNull MyViewPager viewPager, + @Nullable CustomPagerAdapter oldAdapter, @Nullable CustomPagerAdapter newAdapter); + } + + /** + * Annotation which allows marking of views to be decoration views when added to a view + * pager. + * + *

Views marked with this annotation can be added to the view pager with a layout resource. + * An example being {@link}.

+ * + *

You can also control whether a view is a decor view but setting + * {@link LayoutParams#isDecor} on the child's layout params.

+ */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + public @interface DecorView { + } + + public MyViewPager(Context context) { + super(context); + initViewPager(); + } + + public MyViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = configuration.getScaledPagingTouchSlop(); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffect(context); + mRightEdge = new EdgeEffect(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + ViewCompat.setOnApplyWindowInsetsListener(this, + new androidx.core.view.OnApplyWindowInsetsListener() { + /*@Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + return null; + }*/ + + private final Rect mTempRect = new Rect(); + + @Override + public WindowInsetsCompat onApplyWindowInsets(final View v, + final WindowInsetsCompat originalInsets) { + // First let the ViewPager itself try and consume them... + final WindowInsetsCompat applied = + ViewCompat.onApplyWindowInsets(v, originalInsets); + if (applied.isConsumed()) { + // If the ViewPager consumed all insets, return now + return applied; + } + + // Now we'll manually dispatch the insets to our children. Since ViewPager + // children are always full-height, we do not want to use the standard + // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them, + // the rest of the children will not receive any insets. To workaround this + // we manually dispatch the applied insets, not allowing children to + // consume them from each other. We do however keep track of any insets + // which are consumed, returning the union of our children's consumption + final Rect res = mTempRect; + res.left = applied.getSystemWindowInsetLeft(); + res.top = applied.getSystemWindowInsetTop(); + res.right = applied.getSystemWindowInsetRight(); + res.bottom = applied.getSystemWindowInsetBottom(); + + for (int i = 0, count = getChildCount(); i < count; i++) { + final WindowInsetsCompat childInsets = ViewCompat + .dispatchApplyWindowInsets(getChildAt(i), applied); + // Now keep track of any consumed by tracking each dimension's min + // value + res.left = Math.min(childInsets.getSystemWindowInsetLeft(), + res.left); + res.top = Math.min(childInsets.getSystemWindowInsetTop(), + res.top); + res.right = Math.min(childInsets.getSystemWindowInsetRight(), + res.right); + res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(), + res.bottom); + } + + // Now return a new WindowInsets, using the consumed window insets + return applied.replaceSystemWindowInsets( + res.left, res.top, res.right, res.bottom); + } + }); + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + // To be on the safe side, abort the scroller + if ((mScroller != null) && !mScroller.isFinished()) { + mScroller.abortAnimation(); + } + super.onDetachedFromWindow(); + } + + void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + dispatchOnScrollStateChanged(newState); + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(CustomPagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.setViewPagerObserver(null); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final CustomPagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + mAdapter.setViewPagerObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + // Dispatch the change to any listeners + if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) { + for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) { + mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter); + } + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + public CustomPagerAdapter getAdapter() { + return mAdapter; + } + + /** + * Add a listener that will be invoked whenever the adapter for this ViewPager changes. + * + * @param listener listener to add + */ + public void addOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) { + if (mAdapterChangeListeners == null) { + mAdapterChangeListeners = new ArrayList<>(); + } + mAdapterChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnAdapterChangeListener(OnAdapterChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnAdapterChangeListener(@NonNull OnAdapterChangeListener listener) { + if (mAdapterChangeListeners != null) { + mAdapterChangeListeners.remove(listener); + } + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i = 0; i < mItems.size(); i++) { + mItems.get(i).scrolling = true; + } + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + if (dispatchSelected) { + dispatchOnPageSelected(item); + } + requestLayout(); + } else { + populate(item); + scrollToItem(item, smoothScroll, velocity, dispatchSelected); + } + } + + private void scrollToItem(int item, boolean smoothScroll, int velocity, + boolean dispatchSelected) { + final ItemInfo curInfo = infoForPosition(item); + int destX = 0; + if (curInfo != null) { + final int width = getClientWidth(); + destX = (int) (width * Math.max(mFirstOffset, + Math.min(curInfo.offset, mLastOffset))); + } + if (smoothScroll) { + smoothScrollTo(destX, 0, velocity); + if (dispatchSelected) { + dispatchOnPageSelected(item); + } + } else { + if (dispatchSelected) { + dispatchOnPageSelected(item); + } + completeScroll(false); + scrollTo(destX, 0); + pageScrolled(destX); + } + } + + /** + * Set a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + * @param listener Listener to set + * + * @deprecated Use {@link #addOnPageChangeListener(OnPageChangeListener)} + * and {@link #removeOnPageChangeListener(OnPageChangeListener)} instead. + */ + @Deprecated + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOnPageChangeListener = listener; + } + + /** + * Add a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link OnPageChangeListener}. + * + *

Components that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()} + * to remove all attached listeners.

+ * + * @param listener listener to add + */ + public void addOnPageChangeListener(OnPageChangeListener listener) { + if (mOnPageChangeListeners == null) { + mOnPageChangeListeners = new ArrayList<>(); + } + mOnPageChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnPageChangeListener(OnPageChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnPageChangeListener(OnPageChangeListener listener) { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.remove(listener); + } + } + + /** + * Remove all listeners that are notified of any changes in scroll state or position. + */ + public void clearOnPageChangeListeners() { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.clear(); + } + } + + /** + * Sets a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding behavior. + * + *

Note: By default, calling this method will cause contained pages to use + * {@link View#LAYER_TYPE_HARDWARE}. This layer type allows custom alpha transformations, + * but it will cause issues if any of your pages contain a {@link android.view.SurfaceView} + * and you have not called {@link android.view.SurfaceView#setZOrderOnTop(boolean)} to put that + * {@link android.view.SurfaceView} above your app content. To disable this behavior, call + * {@link #setPageTransformer(boolean, PageTransformer,int)} and pass + * {@link View#LAYER_TYPE_NONE} for {@code pageLayerType}.

+ * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + setPageTransformer(reverseDrawingOrder, transformer, View.LAYER_TYPE_HARDWARE); + } + + /** + * Sets a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding behavior. + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + * @param pageLayerType View layer type that should be used for ViewPager pages. It should be + * either {@link View#LAYER_TYPE_HARDWARE}, + * {@link View#LAYER_TYPE_SOFTWARE}, or + * {@link View#LAYER_TYPE_NONE}. + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer, + int pageLayerType) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabled(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + mPageTransformerLayerType = pageLayerType; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = + ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + *

This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.

+ * + *

You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.

+ * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(ContextCompat.getDrawable(getContext(), resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * (float) Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + + int sx; + boolean wasScrolling = (mScroller != null) && !mScroller.isFinished(); + if (wasScrolling) { + // We're in the middle of a previously initiated scrolling. Check to see + // whether that scrolling has actually started (if we always call getStartX + // we can get a stale value from the scroller if it hadn't yet had its first + // computeScrollOffset call) to decide what is the current scrolling position. + sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX(); + // And abort the current scrolling. + mScroller.abortAnimation(); + setScrollingCacheEnabled(false); + } else { + sx = getScrollX(); + } + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth + * distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + // Reset the "scroll started" flag. It will be flipped to true in all places + // where we call computeScrollOffset(). + mIsScrollStarted = false; + mScroller.startScroll(sx, sy, dx, dy, duration); + ViewCompat.postInvalidateOnAnimation(this); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 + && mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + if (mCurItem != newCurrentItem) { + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N - 1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i = 0; i < mItems.size(); i++) { + Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); + } + } + + mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (!lp.isDecor && lp.widthFactor == 0.f) { + // 0 means requery the adapter for this, it doesn't have a valid width. + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.widthFactor = ii.widthFactor; + lp.position = ii.position; + } + } + } + sortChildDrawingOrder(); + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(View.FOCUS_FORWARD)) { + break; + } + } + } + } + } + } + + private void sortChildDrawingOrder() { + if (mDrawingOrder != DRAW_ORDER_DEFAULT) { + if (mDrawingOrderedChildren == null) { + mDrawingOrderedChildren = new ArrayList(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 + ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends AbsSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + SavedState(Parcel in, ClassLoader loader) { + super(in, loader); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + // Any views added via inflation should be classed as part of the decor + lp.isDecor |= isDecorView(child); + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + private static boolean isDecorView(@NonNull View view) { + Class clazz = view.getClass(); + return clazz.getAnnotation(DecorView.class) != null; + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (mAdapter.isViewFromObject(child, ii.object)) { + return ii; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent = child.getParent()) != this) { + if (parent == null || !(parent instanceof View)) { + return null; + } + child = (View) parent; + } + return infoForChild(child); + } + + ItemInfo infoForPosition(int position) { + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.position == position) { + return ii; + } + } + return null; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + final int maxGutterSize = measuredWidth / 10; + mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + /* + * Make sure all children have been properly measured. Decor views first. + * Right now we cheat and make this less complicated by assuming decor + * views won't intersect. We will pin to edges based on gravity. + */ + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp != null && lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + int widthMode = MeasureSpec.AT_MOST; + int heightMode = MeasureSpec.AT_MOST; + boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM; + boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT; + + if (consumeVertical) { + widthMode = MeasureSpec.EXACTLY; + } else if (consumeHorizontal) { + heightMode = MeasureSpec.EXACTLY; + } + + int widthSize = childWidthSize; + int heightSize = childHeightSize; + if (lp.width != LayoutParams.WRAP_CONTENT) { + widthMode = MeasureSpec.EXACTLY; + if (lp.width != LayoutParams.MATCH_PARENT) { + widthSize = lp.width; + } + } + if (lp.height != LayoutParams.WRAP_CONTENT) { + heightMode = MeasureSpec.EXACTLY; + if (lp.height != LayoutParams.MATCH_PARENT) { + heightSize = lp.height; + } + } + final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); + final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + child.measure(widthSpec, heightSpec); + + if (consumeVertical) { + childHeightSize -= child.getMeasuredHeight(); + } else if (consumeHorizontal) { + childWidthSize -= child.getMeasuredWidth(); + } + } + } + } + + mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + if (DEBUG) { + Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec); + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp == null || !lp.isDecor) { + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); + child.measure(widthSpec, mChildHeightMeasureSpec); + } + } + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + // Make sure scroll position is set correctly. + if (w != oldw) { + recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); + } + } + + private void recomputeScrollPosition(int width, int oldWidth, int margin, int oldMargin) { + if (oldWidth > 0 && !mItems.isEmpty()) { + if (!mScroller.isFinished()) { + mScroller.setFinalX(getCurrentItem() * getClientWidth()); + } else { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = + (int) (scrollOffset * (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + if (DEBUG) { + Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + } + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + mIsScrollStarted = true; + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + ViewCompat.postInvalidateOnAnimation(this); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + if (mFirstLayout) { + // If we haven't been laid out yet, we probably just haven't been populated yet. + // Let's skip this call since it doesn't make sense in this state + return false; + } + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) + / (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + @CallSuper + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + dispatchOnPageScrolled(position, offset, offsetPixels); + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrolled(position, offset, offsetPixels); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + } + + private void dispatchOnPageSelected(int position) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(position); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageSelected(position); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + private void dispatchOnScrollStateChanged(int state) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(state); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrollStateChanged(state); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrollStateChanged(state); + } + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + boolean wasScrolling = !mScroller.isFinished(); + if (wasScrolling) { + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (x != oldX) { + pageScrolled(x); + } + } + } + } + mPopulatePending = false; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + if (ii.scrolling) { + needPopulate = true; + ii.scrolling = false; + } + } + if (needPopulate) { + if (postEvents) { + ViewCompat.postOnAnimation(this, mEndScrollRunnable); + } else { + mEndScrollRunnable.run(); + } + } + } + + private boolean isGutterDrag(float x, float dx) { + return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable + ? mPageTransformerLayerType : View.LAYER_TYPE_NONE; + getChildAt(i).setLayerType(layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + final int action = ev.getAction() & MotionEvent.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + resetTouch(); + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + final float x = ev.getX(pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + Log.i("out", "dx==" + Math.abs(dx) + " diffY=" + yDiff); + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) + && canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 + ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + mIsUnableToDrag = false; + + mIsScrollStarted = true; + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING + && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) { + Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + } + break; + } + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = ev.getPointerId(0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex == -1) { + // A child has consumed some touch events and put us into an inconsistent + // state. + needsInvalidate = resetTouch(); + break; + } + final float x = ev.getX(pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = ev.getY(pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) { + Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + } + + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + Log.i("out", "isBeingDragged==" + mIsBeingDragged); + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + Log.i("out", "velocity==" + initialVelocity); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) + / (ii.widthFactor + marginOffset); + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + final float x = ev.getX(activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + needsInvalidate = resetTouch(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + needsInvalidate = resetTouch(); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + final float x = ev.getX(index); + mLastMotionX = x; + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); + break; + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + return true; + } + + private boolean resetTouch() { + boolean needsInvalidate; + mActivePointerId = INVALID_POINTER; + endDrag(); + mLeftEdge.onRelease(); + mRightEdge.onRelease(); + needsInvalidate = mLeftEdge.isFinished() || mRightEdge.isFinished(); + return needsInvalidate; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + mLeftEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + mRightEdge.onPull(Math.abs(over) / width); + needsInvalidate = true; + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + public void setVelocityLimit(int velocityLimit) { + mVelocityLimit = velocityLimit; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + int offset = 0; + if (mVelocityLimit > 0) { + offset = Math.round(Math.abs(velocity) / mVelocityLimit); + } + targetPage = velocity > 0 ? currentPage - offset : currentPage + offset + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = currentPage + (int) (pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = getOverScrollMode(); + if (overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS + && mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds(Math.round(drawAt), mTopPageBounds, + Math.round(drawAt + mPageMargin), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + *

A fake drag can be useful if you want to synchronize the motion of the ViewPager + * with the touch scrolling of another view, while still letting the ViewPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + *

During a fake drag the ViewPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + if (mAdapter != null) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + } + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + if (mAdapter == null) { + return; + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + /** + * Check if this ViewPager can be scrolled horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check scrolling right. + * @return Whether this ViewPager can be scrolled in the specified direction. It will always + * return false if the specified direction is 0. + */ + @Override + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() + && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() + && canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && v.canScrollHorizontally(-dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = pageLeft(); + } else { + handled = arrowScroll(FOCUS_LEFT); + } + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (event.hasModifiers(KeyEvent.META_ALT_ON)) { + handled = pageRight(); + } else { + handled = arrowScroll(FOCUS_RIGHT); + } + break; + case KeyEvent.KEYCODE_TAB: + if (event.hasNoModifiers()) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + break; + } + } + return handled; + } + + /** + * Handle scrolling in response to a left or right arrow click. + * + * @param direction The direction corresponding to the arrow key that was pressed. It should be + * either {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. + * @return Whether the scrolling was handled successfully. + */ + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem - 1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount() - 1)) { + setCurrentItem(mCurItem + 1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if (descendantFocusability != FOCUS_AFTER_DESCENDANTS + || (focusableCount == views.size())) { // No focusable descendants + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE + && isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a ViewPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this ViewPager. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem + && child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegateCompat { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(MyViewPager.class.getName()); + event.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) { + event.setItemCount(mAdapter.getCount()); + event.setFromIndex(mCurItem); + event.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(MyViewPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + PagerObserver() { + } + + @Override + public void onChanged() { + dataSetChanged(); + } + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall ViewPager + * container; constants are defined in {@link Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(MATCH_PARENT, MATCH_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } +} diff --git a/AutoViewPager/src/main/java/com/ggz/autoviewpager/PageTransformerFactory.java b/AutoViewPager/src/main/java/com/ggz/autoviewpager/PageTransformerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..3581c7dd6448b08117b0fbec2fd224d27c45f8a6 --- /dev/null +++ b/AutoViewPager/src/main/java/com/ggz/autoviewpager/PageTransformerFactory.java @@ -0,0 +1,141 @@ +package com.ggz.autoviewpager; + +import android.view.View; + +/******************************************************************************* + * Description: MyViewPager动画 + * + * Author: Freeman + * + * Date: 2018/5/3 + * + * Copyright: all rights reserved by Freeman. + *******************************************************************************/ + +public class PageTransformerFactory { + + public final static MyViewPager.PageTransformer getScaleTransformer(final MyViewPager viewPager, + final int mPagerPaddingLeft, final float defaultScale, final float defaultAlpha) { + + return new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + position = (page.getLeft() - (viewPager.getScrollX() + mPagerPaddingLeft)) * 1.0f / viewPager.getWidth(); + page.setPivotY(page.getHeight() / 2); + if (position >= -1 && position <= 0) { // 当前页面滑出屏幕(0-->-1) + float scaleRatio = (1 - defaultScale) * Math.abs(position); + float alpha = (1 - defaultAlpha) * Math.abs(position); + page.setScaleX(1 - scaleRatio); + page.setScaleY(1 - scaleRatio); + page.setPivotX(page.getWidth()); + page.setAlpha(1 - alpha); + } else if (position > 0 && position <= 1) { // 新页面滑入(1-->0) + float scaleRatio = (1 - defaultScale) * (1 - position); + float alpha = (1 - defaultAlpha) * (1 - position); + page.setPivotX(0); + page.setScaleX(defaultScale + scaleRatio); + page.setScaleY(defaultScale + scaleRatio); + page.setAlpha(defaultAlpha + alpha); + } else { + /** + * Nothing + */ + } + } + }; + } + + public final static MyViewPager.PageTransformer scaleTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + float defaultAlpha = 0.85f; + float defaultScale = 0.6f; + position = position - (int)position; + if (position >= -1 && position <= 0) { // 当前页面滑出屏幕(0-->-1) + float scaleRatio = (1 - defaultScale) * Math.abs(position); + page.setScaleX(1 - scaleRatio); + page.setScaleY(1 - scaleRatio); + page.setPivotX(0f); + page.setPivotX(page.getWidth()); + float alpha = (1 - defaultAlpha) * Math.abs(position); + page.setAlpha(1 - alpha); + } else if (position > 0 && position <= 1) { // 新页面滑入(1-->0) + float scaleRatio = (1 - defaultScale) * (1 - position); + page.setPivotX(0); + page.setScaleX(defaultScale + scaleRatio); + page.setScaleY(defaultScale + scaleRatio); + float alpha = (1 - defaultAlpha) * (1 - position); + page.setAlpha(defaultAlpha + alpha); + } else { + /** + * Nothing + */ + } + } + }; + + public final static MyViewPager.PageTransformer fadeTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + page.setAlpha(1 - Math.abs(position)); + } + }; + + public final static MyViewPager.PageTransformer flipTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + if (position > 0) { + page.setTranslationX(-page.getWidth() * position); + } + page.setPivotX(0); + page.setAlpha(1 - Math.abs(position)); + page.setRotationY(90 * position); + page.setCameraDistance(10000); + } + }; + + public final static MyViewPager.PageTransformer rotateTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + page.setPivotX(page.getWidth()/2); + page.setTranslationX(-page.getWidth() * position); + page.setRotationY(90 * position); + page.setCameraDistance(10000); + page.setAlpha(1 - Math.abs(position)); + } + }; + + public final static MyViewPager.PageTransformer turntableTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + page.setRotation(40 * position); + page.setTranslationX(800 * position); + } + }; + + public final static MyViewPager.PageTransformer squareTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + if (position >= -1 && position <= 0) { + page.setPivotX(page.getWidth()); + } else { + page.setPivotX(0); + } + page.setRotationY(90 *position); + page.setCameraDistance(10000); + } + }; + + public final static MyViewPager.PageTransformer cascadingTransformer = new MyViewPager.PageTransformer() { + @Override + public void transformPage(View page, float position) { + if (position >= -1 && position <= 0) { + page.setTranslationX(-page.getWidth() * position); + page.setAlpha(1 - Math.abs(position)); + page.setScaleX(1 + 0.5f * position); + page.setScaleY(1 + 0.5f * position); + page.setCameraDistance(10000); + } + } + }; +} diff --git a/AutoViewPager/src/main/res/values/attrs.xml b/AutoViewPager/src/main/res/values/attrs.xml new file mode 100644 index 0000000000000000000000000000000000000000..1d2a3e6457daa229d8460b7cf7d9fbabb9a49023 --- /dev/null +++ b/AutoViewPager/src/main/res/values/attrs.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AutoViewPager/src/main/res/values/strings.xml b/AutoViewPager/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..20d77fe87a450ef6cb677a57591a95e42c2abe4a --- /dev/null +++ b/AutoViewPager/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AutoViewPager + diff --git a/AutoViewPager/src/test/java/com/ggz/autoviewpager/ExampleUnitTest.java b/AutoViewPager/src/test/java/com/ggz/autoviewpager/ExampleUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..669019df0f537c58079f643b82e4a0ab0f508f08 --- /dev/null +++ b/AutoViewPager/src/test/java/com/ggz/autoviewpager/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.ggz.autoviewpager; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/H264/build.gradle b/H264/build.gradle index f8280621c107ea2f78a8bc53eee9a30469046bf1..ffc66371710ef1946bf1fa0752ba708bdfe936b0 100644 --- a/H264/build.gradle +++ b/H264/build.gradle @@ -13,7 +13,6 @@ android { versionCode 1 versionName "1.0" - consumerProguardFiles "consumer-rules.pro" } buildTypes { @@ -35,15 +34,15 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.1' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.core:core-ktx:1.6.0' + implementation "androidx.appcompat:appcompat:$appcompat" + implementation 'com.google.android.material:material:1.4.0' implementation project(path: ':code_h264') implementation 'net.butterflytv.utils:rtmp-client:3.0.1' /*网络okhttp引入*/ - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2' - implementation 'com.squareup.okio:okio:3.0.0-alpha.5' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.3' + implementation 'com.squareup.okio:okio:3.0.0' } \ No newline at end of file diff --git a/JsonViewer/.gitignore b/JsonViewer/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3bd8ae1faeeb7a8a26e5f86ae6191facfd82b878 --- /dev/null +++ b/JsonViewer/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +.gradle/ +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea +.idea/ +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/JsonViewer/build.gradle b/JsonViewer/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..13c42b5e192bddf608905e3df575e900ed80ee9e --- /dev/null +++ b/JsonViewer/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 30 + + defaultConfig { + minSdk 24 + targetSdk 30 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion. VERSION_11 + targetCompatibility JavaVersion. VERSION_11 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.6.0' + implementation "androidx.appcompat:appcompat:$appcompat" + implementation 'com.google.android.material:material:1.4.0' +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + // Treat all Kotlin warnings as errors + allWarningsAsErrors = false + freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn' + // Enable experimental coroutines APIs, including Flow + freeCompilerArgs += '-Xopt-in=kotlin.Experimental' + + // Set JVM target to 11 + jvmTarget = "11" + } +} \ No newline at end of file diff --git a/JsonViewer/consumer-rules.pro b/JsonViewer/consumer-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/JsonViewer/proguard-rules.pro b/JsonViewer/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /dev/null +++ b/JsonViewer/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/JsonViewer/src/main/AndroidManifest.xml b/JsonViewer/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..62b7f443e31b9849ace18ac7b40e43668a4512f4 --- /dev/null +++ b/JsonViewer/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/JsonViewer/src/main/java/com/weibo/json/viewer/WBJsonRecyclerView.kt b/JsonViewer/src/main/java/com/weibo/json/viewer/WBJsonRecyclerView.kt new file mode 100644 index 0000000000000000000000000000000000000000..756b0c6c9501093dd53d9dac67cd6a4f1c445bc0 --- /dev/null +++ b/JsonViewer/src/main/java/com/weibo/json/viewer/WBJsonRecyclerView.kt @@ -0,0 +1,194 @@ +package com.weibo.json.viewer + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewParent +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.weibo.json.viewer.adapter.WBBaseJsonViewerAdapter +import com.weibo.json.viewer.adapter.WBJsonViewerAdapter +import com.weibo.json.viewer.view.WBJsonItemView +import org.json.JSONArray +import org.json.JSONObject +import kotlin.math.sqrt + +class WBJsonRecyclerView : RecyclerView { + + private var mAdapter: WBJsonViewerAdapter? = null + var mode = 0 + var oldDist = 0f + + var mSuperParentDisallow = 0 //事件拦截几层父类消耗事件 + + constructor(context: Context) : super(context) { + initView() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initView() + } + + constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + initView() + } + + private fun initView() { + layoutManager = LinearLayoutManager(context) + } + + fun bindJson(jsonStr: String?) { + mAdapter = null + mAdapter = WBJsonViewerAdapter(jsonStr) + adapter = mAdapter + } + + fun bindJson(array: JSONArray?) { + mAdapter = null + mAdapter = WBJsonViewerAdapter(array) + adapter = mAdapter + } + + fun bindJson(jsonObject: JSONObject?) { + mAdapter = null + mAdapter = WBJsonViewerAdapter(jsonObject) + adapter = mAdapter + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + var currentParentView: ViewParent? = parent + if (mSuperParentDisallow > 0) { + + for (i in 1..mSuperParentDisallow) { + if (i == 1) { + continue + } + currentParentView = currentParentView?.parent + } + + + if (ev?.action == MotionEvent.ACTION_DOWN) { + currentParentView?.requestDisallowInterceptTouchEvent(true); + } else if (ev?.action == MotionEvent.ACTION_UP || ev?.action == MotionEvent.ACTION_CANCEL) { + currentParentView?.requestDisallowInterceptTouchEvent(false); + } + } + + return super.dispatchTouchEvent(ev) + } + + fun setKeyColor(color: Int) { + WBBaseJsonViewerAdapter.KEY_COLOR = color + } + + fun setValueTextColor(color: Int) { + WBBaseJsonViewerAdapter.TEXT_COLOR = color + } + + fun setValueNumberColor(color: Int) { + WBBaseJsonViewerAdapter.NUMBER_COLOR = color + } + + fun setValueBooleanColor(color: Int) { + WBBaseJsonViewerAdapter.BOOLEAN_COLOR = color + } + + fun setValueUrlColor(color: Int) { + WBBaseJsonViewerAdapter.URL_COLOR = color + } + + fun setValueNullColor(color: Int) { + WBBaseJsonViewerAdapter.NUMBER_COLOR = color + } + + fun setBracesColor(color: Int) { + WBBaseJsonViewerAdapter.BRACES_COLOR = color + } + + private fun setTextSize(textSizeDP: Float) { + var sizeDP = textSizeDP + if (sizeDP < 10) { + sizeDP = 10f + } else if (sizeDP > 30) { + sizeDP = 30f + } + if (WBBaseJsonViewerAdapter.TEXT_SIZE_DP != sizeDP) { + WBBaseJsonViewerAdapter.TEXT_SIZE_DP = sizeDP + if (mAdapter != null) { + updateAll(sizeDP) + } + } + } + + fun setScaleEnable(enable: Boolean) { + if (enable) { + addOnItemTouchListener(touchListener) + } else { + removeOnItemTouchListener(touchListener) + } + } + + fun updateAll(textSize: Float) { + val manager = layoutManager + val count = manager?.childCount ?: 0 + for (i in 0 until count) { + val view = manager?.getChildAt(i) + loop(view, textSize) + } + } + + private fun loop(view: View?, textSize: Float) { + if (view is WBJsonItemView) { + val group: WBJsonItemView = view as WBJsonItemView + group.setTextSize(textSize) + val childCount: Int = group.childCount + for (i in 0 until childCount) { + val view1: View = group.getChildAt(i) + loop(view1, textSize) + } + } + } + + private fun zoom(f: Float) { + setTextSize(WBBaseJsonViewerAdapter.TEXT_SIZE_DP * f) + } + + private fun spacing(event: MotionEvent): Float { + val x = event.getX(0) - event.getX(1) + val y = event.getY(0) - event.getY(1) + return sqrt((x * x + y * y).toDouble()).toFloat() + } + + private val touchListener: OnItemTouchListener = object : OnItemTouchListener { + override fun onInterceptTouchEvent(rv: RecyclerView, event: MotionEvent): Boolean { + when (event.action and event.actionMasked) { + MotionEvent.ACTION_DOWN -> mode = 1 + MotionEvent.ACTION_UP -> mode = 0 + MotionEvent.ACTION_POINTER_UP -> mode -= 1 + MotionEvent.ACTION_POINTER_DOWN -> { + oldDist = spacing(event) + mode += 1 + } + MotionEvent.ACTION_MOVE -> if (mode >= 2) { + val newDist = spacing(event) + if (Math.abs(newDist - oldDist) > 0.5f) { + zoom(newDist / oldDist) + oldDist = newDist + } + } + } + return false + } + + override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { + } + + override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} + } + +} \ No newline at end of file diff --git a/JsonViewer/src/main/java/com/weibo/json/viewer/adapter/WBBaseJsonViewerAdapter.kt b/JsonViewer/src/main/java/com/weibo/json/viewer/adapter/WBBaseJsonViewerAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..118ce7bc20c1f9cac42a30556898b40a3b4ab7b2 --- /dev/null +++ b/JsonViewer/src/main/java/com/weibo/json/viewer/adapter/WBBaseJsonViewerAdapter.kt @@ -0,0 +1,33 @@ +package com.weibo.json.viewer.adapter + +import androidx.recyclerview.widget.RecyclerView + +abstract class WBBaseJsonViewerAdapter : + RecyclerView.Adapter() { + + companion object { + @JvmStatic + var KEY_COLOR = -0x6dd867 + + @JvmStatic + var TEXT_COLOR = -0xc54ab6 + + @JvmStatic + var NUMBER_COLOR = -0xda551e + + @JvmStatic + var BOOLEAN_COLOR = -0x67d80 + + @JvmStatic + var URL_COLOR = -0x992d2b + + @JvmStatic + var NULL_COLOR = -0x10a6cb + + @JvmStatic + var BRACES_COLOR = -0xb5aaa1 + + @JvmStatic + var TEXT_SIZE_DP = 12f + } +} \ No newline at end of file diff --git a/JsonViewer/src/main/java/com/weibo/json/viewer/adapter/WBJsonViewerAdapter.kt b/JsonViewer/src/main/java/com/weibo/json/viewer/adapter/WBJsonViewerAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4147cf4dde691d77a81b7b4f1e2c9ffef65656c --- /dev/null +++ b/JsonViewer/src/main/java/com/weibo/json/viewer/adapter/WBJsonViewerAdapter.kt @@ -0,0 +1,378 @@ +package com.weibo.json.viewer.adapter + +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.weibo.json.viewer.utils.Utils +import com.weibo.json.viewer.view.WBJsonItemView +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONTokener + +class WBJsonViewerAdapter : WBBaseJsonViewerAdapter { + + private var jsonStr: String? = null + + private var mJSONObject: JSONObject? = null + private var mJSONArray: JSONArray? = null + + constructor(jsonStr: String?) { + this.jsonStr = jsonStr + var objectData: Any? = null + try { + objectData = JSONTokener(jsonStr).nextValue() + } catch (e: JSONException) { + e.printStackTrace() + } + if (objectData != null && objectData is JSONObject) { + mJSONObject = objectData + } else if (objectData != null && objectData is JSONArray) { + mJSONArray = objectData + } else { + throw IllegalArgumentException("jsonStr is illegal.") + } + } + + constructor(jsonObject: JSONObject?) { + mJSONObject = jsonObject + requireNotNull(mJSONObject) { "jsonObject can not be null." } + } + + constructor(jsonArray: JSONArray?) { + mJSONArray = jsonArray + requireNotNull(mJSONArray) { "jsonArray can not be null." } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WBJsonItemViewHolder { + return WBJsonItemViewHolder(WBJsonItemView(parent.context)) + } + + override fun onBindViewHolder(holder: WBJsonItemViewHolder, position: Int) { + val itemView: WBJsonItemView = holder.mMyItemView + itemView.setTextSize(WBBaseJsonViewerAdapter.TEXT_SIZE_DP) + itemView.setRightColor(WBBaseJsonViewerAdapter.BRACES_COLOR) + if (mJSONObject != null) { + if (position == 0) { + itemView.hideLeft() + itemView.hideIcon() + itemView.showRight("{") + return + } else if (position == getItemCount() - 1) { + itemView.hideLeft() + itemView.hideIcon() + itemView.showRight("}") + return + } else if (mJSONObject?.names() == null) { + return + } + val key = mJSONObject?.names()?.optString(position - 1)?:"" // 遍历key + val value = mJSONObject!!.opt(key) + if (position < getItemCount() - 2) { + handleJsonObject(key, value, itemView, true, 1) + } else { + handleJsonObject(key, value, itemView, false, 1) // 最后一组,结尾不需要逗号 + } + } + if (mJSONArray != null) { + if (position == 0) { + itemView.hideLeft() + itemView.hideIcon() + itemView.showRight("[") + return + } else if (position == getItemCount() - 1) { + itemView.hideLeft() + itemView.hideIcon() + itemView.showRight("]") + return + } + val value = mJSONArray!!.opt(position - 1) // 遍历array + if (position < getItemCount() - 2) { + handleJsonArray(value, itemView, true, 1) + } else { + handleJsonArray(value, itemView, false, 1) // 最后一组,结尾不需要逗号 + } + } + } + + override fun getItemCount(): Int { + if (mJSONObject != null) { + return if (mJSONObject!!.names() != null) { + mJSONObject!!.names().length() + 2 + } else { + 2 + } + } + return if (mJSONArray != null) { + mJSONArray!!.length() + 2 + } else 0 + } + + /** + * 处理 value 上级为 JsonObject 的情况,value有key + * + * @param value + * @param key + * @param itemView + * @param appendComma + * @param hierarchy + */ + private fun handleJsonObject( + key: String, + value: Any, + itemView: WBJsonItemView, + appendComma: Boolean, + hierarchy: Int + ) { + val keyBuilder = + SpannableStringBuilder(Utils.getHierarchyStr(hierarchy)) + keyBuilder.append("\"").append(key).append("\"").append(":") + keyBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.KEY_COLOR), + 0, + keyBuilder.length - 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + keyBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.BRACES_COLOR), + keyBuilder.length - 1, + keyBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + itemView.showLeft(keyBuilder) + handleValue(value, itemView, appendComma, hierarchy) + } + + /** + * 处理 value 上级为 JsonArray 的情况,value无key + * + * @param value + * @param itemView + * @param appendComma 结尾是否需要逗号(最后一组 value 不需要逗号) + * @param hierarchy 缩进层级 + */ + fun handleJsonArray( + value: Any, + itemView: WBJsonItemView, + appendComma: Boolean, + hierarchy: Int + ) { + itemView.showLeft( + SpannableStringBuilder( + Utils.getHierarchyStr( + hierarchy + ) + ) + ) + handleValue(value, itemView, appendComma, hierarchy) + } + + /** + * @param value + * @param itemView + * @param appendComma 结尾是否需要逗号(最后一组 key:value 不需要逗号) + * @param hierarchy 缩进层级 + */ + private fun handleValue( + value: Any?, + itemView: WBJsonItemView, + appendComma: Boolean, + hierarchy: Int + ) { + val valueBuilder = SpannableStringBuilder() + if (value is Number) { + valueBuilder.append(value.toString()) + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.NUMBER_COLOR), + 0, + valueBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } else if (value is Boolean) { + valueBuilder.append(value.toString()) + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.BOOLEAN_COLOR), + 0, + valueBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } else if (value is JSONObject) { + itemView.showIcon(true) + valueBuilder.append("Object{...}") + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.BRACES_COLOR), + 0, + valueBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + itemView.setIconClickListener( + JsonItemClickListener( + value, + itemView, + appendComma, + hierarchy + 1 + ) + ) + } else if (value is JSONArray) { + itemView.showIcon(true) + valueBuilder.append("Array[").append(value.length().toString()).append("]") + val len = valueBuilder.length + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.BRACES_COLOR), + 0, + 6, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.NUMBER_COLOR), + 6, + len - 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.BRACES_COLOR), + len - 1, + len, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + itemView.setIconClickListener( + JsonItemClickListener( + value, + itemView, + appendComma, + hierarchy + 1 + ) + ) + } else if (value is String) { + itemView.hideIcon() + valueBuilder.append("\"").append(value.toString()).append("\"") + if (Utils.isUrl(value.toString())) { + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.TEXT_COLOR), + 0, + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.URL_COLOR), + 1, + valueBuilder.length - 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.TEXT_COLOR), + valueBuilder.length - 1, + valueBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } else { + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.TEXT_COLOR), + 0, + valueBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } else if (valueBuilder.length == 0 || value == null) { + itemView.hideIcon() + valueBuilder.append("null") + valueBuilder.setSpan( + ForegroundColorSpan(WBBaseJsonViewerAdapter.NULL_COLOR), + 0, + valueBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + if (appendComma) { + valueBuilder.append(",") + } + itemView.showRight(valueBuilder) + } + + inner class JsonItemClickListener( + private val value: Any?, + itemView: WBJsonItemView, + appendComma: Boolean, + hierarchy: Int + ) : + View.OnClickListener { + private val itemView: WBJsonItemView + private val appendComma: Boolean + private val hierarchy: Int + private var isCollapsed = true + private val isJsonArray: Boolean + override fun onClick(view: View) { + if (itemView.getChildCount() == 1) { // 初始(折叠) --> 展开"" + isCollapsed = false + itemView.showIcon(false) + itemView.setTag(itemView.getRightText()) + itemView.showRight(if (isJsonArray) "[" else "{") + val array = + if (isJsonArray) value as JSONArray? else (value as JSONObject?)!!.names() + var i = 0 + while (array != null && i < array.length()) { + val childItemView = WBJsonItemView(itemView.getContext()) + childItemView.setTextSize(WBBaseJsonViewerAdapter.TEXT_SIZE_DP) + childItemView.setRightColor(WBBaseJsonViewerAdapter.BRACES_COLOR) + val childValue = array.opt(i) + if (isJsonArray) { + handleJsonArray( + childValue, + childItemView, + i < array.length() - 1, + hierarchy + ) + } else { + handleJsonObject( + childValue as String, (value as JSONObject?)!!.opt( + childValue + ), childItemView, i < array.length() - 1, hierarchy + ) + } + itemView.addViewNoInvalidate(childItemView) + i++ + } + val childItemView = WBJsonItemView(itemView.getContext()) + childItemView.setTextSize(WBBaseJsonViewerAdapter.TEXT_SIZE_DP) + childItemView.setRightColor(WBBaseJsonViewerAdapter.BRACES_COLOR) + val builder: StringBuilder = + StringBuilder(Utils.getHierarchyStr(hierarchy - 1)) + builder.append(if (isJsonArray) "]" else "}").append(if (appendComma) "," else "") + childItemView.showRight(builder) + itemView.addViewNoInvalidate(childItemView) + itemView.requestLayout() + itemView.invalidate() + } else { // 折叠 <--> 展开 + val temp: CharSequence? = itemView.getRightText() + itemView.showRight(itemView.getTag() as CharSequence) + itemView.setTag(temp) + itemView.showIcon(!isCollapsed) + for (i in 1 until itemView.getChildCount()) { + itemView.getChildAt(i) + .setVisibility(if (isCollapsed) View.VISIBLE else View.GONE) + } + isCollapsed = !isCollapsed + } + } + + init { + this.itemView = itemView + this.appendComma = appendComma + this.hierarchy = hierarchy + isJsonArray = value != null && value is JSONArray + } + } + + inner class WBJsonItemViewHolder(myItemView: WBJsonItemView) : + RecyclerView.ViewHolder(myItemView) { + var mMyItemView: WBJsonItemView = myItemView + + init { + setIsRecyclable(false) + } + } + +} \ No newline at end of file diff --git a/JsonViewer/src/main/java/com/weibo/json/viewer/utils/Utils.kt b/JsonViewer/src/main/java/com/weibo/json/viewer/utils/Utils.kt new file mode 100644 index 0000000000000000000000000000000000000000..3aa03a2adec98d23c5f5874cbe17598e6d32c7c8 --- /dev/null +++ b/JsonViewer/src/main/java/com/weibo/json/viewer/utils/Utils.kt @@ -0,0 +1,77 @@ +package com.weibo.json.viewer.utils + +import java.util.regex.Pattern + +object Utils { + + private val urlPattern = Pattern.compile( + "^((https|http|ftp|rtsp|mms)?://)" + + "?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?" //ftp的user@ + + "(([0-9]{1,3}\\.){3}[0-9]{1,3}" // IP形式的URL- 199.194.52.184 + + "|" // 允许IP和DOMAIN(域名) + + "([0-9a-z_!~*'()-]+\\.)*" // 域名- www. + + "([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\\." // 二级域名 + + "[a-z]{2,6})" // first level domain- .com or .museum + + "(:[0-9]{1,4})?" // 端口- :80 + + "((/?)|" // a slash isn't required if there is no file name + + "(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)$" + ) + + /** + * 判断字符串是否是url + * + * @param str + * @return + */ + fun isUrl(str: String?): Boolean { + return urlPattern.matcher(str).matches() + } + + /** + * json 格式化缩进(格式化前不能有缩进,最好是格式化从服务端下发的) + * + * @param jsonStr + * @return + */ + fun jsonFormat(jsonStr: String?): String? { + if (jsonStr == null) return "" + var level = 0 + val builder = StringBuilder() + for (i in 0 until jsonStr.length) { + val c = jsonStr[i] + if (level > 0 && '\n' == builder[builder.length - 1]) { + builder.append(getHierarchyStr(level)) + } + when (c) { + '{', '[' -> { + builder.append(c).append("\n") + level++ + } + ',' -> builder.append(c).append("\n") + '}', ']' -> { + builder.append("\n") + level-- + builder.append(getHierarchyStr(level)) + builder.append(c) + } + else -> builder.append(c) + } + } + return builder.toString() + } + + /** + * 对应层级前面所需的空格数 + * + * @param hierarchy 缩进层级 + * @return + */ + fun getHierarchyStr(hierarchy: Int): String? { + val levelStr = StringBuilder() + for (levelI in 0 until hierarchy) { + levelStr.append(" ") + } + return levelStr.toString() + } + +} \ No newline at end of file diff --git a/JsonViewer/src/main/java/com/weibo/json/viewer/view/WBJsonItemView.kt b/JsonViewer/src/main/java/com/weibo/json/viewer/view/WBJsonItemView.kt new file mode 100644 index 0000000000000000000000000000000000000000..8f496bd5f326fa897a7736774e4e67b9778d475c --- /dev/null +++ b/JsonViewer/src/main/java/com/weibo/json/viewer/view/WBJsonItemView.kt @@ -0,0 +1,129 @@ +package com.weibo.json.viewer.view + +import android.content.Context +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.weibo.json.viewer.R +import com.weibo.json.viewer.adapter.WBBaseJsonViewerAdapter + +open class WBJsonItemView : LinearLayout { + private var TEXT_SIZE_DP = 12 + + private var mContext: Context? = null + + private var mTvLeft: TextView? = null + private var mTvRight: TextView? = null + private var mIvIcon: ImageView? = null + + constructor(context: Context?) : super(context) { + mContext = context + initView() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + mContext = context + initView() + } + + constructor( + context: Context?, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + mContext = context + initView() + } + + private fun initView() { + orientation = VERTICAL + LayoutInflater.from(mContext).inflate(R.layout.jsonviewer_layout_item_view, this, true) + mTvLeft = findViewById(R.id.tv_left) + mTvRight = findViewById(R.id.tv_right) + mIvIcon = findViewById(R.id.iv_icon) + } + + fun setTextSize(textViewSizeDp: Float) { + var textSizeDp = textViewSizeDp + if (textSizeDp < 12) { + textSizeDp = 12f + } else if (textSizeDp > 30) { + textSizeDp = 30f + } + TEXT_SIZE_DP = textSizeDp.toInt() + mTvLeft?.textSize = TEXT_SIZE_DP.toFloat() + mTvRight?.textSize = TEXT_SIZE_DP.toFloat() + mTvRight?.setTextColor(WBBaseJsonViewerAdapter.BRACES_COLOR) + + // align the vertically expand/collapse icon to the text + val textSize = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + TEXT_SIZE_DP.toFloat(), + resources.displayMetrics + ) + .toInt() + val layoutParams = mIvIcon?.layoutParams as LayoutParams + layoutParams.height = textSize + layoutParams.width = textSize + layoutParams.topMargin = textSize / 5 + mIvIcon?.layoutParams = layoutParams + } + + fun setRightColor(color: Int) { + mTvRight?.setTextColor(color) + } + + fun hideLeft() { + mTvLeft?.visibility = GONE + } + + fun showLeft(text: CharSequence?) { + mTvLeft?.visibility = VISIBLE + if (text != null) { + mTvLeft?.text = text + } + } + + fun hideRight() { + mTvRight?.visibility = GONE + } + + fun showRight(text: CharSequence?) { + mTvRight?.visibility = VISIBLE + if (text != null) { + mTvRight?.text = text + } + } + + fun getRightText(): CharSequence? { + return mTvRight?.text + } + + fun hideIcon() { + mIvIcon?.visibility = GONE + } + + fun showIcon(isPlus: Boolean) { + mIvIcon?.visibility = VISIBLE + mIvIcon?.setImageResource(if (isPlus) R.drawable.jsonviewer_plus else R.drawable.jsonviewer_minus) + mIvIcon?.contentDescription = + resources.getString(if (isPlus) R.string.jsonViewer_icon_plus else R.string.jsonViewer_icon_minus) + } + + fun setIconClickListener(listener: OnClickListener?) { + mIvIcon!!.setOnClickListener(listener) + } + + fun addViewNoInvalidate(child: View) { + var params = child.layoutParams + if (params == null) { + params = generateDefaultLayoutParams() + requireNotNull(params) { "generateDefaultLayoutParams() cannot return null" } + } + addViewInLayout(child, -1, params) + } +} \ No newline at end of file diff --git a/JsonViewer/src/main/res/drawable/jsonviewer_minus.xml b/JsonViewer/src/main/res/drawable/jsonviewer_minus.xml new file mode 100644 index 0000000000000000000000000000000000000000..d7ec09a4dacf34ee0a1217d2eef5a59008292aad --- /dev/null +++ b/JsonViewer/src/main/res/drawable/jsonviewer_minus.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/JsonViewer/src/main/res/drawable/jsonviewer_plus.xml b/JsonViewer/src/main/res/drawable/jsonviewer_plus.xml new file mode 100644 index 0000000000000000000000000000000000000000..b41fc3102b6b5026e23ba992f7d98812d5c9f0ef --- /dev/null +++ b/JsonViewer/src/main/res/drawable/jsonviewer_plus.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JsonViewer/src/main/res/layout/jsonviewer_layout_item_view.xml b/JsonViewer/src/main/res/layout/jsonviewer_layout_item_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..1310baf76c8792f09e1d4ed13f7e4b01cecb41ef --- /dev/null +++ b/JsonViewer/src/main/res/layout/jsonviewer_layout_item_view.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/JsonViewer/src/main/res/values/strings.xml b/JsonViewer/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d7205ed0ce1424ad53c5b73bf33afc806cc3dc4 --- /dev/null +++ b/JsonViewer/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + expand + collapse + diff --git a/LongImage/.gitignore b/LongImage/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b0d7942b799edcb52ebdb517a4b63e3c3cd17228 --- /dev/null +++ b/LongImage/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +.gradle/ +gradle/ +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea +.idea/ +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/LongImage/build.gradle b/LongImage/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..1ebe4faee95cbf2e27113794adaa4c030f60aca2 --- /dev/null +++ b/LongImage/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdk 30 + + defaultConfig { + minSdk 23 + targetSdk 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.6.0' + implementation "androidx.appcompat:appcompat:$appcompat" + implementation 'com.google.android.material:material:1.4.0' +} \ No newline at end of file diff --git a/LongImage/proguard-rules.pro b/LongImage/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..481bb434814107eb79d7a30b676d344b0df2f8ce --- /dev/null +++ b/LongImage/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/LongImage/src/androidTest/java/com/ggz/picture/ExampleInstrumentedTest.kt b/LongImage/src/androidTest/java/com/ggz/picture/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..13700482769d39f8016b36ac240476f7ba7ad1f1 --- /dev/null +++ b/LongImage/src/androidTest/java/com/ggz/picture/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.ggz.picture + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.ggz.picture", appContext.packageName) + } +} \ No newline at end of file diff --git a/LongImage/src/main/AndroidManifest.xml b/LongImage/src/main/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..2390e32c9a43bee46c58f13719a84b928ee92f6c --- /dev/null +++ b/LongImage/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/LongImage/src/main/assets/long_pic.jpeg b/LongImage/src/main/assets/long_pic.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..eb1359cd68505f1ce7e9ad8a4077866e46bda39d Binary files /dev/null and b/LongImage/src/main/assets/long_pic.jpeg differ diff --git a/LongImage/src/main/java/com/ggz/picture/LongImage.kt b/LongImage/src/main/java/com/ggz/picture/LongImage.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c81d1372de534729d71d38bc832418f3a74e87c --- /dev/null +++ b/LongImage/src/main/java/com/ggz/picture/LongImage.kt @@ -0,0 +1,162 @@ +package com.ggz.picture + +import android.content.Context +import android.graphics.* +import android.os.AsyncTask +import android.util.AttributeSet +import android.util.Log +import android.view.ViewTreeObserver +import java.lang.ref.WeakReference + +class LongImageView : androidx.appcompat.widget.AppCompatImageView { + + companion object { + const val TAG = "LongImageView" + const val LONG_PIC_PATH = "long_pic.jpeg" + } + + private lateinit var mContext: Context + + private lateinit var mBitmapRegionDecoder: BitmapRegionDecoder + + private var mBitmapArray = arrayOfNulls(6) + + var mPreDrawListener: ViewTreeObserver.OnPreDrawListener = + ViewTreeObserver.OnPreDrawListener { + var rect = Rect() + getLocalVisibleRect(rect) + Log.d(TAG, "top = $top; rect.top = ${rect.top}; rect.bottom= ${rect.bottom};") + true + } + + constructor(context: Context) : super(context) { + initView(context) + } + + constructor(context: Context, attr: AttributeSet) : super(context, attr) { + initView(context) + } + + constructor(context: Context, attr: AttributeSet, def: Int) : super(context, attr, def) { + initView(context) + } + + + fun initView(context: Context) { + mContext = context + + //获取图片宽、高度 + var longImageInput = mContext.assets.open(LONG_PIC_PATH) + var temOption: BitmapFactory.Options = BitmapFactory.Options() + temOption.inJustDecodeBounds = true + BitmapFactory.decodeStream(longImageInput, null, temOption) + var longImageWidth: Int = temOption.outWidth + var longImageHeight: Int = temOption.outHeight + Log.d(TAG, "longImageWidth = $longImageWidth ; longImageHeight = $longImageHeight") + post { + layoutParams.width = longImageWidth + layoutParams.height = longImageHeight + } + + //显示区域图片 + mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(longImageInput, false) + var options: BitmapFactory.Options = BitmapFactory.Options() + options.inPreferredConfig = Bitmap.Config.RGB_565 + var bitmap: Bitmap = mBitmapRegionDecoder.decodeRegion( + Rect( + 0, + 0, + longImageWidth, + longImageHeight / 5 + ), options + ) + + setImageBitmap(bitmap) + + } + + fun createBitMaps() { + //获取图片宽、高度 + var longImageInput = mContext.assets.open(LONG_PIC_PATH) + var temOption: BitmapFactory.Options = BitmapFactory.Options() + temOption.inJustDecodeBounds = true + BitmapFactory.decodeStream(longImageInput, null, temOption) + var longImageWidth: Int = temOption.outWidth + var longImageHeight: Int = temOption.outHeight + Log.d(TAG, "longImageWidth = $longImageWidth ; longImageHeight = $longImageHeight") + + var screenHeight = resources.displayMetrics.heightPixels + var rect = Rect() + getLocalVisibleRect(rect) + + + } + + override fun onAttachedToWindow() { + viewTreeObserver.addOnPreDrawListener(mPreDrawListener) + super.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + viewTreeObserver.removeOnPreDrawListener(mPreDrawListener) + super.onDetachedFromWindow() + } + + override fun onDraw(canvas: Canvas?) { + super.onDraw(canvas) + } + + private fun safeDecodeRegionBitmap(tileImage: TileImage?): Bitmap? { + return null + } + + inner class TileImage { + var isShow: Boolean = false + var rect: Rect = Rect() + var destRect: Rect = Rect() + lateinit var bitmap: Bitmap + var opts: BitmapFactory.Options? = null + var exifOrientation: Int = 0 + var regionBitmatTask: RegionBitmatTask? = null + } + + inner class RegionBitmatTask : AsyncTask { + + var refView: WeakReference? = null + var mTileImage: TileImage? = null + + constructor(longImageView: LongImageView) : super() { + refView = WeakReference(longImageView) + } + + override fun onCancelled(result: Bitmap?) { + super.onCancelled(result) + } + + override fun doInBackground(vararg params: TileImage?): Bitmap? { + var view = refView?.get() + if (view == null) { + return null + } + + mTileImage = params[0] + return view.safeDecodeRegionBitmap(mTileImage) + } + + override fun onPreExecute() { + super.onPreExecute() + } + + override fun onPostExecute(result: Bitmap?) { + super.onPostExecute(result) + } + + override fun onProgressUpdate(vararg values: Void?) { + super.onProgressUpdate(*values) + } + + override fun onCancelled() { + super.onCancelled() + } + } +} \ No newline at end of file diff --git a/LongImage/src/main/res/drawable-xxhdpi/long_pic.jpeg b/LongImage/src/main/res/drawable-xxhdpi/long_pic.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..eb1359cd68505f1ce7e9ad8a4077866e46bda39d Binary files /dev/null and b/LongImage/src/main/res/drawable-xxhdpi/long_pic.jpeg differ diff --git a/LongImage/src/main/res/drawable/ic_launcher_background.xml b/LongImage/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000000000000000000000000000000000..07d5da9cbf141911847041df5d7b87f0dd5ef9d4 --- /dev/null +++ b/LongImage/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LongImage/src/main/res/values-night/themes.xml b/LongImage/src/main/res/values-night/themes.xml new file mode 100644 index 0000000000000000000000000000000000000000..8629b2d669f38a398e092d7149fdfdc00874b12f --- /dev/null +++ b/LongImage/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/LongImage/src/main/res/values/colors.xml b/LongImage/src/main/res/values/colors.xml new file mode 100644 index 0000000000000000000000000000000000000000..f8c6127d327620c93d2b2d00342a68e97b98a48d --- /dev/null +++ b/LongImage/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/LongImage/src/main/res/values/strings.xml b/LongImage/src/main/res/values/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e9eab8e04b82ac34639ecf673723266b7d73c15 --- /dev/null +++ b/LongImage/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + LongImage + \ No newline at end of file diff --git a/LongImage/src/main/res/values/themes.xml b/LongImage/src/main/res/values/themes.xml new file mode 100644 index 0000000000000000000000000000000000000000..5b627c01191ed4ff0842c1c6852942d96173111f --- /dev/null +++ b/LongImage/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/LongImage/src/test/java/com/ggz/picture/ExampleUnitTest.kt b/LongImage/src/test/java/com/ggz/picture/ExampleUnitTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..391197540cf66221efdcc7a3d82343535fe7782d --- /dev/null +++ b/LongImage/src/test/java/com/ggz/picture/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.ggz.picture + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0b2d792e7b297de542998426a835148ea98e17e1..e7e1fede98eaff465f3ff906f518701befd7e5bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'kotlin-kapt' id 'androidx.navigation.safeargs.kotlin' } +// id 'com.didi.dokit' /*apply from '../gradlescript/config.gradle'*/ @@ -38,7 +39,7 @@ android { } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" allWarningsAsErrors = true } @@ -46,9 +47,9 @@ android { generateStubs = true } - /* dataBinding{ + dataBinding{ enabled = true - }*/ + } buildFeatures { dataBinding = true @@ -57,8 +58,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } composeOptions { @@ -70,11 +71,21 @@ android { excludes += "/META-INF/AL2.0" excludes += "/META-INF/LGPL2.1" } + + /*configurations.all { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'androidx.fragment' && details.requested.name == 'fragment' && details.requested.version == '1.3.4') { + details.useVersion '1.3.6' + details.because 'fixes critical bug in 1.3.4' + } + } + }*/ } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation project(path: ':AutoViewPager') /*测试用的包*/ testImplementation 'junit:junit:4.13.2' @@ -98,56 +109,88 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navagition_version" /*kotlin的协程包*/ - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_android" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_android" /* 引入datastore */ - implementation 'androidx.datastore:datastore:1.0.0-beta01' + implementation "androidx.datastore:datastore:$data_store" + /*Jetpack引入包*/ implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android.material:material:1.4.0' /***********Jetpack End*************/ - implementation "androidx.activity:activity-compose:1.3.0-alpha08" + implementation "androidx.activity:activity-compose:$compose_activity_version" - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.core:core-ktx:1.3.2' + implementation "androidx.appcompat:appcompat:$appcompat" + implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' /*引入recyclerView*/ - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation "androidx.recyclerview:recyclerview:$recyclerVersion" /*引入viewPager2*/ - implementation "androidx.viewpager2:viewpager2:1.0.0" + implementation "androidx.viewpager2:viewpager2:$viewpager2" /*引入下拉刷新框架*/ - implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swiperefreshlayout" /*网络okhttp引入*/ - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2' + implementation "com.squareup.okhttp3:okhttp:$okhttp" /*引入protobuf*/ - implementation 'com.google.protobuf:protoc:3.15.6' + implementation "com.google.protobuf:protoc:$protobuf" /*数据库的greendao引入*/ - implementation 'org.greenrobot:greendao:3.3.0' + implementation "org.greenrobot:greendao:$greendao" /*RxJava引入包*/ - implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' - implementation 'io.reactivex.rxjava3:rxjava:3.0.8' + implementation "io.reactivex.rxjava3:rxandroid:$rxjava_android" + implementation "io.reactivex.rxjava3:rxjava:$rxjava_java" /*Gson 引入*/ - implementation 'com.google.code.gson:gson:2.8.6' + implementation "com.google.code.gson:gson:$gson" /*引入EventBus3*/ - implementation 'org.greenrobot:eventbus:3.2.0' + implementation "org.greenrobot:eventbus:$eventbus" /*引入Glide*/ - implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation "com.github.bumptech.glide:glide:$glide" + + /*引入AspectJ*/ + implementation "org.aspectj:aspectjrt:$aspectj" + + /*引入DoKit Start*/ + //核心模块 + /*debugImplementation("io.github.didi.dokit:dokitx:$doKit") { + exclude module: 'androidx-fragment' + }*/ + + //文件同步模块 + //debugImplementation "io.github.didi.dokit:dokitx-ft:$doKit" + + //一机多控模块 + //debugImplementation "io.github.didi.dokit:dokitx-mc:$doKit" + + //weex模块 + //debugImplementation "io.github.didi.dokit:dokitx-weex:$doKit" + + //no-op 模块 + //releaseImplementation "io.github.didi.dokit:dokitx-no-op:$doKit" + /*引入DoKit End*/ + + /*Lottie*/ + implementation "com.airbnb.android:lottie:$lottie" + + /*引入AutoViewPager*/ +// implementation 'com.github.JuHonggang:Autoviewpager:v0.1' kapt "com.android.databinding:compiler:4.0.2" + implementation "com.yuyh.json:jsonviewer:1.0.6" + kapt project(':apt') implementation project(':network') @@ -155,6 +198,15 @@ dependencies { /*引入推送的api*/ implementation project(':pushbase:jiguangpush') implementation project(':pushbase:mipush') + + /*引入长图组件*/ + implementation project(':LongImage') + + /*引入JsonViewer*/ + implementation project(':JsonViewer') + + /*引入AutoViewPager*/ + implementation project(':AutoViewPager') } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { @@ -165,7 +217,91 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { // Enable experimental coroutines APIs, including Flow freeCompilerArgs += '-Xopt-in=kotlin.Experimental' - // Set JVM target to 1.8 - jvmTarget = "1.8" + // Set JVM target to 11 + jvmTarget = "11" } } + + +/*添加aspectj*/ +import org.aspectj.bridge.IMessage +import org.aspectj.bridge.MessageHandler +import org.aspectj.tools.ajc.Main + +final def log = project.logger +final def variants = project.android.applicationVariants +variants.all { variant -> + /*if (!variant.buildType.isDebuggable()) { + log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") + return; + }*/ + JavaCompile javaCompile = variant.javaCompile + javaCompile.doLast { + String[] args = ["-showWeaveInfo", + "-11", + "-inpath", javaCompile.destinationDir.toString(), + "-aspectpath", javaCompile.classpath.asPath, + "-d", javaCompile.destinationDir.toString(), + "-classpath", javaCompile.classpath.asPath, + "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] + log.debug "ajc args: " + Arrays.toString(args) + MessageHandler handler = new MessageHandler(true); + new Main().run(args, handler); + for (IMessage message : handler.getMessages(null, true)) { + switch (message.getKind()) { + case IMessage.ABORT: + case IMessage.ERROR: + case IMessage.FAIL: + log.error message.message, message.thrown + break; + case IMessage.WARNING: + log.warn message.message, message.thrown + break; + case IMessage.INFO: + log.info message.message, message.thrown + break; + case IMessage.DEBUG: + log.debug message.message, message.thrown + break; + } + } + } +} + + +/*添加DoKit*/ +/* +dokitExt { + //通用设置 + comm { + //地图经纬度开关 + gpsSwitch true + //网络开关 + networkSwitch true + //大图开关 + bigImgSwitch true + //webView js 抓包 + webViewSwitch true + } + + slowMethod { + //调用栈模式配置 对应gradle.properties中DOKIT_METHOD_STRATEGY=0 + stackMethod { + //默认值为 5ms 小于该值的函数在调用栈中不显示 + thresholdTime 10 + //调用栈函数入口 千万不要用我默认的配置 如果有特殊需求修改成项目中自己的入口 假如不需要可以去掉该字段 + enterMethods = ["com.didichuxing.doraemondemo.MainDebugActivity.test1"] + //黑名单 粒度最小到类 暂不支持到方法 千万不要用我默认的配置 如果有特殊需求修改成项目中自己的入口 假如不需要可以去掉该字段 + methodBlacklist = ["com.facebook.drawee.backends.pipeline.Fresco"] + } + //普通模式配置 对应gradle.properties中DOKIT_METHOD_STRATEGY=1 + normalMethod { + //默认值为 500ms 小于该值的函数在运行时不会在控制台中被打印 + thresholdTime 500 + //需要针对函数插装的包名 千万不要用我默认的配置 如果有特殊需求修改成项目中自己的项目包名 假如不需要可以去掉该字段 + packageNames = ["com.didichuxing.doraemondemo"] + //不需要针对函数插装的包名&类名 千万不要用我默认的配置 如果有特殊需求修改成项目中自己的项目包名 假如不需要可以去掉该字段 + methodBlacklist = ["com.didichuxing.doraemondemo.dokit"] + } + } +}*/ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d3a88133526d49d11b4e15ebaec4a4408a460e75..eab7172388a12ab82a9dab7817301a8a55ecc36d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,12 @@ android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" tools:ignore="ProtectedPermissions" /> + + + + + + - + @@ -78,9 +86,43 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + , + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + Log.d( + TAG, + "AActivity requestCode = " + requestCode + "; permissions = " + permissions.contentToString() + + "; grantResults = " + grantResults.contentToString() + ) + } + + fun requestPermission( + activity: Activity, + permissions: Array, + requestCode: Int + ) { + if (Build.VERSION.SDK_INT >= 23) { + + ActivityCompat.requestPermissions(parent, permissions, requestCode) + } + + } } \ No newline at end of file diff --git a/app/src/main/java/com/guiguzi/androidtest/activity/ButtonsActivity.kt b/app/src/main/java/com/guiguzi/androidtest/activity/ButtonsActivity.kt index 002b3530ca0557c22ea4b46b0d6affcec11a1528..94bd417fee88dcc8818dcd72cddf22f21547d89f 100644 --- a/app/src/main/java/com/guiguzi/androidtest/activity/ButtonsActivity.kt +++ b/app/src/main/java/com/guiguzi/androidtest/activity/ButtonsActivity.kt @@ -1,10 +1,13 @@ package com.guiguzi.androidtest.activity +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle -import android.os.PersistableBundle import android.util.Log +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.FrameLayout @@ -14,9 +17,10 @@ import com.guiguzi.androidtest.R class ButtonsActivity : Activity() { companion object { - const val TAG = "ButtonsActivity" + const val TAG = "ButtonsActivityTAG" } + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val linearLayout = LinearLayout(baseContext) @@ -37,10 +41,10 @@ class ButtonsActivity : Activity() { ) val button1 = Button(baseContext) - val buttonParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) + val buttonParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) button1.setText("brand") button1.textSize = 22f button1.setTextColor(resources.getColor(R.color.colorAccent)) @@ -50,10 +54,64 @@ class ButtonsActivity : Activity() { button1.setOnClickListener { val brand = android.os.Build.BRAND - Log.d(TAG, "mobile brand = $brand") +// Log.d(TAG, "mobile brand = $brand") + try { + val i = Intent(Intent.ACTION_MAIN) + i.flags = Intent.FLAG_ACTIVITY_NEW_TASK // 必须加入new task标识 + i.addCategory(Intent.CATEGORY_HOME) + startActivity(i) + } catch (e: Exception) { + Log.d(TAG, "出现异常 e = ${e.toString()}") + } + } - //测试显示启动 - val intent = Intent(this,MainActivity.javaClass) + val button2 = Button(baseContext) + button2.text = "双击检测" + button2.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + GestureDetector(baseContext, object : GestureDetector.OnGestureListener { + override fun onDown(e: MotionEvent?): Boolean { + Log.d(TAG, "onDown()") + return false + } + + override fun onShowPress(e: MotionEvent?) { + Log.d(TAG, "onShowPress()") + } + + override fun onSingleTapUp(e: MotionEvent?): Boolean { + Log.d(TAG, "onSingleTapUp()") + return false + } + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent?, + distanceX: Float, + distanceY: Float + ): Boolean { + Log.d(TAG, "onScroll()") + return false + } + + override fun onLongPress(e: MotionEvent?) { + Log.d(TAG, "onLongPress()") + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + Log.d(TAG, "onFling()") + return false + } + }).onTouchEvent(event) + return true + } + }) + linearLayout.addView(button2, buttonParams) } } \ No newline at end of file diff --git a/app/src/main/java/com/guiguzi/androidtest/activity/CustomViewPagerActivity.kt b/app/src/main/java/com/guiguzi/androidtest/activity/CustomViewPagerActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..6e8c81f676670ecba1f4083b4384446ee9984200 --- /dev/null +++ b/app/src/main/java/com/guiguzi/androidtest/activity/CustomViewPagerActivity.kt @@ -0,0 +1,324 @@ +package com.guiguzi.androidtest.activity + +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.widget.EdgeEffectCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager.widget.PagerAdapter +import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 +import com.ggz.autoviewpager.AutoViewPager +import com.guiguzi.androidtest.R +import com.guiguzi.androidtest.customView.CustomViewPager +import java.lang.reflect.Field + +class CustomViewPagerActivity : Activity() { + + val TAG: String = this.javaClass.name + + lateinit var mCustomViewPager: CustomViewPager + lateinit var mCustomViewPager2: CustomViewPager + lateinit var mViewPager: ViewPager + + lateinit var mViewPager2: ViewPager2 + lateinit var mPagerAdapter:RecyclerView.Adapter + + lateinit var mAutoViewPager: AutoViewPager + + lateinit var mLeftEdge: EdgeEffectCompat + lateinit var mRightEdge: EdgeEffectCompat + + private val items = intArrayOf( + R.mipmap.image_01, + R.mipmap.image_02, + R.mipmap.image_03, + R.mipmap.image_04, + R.mipmap.image_05, + R.mipmap.image_01, + R.mipmap.image_02, + R.mipmap.image_03, + R.mipmap.image_04, + R.mipmap.image_05, + R.mipmap.image_01, + R.mipmap.image_02, + R.mipmap.image_03, + R.mipmap.image_04, + R.mipmap.image_05 + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val rootView = LinearLayout(baseContext) + rootView.orientation = LinearLayout.VERTICAL + + var customParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 300 + ) + var textParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + customParams.gravity = Gravity.CENTER + + mCustomViewPager = CustomViewPager(this, null) + mCustomViewPager.setOverscrollTranslation(300) +// mCustomViewPager.onNestedFling() +// mCustomViewPager.isNestedScrollingEnabled = false +// mCustomViewPager.clipChildren = true +// mCustomViewPager.disableLRScroll() + mCustomViewPager.pageMargin = 40 + val textView = TextView(baseContext) + textView.text = "1111111111" + textView.gravity = Gravity.CENTER + rootView.addView(textView, textParams) + rootView.addView(mCustomViewPager, customParams) + + + mAutoViewPager = AutoViewPager(baseContext) + mAutoViewPager.setVelocityLimit(3000) +// mAutoViewPager.setDefaultScale(0.4f) + mAutoViewPager.pagerWidth = 400 + val textView4 = TextView(baseContext) + textView4.text = "======AutoViewPager======" + textView4.gravity = Gravity.CENTER + rootView.addView(textView4, textParams) + rootView.addView(mAutoViewPager, customParams) + + mCustomViewPager2 = CustomViewPager(this, null) + val textView2 = TextView(baseContext) + textView2.text = "222222222222" + textView2.gravity = Gravity.CENTER + rootView.addView(textView2, textParams) + rootView.addView(mCustomViewPager2, customParams) + + mViewPager = ViewPager(baseContext) + val textView3 = TextView(baseContext) + textView3.text = "333333333" + textView3.gravity = Gravity.CENTER + rootView.addView(textView3, textParams) + rootView.addView(mViewPager, customParams) + + mViewPager2 = ViewPager2(baseContext) + val textView5 = TextView(baseContext) + textView5.text = "ViewPager2" + textView5.gravity = Gravity.CENTER + rootView.addView(textView5, textParams) + rootView.addView(mViewPager2, customParams) + + init() + + var rootParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + + setContentView(rootView) + } + + fun init() { + + var items = listOf( + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14" + ) + + var items2 = listOf( + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14" + ) + + var items3 = listOf( + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14" + ) + + var customViewPagerAdapter = CustomViewPagerAdapter() + customViewPagerAdapter.itemsData = items + mCustomViewPager.adapter = customViewPagerAdapter +// mCustomViewPager.setVelocityLimit(30000) + /* mCustomViewPager.setOnPageChangeListener(object : CenterViewPager.OnPageChangeListener { + override fun onPageScrolled( + position: Int, + positionOffset: Float, + positionOffsetPixels: Int + ) { + if (mLeftEdge != null && mRightEdge != null) { + mLeftEdge.finish() + mRightEdge.finish() + mLeftEdge.setSize(0, 0) + mRightEdge.setSize(0, 0) + } + + } + + override fun onPageSelected(position: Int) { + TODO("Not yet implemented") + } + + override fun onPageScrollStateChanged(state: Int) { + TODO("Not yet implemented") + } + })*/ + + var customViewPagerAdapter2 = CustomViewPagerAdapter() + customViewPagerAdapter2.itemsData = items2 + mCustomViewPager2.adapter = customViewPagerAdapter2 + mCustomViewPager2.isCutOff = true + + var customViewPagerAdapter3 = CustomViewPagerAdapter() + customViewPagerAdapter3.itemsData = items3 + mViewPager.adapter = customViewPagerAdapter3 + + var customViewPagerAdapter4 = CustomViewPagerAdapter() + customViewPagerAdapter4.itemsData = items3 + setPagerAdapter(mAutoViewPager) + + var customViewPagerAdapter5 = CustomViewPagerAdapter() + customViewPagerAdapter5.itemsData = items3 +// mViewPager2.adapter = customViewPagerAdapter5 + + /*mPagerAdapter = object : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder { + var itemView = LayoutInflater.from(this@CustomViewPagerActivity) + .inflate(R.layout.layout_view_page, parent, false) + val pageViewHolder = PageViewHolder(itemView) + return pageViewHolder + } + + override fun onBindViewHolder(holder: MainActivity.PageViewHolder, position: Int) { + holder.textView.setText(items3.get(position)) + } + + override fun getItemCount(): Int { + return items3.size + } + }*/ + +// mViewPager.setVelocityLimit(3000) +// disableLRScroll() + + } + + private fun setPagerAdapter(viewPager: AutoViewPager) { + viewPager.setItemView { container, position -> + val imageView = ImageView(this@CustomViewPagerActivity) + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP) + imageView.setImageResource(items[position]) + container.addView(imageView) + imageView + } + viewPager.setAdapter(items.size) + } + + inner class CustomViewPagerAdapter : PagerAdapter() { + + var itemsData: List? = null + + override fun getCount(): Int { + return itemsData?.size ?: 0 + } + + override fun isViewFromObject(view: View, any: Any): Boolean { + return view === any + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + + var linearLayout = LinearLayout(baseContext) + var linearParams = ViewGroup.MarginLayoutParams( + 230, + 180 + ) + linearLayout.setBackgroundColor(baseContext.getColor(R.color.colorPrimaryDark)) + + var text_view: TextView = TextView(baseContext) + var params = LinearLayout.LayoutParams( + 200, + 150 + ) + text_view.text = itemsData?.get(position) ?: "没有内容" + text_view.setTextColor(baseContext.getColor(R.color.colorAccent)) + text_view.setBackgroundColor(baseContext.getColor(R.color.colorBlack)) + text_view.gravity = Gravity.CENTER + params.gravity = Gravity.CENTER + + linearLayout.addView(text_view, params) + + container.addView(linearLayout, linearParams) + return linearLayout + } + + override fun destroyItem(container: ViewGroup, position: Int, any: Any) { + container.removeView(any as View) + } + + override fun getPageWidth(position: Int): Float { + return 0.4F + } + } + + class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var textView = itemView.findViewById(R.id.page_text) + } + + fun disableLRScroll() { + try { + var leftEdgeField: Field? = mCustomViewPager.javaClass.getDeclaredField("mLeftEdge"); + var rightEdgeField: Field? = mCustomViewPager.javaClass.getDeclaredField("mRightEdge"); + if (leftEdgeField != null && rightEdgeField != null) { + leftEdgeField.setAccessible(true) + rightEdgeField.setAccessible(true) + mLeftEdge = leftEdgeField.get(mCustomViewPager) as EdgeEffectCompat + mRightEdge = rightEdgeField.get(mCustomViewPager) as EdgeEffectCompat + } + } catch (e: Exception) { + Log.d(TAG, "出现异常:${e.toString()}") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/guiguzi/androidtest/activity/DialogActivity.kt b/app/src/main/java/com/guiguzi/androidtest/activity/DialogActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..87af83def94d1ebca56b52eca887294a85867ff8 --- /dev/null +++ b/app/src/main/java/com/guiguzi/androidtest/activity/DialogActivity.kt @@ -0,0 +1,48 @@ +package com.guiguzi.androidtest.activity + +import android.os.Bundle +import android.view.Gravity +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import com.guiguzi.androidtest.dialog.CustomDialog + +class DialogActivity : AppCompatActivity() { + + lateinit var mRootView: LinearLayout + lateinit var mButton1: Button + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initView() + } + + fun initView() { + mButton1 = Button(baseContext) + mRootView = LinearLayout(baseContext) + mRootView.orientation = LinearLayout.VERTICAL + + var linearParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + linearParams.gravity = Gravity.CENTER + mButton1.text = "Dialog" + mRootView.addView(mButton1, linearParams) + + var matchParams = ViewGroup.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + + setContentView(mRootView, matchParams) + + mButton1.setOnClickListener { + val dialog: CustomDialog = CustomDialog(this) + dialog.show() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/guiguzi/androidtest/activity/EActivity.kt b/app/src/main/java/com/guiguzi/androidtest/activity/EActivity.kt index e862ab65e2146c0fc06f90197f28ea89f321de15..b267563a537488b4980c9ab3012457c38716ee8e 100644 --- a/app/src/main/java/com/guiguzi/androidtest/activity/EActivity.kt +++ b/app/src/main/java/com/guiguzi/androidtest/activity/EActivity.kt @@ -24,7 +24,8 @@ class EActivity : Activity() { var textView = TextView(this) textView.text = "hello world,EEEEEEEEEEE" textView.setOnClickListener { - var intent = Intent(this, BActivity::class.java) +// var intent = Intent(this, BActivity::class.java) + var intent = Intent(this, FActivity::class.java) startActivity(intent) } diff --git a/app/src/main/java/com/guiguzi/androidtest/activity/ErrorAActivity.kt b/app/src/main/java/com/guiguzi/androidtest/activity/ErrorAActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..a8aca397e1370870d8f506970ce6cc98d5bb72f6 --- /dev/null +++ b/app/src/main/java/com/guiguzi/androidtest/activity/ErrorAActivity.kt @@ -0,0 +1,41 @@ +package com.guiguzi.androidtest.activity + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Button +import android.widget.TextView +import com.guiguzi.androidtest.R + +class ErrorAActivity : Activity() { + companion object { + const val TAG = "ErrorTestTAG" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_error_a) + initView() + } + + fun initView() { + + findViewById(R.id.title).setText("页面AAAAAAAAAAAAAAA") + + findViewById