diff --git a/README.md b/README.md index 7c76f42ef..bd9839bc5 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - [Credits & Third party components and their licenses](#credits--third-party-components-and-their-licenses) ## Introduction -PojavLauncher is a Minecraft: Java Edition launcher for Android and iOS based on [Boardwalk](https://github.com/zhuowei/Boardwalk). This launcher can launch almost all available Minecraft versions (from rd-132211 to 1.18 snapshots (kinda), including Combat Test versions. Modding via Forge and Fabric are also supported. This repository contains source code for Android. For iOS/iPadOS, check out [PojavLauncher_iOS](https://github.com/PojavLauncherTeam/PojavLauncher_iOS). +PojavLauncher is a Minecraft: Java Edition launcher for Android and iOS based on [Boardwalk](https://github.com/zhuowei/Boardwalk). This launcher can launch almost all available Minecraft versions (from rd-132211 to 1.18 snapshots (kinda)), including Combat Test versions. Modding via Forge (up to 1.16.5) and Fabric are also supported. This repository contains source code for Android. For iOS/iPadOS, check out [PojavLauncher_iOS](https://github.com/PojavLauncherTeam/PojavLauncher_iOS). ## Building To get started, you can just get prebuilt app from [stable release](https://github.com/PojavLauncherTeam/PojavLauncher/releases) or [automatic builds](https://github.com/PojavLauncherTeam/PojavLauncher/actions). If you want to build after launcher code changes, follow steps below. @@ -80,13 +80,13 @@ cp jre_lwjgl3glfw/build/libs/jre_lwjgl3glfw-3.2.3.jar app_pojavlauncher/src/main - [ ] More... ## Known Issues -- Minecraft `21w10a` or newer are currently not yet supported due to the new GLSL usage. -- In 1.16 and up spawn eggs banners are white (you can fix this by switching renderer -to `gl4es 1.1.5`, only works on 1.16 and up, do not use under this version) -- Controller mods aren't working +- Minecraft `21w10a` or newer are currently not yet supported due to the new GLSL usage. Fortunately, a workaround is provided and built into the launcher. +- In 1.16 and up, spawn eggs banners are white (you can fix this by switching renderer +to `gl4es 1.1.5`, only works on 1.16 and up, do not use under this version, as the texture +will bug out when hit a mob) +- Controller mods aren't working. - Random crashes could happen very often on Android 5.x during game load or join world. - With big modpacks textures could be messed up -- If you're using gl4es 1.1.5 on 1.16 and lower texture will bug out when hit a mob - probably more, that's why we have a bug tracker ;) ## License @@ -96,7 +96,7 @@ to `gl4es 1.1.5`, only works on 1.16 and up, do not use under this version) Contributions are welcome! We welcome any type of contribution, not only code. Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. -## Credits & Third party components and their licenses +## Credits & Third party components and their licenses (if available) - [Boardwalk](https://github.com/zhuowei/Boardwalk) (JVM Launcher): Unknown License/[Apache License 2.0](https://github.com/zhuowei/Boardwalk/blob/master/LICENSE) or GNU GPLv2. - Android Support Libraries: [Apache License 2.0](https://android.googlesource.com/platform/prebuilts/maven_repo/android/+/master/NOTICE.txt). - [GL4ES](https://github.com/PojavLauncherTeam/gl4es): [MIT License](https://github.com/ptitSeb/gl4es/blob/master/LICENSE).
@@ -108,4 +108,3 @@ Any code change should be submitted as a pull request. The description should ex - [xHook](https://github.com/iqiyi/xHook) (Used for exit code trapping): [MIT and BSD-style licenses](https://github.com/iqiyi/xHook/blob/master/LICENSE). - [libepoxy](https://github.com/anholt/libepoxy): [MIT License](https://github.com/anholt/libepoxy/blob/master/COPYING). - [virglrenderer](https://github.com/PojavLauncherTeam/virglrenderer): [MIT License](https://gitlab.freedesktop.org/virgl/virglrenderer/-/blob/master/COPYING). - diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index 51fb8d6fe..1c491df1c 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -118,13 +118,13 @@ dependencies { // implementation 'com.wu-man:android-bsf-api:3.1.3' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.preference:preference:1.1.1' - implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.annotation:annotation:1.2.0' implementation 'androidx.browser:browser:1.3.0' - implementation "androidx.constraintlayout:constraintlayout:2.0.4" + implementation "androidx.constraintlayout:constraintlayout:2.1.1" implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:3.0.1' implementation 'com.github.duanhong169:checkerboarddrawable:1.0.2' diff --git a/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar b/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar index 3ae78b0a7..3c4a31e2b 100644 Binary files a/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar and b/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar differ diff --git a/app_pojavlauncher/src/main/java/android/support/design/widget/VerticalTabLayout.java b/app_pojavlauncher/src/main/java/android/support/design/widget/VerticalTabLayout.java deleted file mode 100644 index 24cbe89c9..000000000 --- a/app_pojavlauncher/src/main/java/android/support/design/widget/VerticalTabLayout.java +++ /dev/null @@ -1,2329 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.support.design.widget; - -import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; -import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING; -import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE; -import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.ColorStateList; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.os.Build; -import androidx.annotation.ColorInt; -import androidx.annotation.DrawableRes; -import androidx.annotation.IntDef; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; -import androidx.annotation.StringRes; -import com.google.android.material.R; - -import androidx.core.util.Pools; -import androidx.core.view.GravityCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; -import androidx.viewpager.widget.PagerAdapter; -import androidx.core.view.PointerIconCompat; -import androidx.core.view.ViewCompat; -import androidx.viewpager.widget.ViewPager; -import androidx.core.widget.TextViewCompat; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.appcompat.widget.TooltipCompat; -import android.text.Layout; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.SoundEffectConstants; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.*; -import androidx.core.app.*; - -import com.google.android.material.animation.AnimationUtils; -import com.google.android.material.tabs.TabItem; -import com.google.android.material.tabs.TabLayout; - -/** - * VerticalTabLayout provides a vertical layout to display tabs. - * - *

Population of the tabs to display is - * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can - * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)} - * respectively. To display the tab, you need to add it to the layout via one of the - * {@link #addTab(Tab)} methods. For example: - *

- * VerticalTabLayout tabLayout = ...;
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
- * 
- * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be - * notified when any tab's selection state has been changed. - * - *

You can also add items to VerticalTabLayout in your layout through the use of {@link TabItem}. - * An example usage is like so:

- * - *
- * <android.support.design.widget.TabLayout
- *         android:layout_height="wrap_content"
- *         android:layout_width="match_parent">
- *
- *     <android.support.design.widget.TabItem
- *             android:text="@string/tab_text"/>
- *
- *     <android.support.design.widget.TabItem
- *             android:icon="@drawable/ic_android"/>
- *
- * </android.support.design.widget.TabLayout>
- * 
- * - *

ViewPager integration

- *

- * If you're using a {@link ViewPager} together - * with this layout, you can call {@link #setupWithViewPager(ViewPager)} to link the two together. - * This layout will be automatically populated from the {@link PagerAdapter}'s page titles.

- * - *

- * This view also supports being used as part of a ViewPager's decor, and can be added - * directly to the ViewPager in a layout resource file like so:

- * - *
- * <android.support.v4.view.ViewPager
- *     android:layout_width="match_parent"
- *     android:layout_height="match_parent">
- *
- *     <android.support.design.widget.TabLayout
- *         android:layout_width="match_parent"
- *         android:layout_height="wrap_content"
- *         android:layout_gravity="top" />
- *
- * </android.support.v4.view.ViewPager>
- * 
- * - * @see Tabs - * - * @attr ref android.support.design.R.styleable#TabLayout_tabPadding - * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingStart - * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingTop - * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingEnd - * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingBottom - * @attr ref android.support.design.R.styleable#TabLayout_tabContentStart - * @attr ref android.support.design.R.styleable#TabLayout_tabBackground - * @attr ref android.support.design.R.styleable#TabLayout_tabMinWidth - * @attr ref android.support.design.R.styleable#TabLayout_tabMaxWidth - * @attr ref android.support.design.R.styleable#TabLayout_tabTextAppearance - */ -@ViewPager.DecorView -public class VerticalTabLayout extends LinearLayout { - - private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps - static final int DEFAULT_GAP_TEXT_ICON = 8; // dps - private static final int INVALID_WIDTH = -1; - private static final int DEFAULT_HEIGHT = 48; // dps - private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps - static final int FIXED_WRAP_GUTTER_MIN = 16; //dps - static final int MOTION_NON_ADJACENT_OFFSET = 24; - - private static final int ANIMATION_DURATION = 300; - - private static final Pools.Pool sTabPool = new Pools.SynchronizedPool<>(16); - - /** - * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab - * labels and a larger number of tabs. They are best used for browsing contexts in touch - * interfaces when users don’t need to directly compare the tab labels. - * - * @see #setTabMode(int) - * @see #getTabMode() - */ - public static final int MODE_SCROLLABLE = 0; - - /** - * Fixed tabs display all tabs concurrently and are best used with content that benefits from - * quick pivots between tabs. The maximum number of tabs is limited by the view’s width. - * Fixed tabs have equal width, based on the widest tab label. - * - * @see #setTabMode(int) - * @see #getTabMode() - */ - public static final int MODE_FIXED = 1; - - /** - * @hide - */ - @RestrictTo(LIBRARY_GROUP) - @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED}) - @Retention(RetentionPolicy.SOURCE) - public @interface Mode {} - - /** - * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect - * when used with {@link #MODE_FIXED}. - * - * @see #setTabGravity(int) - * @see #getTabGravity() - */ - public static final int GRAVITY_FILL = 0; - - /** - * Gravity used to lay out the tabs in the center of the {@link TabLayout}. - * - * @see #setTabGravity(int) - * @see #getTabGravity() - */ - public static final int GRAVITY_CENTER = 1; - - /** - * @hide - */ - @RestrictTo(LIBRARY_GROUP) - @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER}) - @Retention(RetentionPolicy.SOURCE) - public @interface TabGravity {} - - /** - * Callback interface invoked when a tab's selection state changes. - */ - public interface OnTabSelectedListener { - - /** - * Called when a tab enters the selected state. - * - * @param tab The tab that was selected - */ - public void onTabSelected(Tab tab); - - /** - * Called when a tab exits the selected state. - * - * @param tab The tab that was unselected - */ - public void onTabUnselected(Tab tab); - - /** - * Called when a tab that is already selected is chosen again by the user. Some applications - * may use this action to return to the top level of a category. - * - * @param tab The tab that was reselected. - */ - public void onTabReselected(Tab tab); - } - - private final ArrayList mTabs = new ArrayList<>(); - private Tab mSelectedTab; - - private final ScrollView mTopScrollView; - private final SlidingTabStrip mTabStrip; - - int mTabPaddingStart; - int mTabPaddingTop; - int mTabPaddingEnd; - int mTabPaddingBottom; - - int mTabTextAppearance; - ColorStateList mTabTextColors; - float mTabTextSize; - float mTabTextMultiLineSize; - - final int mTabBackgroundResId; - - int mTabMaxWidth = Integer.MAX_VALUE; - private final int mRequestedTabMinWidth; - private final int mRequestedTabMaxWidth; - private final int mScrollableTabMinWidth; - - private int mContentInsetStart; - - int mTabGravity; - int mMode; - - private OnTabSelectedListener mSelectedListener; - private final ArrayList mSelectedListeners = new ArrayList<>(); - private OnTabSelectedListener mCurrentVpSelectedListener; - - private ValueAnimator mScrollAnimator; - - ViewPager mViewPager; - private ViewPagerAdapter mPagerAdapter; - private DataSetObserver mPagerAdapterObserver; - private VerticalTabLayoutOnPageChangeListener mPageChangeListener; - private AdapterChangeListener mAdapterChangeListener; - private boolean mSetupViewPagerImplicitly; - - // Pool we use as a simple RecyclerBin - private final Pools.Pool mTabViewPool = new Pools.SimplePool<>(12); - - public VerticalTabLayout(Context context) { - this(context, null); - } - - public VerticalTabLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public VerticalTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - //ndroidx.appcompat.widget.ThemeUtils.checkAppCompatTheme(context); - - setOrientation(VERTICAL); - - // Disable the Scroll Bar - // setVerticalScrollBarEnabled(false); - - LayoutParams scrollViewParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); - scrollViewParams.weight = 1f; - mTopScrollView = new ScrollView(context); - mTopScrollView.setLayoutParams(scrollViewParams); - - // Add the TabStrip - mTabStrip = new SlidingTabStrip(context); - mTopScrollView.addView(mTabStrip, 0, new ScrollView.LayoutParams( - LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); - super.addView(mTopScrollView); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout, - defStyleAttr, R.style.Widget_Design_TabLayout); - - mTabStrip.setSelectedIndicatorHeight( - a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0)); - mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0)); - - mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a - .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0); - mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart, - mTabPaddingStart); - mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop, - mTabPaddingTop); - mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd, - mTabPaddingEnd); - mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom, - mTabPaddingBottom); - - mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance, - R.style.TextAppearance_Design_Tab); - - // Text colors/sizes come from the text appearance first - final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance, - androidx.appcompat.R.styleable.TextAppearance); - try { - mTabTextSize = ta.getDimensionPixelSize( - androidx.appcompat.R.styleable.TextAppearance_android_textSize, 0); - mTabTextColors = ta.getColorStateList( - androidx.appcompat.R.styleable.TextAppearance_android_textColor); - } finally { - ta.recycle(); - } - - if (a.hasValue(R.styleable.TabLayout_tabTextColor)) { - // If we have an explicit text color set, use it instead - mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor); - } - - if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) { - // We have an explicit selected text color set, so we need to make merge it with the - // current colors. This is exposed so that developers can use theme attributes to set - // this (theme attrs in ColorStateLists are Lollipop+) - final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0); - mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected); - } - - mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth, - INVALID_WIDTH); - mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth, - INVALID_WIDTH); - mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0); - mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0); - mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED); - mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL); - a.recycle(); - - // TODO add attr for these - final Resources res = getResources(); - mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line); - mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width); - - // Now apply the tab mode and gravity - applyModeAndGravity(); - } - - /** - * Sets the tab indicator's color for the currently selected tab. - * - * @param color color to use for the indicator - * - * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorColor - */ - public void setSelectedTabIndicatorColor(@ColorInt int color) { - mTabStrip.setSelectedIndicatorColor(color); - } - - /** - * Sets the tab indicator's height for the currently selected tab. - * - * @param height height to use for the indicator in pixels - * - * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorHeight - */ - public void setSelectedTabIndicatorHeight(int height) { - mTabStrip.setSelectedIndicatorHeight(height); - } - - /** - * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as - * part of a scrolling container such as {@link ViewPager}. - *

- * Calling this method does not update the selected tab, it is only used for drawing purposes. - * - * @param position current scroll position - * @param positionOffset Value from [0, 1) indicating the offset from {@code position}. - * @param updateSelectedText Whether to update the text's selected state. - */ - public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) { - setScrollPosition(position, positionOffset, updateSelectedText, true); - } - - void setScrollPosition(int position, float positionOffset, boolean updateSelectedText, - boolean updateIndicatorPosition) { - final int roundedPosition = Math.round(position + positionOffset); - if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) { - return; - } - - // Set the indicator position, if enabled - if (updateIndicatorPosition) { - mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset); - } - - // Now update the scroll position, canceling any running animation - if (mScrollAnimator != null && mScrollAnimator.isRunning()) { - mScrollAnimator.cancel(); - } - mTopScrollView.scrollTo(calculateScrollXForTab(position, positionOffset), 0); - - // Update the 'selected state' view as we scroll, if enabled - if (updateSelectedText) { - setSelectedTabView(roundedPosition); - } - } - - private float getScrollPosition() { - return mTabStrip.getIndicatorPosition(); - } - - public void setLastTabAsBottom() { - final int position = mTabs.size() - 1; - final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; - final TabView view = (TabView) mTabStrip.getChildAt(position); - view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - mTabStrip.removeViewAt(position); - requestLayout(); - - mTabs.remove(position); - super.addView(view); - - final int newTabCount = mTabs.size(); - for (int i = position; i < newTabCount; i++) { - mTabs.get(i).setPosition(i); - } - - if (selectedTabPosition == position) { - selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); - } - } - - /** - * Add a tab to this layout. The tab will be added at the end of the list. - * If this is the first tab to be added it will become the selected tab. - * - * @param tab Tab to add - */ - public void addTab(@NonNull Tab tab) { - addTab(tab, mTabs.isEmpty()); - } - - /** - * Add a tab to this layout. The tab will be inserted at position. - * If this is the first tab to be added it will become the selected tab. - * - * @param tab The tab to add - * @param position The new position of the tab - */ - public void addTab(@NonNull Tab tab, int position) { - addTab(tab, position, mTabs.isEmpty()); - } - - /** - * Add a tab to this layout. The tab will be added at the end of the list. - * - * @param tab Tab to add - * @param setSelected True if the added tab should become the selected tab. - */ - public void addTab(@NonNull Tab tab, boolean setSelected) { - addTab(tab, mTabs.size(), setSelected); - } - - /** - * Add a tab to this layout. The tab will be inserted at position. - * - * @param tab The tab to add - * @param position The new position of the tab - * @param setSelected True if the added tab should become the selected tab. - */ - public void addTab(@NonNull Tab tab, int position, boolean setSelected) { - if (tab.mParent != this) { - throw new IllegalArgumentException("Tab belongs to a different TabLayout."); - } - configureTab(tab, position); - addTabView(tab); - - if (setSelected) { - tab.select(); - } - } - - private void addTabFromItemView(@NonNull TabItem item) { - - final Tab tab = newTab(); - if (item.text != null) { - tab.setText(item.text); - } - if (item.icon != null) { - tab.setIcon(item.icon); - } - if (item.customLayout != 0) { - tab.setCustomView(item.customLayout); - } - if (!TextUtils.isEmpty(item.getContentDescription())) { - tab.setContentDescription(item.getContentDescription()); - } - addTab(tab); - } - - /** - * @deprecated Use {@link #addOnTabSelectedListener(OnTabSelectedListener)} and - * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}. - */ - @Deprecated - public void setOnTabSelectedListener(@Nullable OnTabSelectedListener listener) { - // The logic in this method emulates what we had before support for multiple - // registered listeners. - if (mSelectedListener != null) { - removeOnTabSelectedListener(mSelectedListener); - } - // Update the deprecated field so that we can remove the passed listener the next - // time we're called - mSelectedListener = listener; - if (listener != null) { - addOnTabSelectedListener(listener); - } - } - - /** - * Add a {@link TabLayout.OnTabSelectedListener} that will be invoked when tab selection - * changes. - * - *

Components that add a listener should take care to remove it when finished via - * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.

- * - * @param listener listener to add - */ - public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { - if (!mSelectedListeners.contains(listener)) { - mSelectedListeners.add(listener); - } - } - - /** - * Remove the given {@link TabLayout.OnTabSelectedListener} that was previously added via - * {@link #addOnTabSelectedListener(OnTabSelectedListener)}. - * - * @param listener listener to remove - */ - public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) { - mSelectedListeners.remove(listener); - } - - /** - * Remove all previously added {@link TabLayout.OnTabSelectedListener}s. - */ - public void clearOnTabSelectedListeners() { - mSelectedListeners.clear(); - } - - /** - * Create and return a new {@link Tab}. You need to manually add this using - * {@link #addTab(Tab)} or a related method. - * - * @return A new Tab - * @see #addTab(Tab) - */ - @NonNull - public Tab newTab() { - Tab tab = sTabPool.acquire(); - if (tab == null) { - tab = new Tab(); - } - tab.mParent = this; - tab.mView = createTabView(tab); - return tab; - } - - /** - * Returns the number of tabs currently registered with the action bar. - * - * @return Tab count - */ - public int getTabCount() { - return mTabs.size(); - } - - /** - * Returns the tab at the specified index. - */ - @Nullable - public Tab getTabAt(int index) { - return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index); - } - - /** - * Returns the position of the current selected tab. - * - * @return selected tab position, or {@code -1} if there isn't a selected tab. - */ - public int getSelectedTabPosition() { - return mSelectedTab != null ? mSelectedTab.getPosition() : -1; - } - - /** - * Remove a tab from the layout. If the removed tab was selected it will be deselected - * and another tab will be selected if present. - * - * @param tab The tab to remove - */ - public void removeTab(Tab tab) { - if (tab.mParent != this) { - throw new IllegalArgumentException("Tab does not belong to this TabLayout."); - } - - removeTabAt(tab.getPosition()); - } - - /** - * Remove a tab from the layout. If the removed tab was selected it will be deselected - * and another tab will be selected if present. - * - * @param position Position of the tab to remove - */ - public void removeTabAt(int position) { - final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0; - removeTabViewAt(position); - - final Tab removedTab = mTabs.remove(position); - if (removedTab != null) { - removedTab.reset(); - sTabPool.release(removedTab); - } - - final int newTabCount = mTabs.size(); - for (int i = position; i < newTabCount; i++) { - mTabs.get(i).setPosition(i); - } - - if (selectedTabPosition == position) { - selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1))); - } - } - - /** - * Remove all tabs from the action bar and deselect the current tab. - */ - public void removeAllTabs() { - // Remove all the views - for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) { - removeTabViewAt(i); - } - - for (final Iterator i = mTabs.iterator(); i.hasNext();) { - final Tab tab = i.next(); - i.remove(); - tab.reset(); - sTabPool.release(tab); - } - - mSelectedTab = null; - } - - /** - * Set the behavior mode for the Tabs in this layout. The valid input options are: - *
    - *
  • {@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used - * with content that benefits from quick pivots between tabs.
  • - *
  • {@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment, - * and can contain longer tab labels and a larger number of tabs. They are best used for - * browsing contexts in touch interfaces when users don’t need to directly compare the tab - * labels. This mode is commonly used with a {@link ViewPager}.
  • - *
- * - * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}. - * - * @attr ref android.support.design.R.styleable#TabLayout_tabMode - */ - public void setTabMode(@Mode int mode) { - if (mode != mMode) { - mMode = mode; - applyModeAndGravity(); - } - } - - /** - * Returns the current mode used by this {@link TabLayout}. - * - * @see #setTabMode(int) - */ - @Mode - public int getTabMode() { - return mMode; - } - - /** - * Set the gravity to use when laying out the tabs. - * - * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. - * - * @attr ref android.support.design.R.styleable#TabLayout_tabGravity - */ - public void setTabGravity(@TabGravity int gravity) { - if (mTabGravity != gravity) { - mTabGravity = gravity; - applyModeAndGravity(); - } - } - - /** - * The current gravity used for laying out tabs. - * - * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}. - */ - @TabGravity - public int getTabGravity() { - return mTabGravity; - } - - /** - * Sets the text colors for the different states (normal, selected) used for the tabs. - * - * @see #getTabTextColors() - */ - public void setTabTextColors(@Nullable ColorStateList textColor) { - if (mTabTextColors != textColor) { - mTabTextColors = textColor; - updateAllTabs(); - } - } - - /** - * Gets the text colors for the different states (normal, selected) used for the tabs. - */ - @Nullable - public ColorStateList getTabTextColors() { - return mTabTextColors; - } - - /** - * Sets the text colors for the different states (normal, selected) used for the tabs. - * - * @attr ref android.support.design.R.styleable#TabLayout_tabTextColor - * @attr ref android.support.design.R.styleable#TabLayout_tabSelectedTextColor - */ - public void setTabTextColors(int normalColor, int selectedColor) { - setTabTextColors(createColorStateList(normalColor, selectedColor)); - } - - /** - * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}. - * - *

This is the same as calling {@link #setupWithViewPager(ViewPager, boolean)} with - * auto-refresh enabled.

- * - * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link - */ - public void setupWithViewPager(@Nullable ViewPager viewPager) { - setupWithViewPager(viewPager, true); - } - - /** - * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}. - * - *

This method will link the given ViewPager and this VerticalTabLayout together so that - * changes in one are automatically reflected in the other. This includes scroll state changes - * and clicks. The tabs displayed in this layout will be populated - * from the ViewPager adapter's page titles.

- * - *

If {@code autoRefresh} is {@code true}, any changes in the {@link PagerAdapter} will - * trigger this layout to re-populate itself from the adapter's titles.

- * - *

If the given ViewPager is non-null, it needs to already have a - * {@link PagerAdapter} set.

- * - * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link - * @param autoRefresh whether this layout should refresh its contents if the given ViewPager's - * content changes - */ - public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) { - setupWithViewPager(viewPager, autoRefresh, false); - } - - private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh, - boolean implicitSetup) { - if (mViewPager != null) { - // If we've already been setup with a ViewPager, remove us from it - if (mPageChangeListener != null) { - mViewPager.removeOnPageChangeListener(mPageChangeListener); - } - if (mAdapterChangeListener != null) { - mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener); - } - } - - if (mCurrentVpSelectedListener != null) { - // If we already have a tab selected listener for the ViewPager, remove it - removeOnTabSelectedListener(mCurrentVpSelectedListener); - mCurrentVpSelectedListener = null; - } - - if (viewPager != null) { - mViewPager = viewPager; - - // Add our custom OnPageChangeListener to the ViewPager - if (mPageChangeListener == null) { - mPageChangeListener = new VerticalTabLayoutOnPageChangeListener(this); - } - mPageChangeListener.reset(); - viewPager.addOnPageChangeListener(mPageChangeListener); - - // Now we'll add a tab selected listener to set ViewPager's current item - mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager); - addOnTabSelectedListener(mCurrentVpSelectedListener); - - final ViewPagerAdapter adapter = (VerticalTabLayout.ViewPagerAdapter) viewPager.getAdapter(); - if (adapter != null) { - // Now we'll populate ourselves from the pager adapter, adding an observer if - // autoRefresh is enabled - setPagerAdapter(adapter, autoRefresh); - } - - // Add a listener so that we're notified of any adapter changes - if (mAdapterChangeListener == null) { - mAdapterChangeListener = new AdapterChangeListener(); - } - mAdapterChangeListener.setAutoRefresh(autoRefresh); - viewPager.addOnAdapterChangeListener(mAdapterChangeListener); - - // Now update the scroll position to match the ViewPager's current item - setScrollPosition(viewPager.getCurrentItem(), 0f, true); - } else { - // We've been given a null ViewPager so we need to clear out the internal state, - // listeners and observers - mViewPager = null; - setPagerAdapter(null, false); - } - - mSetupViewPagerImplicitly = implicitSetup; - } - - /** - * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a VerticalTabLayout with a ViewPager - * together. When that method is used, the VerticalTabLayout will be automatically updated - * when the {@link PagerAdapter} is changed. - */ - @Deprecated - public void setTabsFromPagerAdapter(@Nullable final ViewPagerAdapter adapter) { - setPagerAdapter(adapter, false); - } - - @Override - public boolean shouldDelayChildPressedState() { - // Only delay the pressed state if the tabs can scroll - return getTabScrollRange() > 0; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - if (mViewPager == null) { - // If we don't have a ViewPager already, check if our parent is a ViewPager to - // setup with it automatically - final ViewParent vp = getParent(); - if (vp instanceof ViewPager) { - // If we have a ViewPager parent and we've been added as part of its decor, let's - // assume that we should automatically setup to display any titles - setupWithViewPager((ViewPager) vp, true, true); - } - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - - if (mSetupViewPagerImplicitly) { - // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc - setupWithViewPager(null); - mSetupViewPagerImplicitly = false; - } - } - - private int getTabScrollRange() { - return Math.max(0, mTabStrip.getWidth() - mTopScrollView.getWidth() - mTopScrollView.getPaddingLeft() - - mTopScrollView.getPaddingRight()); - } - - void setPagerAdapter(@Nullable final ViewPagerAdapter adapter, final boolean addObserver) { - if (mPagerAdapter != null && mPagerAdapterObserver != null) { - // If we already have a PagerAdapter, unregister our observer - mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver); - } - - mPagerAdapter = adapter; - - if (addObserver && adapter != null) { - // Register our observer on the new adapter - if (mPagerAdapterObserver == null) { - mPagerAdapterObserver = new PagerAdapterObserver(); - } - adapter.registerDataSetObserver(mPagerAdapterObserver); - } - - // Finally make sure we reflect the new adapter - populateFromPagerAdapter(); - } - - void populateFromPagerAdapter() { - removeAllTabs(); - - if (mPagerAdapter != null) { - final int adapterCount = mPagerAdapter.getCount(); - for (int i = 0; i < adapterCount; i++) { - addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false); - if (mPagerAdapter.getIcon(i) != 0) { - getTabAt(i).setIcon(mPagerAdapter.getIcon(i)); - } - } - - // Make sure we reflect the currently set ViewPager item - if (mViewPager != null && adapterCount > 0) { - final int curItem = mViewPager.getCurrentItem(); - if (curItem != getSelectedTabPosition() && curItem < getTabCount()) { - selectTab(getTabAt(curItem)); - } - } - } - } - - private void updateAllTabs() { - for (int i = 0, z = mTabs.size(); i < z; i++) { - mTabs.get(i).updateView(); - } - } - - private TabView createTabView(@NonNull final Tab tab) { - TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null; - if (tabView == null) { - tabView = new TabView(getContext()); - } - tabView.setTab(tab); - tabView.setFocusable(true); - tabView.setMinimumWidth(getTabMinWidth()); - return tabView; - } - - private void configureTab(Tab tab, int position) { - tab.setPosition(position); - mTabs.add(position, tab); - - final int count = mTabs.size(); - for (int i = position + 1; i < count; i++) { - mTabs.get(i).setPosition(i); - } - } - - private void addTabView(Tab tab) { - final TabView tabView = tab.mView; - mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs()); - } - - // @Override - public void addViewItem(View child) { - addViewInternal(child); - } - - // @Override - public void addViewItem(View child, int index) { - addViewInternal(child); - } - - // @Override - public void addViewItem(View child, ViewGroup.LayoutParams params) { - addViewInternal(child); - } - - // @Override - public void addViewItem(View child, int index, ViewGroup.LayoutParams params) { - addViewInternal(child); - } - - private void addViewInternal(final View child) { - if (child instanceof TabItem) { - addTabFromItemView((TabItem) child); - } else { - throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout"); - } - } - - private LinearLayout.LayoutParams createLayoutParamsForTabs() { - final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); - updateTabViewLayoutParams(lp); - return lp; - } - - private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) { - if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) { - lp.width = 0; - lp.weight = 1; - } else { - lp.width = LinearLayout.LayoutParams.WRAP_CONTENT; - lp.weight = 0; - } - } - - int dpToPx(int dps) { - return Math.round(getResources().getDisplayMetrics().density * dps); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // If we have a MeasureSpec which allows us to decide our height, try and use the default - // height - final int idealHeight = dpToPx(getDefaultHeight()) + mTopScrollView.getPaddingTop() + mTopScrollView.getPaddingBottom(); - - switch (MeasureSpec.getMode(heightMeasureSpec)) { - case MeasureSpec.AT_MOST: - heightMeasureSpec = MeasureSpec.makeMeasureSpec( - Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)), - MeasureSpec.EXACTLY); - break; - case 0 /* MeasureSpec.UNSPECIFIED */: - heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY); - break; - } - - final int specWidth = MeasureSpec.getSize(widthMeasureSpec); - if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) { - // If we don't have an unspecified width spec, use the given size to calculate - // the max tab width - mTabMaxWidth = mRequestedTabMaxWidth > 0 - ? mRequestedTabMaxWidth - : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN); - } - - // Now super measure itself using the (possibly) modified height spec - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - if (getChildCount() == 1) { - // If we're in fixed mode then we need to make the tab strip is the same width as us - // so we don't scroll - final View child = getChildAt(0); - boolean remeasure = false; - - switch (mMode) { - case MODE_SCROLLABLE: - // We only need to resize the child if it's smaller than us. This is similar - // to fillViewport - remeasure = child.getMeasuredWidth() < mTopScrollView.getMeasuredWidth(); - break; - case MODE_FIXED: - // Resize the child so that it doesn't scroll - remeasure = child.getMeasuredWidth() != mTopScrollView.getMeasuredWidth(); - break; - } - - if (remeasure) { - // Re-measure the child with a widthSpec set to be exactly our measure width - int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mTopScrollView.getPaddingTop() - + mTopScrollView.getPaddingBottom(), child.getLayoutParams().height); - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( - mTopScrollView.getMeasuredWidth(), MeasureSpec.EXACTLY); - child.measure(childWidthMeasureSpec, childHeightMeasureSpec); - } - } - } - - private void removeTabViewAt(int position) { - final TabView view = (TabView) mTabStrip.getChildAt(position); - mTabStrip.removeViewAt(position); - if (view != null) { - view.reset(); - mTabViewPool.release(view); - } - requestLayout(); - } - - private void animateToTab(int newPosition) { - if (newPosition == Tab.INVALID_POSITION) { - return; - } - - if (getWindowToken() == null || !ViewCompat.isLaidOut(this) - || mTabStrip.childrenNeedLayout()) { - // If we don't have a window token, or we haven't been laid out yet just draw the new - // position now - setScrollPosition(newPosition, 0f, true); - return; - } - - final int startScrollX = mTopScrollView.getScrollX(); - final int targetScrollX = calculateScrollXForTab(newPosition, 0); - - if (startScrollX != targetScrollX) { - ensureScrollAnimator(); - - mScrollAnimator.setIntValues(startScrollX, targetScrollX); - mScrollAnimator.start(); - } - - // Now animate the indicator - mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION); - } - - private void ensureScrollAnimator() { - if (mScrollAnimator == null) { - mScrollAnimator = new ValueAnimator(); - mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); - mScrollAnimator.setDuration(ANIMATION_DURATION); - mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animator) { - scrollTo((int) animator.getAnimatedValue(), 0); - } - }); - } - } - - void setScrollAnimatorListener(Animator.AnimatorListener listener) { - ensureScrollAnimator(); - mScrollAnimator.addListener(listener); - } - - private void setSelectedTabView(int position) { - final int tabCount = mTabStrip.getChildCount(); - if (position < tabCount) { - for (int i = 0; i < tabCount; i++) { - final View child = mTabStrip.getChildAt(i); - child.setSelected(i == position); - } - } - } - - void selectTab(Tab tab) { - selectTab(tab, true); - } - - void selectTab(final Tab tab, boolean updateIndicator) { - final Tab currentTab = mSelectedTab; - - if (currentTab == tab) { - if (currentTab != null) { - dispatchTabReselected(tab); - animateToTab(tab.getPosition()); - } - } else { - final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION; - if (updateIndicator) { - if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION) - && newPosition != Tab.INVALID_POSITION) { - // If we don't currently have a tab, just draw the indicator - setScrollPosition(newPosition, 0f, true); - } else { - animateToTab(newPosition); - } - if (newPosition != Tab.INVALID_POSITION) { - setSelectedTabView(newPosition); - } - } - if (currentTab != null) { - dispatchTabUnselected(currentTab); - } - mSelectedTab = tab; - if (tab != null) { - dispatchTabSelected(tab); - } - } - } - - private void dispatchTabSelected(@NonNull final Tab tab) { - for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { - mSelectedListeners.get(i).onTabSelected(tab); - } - } - - private void dispatchTabUnselected(@NonNull final Tab tab) { - for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { - mSelectedListeners.get(i).onTabUnselected(tab); - } - } - - private void dispatchTabReselected(@NonNull final Tab tab) { - for (int i = mSelectedListeners.size() - 1; i >= 0; i--) { - mSelectedListeners.get(i).onTabReselected(tab); - } - } - - private int calculateScrollXForTab(int position, float positionOffset) { - if (mMode == MODE_SCROLLABLE) { - final View selectedChild = mTabStrip.getChildAt(position); - final View nextChild = position + 1 < mTabStrip.getChildCount() - ? mTabStrip.getChildAt(position + 1) - : null; - final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0; - final int nextWidth = nextChild != null ? nextChild.getWidth() : 0; - - // base scroll amount: places center of tab in center of parent - int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (mTopScrollView.getWidth() / 2); - // offset amount: fraction of the distance between centers of tabs - int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset); - - return (ViewCompat.getLayoutDirection(mTopScrollView) == ViewCompat.LAYOUT_DIRECTION_LTR) - ? scrollBase + scrollOffset - : scrollBase - scrollOffset; - } - return 0; - } - - private void applyModeAndGravity() { - int paddingStart = 0; - if (mMode == MODE_SCROLLABLE) { - // If we're scrollable, or fixed at start, inset using padding - paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart); - } - ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0); - - switch (mMode) { - case MODE_FIXED: - mTabStrip.setGravity(Gravity.CENTER_VERTICAL); - break; - case MODE_SCROLLABLE: - mTabStrip.setGravity(GravityCompat.START); - break; - } - - updateTabViews(true); - } - - void updateTabViews(final boolean requestLayout) { - for (int i = 0; i < mTabStrip.getChildCount(); i++) { - View child = mTabStrip.getChildAt(i); - child.setMinimumWidth(getTabMinWidth()); - updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams()); - if (requestLayout) { - child.requestLayout(); - } - } - } - - /** - * A tab in this layout. Instances can be created via {@link #newTab()}. - */ - public static final class Tab { - - /** - * An invalid position for a tab. - * - * @see #getPosition() - */ - public static final int INVALID_POSITION = -1; - - private Object mTag; - private Drawable mIcon; - private CharSequence mText; - private CharSequence mContentDesc; - private int mPosition = INVALID_POSITION; - private View mCustomView; - - VerticalTabLayout mParent; - TabView mView; - - Tab() { - // Private constructor - } - - /** - * @return This Tab's tag object. - */ - @Nullable - public Object getTag() { - return mTag; - } - - /** - * Give this Tab an arbitrary object to hold for later use. - * - * @param tag Object to store - * @return The current instance for call chaining - */ - @NonNull - public Tab setTag(@Nullable Object tag) { - mTag = tag; - return this; - } - - - /** - * Returns the custom view used for this tab. - * - * @see #setCustomView(View) - * @see #setCustomView(int) - */ - @Nullable - public View getCustomView() { - return mCustomView; - } - - /** - * Set a custom view to be used for this tab. - *

- * If the provided view contains a {@link TextView} with an ID of - * {@link android.R.id#text1} then that will be updated with the value given - * to {@link #setText(CharSequence)}. Similarly, if this layout contains an - * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with - * the value given to {@link #setIcon(Drawable)}. - *

- * - * @param view Custom view to be used as a tab. - * @return The current instance for call chaining - */ - @NonNull - public Tab setCustomView(@Nullable View view) { - mCustomView = view; - updateView(); - return this; - } - - /** - * Set a custom view to be used for this tab. - *

- * If the inflated layout contains a {@link TextView} with an ID of - * {@link android.R.id#text1} then that will be updated with the value given - * to {@link #setText(CharSequence)}. Similarly, if this layout contains an - * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with - * the value given to {@link #setIcon(Drawable)}. - *

- * - * @param resId A layout resource to inflate and use as a custom tab view - * @return The current instance for call chaining - */ - @NonNull - public Tab setCustomView(@LayoutRes int resId) { - final LayoutInflater inflater = LayoutInflater.from(mView.getContext()); - return setCustomView(inflater.inflate(resId, mView, false)); - } - - /** - * Return the icon associated with this tab. - * - * @return The tab's icon - */ - @Nullable - public Drawable getIcon() { - return mIcon; - } - - /** - * Return the current position of this tab in the action bar. - * - * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in - * the action bar. - */ - public int getPosition() { - return mPosition; - } - - void setPosition(int position) { - mPosition = position; - } - - /** - * Return the text of this tab. - * - * @return The tab's text - */ - @Nullable - public CharSequence getText() { - return mText; - } - - /** - * Set the icon displayed on this tab. - * - * @param icon The drawable to use as an icon - * @return The current instance for call chaining - */ - @NonNull - public Tab setIcon(@Nullable Drawable icon) { - mIcon = icon; - updateView(); - return this; - } - - /** - * Set the icon displayed on this tab. - * - * @param resId A resource ID referring to the icon that should be displayed - * @return The current instance for call chaining - */ - @NonNull - public Tab setIcon(@DrawableRes int resId) { - if (mParent == null) { - throw new IllegalArgumentException("Tab not attached to a TabLayout"); - } - return setIcon(resId == 0 ? null : AppCompatResources.getDrawable(mParent.getContext(), resId)); - } - - /** - * Set the text displayed on this tab. Text may be truncated if there is not room to display - * the entire string. - * - * @param text The text to display - * @return The current instance for call chaining - */ - @NonNull - public Tab setText(@Nullable CharSequence text) { - mText = text; - updateView(); - return this; - } - - /** - * Set the text displayed on this tab. Text may be truncated if there is not room to display - * the entire string. - * - * @param resId A resource ID referring to the text that should be displayed - * @return The current instance for call chaining - */ - @NonNull - public Tab setText(@StringRes int resId) { - if (mParent == null) { - throw new IllegalArgumentException("Tab not attached to a TabLayout"); - } - return setText(mParent.getResources().getText(resId)); - } - - /** - * Select this tab. Only valid if the tab has been added to the action bar. - */ - public void select() { - if (mParent == null) { - throw new IllegalArgumentException("Tab not attached to a TabLayout"); - } - mParent.selectTab(this); - } - - /** - * Returns true if this tab is currently selected. - */ - public boolean isSelected() { - if (mParent == null) { - throw new IllegalArgumentException("Tab not attached to a TabLayout"); - } - return mParent.getSelectedTabPosition() == mPosition; - } - - /** - * Set a description of this tab's content for use in accessibility support. If no content - * description is provided the title will be used. - * - * @param resId A resource ID referring to the description text - * @return The current instance for call chaining - * @see #setContentDescription(CharSequence) - * @see #getContentDescription() - */ - @NonNull - public Tab setContentDescription(@StringRes int resId) { - if (mParent == null) { - throw new IllegalArgumentException("Tab not attached to a TabLayout"); - } - return setContentDescription(mParent.getResources().getText(resId)); - } - - /** - * Set a description of this tab's content for use in accessibility support. If no content - * description is provided the title will be used. - * - * @param contentDesc Description of this tab's content - * @return The current instance for call chaining - * @see #setContentDescription(int) - * @see #getContentDescription() - */ - @NonNull - public Tab setContentDescription(@Nullable CharSequence contentDesc) { - mContentDesc = contentDesc; - updateView(); - return this; - } - - /** - * Gets a brief description of this tab's content for use in accessibility support. - * - * @return Description of this tab's content - * @see #setContentDescription(CharSequence) - * @see #setContentDescription(int) - */ - @Nullable - public CharSequence getContentDescription() { - return mContentDesc; - } - - void updateView() { - if (mView != null) { - mView.update(); - } - } - - void reset() { - mParent = null; - mView = null; - mTag = null; - mIcon = null; - mText = null; - mContentDesc = null; - mPosition = INVALID_POSITION; - mCustomView = null; - } - } - - class TabView extends LinearLayout { - private Tab mTab; - private TextView mTextView; - private ImageView mIconView; - - private View mCustomView; - private TextView mCustomTextView; - private ImageView mCustomIconView; - - private int mDefaultMaxLines = 2; - - public TabView(Context context) { - super(context); - if (mTabBackgroundResId != 0) { - ViewCompat.setBackground( - this, AppCompatResources.getDrawable(context, mTabBackgroundResId)); - } - ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop, - mTabPaddingEnd, mTabPaddingBottom); - setGravity(Gravity.CENTER_VERTICAL); - setOrientation(HORIZONTAL); - setClickable(true); - ViewCompat.setPointerIcon(this, - PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND)); - } - - @Override - public boolean performClick() { - final boolean handled = super.performClick(); - - if (mTab != null) { - if (!handled) { - playSoundEffect(SoundEffectConstants.CLICK); - } - mTab.select(); - return true; - } else { - return handled; - } - } - - @Override - public void setSelected(final boolean selected) { - final boolean changed = isSelected() != selected; - - super.setSelected(selected); - - if (changed && selected && Build.VERSION.SDK_INT < 16) { - // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } - - // Always dispatch this to the child views, regardless of whether the value has - // changed - if (mTextView != null) { - mTextView.setSelected(selected); - } - if (mIconView != null) { - mIconView.setSelected(selected); - } - if (mCustomView != null) { - mCustomView.setSelected(selected); - } - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - // This view masquerades as an action bar tab. - event.setClassName(ActionBar.Tab.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - // This view masquerades as an action bar tab. - info.setClassName(ActionBar.Tab.class.getName()); - } - - @Override - public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) { - final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec); - final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec); - final int maxWidth = getTabMaxWidth(); - - final int widthMeasureSpec; - final int heightMeasureSpec = origHeightMeasureSpec; - - if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED - || specWidthSize > maxWidth)) { - // If we have a max width and a given spec which is either unspecified or - // larger than the max width, update the width spec using the same mode - widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST); - } else { - // Else, use the original width spec - widthMeasureSpec = origWidthMeasureSpec; - } - - // Now lets measure - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - // We need to switch the text size based on whether the text is spanning 2 lines or not - if (mTextView != null) { - // final Resources res = getResources(); - float textSize = mTabTextSize; - int maxLines = mDefaultMaxLines; - - if (mIconView != null && mIconView.getVisibility() == VISIBLE) { - // If the icon view is being displayed, we limit the text to 1 line - maxLines = 2; - } else if (mTextView != null && mTextView.getLineCount() > 1) { - // Otherwise when we have text which wraps we reduce the text size - textSize = mTabTextMultiLineSize; - } - - final float curTextSize = mTextView.getTextSize(); - final int curLineCount = mTextView.getLineCount(); - final int curMaxLines = TextViewCompat.getMaxLines(mTextView); - - if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) { - // We've got a new text size and/or max lines... - boolean updateTextView = true; - - if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) { - // If we're in fixed mode, going up in text size and currently have 1 line - // then it's very easy to get into an infinite recursion. - // To combat that we check to see if the change in text size - // will cause a line count change. If so, abort the size change and stick - // to the smaller size. - final Layout layout = mTextView.getLayout(); - if (layout == null || approximateLineWidth(layout, 0, textSize) - > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) { - updateTextView = false; - } - } - - if (updateTextView) { - mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); - mTextView.setMaxLines(maxLines); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - } - } - } - - void setTab(@Nullable final Tab tab) { - if (tab != mTab) { - mTab = tab; - update(); - } - } - - void reset() { - setTab(null); - setSelected(false); - } - - final void update() { - final Tab tab = mTab; - final View custom = tab != null ? tab.getCustomView() : null; - if (custom != null) { - final ViewParent customParent = custom.getParent(); - if (customParent != this) { - if (customParent != null) { - ((ViewGroup) customParent).removeView(custom); - } - addView(custom); - } - mCustomView = custom; - if (mTextView != null) { - mTextView.setVisibility(GONE); - } - if (mIconView != null) { - mIconView.setVisibility(GONE); - mIconView.setImageDrawable(null); - } - - mCustomTextView = (TextView) custom.findViewById(android.R.id.text1); - if (mCustomTextView != null) { - mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView); - } - mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon); - } else { - // We do not have a custom view. Remove one if it already exists - if (mCustomView != null) { - removeView(mCustomView); - mCustomView = null; - } - mCustomTextView = null; - mCustomIconView = null; - } - - if (mCustomView == null) { - // If there isn't a custom view, we'll us our own in-built layouts - if (mIconView == null) { - ImageView iconView = (ImageView) LayoutInflater.from(getContext()) - .inflate(R.layout.design_layout_tab_icon, this, false); - iconView.setLayoutParams(new LinearLayout.LayoutParams(dpToPx(25), - dpToPx(25))); - addView(iconView, 0); - mIconView = iconView; - } - if (mTextView == null) { - TextView textView = (TextView) LayoutInflater.from(getContext()) - .inflate(R.layout.design_layout_tab_text, this, false); - LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - textParams.gravity = Gravity.CENTER_VERTICAL; - textView.setLayoutParams(textParams); - addView(textView); - mTextView = textView; - mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView); - } - TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance); - if (mTabTextColors != null) { - mTextView.setTextColor(mTabTextColors); - } - updateTextAndIcon(mTextView, mIconView); - } else { - // Else, we'll see if there is a TextView or ImageView present and update them - if (mCustomTextView != null || mCustomIconView != null) { - updateTextAndIcon(mCustomTextView, mCustomIconView); - } - } - - // Finally update our selected state - setSelected(tab != null && tab.isSelected()); - } - - private void updateTextAndIcon(@Nullable final TextView textView, - @Nullable final ImageView iconView) { - final Drawable icon = mTab != null ? mTab.getIcon() : null; - final CharSequence text = mTab != null ? mTab.getText() : null; - final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null; - - if (iconView != null) { - if (icon != null) { - iconView.setImageDrawable(icon); - iconView.setVisibility(VISIBLE); - setVisibility(VISIBLE); - } else { - iconView.setVisibility(GONE); - iconView.setImageDrawable(null); - } - iconView.setContentDescription(contentDesc); - } - - final boolean hasText = !TextUtils.isEmpty(text); - if (textView != null) { - if (hasText) { - textView.setText(text); - textView.setVisibility(VISIBLE); - setVisibility(VISIBLE); - } else { - textView.setVisibility(GONE); - textView.setText(null); - } - textView.setContentDescription(contentDesc); - } - - if (iconView != null) { - MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams()); - int bottomMargin = 0; - if (hasText && iconView.getVisibility() == VISIBLE) { - // If we're showing both text and icon, add some margin bottom to the icon - bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON); - } - if (bottomMargin != lp.bottomMargin) { - lp.bottomMargin = bottomMargin; - iconView.requestLayout(); - } - } - TooltipCompat.setTooltipText(this, hasText ? null : contentDesc); - } - - public Tab getTab() { - return mTab; - } - - /** - * Approximates a given lines width with the new provided text size. - */ - private float approximateLineWidth(Layout layout, int line, float textSize) { - return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize()); - } - } - - private class SlidingTabStrip extends LinearLayout { - private int mSelectedIndicatorHeight; - private final Paint mSelectedIndicatorPaint; - - int mSelectedPosition = -1; - float mSelectionOffset; - - private int mLayoutDirection = -1; - - private int mIndicatorTop = -1; - private int mIndicatorBottom = -1; - - private ValueAnimator mIndicatorAnimator; - - SlidingTabStrip(Context context) { - super(context); - setWillNotDraw(false); - - - // Easy way to get vertical tab view - setOrientation(VERTICAL); - - mSelectedIndicatorPaint = new Paint(); - } - - void setSelectedIndicatorColor(int color) { - if (mSelectedIndicatorPaint.getColor() != color) { - mSelectedIndicatorPaint.setColor(color); - ViewCompat.postInvalidateOnAnimation(this); - } - } - - void setSelectedIndicatorHeight(int height) { - if (mSelectedIndicatorHeight != height) { - mSelectedIndicatorHeight = height; - ViewCompat.postInvalidateOnAnimation(this); - } - } - - boolean childrenNeedLayout() { - for (int i = 0, z = getChildCount(); i < z; i++) { - final View child = getChildAt(i); - if (child.getWidth() <= 0) { - return true; - } - } - return false; - } - - void setIndicatorPositionFromTabPosition(int position, float positionOffset) { - if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { - mIndicatorAnimator.cancel(); - } - - mSelectedPosition = position; - mSelectionOffset = positionOffset; - updateIndicatorPosition(); - } - - float getIndicatorPosition() { - return mSelectedPosition + mSelectionOffset; - } - - @Override - public void onRtlPropertiesChanged(int layoutDirection) { - super.onRtlPropertiesChanged(layoutDirection); - - // Workaround for a bug before Android M where LinearLayout did not relayout itself when - // layout direction changed. - if (Build.VERSION.SDK_INT < 23 /* Build.VERSION_CODES.M */) { - //noinspection WrongConstant - if (mLayoutDirection != layoutDirection) { - requestLayout(); - mLayoutDirection = layoutDirection; - } - } - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) { - // ScrollView will first measure use with UNSPECIFIED, and then with - // EXACTLY. Ignore the first call since anything we do will be overwritten anyway - return; - } - - if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) { - final int count = getChildCount(); - - // First we'll find the widest tab - int largestTabWidth = 0; - for (int i = 0, z = count; i < z; i++) { - View child = getChildAt(i); - if (child.getVisibility() == VISIBLE) { - largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth()); - } - } - - if (largestTabWidth <= 0) { - // If we don't have a largest child yet, skip until the next measure pass - return; - } - - final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN); - boolean remeasure = false; - - if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) { - // If the tabs fit within our width minus gutters, we will set all tabs to have - // the same width - for (int i = 0; i < count; i++) { - final LinearLayout.LayoutParams lp = - (LayoutParams) getChildAt(i).getLayoutParams(); - if (lp.width != largestTabWidth || lp.weight != 0) { - lp.width = largestTabWidth; - lp.weight = 0; - remeasure = true; - } - } - } else { - // If the tabs will wrap to be larger than the width minus gutters, we need - // to switch to GRAVITY_FILL - mTabGravity = GRAVITY_FILL; - updateTabViews(false); - remeasure = true; - } - - if (remeasure) { - // Now re-measure after our changes - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - } - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - - if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { - // If we're currently running an animation, lets cancel it and start a - // new animation with the remaining duration - mIndicatorAnimator.cancel(); - final long duration = mIndicatorAnimator.getDuration(); - animateIndicatorToPosition(mSelectedPosition, - Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration)); - } else { - // If we've been layed out, update the indicator position - updateIndicatorPosition(); - } - } - - private void updateIndicatorPosition() { - final View selectedTitle = getChildAt(mSelectedPosition); - int top, bottom; - - if (selectedTitle != null && selectedTitle.getWidth() > 0) { - top = selectedTitle.getTop(); - bottom = selectedTitle.getBottom(); - - if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) { - // Draw the selection partway between the tabs - View nextTitle = getChildAt(mSelectedPosition + 1); - top = (int) (mSelectionOffset * nextTitle.getTop() + - (1.0f - mSelectionOffset) * top); - bottom = (int) (mSelectionOffset * nextTitle.getBottom() + - (1.0f - mSelectionOffset) * bottom); - } - } else { - top = bottom = -1; - } - - setIndicatorPosition(top, bottom); - } - - void setIndicatorPosition(int top, int bottom) { - if (top != mIndicatorTop || bottom != mIndicatorBottom) { - // If the indicator's left/right has changed, invalidate - mIndicatorTop = top; - mIndicatorBottom = bottom; - ViewCompat.postInvalidateOnAnimation(this); - } - } - - void animateIndicatorToPosition(final int position, int duration) { - if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) { - mIndicatorAnimator.cancel(); - } - - final View targetView = getChildAt(position); - if (targetView == null) { - // If we don't have a view, just update the position now and return - updateIndicatorPosition(); - return; - } - - final int targetTop = targetView.getTop(); - final int targetBottom = targetView.getBottom(); - final int startTop; - final int startBottom; - - if (Math.abs(position - mSelectedPosition) <= 1) { - // If the views are adjacent, we'll animate from edge-to-edge - startTop = mIndicatorTop; - startBottom = mIndicatorBottom; - } else { - // Else, we'll just grow from the nearest edge - final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET); - if (position < mSelectedPosition) { - // We're going end-to-start - /* - if (isRtl) { - startTop = startBottom = targetTop - offset; - } else { - */ - startTop = startBottom = targetBottom + offset; - // } - } else { - // We're going start-to-end - /* - if (isRtl) { - startLeft = startRight = targetRight + offset; - } else { - */ - startTop = startBottom = targetTop - offset; - // } - } - } - - if (startTop != targetTop || startBottom != targetBottom) { - ValueAnimator animator = mIndicatorAnimator = new ValueAnimator(); - animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR); - animator.setDuration(duration); - animator.setFloatValues(0, 1); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @SuppressLint("RestrictedApi") - @Override - public void onAnimationUpdate(ValueAnimator animator) { - final float fraction = animator.getAnimatedFraction(); - setIndicatorPosition( - AnimationUtils.lerp(startTop, targetTop, fraction), - AnimationUtils.lerp(startBottom, targetBottom, fraction)); - } - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - mSelectedPosition = position; - mSelectionOffset = 0f; - } - }); - animator.start(); - } - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - // Thick colored line at the left side of the current selection - if (mIndicatorTop >= 0 && mIndicatorBottom > mIndicatorTop) { - canvas.drawRect(0, mIndicatorTop, - mSelectedIndicatorHeight, mIndicatorBottom, mSelectedIndicatorPaint); - } - } - } - - private static ColorStateList createColorStateList(int defaultColor, int selectedColor) { - final int[][] states = new int[2][]; - final int[] colors = new int[2]; - int i = 0; - - states[i] = SELECTED_STATE_SET; - colors[i] = selectedColor; - i++; - - // Default enabled state - states[i] = EMPTY_STATE_SET; - colors[i] = defaultColor; - i++; - - return new ColorStateList(states, colors); - } - - private int getDefaultHeight() { - boolean hasIconAndText = false; - for (int i = 0, count = mTabs.size(); i < count; i++) { - Tab tab = mTabs.get(i); - if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) { - hasIconAndText = true; - break; - } - } - return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT; - } - - private int getTabMinWidth() { - if (mRequestedTabMinWidth != INVALID_WIDTH) { - // If we have been given a min width, use it - return mRequestedTabMinWidth; - } - // Else, we'll use the default value - return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0; - } - - @Override - public LayoutParams generateLayoutParams(AttributeSet attrs) { - // We don't care about the layout params of any views added to us, since we don't actually - // add them. The only view we add is the SlidingTabStrip, which is done manually. - // We return the default layout params so that we don't blow up if we're given a TabItem - // without android:layout_* values. - return generateDefaultLayoutParams(); - } - - int getTabMaxWidth() { - return mTabMaxWidth; - } - - /** - * A {@link ViewPager.OnPageChangeListener} class which contains the - * necessary calls back to the provided {@link TabLayout} so that the tab position is - * kept in sync. - * - *

This class stores the provided VerticalTabLayout weakly, meaning that you can use - * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener) - * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and - * not cause a leak. - */ - public static class VerticalTabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener { - private final WeakReference mTabLayoutRef; - private int mPreviousScrollState; - private int mScrollState; - - public VerticalTabLayoutOnPageChangeListener(VerticalTabLayout tabLayout) { - mTabLayoutRef = new WeakReference<>(tabLayout); - } - - @Override - public void onPageScrollStateChanged(final int state) { - mPreviousScrollState = mScrollState; - mScrollState = state; - } - - @Override - public void onPageScrolled(final int position, final float positionOffset, - final int positionOffsetPixels) { - final VerticalTabLayout tabLayout = mTabLayoutRef.get(); - if (tabLayout != null) { - // Only update the text selection if we're not settling, or we are settling after - // being dragged - final boolean updateText = mScrollState != SCROLL_STATE_SETTLING || - mPreviousScrollState == SCROLL_STATE_DRAGGING; - // Update the indicator if we're not settling after being idle. This is caused - // from a setCurrentItem() call and will be handled by an animation from - // onPageSelected() instead. - final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING - && mPreviousScrollState == SCROLL_STATE_IDLE); - tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator); - } - } - - @Override - public void onPageSelected(final int position) { - final VerticalTabLayout tabLayout = mTabLayoutRef.get(); - if (tabLayout != null && tabLayout.getSelectedTabPosition() != position - && position < tabLayout.getTabCount()) { - // Select the tab, only updating the indicator if we're not being dragged/settled - // (since onPageScrolled will handle that). - final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE - || (mScrollState == SCROLL_STATE_SETTLING - && mPreviousScrollState == SCROLL_STATE_IDLE); - tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator); - } - } - - void reset() { - mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE; - } - } - - /** - * A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back - * to the provided {@link ViewPager} so that the tab position is kept in sync. - */ - public static class ViewPagerOnTabSelectedListener implements VerticalTabLayout.OnTabSelectedListener { - private final ViewPager mViewPager; - - public ViewPagerOnTabSelectedListener(ViewPager viewPager) { - mViewPager = viewPager; - } - - @Override - public void onTabSelected(VerticalTabLayout.Tab tab) { - mViewPager.setCurrentItem(tab.getPosition()); - } - - @Override - public void onTabUnselected(VerticalTabLayout.Tab tab) { - // No-op - } - - @Override - public void onTabReselected(VerticalTabLayout.Tab tab) { - // No-op - } - } - - private class PagerAdapterObserver extends DataSetObserver { - PagerAdapterObserver() { - } - - @Override - public void onChanged() { - populateFromPagerAdapter(); - } - - @Override - public void onInvalidated() { - populateFromPagerAdapter(); - } - } - - private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener { - private boolean mAutoRefresh; - - AdapterChangeListener() { - } - - @Override - public void onAdapterChanged(@NonNull ViewPager viewPager, - @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) { - if (mViewPager == viewPager) { - setPagerAdapter((ViewPagerAdapter) newAdapter, mAutoRefresh); - } - } - - void setAutoRefresh(boolean autoRefresh) { - mAutoRefresh = autoRefresh; - } - } - - public static class ViewPagerAdapter extends FragmentPagerAdapter { - List viewPagerList = new ArrayList<>(); - - public ViewPagerAdapter(FragmentManager fragmentManager) { - super(fragmentManager); - } - - @Override - public Fragment getItem(int position) { - return viewPagerList.get(position).fragment; - } - - @Override - public int getCount() { - return viewPagerList.size(); - } - - public int getIcon(int position) { - return viewPagerList.get(position).icon; - } - - @Override - public CharSequence getPageTitle(int position) { - return viewPagerList.get(position).title; - } - - public void addFragment(Fragment fragment, int icon, String name) { - ViewPagerItem item = new ViewPagerItem(); - item.fragment = fragment; - item.icon = icon; - item.title = name; - viewPagerList.add(item); - } - - public void setFragment(int index, Fragment fragment, int icon, String name) { - ViewPagerItem item = new ViewPagerItem(); - item.fragment = fragment; - item.icon = icon; - item.title = name; - viewPagerList.set(index, item); - } - - public void removeFragment(int index) { - viewPagerList.remove(index); - } - } - - public static class ViewPagerItem { - public Fragment fragment; - public String title; - public int icon; - } -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseLauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseLauncherActivity.java index 064be7298..668050da2 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseLauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseLauncherActivity.java @@ -1,5 +1,7 @@ package net.kdt.pojavlaunch; +import static net.kdt.pojavlaunch.Tools.getFileName; + import android.app.*; import android.content.*; import android.database.Cursor; @@ -28,12 +30,10 @@ import org.apache.commons.io.IOUtils; public abstract class BaseLauncherActivity extends BaseActivity { public Button mPlayButton; - public ConsoleFragment mConsoleView; - public CrashFragment mCrashView; public ProgressBar mLaunchProgress; public Spinner mVersionSelector; public MultiRTConfigDialog mRuntimeConfigDialog; - public TextView mLaunchTextStatus, mTextVersion; + public TextView mLaunchTextStatus; public JMinecraftVersionList mVersionList; public MinecraftDownloaderTask mTask; @@ -45,52 +45,24 @@ public abstract class BaseLauncherActivity extends BaseActivity { public abstract void statusIsLaunching(boolean isLaunching); - public void mcaccLogout(View view) { - //PojavProfile.reset(); - finish(); + + /** + * Used by the custom control button from the layout_main_v4 + * @param view The view triggering the function + */ + public void launchCustomControlsActivity(View view){ + startActivity(new Intent(BaseLauncherActivity.this, CustomControlsActivity.class)); } - - public void launcherMenu(View view) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.mcl_options); - builder.setItems(R.array.mcl_options, new DialogInterface.OnClickListener(){ - - @Override - public void onClick(DialogInterface p1, int p2) - { - switch (p2) { - case 0: // Mod installer - installMod(false); - break; - case 1: // Mod installer with java args - installMod(true); - break; - case 2: // Custom controls - startActivity(new Intent(BaseLauncherActivity.this, CustomControlsActivity.class)); - break; - case 3: { // About - final AlertDialog.Builder aboutB = new AlertDialog.Builder(BaseLauncherActivity.this); - aboutB.setTitle(R.string.mcl_option_about); - try { - aboutB.setMessage(Html.fromHtml(String.format(Tools.read(getAssets().open("about_en.txt")), - Tools.APP_NAME, - BuildConfig.VERSION_NAME, - "3.2.3") - )); - } catch (Exception e) { - throw new RuntimeException(e); - } - aboutB.setPositiveButton(android.R.string.ok, null); - AlertDialog aboutDialog = aboutB.show(); - TextView aboutTv = aboutDialog.findViewById(android.R.id.message); - aboutTv.setMovementMethod(LinkMovementMethod.getInstance()); - } break; - } - } - }); - builder.show(); + /** + * Used by the install button from the layout_main_v4 + * @param view The view triggering the function + */ + public void installJarFile(View view){ + installMod(false); } + + public static final int RUN_MOD_INSTALLER = 2050; private void installMod(boolean customJavaArgs) { if (customJavaArgs) { @@ -101,15 +73,12 @@ public abstract class BaseLauncherActivity extends BaseActivity { final EditText edit = new EditText(this); edit.setSingleLine(); edit.setHint("-jar/-cp /path/to/file.jar ..."); - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener(){ - @Override - public void onClick(DialogInterface di, int i) { - Intent intent = new Intent(BaseLauncherActivity.this, JavaGUILauncherActivity.class); - intent.putExtra("skipDetectMod", true); - intent.putExtra("javaArgs", edit.getText().toString()); - startActivity(intent); - } - }); + builder.setPositiveButton(android.R.string.ok, (di, i) -> { + Intent intent = new Intent(BaseLauncherActivity.this, JavaGUILauncherActivity.class); + intent.putExtra("skipDetectMod", true); + intent.putExtra("javaArgs", edit.getText().toString()); + startActivity(intent); + }); dialog = builder.create(); dialog.setView(edit); dialog.show(); @@ -132,7 +101,7 @@ public abstract class BaseLauncherActivity extends BaseActivity { v.setEnabled(false); mTask = new MinecraftDownloaderTask(this); mTask.execute(mProfile.selectedVersion); - mCrashView.resetCrashLog = true; + } } @@ -160,19 +129,17 @@ public abstract class BaseLauncherActivity extends BaseActivity { decorView.setSystemUiVisibility(uiOptions); System.out.println("call to onResume; E"); } + SharedPreferences.OnSharedPreferenceChangeListener listRefreshListener = null; @Override protected void onResumeFragments() { super.onResumeFragments(); if(listRefreshListener == null) { final BaseLauncherActivity thiz = this; - listRefreshListener = new SharedPreferences.OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if(key.startsWith("vertype_")) { - System.out.println("Verlist update needed!"); - new RefreshVersionListTask(thiz).execute(); - } + listRefreshListener = (sharedPreferences, key) -> { + if(key.startsWith("vertype_")) { + System.out.println("Verlist update needed!"); + new RefreshVersionListTask(thiz).execute(); } }; } @@ -181,76 +148,11 @@ public abstract class BaseLauncherActivity extends BaseActivity { System.out.println("call to onResumeFragments"); mRuntimeConfigDialog = new MultiRTConfigDialog(); mRuntimeConfigDialog.prepare(this); - try{ - final ProgressDialog barrier = new ProgressDialog(this); - barrier.setMessage(getString(R.string.global_waiting)); - barrier.setProgressStyle(barrier.STYLE_SPINNER); - barrier.setCancelable(false); - barrier.show(); - new Thread(new Runnable(){ - - @Override - public void run() - { - while (mConsoleView == null) { - try { - Thread.sleep(20); - } catch (Throwable th) {} - } - - try { - Thread.sleep(100); - } catch (Throwable th) {} - - runOnUiThread(new Runnable() { - @Override - public void run() - { - try { - mConsoleView.putLog(""); - barrier.dismiss(); - } catch (Throwable th) { - startActivity(getIntent()); - finish(); - } - } - }); - } - }).start(); - - File lastCrashFile = Tools.lastFileModified(Tools.DIR_HOME_CRASH); - if(CrashFragment.isNewCrash(lastCrashFile) || !mCrashView.getLastCrash().isEmpty()){ - mCrashView.resetCrashLog = false; - initTabs(2); - - } /*else throw new Exception();*/ - } catch(Throwable e) { - e.printStackTrace(); - } + //TODO ADD CRASH CHECK AND FOCUS System.out.println("call to onResumeFragments; E"); } - public static String getFileName(Context ctx, Uri uri) { - String result = null; - if (uri.getScheme().equals("content")) { - Cursor cursor = ctx.getContentResolver().query(uri, null, null, null, null); - try { - if (cursor != null && cursor.moveToFirst()) { - result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); - } - } finally { - cursor.close(); - } - } - if (result == null) { - result = uri.getPath(); - int cut = result.lastIndexOf('/'); - if (cut != -1) { - result = result.substring(cut + 1); - } - } - return result; - } + @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode,resultCode,data); @@ -260,62 +162,58 @@ public abstract class BaseLauncherActivity extends BaseActivity { barrier.setProgressStyle(barrier.STYLE_SPINNER); barrier.setCancelable(false); barrier.show(); - if (requestCode == MultiRTConfigDialog.MULTIRT_PICK_RUNTIME) { - if (data != null) { - final Uri uri = data.getData(); - Thread t = new Thread(() -> { - try { - String name = getFileName(this, uri); - MultiRTUtils.installRuntimeNamed(getContentResolver().openInputStream(uri), name, - (resid, stuff) -> BaseLauncherActivity.this.runOnUiThread( - () -> barrier.setMessage(BaseLauncherActivity.this.getString(resid, stuff)))); - MultiRTUtils.postPrepare(BaseLauncherActivity.this, name); - } catch (IOException e) { - Tools.showError(BaseLauncherActivity.this - , e); - } - BaseLauncherActivity.this.runOnUiThread(new Runnable() { - @Override - public void run() { - barrier.dismiss(); - mRuntimeConfigDialog.refresh(); - mRuntimeConfigDialog.dialog.show(); - } - }); - }); - t.start(); - } - } else if (requestCode == RUN_MOD_INSTALLER) { - if (data != null) { - final Uri uri = data.getData(); - barrier.setMessage(BaseLauncherActivity.this.getString(R.string.multirt_progress_caching)); - Thread t = new Thread(()->{ - try { - final String name = getFileName(this, uri); - final File modInstallerFile = new File(getCacheDir(), name); - FileOutputStream fos = new FileOutputStream(modInstallerFile); - IOUtils.copy(getContentResolver().openInputStream(uri), fos); - fos.close(); - BaseLauncherActivity.this.runOnUiThread(() -> { - barrier.dismiss(); - Intent intent = new Intent(BaseLauncherActivity.this, JavaGUILauncherActivity.class); - intent.putExtra("modFile", modInstallerFile); - startActivity(intent); - }); - }catch(IOException e) { - Tools.showError(BaseLauncherActivity.this,e); - } - }); - t.start(); - } - } - } - } - // Catching touch exception - @Override - public boolean onTouchEvent(MotionEvent event) { - return super.onTouchEvent(event); + // Install the runtime + if (requestCode == MultiRTConfigDialog.MULTIRT_PICK_RUNTIME) { + if (data == null) return; + + final Uri uri = data.getData(); + Thread t = new Thread(() -> { + try { + String name = getFileName(this, uri); + MultiRTUtils.installRuntimeNamed(getContentResolver().openInputStream(uri), name, + (resid, stuff) -> BaseLauncherActivity.this.runOnUiThread( + () -> barrier.setMessage(BaseLauncherActivity.this.getString(resid, stuff)))); + MultiRTUtils.postPrepare(BaseLauncherActivity.this, name); + } catch (IOException e) { + Tools.showError(BaseLauncherActivity.this, e); + } + BaseLauncherActivity.this.runOnUiThread(() -> { + barrier.dismiss(); + mRuntimeConfigDialog.refresh(); + mRuntimeConfigDialog.dialog.show(); + }); + }); + t.start(); + } + + // Run a mod installer + if (requestCode == RUN_MOD_INSTALLER) { + if (data == null) return; + + final Uri uri = data.getData(); + barrier.setMessage(BaseLauncherActivity.this.getString(R.string.multirt_progress_caching)); + Thread t = new Thread(()->{ + try { + final String name = getFileName(this, uri); + final File modInstallerFile = new File(getCacheDir(), name); + FileOutputStream fos = new FileOutputStream(modInstallerFile); + IOUtils.copy(getContentResolver().openInputStream(uri), fos); + fos.close(); + BaseLauncherActivity.this.runOnUiThread(() -> { + barrier.dismiss(); + Intent intent = new Intent(BaseLauncherActivity.this, JavaGUILauncherActivity.class); + intent.putExtra("modFile", modInstallerFile); + startActivity(intent); + }); + }catch(IOException e) { + Tools.showError(BaseLauncherActivity.this,e); + } + }); + t.start(); + } + + } } protected abstract void initTabs(int pageIndex); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseMainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseMainActivity.java index c7fc40c1b..fa1fbeed1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseMainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/BaseMainActivity.java @@ -48,8 +48,9 @@ public class BaseMainActivity extends LoggableActivity { public float scaleFactor = 1; public double sensitivityFactor; private final int fingerStillThreshold = (int) Tools.dpToPx(9); + private final int fingerScrollThreshold = (int) Tools.dpToPx(6); private float initialX, initialY; - private int scrollInitialX, scrollInitialY; + private float scrollLastInitialX, scrollLastInitialY; private float prevX, prevY; private int currentPointerID; @@ -62,8 +63,8 @@ public class BaseMainActivity extends LoggableActivity { switch (msg.what) { case MSG_LEFT_MOUSE_BUTTON_CHECK: if(LauncherPreferences.PREF_DISABLE_GESTURES) break; - int x = CallbackBridge.mouseX; - int y = CallbackBridge.mouseY; + float x = CallbackBridge.mouseX; + float y = CallbackBridge.mouseY; if (CallbackBridge.isGrabbing() && Math.abs(initialX - x) < fingerStillThreshold && Math.abs(initialY - y) < fingerStillThreshold) { @@ -96,8 +97,9 @@ public class BaseMainActivity extends LoggableActivity { private TextView textLog; private ScrollView contentScroll; private ToggleButton toggleLog; - private GestureDetector gestureDetector; - private DoubleTapDetector doubleTapDetector; + + private TapDetector singleTapDetector; + private TapDetector doubleTapDetector; private TextView debugText; private NavigationView.OnNavigationItemSelectedListener gameActionListener; @@ -164,8 +166,9 @@ public class BaseMainActivity extends LoggableActivity { System.out.println("WidthHeight: " + windowWidth + ":" + windowHeight); - gestureDetector = new GestureDetector(this, new SingleTapConfirm()); - doubleTapDetector = new DoubleTapDetector(); + + singleTapDetector = new TapDetector(1, TapDetector.DETECTION_METHOD_BOTH); + doubleTapDetector = new TapDetector(2, TapDetector.DETECTION_METHOD_DOWN); // Menu @@ -262,7 +265,7 @@ public class BaseMainActivity extends LoggableActivity { float mouseX = mousePointer.getX(); float mouseY = mousePointer.getY(); - if (gestureDetector.onTouchEvent(event)) { + if (singleTapDetector.onTouchEvent(event)) { mouse_x = (mouseX * scaleFactor); mouse_y = (mouseY * scaleFactor); CallbackBridge.sendCursorPos(mouse_x, mouse_y); @@ -273,8 +276,8 @@ public class BaseMainActivity extends LoggableActivity { } else { switch (action) { case MotionEvent.ACTION_POINTER_DOWN: // 5 - scrollInitialX = CallbackBridge.mouseX; - scrollInitialY = CallbackBridge.mouseY; + scrollLastInitialX = event.getX(); + scrollLastInitialY = event.getY(); break; case MotionEvent.ACTION_DOWN: @@ -285,10 +288,15 @@ public class BaseMainActivity extends LoggableActivity { case MotionEvent.ACTION_MOVE: // 2 - if (!CallbackBridge.isGrabbing() && event.getPointerCount() == 2 && !LauncherPreferences.PREF_DISABLE_GESTURES) { //Scrolling feature - CallbackBridge.sendScroll( Tools.pxToDp(CallbackBridge.mouseX - scrollInitialX)/30, Tools.pxToDp(CallbackBridge.mouseY - scrollInitialY)/30); - scrollInitialX = CallbackBridge.mouseX; - scrollInitialY = CallbackBridge.mouseY; + if (!CallbackBridge.isGrabbing() && event.getPointerCount() >= 2 && !LauncherPreferences.PREF_DISABLE_GESTURES) { //Scrolling feature + int hScroll = ((int) (event.getX() - scrollLastInitialX)) / fingerScrollThreshold; + int vScroll = ((int) (event.getY() - scrollLastInitialY)) / fingerScrollThreshold; + + if(vScroll != 0 || hScroll != 0){ + CallbackBridge.sendScroll(hScroll, vScroll); + scrollLastInitialX = event.getX(); + scrollLastInitialY = event.getY(); + } } else { if(currentPointerID == event.getPointerId(0)) { mouseX = Math.max(0, Math.min(displayMetrics.widthPixels, mouseX + (x - prevX) * LauncherPreferences.PREF_MOUSESPEED)); @@ -354,7 +362,7 @@ public class BaseMainActivity extends LoggableActivity { mouse_x = (e.getX() * scaleFactor); mouse_y = (e.getY() * scaleFactor); //One android click = one MC click - if(gestureDetector.onTouchEvent(e)){ + if(singleTapDetector.onTouchEvent(e)){ CallbackBridge.putMouseEventWithCoords(rightOverride ? (byte) 1 : (byte) 0, (int)mouse_x, (int)mouse_y); return true; } @@ -424,8 +432,8 @@ public class BaseMainActivity extends LoggableActivity { break; case MotionEvent.ACTION_POINTER_DOWN: // 5 - scrollInitialX = CallbackBridge.mouseX; - scrollInitialY = CallbackBridge.mouseY; + scrollLastInitialX = e.getX(); + scrollLastInitialY = e.getY(); //Checking if we are pressing the hotbar to select the item hudKeyHandled = handleGuiBar((int)e.getX(e.getPointerCount()-1), (int) e.getY(e.getPointerCount()-1)); if(hudKeyHandled != -1){ @@ -440,10 +448,17 @@ public class BaseMainActivity extends LoggableActivity { break; case MotionEvent.ACTION_MOVE: - if (!CallbackBridge.isGrabbing() && e.getPointerCount() == 2 && !LauncherPreferences.PREF_DISABLE_GESTURES) { //Scrolling feature - CallbackBridge.sendScroll(Tools.pxToDp(mouse_x - scrollInitialX)/30 , Tools.pxToDp(mouse_y - scrollInitialY)/30); - scrollInitialX = (int)mouse_x; - scrollInitialY = (int)mouse_y; + if (!CallbackBridge.isGrabbing() && e.getPointerCount() >= 2 && !LauncherPreferences.PREF_DISABLE_GESTURES) { //Scrolling feature + int hScroll = ((int) (e.getX() - scrollLastInitialX)) / fingerScrollThreshold; + int vScroll = ((int) (e.getY() - scrollLastInitialY)) / fingerScrollThreshold; + + if(vScroll != 0 || hScroll != 0){ + CallbackBridge.sendScroll(hScroll, vScroll); + scrollLastInitialX = e.getX(); + scrollLastInitialY = e.getY(); + } + + } else if (!CallbackBridge.isGrabbing() && e.getPointerCount() == 1) { //Touch hover CallbackBridge.sendCursorPos(mouse_x, mouse_y); prevX = e.getX(); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/DoubleTapDetector.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/DoubleTapDetector.java deleted file mode 100644 index e87c893f9..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/DoubleTapDetector.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.kdt.pojavlaunch; - -import android.view.MotionEvent; - -import static android.view.MotionEvent.ACTION_DOWN; -import static android.view.MotionEvent.ACTION_POINTER_DOWN; - -/** - * Class aiming at better detecting double tap events for EVERY POINTER - * Only uses the least amount of events possible, - * since we aren't guaranteed to have all events in order - */ -public class DoubleTapDetector { - - private final static int DOUBLE_TAP_MIN_DELTA_MS = 50; - private final static int DOUBLE_TAP_MAX_DELTA_MS = 300; - private final static int DOUBLE_TAP_SLOP_SQUARE_PX = (int) Math.pow(Tools.dpToPx(100), 2); - - private long mLastEventTime = 0; - private float mLastX = 9999; - private float mLastY = 9999; - - /** - * A function to call when you have a touch event. - * @param e The MotionEvent to inspect - * @return whether or not a double tap happened for a pointer - */ - public boolean onTouchEvent(MotionEvent e){ - int eventAction = e.getActionMasked(); - int pointerIndex; - - //Get the pointer index we want to look at - if(eventAction == ACTION_DOWN) pointerIndex = 0; - else if(eventAction == ACTION_POINTER_DOWN) pointerIndex = e.getActionIndex(); - else return false; - - float eventX = e.getX(pointerIndex); - float eventY = e.getY(pointerIndex); - long eventTime = e.getEventTime(); - - long deltaTime = eventTime - mLastEventTime; - if(deltaTime > DOUBLE_TAP_MIN_DELTA_MS && deltaTime < DOUBLE_TAP_MAX_DELTA_MS){ - int deltaX = (int) mLastX - (int) eventX; - int deltaY = (int) mLastY - (int) eventY; - if((deltaX*deltaX + deltaY*deltaY) < DOUBLE_TAP_SLOP_SQUARE_PX){ - //Then I guess there is a double tap :thonk: - resetDoubleTapState(); - return true; - } - } - - mLastEventTime = eventTime; - mLastX = eventX; - mLastY = eventY; - return false; - } - - /** - * Reset the double tap values. - */ - private void resetDoubleTapState(){ - mLastEventTime = 0; - mLastX = 9999; - mLastY = 9999; - } - -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java index 5a7ab0bbd..ee80acb16 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java @@ -49,8 +49,8 @@ public class JavaGUILauncherActivity extends LoggableActivity implements View.On public void handleMessage(Message msg) { switch (msg.what) { case MSG_LEFT_MOUSE_BUTTON_CHECK: { - int x = CallbackBridge.mouseX; - int y = CallbackBridge.mouseY; + float x = CallbackBridge.mouseX; + float y = CallbackBridge.mouseY; if (CallbackBridge.isGrabbing() && Math.abs(initialX - x) < fingerStillThreshold && Math.abs(initialY - y) < fingerStillThreshold) { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLauncherActivity.java index bc6037f5d..7f95c8bec 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLauncherActivity.java @@ -1,7 +1,11 @@ package net.kdt.pojavlaunch; +import static android.os.Build.VERSION_CODES.P; +import static net.kdt.pojavlaunch.Tools.ignoreNotch; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_HIDE_SIDEBAR; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE; + import android.animation.ValueAnimator; -import android.content.SharedPreferences; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Typeface; @@ -9,126 +13,140 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.design.widget.VerticalTabLayout.ViewPagerAdapter; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; -import android.widget.ProgressBar; +import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; - import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.Guideline; -import androidx.viewpager.widget.ViewPager; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; +import net.kdt.pojavlaunch.extra.ExtraCore; +import net.kdt.pojavlaunch.extra.ExtraListener; import net.kdt.pojavlaunch.fragments.ConsoleFragment; import net.kdt.pojavlaunch.fragments.CrashFragment; import net.kdt.pojavlaunch.fragments.LauncherFragment; -import net.kdt.pojavlaunch.prefs.LauncherPreferenceFragment; import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import net.kdt.pojavlaunch.prefs.screens.LauncherPreferenceFragment; import net.kdt.pojavlaunch.value.MinecraftAccount; import java.io.File; import java.util.ArrayList; import java.util.List; -import static android.os.Build.VERSION_CODES.P; -import static net.kdt.pojavlaunch.Tools.ignoreNotch; -import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_HIDE_SIDEBAR; -import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_IGNORE_NOTCH; -import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE; - public class PojavLauncherActivity extends BaseLauncherActivity { - private ViewPager viewPager; + // An equivalent ViewPager2 adapter class + private static class ScreenSlidePagerAdapter extends FragmentStateAdapter { + public ScreenSlidePagerAdapter(FragmentActivity fa) { + super(fa); + } - private TextView tvUsernameView, tvConnectStatus; + @Override + public Fragment createFragment(int position) { + if (position == 0) return new LauncherFragment(); + if (position == 1) return new ConsoleFragment(); + if (position == 2) return new CrashFragment(); + if (position == 3) return new LauncherPreferenceFragment(); + return null; + } + + @Override + public int getItemCount() { + return 4; + } + } + + + private TextView tvConnectStatus; private Spinner accountSelector; - private ViewPagerAdapter viewPageAdapter; + private ViewPager2 viewPager; private final Button[] Tabs = new Button[4]; - private View selected; + private View selectedTab; + private ImageView accountFaceImageView; private Button logoutBtn; // MineButtons + private ExtraListener backPreferenceListener; public PojavLauncherActivity() { } @Override - protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.launcher_main_v4); - - if (BuildConfig.DEBUG) { - Toast.makeText(this, "Launcher process id: " + android.os.Process.myPid(), Toast.LENGTH_LONG).show(); - } - - + //Boilerplate linking/initialisation viewPager = findViewById(R.id.launchermainTabPager); - selected = findViewById(R.id.viewTabSelected); - - mConsoleView = new ConsoleFragment(); - mCrashView = new CrashFragment(); - - viewPageAdapter = new ViewPagerAdapter(getSupportFragmentManager()); - viewPageAdapter.addFragment(new LauncherFragment(), 0, getString(R.string.mcl_tab_news)); - viewPageAdapter.addFragment(mConsoleView, 0, getString(R.string.mcl_tab_console)); - viewPageAdapter.addFragment(mCrashView, 0, getString(R.string.mcl_tab_crash)); - viewPageAdapter.addFragment(new LauncherPreferenceFragment(), 0, getString(R.string.mcl_option_settings)); - - viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { - @Override - public void onPageSelected(int position) { - setTabActive(position); - } - - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - - } - - @Override - public void onPageScrollStateChanged(int state) { - - } - }); - viewPager.setAdapter(viewPageAdapter); - - tvConnectStatus = (TextView) findViewById(R.id.launchermain_text_accountstatus); - tvUsernameView = (TextView) findViewById(R.id.launchermain_text_welcome); - mTextVersion = (TextView) findViewById(R.id.launcherMainVersionView); - - //The following line is used to make this TextView horizontally scroll if the version name is larger than the view - mTextVersion.setSelected(true); - + selectedTab = findViewById(R.id.viewTabSelected); + tvConnectStatus = findViewById(R.id.launchermain_text_accountstatus); + accountFaceImageView = findViewById(R.id.launchermain_account_image); + accountSelector = findViewById(R.id.launchermain_spinner_account); + mVersionSelector = findViewById(R.id.launchermain_spinner_version); + mLaunchProgress = findViewById(R.id.progressDownloadBar); + mLaunchTextStatus = findViewById(R.id.progressDownloadText); + logoutBtn = findViewById(R.id.installJarButton); + mPlayButton = findViewById(R.id.launchermainPlayButton); Tabs[0] = findViewById(R.id.btnTab1); Tabs[1] = findViewById(R.id.btnTab2); Tabs[2] = findViewById(R.id.btnTab3); Tabs[3] = findViewById(R.id.btnTab4); - pickAccount(); + if (BuildConfig.DEBUG) { + Toast.makeText(this, "Launcher process id: " + android.os.Process.myPid(), Toast.LENGTH_LONG).show(); + } + + // Setup the viewPager to slide across fragments + viewPager.setAdapter(new ScreenSlidePagerAdapter(this)); + viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + setTabActive(position); + } + }); + initTabs(0); + + //Setup listener to the backPreference system + backPreferenceListener = new ExtraListener() { + @Override + public boolean onValueSet(String key, String value) { + if(value.equals("true")){ + onBackPressed(); + ExtraCore.setValue(key, "false"); + } + return false; + } + }; + ExtraCore.addExtraListener("back_preference", backPreferenceListener); - final List accountList = new ArrayList(); - final MinecraftAccount tempProfile = PojavProfile.getTempProfileContent(this); + // Try to load the temporary account + final List accountList = new ArrayList<>(); + final MinecraftAccount tempProfile = PojavProfile.getTempProfileContent(); if (tempProfile != null) { accountList.add(tempProfile.username); } for (String s : new File(Tools.DIR_ACCOUNT_NEW).list()) { accountList.add(s.substring(0, s.length() - 5)); } - + + // Setup account spinner + pickAccount(); ArrayAdapter adapterAcc = new ArrayAdapter(this, android.R.layout.simple_spinner_item, accountList); adapterAcc.setDropDownViewResource(android.R.layout.simple_list_item_single_choice); - accountSelector = (Spinner) findViewById(R.id.launchermain_spinner_account); accountSelector.setAdapter(adapterAcc); + if (tempProfile != null) { accountSelector.setSelection(0); } else { @@ -157,8 +175,9 @@ public class PojavLauncherActivity extends BaseLauncherActivity // TODO: Implement this method } }); - - List versions = new ArrayList(); + + // Setup the minecraft version list + List versions = new ArrayList<>(); final File fVers = new File(Tools.DIR_HOME_VERSION); try { @@ -179,40 +198,28 @@ public class PojavLauncherActivity extends BaseLauncherActivity } //mAvailableVersions; - ArrayAdapter adapterVer = new ArrayAdapter(this, android.R.layout.simple_spinner_item, mAvailableVersions); + ArrayAdapter adapterVer = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, mAvailableVersions); adapterVer.setDropDownViewResource(android.R.layout.simple_list_item_single_choice); - mVersionSelector = (Spinner) findViewById(R.id.launchermain_spinner_version); mVersionSelector.setAdapter(adapterVer); - mLaunchProgress = (ProgressBar) findViewById(R.id.progressDownloadBar); - mLaunchTextStatus = (TextView) findViewById(R.id.progressDownloadText); - logoutBtn = (Button) findViewById(R.id.switchUserBtn); - - mPlayButton = (Button) findViewById(R.id.launchermainPlayButton); - statusIsLaunching(false); - initTabs(0); - LauncherPreferences.DEFAULT_PREF.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if(key.equals("hideSidebar")){ - changeLookAndFeel(sharedPreferences.getBoolean("hideSidebar",false)); - return; - } + //Add the preference changed listener + LauncherPreferences.DEFAULT_PREF.registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> { + if(key.equals("hideSidebar")){ + changeLookAndFeel(sharedPreferences.getBoolean("hideSidebar",false)); + return; + } - if(key.equals("ignoreNotch")){ - ignoreNotch(sharedPreferences.getBoolean("ignoreNotch", true), PojavLauncherActivity.this); - return; - } + if(key.equals("ignoreNotch")){ + ignoreNotch(sharedPreferences.getBoolean("ignoreNotch", true), PojavLauncherActivity.this); + return; } }); changeLookAndFeel(PREF_HIDE_SIDEBAR); - ignoreNotch(PREF_IGNORE_NOTCH, PojavLauncherActivity.this); } - private void selectTabPage(int pageIndex){ viewPager.setCurrentItem(pageIndex); setTabActive(pageIndex); @@ -221,8 +228,9 @@ public class PojavLauncherActivity extends BaseLauncherActivity private void pickAccount() { try { mProfile = PojavProfile.getCurrentProfileContent(this); + accountFaceImageView.setImageBitmap(mProfile.getSkinFace()); - tvUsernameView.setText(getString(R.string.main_welcome, mProfile.username)); + //TODO FULL BACKGROUND LOGIN tvConnectStatus.setText(mProfile.accessToken.equals("0") ? R.string.mcl_account_offline : R.string.mcl_account_connected); } catch(Exception e) { mProfile = new MinecraftAccount(); @@ -259,21 +267,18 @@ public class PojavLauncherActivity extends BaseLauncherActivity Tabs[index].setTextColor(Color.WHITE); //Animating the white bar on the left - ValueAnimator animation = ValueAnimator.ofFloat(selected.getY(), Tabs[index].getY()+(Tabs[index].getHeight()-selected.getHeight())/2f); + ValueAnimator animation = ValueAnimator.ofFloat(selectedTab.getY(), Tabs[index].getY()+(Tabs[index].getHeight()- selectedTab.getHeight())/2f); animation.setDuration(250); - animation.addUpdateListener(animation1 -> selected.setY((float) animation1.getAnimatedValue())); + animation.addUpdateListener(animation1 -> selectedTab.setY((float) animation1.getAnimatedValue())); animation.start(); } protected void initTabs(int activeTab){ final Handler handler = new Handler(Looper.getMainLooper()); - handler.postDelayed(new Runnable() { - @Override - public void run() { - //Do something after 100ms - selectTabPage(activeTab); - } - }, 500); + handler.post(() -> { + //Do something after 100ms + selectTabPage(activeTab); + }); } private void changeLookAndFeel(boolean useOldLook){ @@ -286,8 +291,9 @@ public class PojavLauncherActivity extends BaseLauncherActivity params.guidePercent = 0; // 0%, range: 0 <-> 1 guideLine.setLayoutParams(params); - //Remove the selected Tab - selected.setVisibility(View.GONE); + //Remove the selected Tab and the head image + selectedTab.setVisibility(View.GONE); + accountFaceImageView.setVisibility(View.GONE); //Enlarge the button, but just a bit. params = (ConstraintLayout.LayoutParams) mPlayButton.getLayoutParams(); @@ -299,7 +305,8 @@ public class PojavLauncherActivity extends BaseLauncherActivity guideLine.setLayoutParams(params); //Show the selected Tab - selected.setVisibility(View.VISIBLE); + selectedTab.setVisibility(View.VISIBLE); + accountFaceImageView.setVisibility(View.VISIBLE); //Set the default button size params = (ConstraintLayout.LayoutParams) mPlayButton.getLayoutParams(); @@ -323,5 +330,22 @@ public class PojavLauncherActivity extends BaseLauncherActivity } } + /** + * Custom back stack system. Use the classic backstack when the focus is on the setting screen, + * finish the activity and remove the back_preference listener otherwise + */ + @Override + public void onBackPressed() { + int count = getSupportFragmentManager().getBackStackEntryCount(); + + if(count > 0 && viewPager.getCurrentItem() == 3){ + getSupportFragmentManager().popBackStack(); + }else{ + super.onBackPressed(); + //additional code + ExtraCore.removeExtraListenerFromValue("back_preference", backPreferenceListener); + finish(); + } + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java index 3bd485b38..7f71feffe 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavLoginActivity.java @@ -1,6 +1,7 @@ package net.kdt.pojavlaunch; import static net.kdt.pojavlaunch.Architecture.archAsString; +import static net.kdt.pojavlaunch.Tools.getFileName; import android.Manifest; import android.app.Activity; @@ -379,7 +380,7 @@ public class PojavLoginActivity extends BaseActivity final Uri uri = data.getData(); Thread t = new Thread(() -> { try { - MultiRTUtils.installRuntimeNamed(getContentResolver().openInputStream(uri), BaseLauncherActivity.getFileName(this, uri), + MultiRTUtils.installRuntimeNamed(getContentResolver().openInputStream(uri), getFileName(this, uri), (resid, stuff) -> PojavLoginActivity.this.runOnUiThread( () -> { if (startupTextView != null) @@ -507,14 +508,8 @@ public class PojavLoginActivity extends BaseActivity ImageView imageView = child.findViewById(R.id.account_head); String accNameStr = s.substring(0, s.length() - 5); - String skinFaceBase64 = MinecraftAccount.load(accNameStr).skinFaceBase64; - if (skinFaceBase64 != null) { - byte[] faceIconBytes = Base64.decode(skinFaceBase64, Base64.DEFAULT); - Bitmap bitmap = BitmapFactory.decodeByteArray(faceIconBytes, 0, faceIconBytes.length); + imageView.setImageBitmap(MinecraftAccount.load(accNameStr).getSkinFace()); - imageView.setImageDrawable(new BitmapDrawable(getResources(), - bitmap)); - } accountName.setText(accNameStr); accountListLayout.addView(child); @@ -635,7 +630,6 @@ public class PojavLoginActivity extends BaseActivity builder.clientToken = result[2]; builder.profileId = result[3]; builder.username = result[4]; - builder.selectedVersion = "1.12.2"; builder.updateSkinFace(); mProfile = builder; } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavProfile.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavProfile.java index 999651a78..8a6c1b339 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavProfile.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavProfile.java @@ -25,13 +25,13 @@ public class PojavProfile public static MinecraftAccount getCurrentProfileContent(Context ctx) throws JsonSyntaxException { MinecraftAccount build = MinecraftAccount.load(getCurrentProfileName(ctx)); if (build == null) { - System.out.println("isTempProfile null? " + (getTempProfileContent(ctx) == null)); - return getTempProfileContent(ctx); + System.out.println("isTempProfile null? " + (getTempProfileContent() == null)); + return getTempProfileContent(); } return build; } - public static MinecraftAccount getTempProfileContent(Context ctx) { + public static MinecraftAccount getTempProfileContent() { try { MinecraftAccount acc = MinecraftAccount.parse(Tools.read(Tools.DIR_DATA+"/cache/tempacc.json")); if (acc.accessToken == null) { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/TapDetector.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/TapDetector.java new file mode 100644 index 000000000..9aaeeeeca --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/TapDetector.java @@ -0,0 +1,122 @@ +package net.kdt.pojavlaunch; + +import android.view.MotionEvent; + +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_POINTER_DOWN; +import static android.view.MotionEvent.ACTION_POINTER_UP; +import static android.view.MotionEvent.ACTION_UP; + +/** + * Class aiming at better detecting X-tap events regardless of the POINTERS + * Only uses the least amount of events possible, + * since we aren't guaranteed to have all events in order + */ +public class TapDetector { + + public final static int DETECTION_METHOD_DOWN = 0x1; + public final static int DETECTION_METHOD_UP = 0x2; + public final static int DETECTION_METHOD_BOTH = 0x3; //Unused for now + + private final static int TAP_MIN_DELTA_MS = 10; + private final static int TAP_MAX_DELTA_MS = 300; + private final static int TAP_SLOP_SQUARE_PX = (int) Math.pow(Tools.dpToPx(100), 2); + + private final int tapNumberToDetect; + private int currentTapNumber = 0; + + private final int detectionMethod; + + private long mLastEventTime = 0; + private float mLastX = 9999; + private float mLastY = 9999; + + /** + * @param tapNumberToDetect How many taps are needed before onTouchEvent returns True. + * @param detectionMethod Method used to detect touches. See DETECTION_METHOD constants above. + */ + public TapDetector(int tapNumberToDetect, int detectionMethod){ + this.detectionMethod = detectionMethod; + //We expect both ACTION_DOWN and ACTION_UP for the DETECTION_METHOD_BOTH + this.tapNumberToDetect = detectBothTouch() ? 2*tapNumberToDetect : tapNumberToDetect; + } + + /** + * A function to call when you have a touch event. + * @param e The MotionEvent to inspect + * @return whether or not a X-tap happened for a pointer + */ + public boolean onTouchEvent(MotionEvent e){ + int eventAction = e.getActionMasked(); + int pointerIndex = -1; + + //Get the event to look forward + if(detectDownTouch()){ + if(eventAction == ACTION_DOWN) pointerIndex = 0; + else if(eventAction == ACTION_POINTER_DOWN) pointerIndex = e.getActionIndex(); + } + if(detectUpTouch()){ + if(eventAction == ACTION_UP) pointerIndex = 0; + else if(eventAction == ACTION_POINTER_UP) pointerIndex = e.getActionIndex(); + } + + if(pointerIndex == -1) return false; // Useless event + + //Store current event info + float eventX = e.getX(pointerIndex); + float eventY = e.getY(pointerIndex); + long eventTime = e.getEventTime(); + + //Compute deltas + long deltaTime = eventTime - mLastEventTime; + int deltaX = (int) mLastX - (int) eventX; + int deltaY = (int) mLastY - (int) eventY; + + //Store current event info to persist on next event + mLastEventTime = eventTime; + mLastX = eventX; + mLastY = eventY; + + //Check for high enough speed and precision + if(currentTapNumber > 0){ + if ((deltaTime < TAP_MIN_DELTA_MS || deltaTime > TAP_MAX_DELTA_MS) || + ((deltaX*deltaX + deltaY*deltaY) > TAP_SLOP_SQUARE_PX)) { + // We invalidate previous taps, not this one though + currentTapNumber = 0; + } + } + + //A worthy tap happened + currentTapNumber += 1; + if(currentTapNumber >= tapNumberToDetect){ + resetTapDetectionState(); + return true; + } + + //If not enough taps are reached + return false; + } + + /** + * Reset the double tap values. + */ + private void resetTapDetectionState(){ + currentTapNumber = 0; + mLastEventTime = 0; + mLastX = 9999; + mLastY = 9999; + } + + + private boolean detectDownTouch(){ + return (detectionMethod & DETECTION_METHOD_DOWN) == DETECTION_METHOD_DOWN; + } + + private boolean detectUpTouch(){ + return (detectionMethod & DETECTION_METHOD_UP) == DETECTION_METHOD_UP; + } + + private boolean detectBothTouch(){ + return detectionMethod == DETECTION_METHOD_BOTH; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index dfa198dfb..655bf6977 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -2,8 +2,10 @@ package net.kdt.pojavlaunch; import android.app.*; import android.content.*; +import android.database.Cursor; import android.net.*; import android.os.*; +import android.provider.OpenableColumns; import android.system.*; import android.util.*; import com.google.gson.*; @@ -861,4 +863,26 @@ public final class Tools { if(displaySideRes % 2 != 0) displaySideRes ++; return displaySideRes; } + + public static String getFileName(Context ctx, Uri uri) { + String result = null; + if (uri.getScheme().equals("content")) { + Cursor cursor = ctx.getContentResolver().query(uri, null, null, null, null); + try { + if (cursor != null && cursor.moveToFirst()) { + result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + } + } finally { + cursor.close(); + } + } + if (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) { + result = result.substring(cut + 1); + } + } + return result; + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java index ee6a6ed67..6426dac88 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/authenticator/microsoft/MicrosoftAuthTask.java @@ -71,7 +71,7 @@ public class MicrosoftAuthTask extends AsyncTask { */ Msa msa = new Msa(this, Boolean.parseBoolean(args[0]), args[1]); - MinecraftAccount acc = new MinecraftAccount(); + MinecraftAccount acc = MinecraftAccount.load(msa.mcName); if (msa.doesOwnGame) { acc.clientToken = "0"; /* FIXME */ acc.accessToken = msa.mcToken; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java index 22c25e977..92ec54d92 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/buttons/ControlButton.java @@ -23,6 +23,10 @@ import org.lwjgl.glfw.*; import static net.kdt.pojavlaunch.BaseMainActivity.sendMouseButton; import static net.kdt.pojavlaunch.LWJGLGLFWKeycode.GLFW_KEY_UNKNOWN; import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_BUTTONSIZE; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_BOTTOM_OFFSET; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_LEFT_OFFSET; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_RIGHT_OFFSET; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_TOP_OFFSET; @SuppressLint("ViewConstructor") public class ControlButton extends androidx.appcompat.widget.AppCompatButton implements OnLongClickListener @@ -152,16 +156,57 @@ public class ControlButton extends androidx.appcompat.widget.AppCompatButton imp @Override public void setX(float x) { + // We have to account for control offset preference + if(x + (mProperties.getWidth()/2f) > CallbackBridge.physicalWidth/2f){ + x -= PREF_CONTROL_RIGHT_OFFSET; + }else{ + x += PREF_CONTROL_LEFT_OFFSET; + } + super.setX(x); setModified(true); } @Override public void setY(float y) { + // We have to account for control offset preference + if(y - PREF_CONTROL_TOP_OFFSET + (mProperties.getHeight()/2f) > CallbackBridge.physicalHeight/2f){ + y -= PREF_CONTROL_BOTTOM_OFFSET; + }else{ + y += PREF_CONTROL_TOP_OFFSET; + } + super.setY(y); setModified(true); } + @Override + public float getX() { + float x = super.getX(); + // We have to account for control offset preference + if(x + (mProperties.getWidth()/2f) > (CallbackBridge.physicalWidth)/2f){ + x += PREF_CONTROL_RIGHT_OFFSET; + }else{ + x -= PREF_CONTROL_LEFT_OFFSET; + } + + return x; + } + + @Override + public float getY(){ + // We have to account for control offset preference + float y = super.getY(); + if(y + (mProperties.getHeight()/2f) > CallbackBridge.physicalHeight/2f){ + y += PREF_CONTROL_BOTTOM_OFFSET; + }else{ + y -= PREF_CONTROL_TOP_OFFSET; + } + + + return y; + } + /** * Apply the dynamic equation on the x axis. * @param dynamicX The equation to compute the position from diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java index dfb56b474..175e8e4f1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/gamepad/Gamepad.java @@ -3,6 +3,8 @@ package net.kdt.pojavlaunch.customcontrols.gamepad; import android.os.Handler; import android.os.Looper; +import android.util.Log; +import android.view.Choreographer; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; @@ -59,10 +61,22 @@ public class Gamepad { private final boolean mModifierDigitalTriggers; private boolean mModifierSwappedAxis = true; //Triggers and right stick axis are swapped. - private final Handler inputHandler = new Handler(Looper.getMainLooper()); - private final Runnable switchStateRunnable; + private final Choreographer screenChoreographer; + private long lastFrameTime; public Gamepad(BaseMainActivity gameActivity, InputDevice inputDevice){ + screenChoreographer = Choreographer.getInstance(); + Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + updateGrabbingState(); + tick(frameTimeNanos); + screenChoreographer.postFrameCallback(this); + } + }; + screenChoreographer.postFrameCallback(frameCallback); + lastFrameTime = System.nanoTime(); + //Toast.makeText(gameActivity.getApplicationContext(),"GAMEPAD CREATED", Toast.LENGTH_LONG).show(); for(InputDevice.MotionRange range : inputDevice.getMotionRanges()){ if(range.getAxis() == MotionEvent.AXIS_RTRIGGER @@ -88,46 +102,11 @@ public class Gamepad { pointerView.getDrawable().setFilterBitmap(false); notifyGUISizeChange(gameActivity.getMcScale()); - Runnable handlerRunnable = new Runnable() { - - @Override - public void run() { - updateGrabbingState(); - tick(); - - inputHandler.postDelayed(this, 16); - } - }; - - inputHandler.postDelayed(handlerRunnable, 16); - - //Initialize runnables to be used by the input system, avoiding generating one each time is better memory. - switchStateRunnable = () -> { - currentMap.resetPressedState(); - if(lastGrabbingState){ - currentMap = gameMap; - pointerView.setVisibility(View.INVISIBLE); - mouseSensitivity = 18; - return; - } - - currentMap = menuMap; - sendDirectionalKeycode(currentJoystickDirection, false, gameMap); // removing what we were doing - - gameActivity.mouse_x = CallbackBridge.windowWidth/2; - gameActivity.mouse_y = CallbackBridge.windowHeight/2; - CallbackBridge.sendCursorPos(gameActivity.mouse_x, gameActivity.mouse_y); - placePointerView(CallbackBridge.physicalWidth/2, CallbackBridge.physicalHeight/2); - pointerView.setVisibility(View.VISIBLE); - //sensitivity in menu is MC and HARDWARE resolution dependent - mouseSensitivity = 19 * gameActivity.scaleFactor / gameActivity.sensitivityFactor; - }; } - - private void tick(){ + public void tick(long frameTimeNanos){ //update mouse position if(lastHorizontalValue != 0 || lastVerticalValue != 0){ GamepadJoystick currentJoystick = lastGrabbingState ? leftJoystick : rightJoystick; @@ -136,8 +115,15 @@ public class Gamepad { acceleration = Math.pow(acceleration, mouseMaxAcceleration); if(acceleration > 1) acceleration = 1; - CallbackBridge.mouseX += Math.cos(mouseAngle) * acceleration * mouseSensitivity; - CallbackBridge.mouseY -= Math.sin(mouseAngle) * acceleration * mouseSensitivity; + // Compute delta since last tick time + float deltaX = (float) (Math.cos(mouseAngle) * acceleration * mouseSensitivity); + float deltaY = (float) (Math.sin(mouseAngle) * acceleration * mouseSensitivity); + float deltaTimeScale = ((frameTimeNanos - lastFrameTime) / 16666666f); // Scale of 1 = 60Hz + deltaX *= deltaTimeScale; + deltaY *= deltaTimeScale; + + CallbackBridge.mouseX += deltaX; + CallbackBridge.mouseY -= deltaY; if(!lastGrabbingState){ CallbackBridge.mouseX = MathUtils.clamp(CallbackBridge.mouseX, 0, CallbackBridge.windowWidth); @@ -152,14 +138,36 @@ public class Gamepad { CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); } + // Update last nano time + lastFrameTime = frameTimeNanos; } + /** Update the grabbing state, and change the currentMap, mouse position and sensibility */ private void updateGrabbingState() { boolean lastGrabbingValue = lastGrabbingState; lastGrabbingState = CallbackBridge.isGrabbing(); - if(lastGrabbingValue != lastGrabbingState){ - gameActivity.runOnUiThread(switchStateRunnable); + if(lastGrabbingValue == lastGrabbingState) return; + + // Switch grabbing state then + currentMap.resetPressedState(); + if(lastGrabbingState){ + currentMap = gameMap; + pointerView.setVisibility(View.INVISIBLE); + mouseSensitivity = 18; + return; } + + currentMap = menuMap; + sendDirectionalKeycode(currentJoystickDirection, false, gameMap); // removing what we were doing + + gameActivity.mouse_x = CallbackBridge.windowWidth/2; + gameActivity.mouse_y = CallbackBridge.windowHeight/2; + CallbackBridge.sendCursorPos(gameActivity.mouse_x, gameActivity.mouse_y); + placePointerView(CallbackBridge.physicalWidth/2, CallbackBridge.physicalHeight/2); + pointerView.setVisibility(View.VISIBLE); + // Sensitivity in menu is MC and HARDWARE resolution dependent + mouseSensitivity = 19 * gameActivity.scaleFactor / gameActivity.sensitivityFactor; + } public void update(KeyEvent event){ @@ -188,10 +196,10 @@ public class Gamepad { int lastJoystickDirection = currentJoystickDirection; currentJoystickDirection = currentJoystick.getHeightDirection(event); - if(currentJoystickDirection != lastJoystickDirection){ - sendDirectionalKeycode(lastJoystickDirection, false, getCurrentMap()); - sendDirectionalKeycode(currentJoystickDirection, true, getCurrentMap()); - } + if(currentJoystickDirection == lastJoystickDirection) return; + + sendDirectionalKeycode(lastJoystickDirection, false, getCurrentMap()); + sendDirectionalKeycode(currentJoystickDirection, true, getCurrentMap()); } private void updateAnalogTriggers(MotionEvent event){ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlButtonPopup.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlButtonPopup.java index eb6ff7fb9..6b4071bb3 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlButtonPopup.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlButtonPopup.java @@ -29,7 +29,7 @@ import static net.kdt.pojavlaunch.customcontrols.handleview.ActionPopupWindow.se public class EditControlButtonPopup { - protected Dialog dialog; + protected AlertDialog dialog; protected View v; protected AlertDialog.Builder builder; @@ -75,7 +75,6 @@ public class EditControlButtonPopup { dialog = builder.create(); dialog.setOnShowListener(dialogInterface -> setEditDialogValues()); - dialog.show(); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlDrawerPopup.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlDrawerPopup.java index 593832452..6055be5ed 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlDrawerPopup.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/customcontrols/handleview/EditControlDrawerPopup.java @@ -5,6 +5,7 @@ import android.content.DialogInterface; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Spinner; +import android.widget.Toast; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.customcontrols.ControlData; @@ -32,6 +33,11 @@ public class EditControlDrawerPopup extends EditControlButtonPopup{ checkPassThrough.setVisibility(View.GONE); checkToggle.setVisibility(View.GONE); checkBoxSwipeable.setVisibility(View.GONE); + + (v.findViewById(R.id.editDynamicPositionX_textView)).setVisibility(View.GONE); + (v.findViewById(R.id.editDynamicPositionY_textView)).setVisibility(View.GONE); + editDynamicX.setVisibility(View.GONE); + editDynamicY.setVisibility(View.GONE); } @Override @@ -52,18 +58,27 @@ public class EditControlDrawerPopup extends EditControlButtonPopup{ super.setEditDialogValues(); spinnerOrientation.setSelection(ControlDrawerData.orientationToInt(drawerData.orientation)); + + + //Using the dialog to replace the button behavior allows us not to dismiss the window + dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> { + ControlLayout layout = (ControlLayout) drawer.getParent(); + ControlData controlData = new ControlData(drawerData.properties); + controlData.name = "new"; + layout.addSubButton(drawer, controlData); + + Context ctx = dialog.getContext(); + Toast.makeText(ctx, ctx.getString(R.string.customctrl_add_subbutton_message, + drawer.getDrawerData().buttonProperties.size()), Toast.LENGTH_SHORT).show(); + }); + } @Override protected void setupDialogButtons() { super.setupDialogButtons(); - builder.setNeutralButton(v.getResources().getString(R.string.customctrl_addsubbutton), (dialogInterface, i) -> { - ControlLayout layout = (ControlLayout) drawer.getParent(); - ControlData controlData = new ControlData(drawerData.properties); - controlData.name = "new"; - layout.addSubButton(drawer, controlData); - }); + builder.setNeutralButton(v.getResources().getString(R.string.customctrl_addsubbutton), null); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/extra/ExtraCore.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/extra/ExtraCore.java new file mode 100644 index 000000000..eabb85aa1 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/extra/ExtraCore.java @@ -0,0 +1,129 @@ +package net.kdt.pojavlaunch.extra; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Class providing callback across all of a program + * to allow easy thread safe implementations of UI update without context leak + * + * This class uses a singleton pattern to simplify access to it + */ +public final class ExtraCore { + // No unwanted instantiation + private ExtraCore(){} + + // Store the key-value pair + private final Map valueMap = new ConcurrentHashMap<>(); + + // Store what each ExtraListener listen to + private final Map>> listenerMap = new ConcurrentHashMap<>(); + + // Inner class for singleton implementation + private static class ExtraCoreSingleton { + private static final ExtraCore extraCore = new ExtraCore(); + } + + // All public methods will pass through this one + private static ExtraCore getInstance(){ + return ExtraCoreSingleton.extraCore; + } + + /** + * Set the value associated to a key and trigger all listeners + * @param key The key + * @param value The value + */ + public static void setValue(String key, String value){ + getInstance().valueMap.put(key, value); + ConcurrentLinkedQueue> extraListenerList = getInstance().listenerMap.get(key); + for(WeakReference listener : extraListenerList){ + if(listener.get() == null){ + extraListenerList.remove(listener); + continue; + } + listener.get().notifyDataChanged(key, value); + } + } + + /** @return The value behind the key */ + public static String getValue(String key){ + return getInstance().valueMap.get(key); + } + + /** Remove the key and its value from the valueMap */ + public static void removeValue(String key){ + getInstance().valueMap.remove(key); + } + + /** Remove all values */ + public static void removeAllValues(){ + getInstance().valueMap.clear(); + } + + /** + * Link an ExtraListener to a value + * @param key The value key to look for + * @param listener The ExtraListener to link + */ + public static void addExtraListener(String key, ExtraListener listener){ + ConcurrentLinkedQueue> listenerList = getInstance().listenerMap.get(key); + // Look for new sets + if(listenerList == null){ + listenerList = new ConcurrentLinkedQueue<>(); + getInstance().listenerMap.put(key, listenerList); + } + + // This is kinda naive, I should look for duplicates + listenerList.add(new WeakReference<>(listener)); + } + + /** + * Unlink an ExtraListener from a value. + * Unlink null references found along the way + * @param key The value key to ignore now + * @param listener The ExtraListener to unlink + */ + public static void removeExtraListenerFromValue(String key, ExtraListener listener){ + ConcurrentLinkedQueue> listenerList = getInstance().listenerMap.get(key); + // Look for new sets + if(listenerList == null){ + listenerList = new ConcurrentLinkedQueue<>(); + getInstance().listenerMap.put(key, listenerList); + } + + // Removes all occurrences of ExtraListener and all null references + for(WeakReference listenerWeakReference : listenerList){ + ExtraListener actualListener = listenerWeakReference.get(); + + if(actualListener == null || actualListener == listener){ + listenerList.remove(listenerWeakReference); + } + } + } + + /** + * Unlink all ExtraListeners from a value + * @param key The key to which ExtraListener are linked + */ + public static void removeAllExtraListenersFromValue(String key){ + ConcurrentLinkedQueue> listenerList = getInstance().listenerMap.get(key); + // Look for new sets + if(listenerList == null){ + listenerList = new ConcurrentLinkedQueue<>(); + getInstance().listenerMap.put(key, listenerList); + } + + listenerList.clear(); + } + + /** + * Remove all ExtraListeners from listening to any value + */ + public static void removeAllExtraListeners(){ + getInstance().listenerMap.clear(); + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/extra/ExtraListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/extra/ExtraListener.java new file mode 100644 index 000000000..4c310bdae --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/extra/ExtraListener.java @@ -0,0 +1,26 @@ +package net.kdt.pojavlaunch.extra; + +/** + * Listener class for the ExtraCore + * An ExtraListener can listen to a virtually unlimited amount of values + */ +public abstract class ExtraListener { + + /** + * Called by the ExtraCore after a value is set. + * Technically, it can be triggered from outside but is seems pointless + */ + public final void notifyDataChanged(String key, String value){ + if(onValueSet(key, value)){ + ExtraCore.removeExtraListenerFromValue(key, this); + } + } + + /** + * Called upon a new value being set + * @param key The name of the value + * @param value The new value as a string + * @return Whether you consume the Listener (stop listening) + */ + public abstract boolean onValueSet(String key, String value); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/BackButtonPreference.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/BackButtonPreference.java new file mode 100644 index 000000000..8621cbe2f --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/BackButtonPreference.java @@ -0,0 +1,36 @@ +package net.kdt.pojavlaunch.prefs; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.preference.Preference; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.extra.ExtraCore; + +public class BackButtonPreference extends Preference { + public BackButtonPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public BackButtonPreference(Context context) { + this(context, null); + } + + private void init(){ + if(getTitle() == null){ + setTitle(R.string.preference_back_title); + } + if(getIcon() == null){ + setIcon(R.drawable.ic_arrow_back_white); + } + } + + + @Override + protected void onClick() { + // It is caught by an ExtraListener in the LauncherActivity + ExtraCore.setValue("back_preference", "true"); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/ControlOffsetPreference.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/ControlOffsetPreference.java new file mode 100644 index 000000000..c11c569eb --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/ControlOffsetPreference.java @@ -0,0 +1,127 @@ +package net.kdt.pojavlaunch.prefs; + +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.DEFAULT_PREF; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_BOTTOM_OFFSET; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_LEFT_OFFSET; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_RIGHT_OFFSET; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_CONTROL_TOP_OFFSET; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.AttributeSet; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.preference.Preference; + +import net.kdt.pojavlaunch.R; + +/** Custom preference class displaying a dialog */ +public class ControlOffsetPreference extends Preference { + + private AlertDialog preferenceDialog; + + + + public ControlOffsetPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ControlOffsetPreference(Context context) { + super(context); + init(); + } + + + private void init(){ + // Setup visual values + if(getTitle() == null){ + setTitle(R.string.preference_control_offset_title); + setSummary(R.string.preference_control_offset_description); + } + if(getIcon() == null){ + setIcon(android.R.drawable.radiobutton_off_background); + } + + // Prepare Alert dialog + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()); + dialogBuilder.setView(R.layout.control_offset_preference_dialog); + dialogBuilder.setTitle(getContext().getString(R.string.control_offset_title)); + + dialogBuilder.setPositiveButton(android.R.string.ok, null); + dialogBuilder.setNegativeButton(android.R.string.cancel, null); + + preferenceDialog = dialogBuilder.create(); + + + } + + @Override + protected void onClick() { + preferenceDialog.show(); + + SeekBar topOffsetSeekbar = preferenceDialog.findViewById(R.id.control_offset_top_seekbar); + SeekBar rightOffsetSeekbar = preferenceDialog.findViewById(R.id.control_offset_right_seekbar); + SeekBar bottomOffsetSeekbar = preferenceDialog.findViewById(R.id.control_offset_bottom_seekbar); + SeekBar leftOffsetSeekbar = preferenceDialog.findViewById(R.id.control_offset_left_seekbar); + + TextView topOffsetTextView = preferenceDialog.findViewById(R.id.control_offset_top_textview); + TextView rightOffsetTextView = preferenceDialog.findViewById(R.id.control_offset_right_textview); + TextView bottomOffsetTextView = preferenceDialog.findViewById(R.id.control_offset_bottom_textview); + TextView leftOffsetTextView = preferenceDialog.findViewById(R.id.control_offset_left_textview); + + SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + if(seekBar == topOffsetSeekbar){ + String text = String.format("%s%d", getContext().getString(R.string.control_top_offset), i); + topOffsetTextView.setText(text); + return; + } + if(seekBar == rightOffsetSeekbar){ + String text = String.format("%s%d", getContext().getString(R.string.control_right_offset), i); + rightOffsetTextView.setText(text); + return; + } + if(seekBar == bottomOffsetSeekbar){ + String text = String.format("%s%d", getContext().getString(R.string.control_bottom_offset), i); + bottomOffsetTextView.setText(text); + return; + } + if(seekBar == leftOffsetSeekbar){ + String text = String.format("%s%d", getContext().getString(R.string.control_left_offset), i); + leftOffsetTextView.setText(text); + return; + } + } + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + }; + + topOffsetSeekbar.setOnSeekBarChangeListener(seekBarChangeListener); + rightOffsetSeekbar.setOnSeekBarChangeListener(seekBarChangeListener); + bottomOffsetSeekbar.setOnSeekBarChangeListener(seekBarChangeListener); + leftOffsetSeekbar.setOnSeekBarChangeListener(seekBarChangeListener); + + topOffsetSeekbar.setProgress(PREF_CONTROL_TOP_OFFSET); + rightOffsetSeekbar.setProgress(PREF_CONTROL_RIGHT_OFFSET); + bottomOffsetSeekbar.setProgress(PREF_CONTROL_BOTTOM_OFFSET); + leftOffsetSeekbar.setProgress(PREF_CONTROL_LEFT_OFFSET); + + // Custom writing to preferences + preferenceDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> { + DEFAULT_PREF.edit().putInt("controlTopOffset", topOffsetSeekbar.getProgress()).apply(); + DEFAULT_PREF.edit().putInt("controlRightOffset", rightOffsetSeekbar.getProgress()).apply(); + DEFAULT_PREF.edit().putInt("controlBottomOffset", bottomOffsetSeekbar.getProgress()).apply(); + DEFAULT_PREF.edit().putInt("controlLeftOffset", leftOffsetSeekbar.getProgress()).apply(); + + + preferenceDialog.dismiss(); + }); + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java index f973c8813..f56cbe1c8 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/CustomSeekBarPreference.java @@ -15,10 +15,14 @@ import net.kdt.pojavlaunch.R; public class CustomSeekBarPreference extends SeekBarPreference { + /** The suffix displayed */ private String suffix = ""; + /** Custom minimum value to provide the same behavior as the usual setMin */ private int mMin; + /** The textview associated by default to the preference */ private TextView textView; + public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); TypedArray a = context.obtainStyledAttributes( @@ -59,22 +63,26 @@ public class CustomSeekBarPreference extends SeekBarPreference { SeekBar seekBar = (SeekBar) view.findViewById(R.id.seekbar); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + progress = progress / getSeekBarIncrement(); + progress = progress * getSeekBarIncrement(); + textView.setText(String.valueOf(progress + mMin)); updateTextViewWithSuffix(); } @Override - public void onStartTrackingTouch(SeekBar seekBar) { - - } + public void onStartTrackingTouch(SeekBar seekBar) {} @Override public void onStopTrackingTouch(SeekBar seekBar) { - setValue(seekBar.getProgress() + mMin); - updateTextViewWithSuffix(); + int progress = seekBar.getProgress() / getSeekBarIncrement(); + progress *= getSeekBarIncrement(); + setValue(progress + mMin); + updateTextViewWithSuffix(); } }); @@ -82,7 +90,6 @@ public class CustomSeekBarPreference extends SeekBarPreference { } - private void updateTextViewWithSuffix(){ if(!textView.getText().toString().endsWith(suffix)){ textView.setText(String.format("%s%s", textView.getText(), suffix)); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferenceFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferenceFragment.java deleted file mode 100644 index fca26f1c9..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferenceFragment.java +++ /dev/null @@ -1,104 +0,0 @@ -package net.kdt.pojavlaunch.prefs; - - -import android.graphics.Color; -import android.os.*; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.*; -import net.kdt.pojavlaunch.R; -import net.kdt.pojavlaunch.fragments.LauncherFragment; - -import android.content.*; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import static net.kdt.pojavlaunch.Architecture.is32BitsDevice; -import static net.kdt.pojavlaunch.Tools.getTotalDeviceMemory; -import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE; - -public class LauncherPreferenceFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener -{ - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - view.setBackgroundColor(Color.parseColor("#44000000")); - super.onViewCreated(view, savedInstanceState); - } - - @Override - public void onCreatePreferences(Bundle b, String str) { - addPreferencesFromResource(R.xml.pref_main); - - //Disable notch checking behavior on android 8.1 and below. - findPreference("ignoreNotch").setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && PREF_NOTCH_SIZE != 0); - - CustomSeekBarPreference seek2 = findPreference("timeLongPressTrigger"); - seek2.setRange(100, 1000); - seek2.setValue(LauncherPreferences.PREF_LONGPRESS_TRIGGER); - seek2.setSuffix(" ms"); - - CustomSeekBarPreference seek3 = findPreference("buttonscale"); - seek3.setRange(80, 250); - seek3.setValue((int) LauncherPreferences.PREF_BUTTONSIZE); - seek3.setSuffix(" %"); - - CustomSeekBarPreference seek4 = findPreference("mousescale"); - seek4.setRange(25, 300); - seek4.setValue((int) LauncherPreferences.PREF_MOUSESCALE); - seek4.setSuffix(" %"); - - CustomSeekBarPreference seek5 = findPreference("resolutionRatio"); - seek5.setMin(25); - seek5.setSuffix(" %"); - - CustomSeekBarPreference seek6 = findPreference("mousespeed"); - seek6.setRange(25, 300); - seek6.setValue((int)(LauncherPreferences.PREF_MOUSESPEED*100f)); - seek6.setSuffix(" %"); - - - int maxRAM; - int deviceRam = getTotalDeviceMemory(getContext()); - - - CustomSeekBarPreference seek7 = findPreference("allocation"); - seek7.setMin(256); - - if(is32BitsDevice()) maxRAM = Math.min(1100, deviceRam); - else maxRAM = deviceRam - (deviceRam < 3064 ? 800 : 1024); //To have a minimum for the device to breathe - - seek7.setMax(maxRAM); - seek7.setValue(LauncherPreferences.PREF_RAM_ALLOCATION); - seek7.setSuffix(" MB"); - - // #724 bug fix - if (seek5.getValue() < 25) { - seek5.setValue(100); - } - - EditTextPreference editJVMArgs = findPreference("javaArgs"); - if (editJVMArgs != null) { - editJVMArgs.setOnBindEditTextListener((editText) -> editText.setSingleLine()); - } - } - - @Override - public void onResume() { - super.onResume(); - getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onPause() { - getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - super.onPause(); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences p, String s) { - LauncherPreferences.loadPreferences(getContext()); - } -} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java index ee8261885..501edadb2 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/LauncherPreferences.java @@ -29,6 +29,12 @@ public class LauncherPreferences public static float PREF_MOUSESPEED = 1f; public static int PREF_RAM_ALLOCATION; public static String PREF_DEFAULT_RUNTIME; + public static int PREF_CONTROL_TOP_OFFSET = 0; + public static int PREF_CONTROL_RIGHT_OFFSET = 0; + public static int PREF_CONTROL_BOTTOM_OFFSET = 0; + public static int PREF_CONTROL_LEFT_OFFSET = 0; + + public static void loadPreferences(Context ctx) { //Required for the data folder. Tools.initContextConstants(ctx); @@ -51,6 +57,11 @@ public class LauncherPreferences PREF_DISABLE_GESTURES = DEFAULT_PREF.getBoolean("disableGestures",false); PREF_RAM_ALLOCATION = DEFAULT_PREF.getInt("allocation", findBestRAMAllocation(ctx)); PREF_CUSTOM_JAVA_ARGS = DEFAULT_PREF.getString("javaArgs", ""); + PREF_CONTROL_TOP_OFFSET = DEFAULT_PREF.getInt("controlTopOffset", 0); + PREF_CONTROL_RIGHT_OFFSET = DEFAULT_PREF.getInt("controlRightOffset", 0); + PREF_CONTROL_BOTTOM_OFFSET = DEFAULT_PREF.getInt("controlBottomOffset", 0); + PREF_CONTROL_LEFT_OFFSET = DEFAULT_PREF.getInt("controlTopOffset", 0); + /* if (PREF_CUSTOM_JAVA_ARGS.isEmpty()) { String DEFAULT_JAVA_ARGS = ""; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java new file mode 100644 index 000000000..7c3920a26 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceControlFragment.java @@ -0,0 +1,58 @@ +package net.kdt.pojavlaunch.prefs.screens; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.prefs.CustomSeekBarPreference; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; + +public class LauncherPreferenceControlFragment extends LauncherPreferenceFragment { + + @Override + public void onCreatePreferences(Bundle b, String str) { + // Get values + int longPressTrigger = LauncherPreferences.PREF_LONGPRESS_TRIGGER; + int prefButtonSize = (int) LauncherPreferences.PREF_BUTTONSIZE; + int mouseScale = (int) LauncherPreferences.PREF_MOUSESCALE; + float mouseSpeed = LauncherPreferences.PREF_MOUSESPEED; + + //Triggers a write for some reason which resets the value + addPreferencesFromResource(R.xml.pref_control); + + CustomSeekBarPreference seek2 = findPreference("timeLongPressTrigger"); + seek2.setRange(100, 1000); + seek2.setValue(longPressTrigger); + seek2.setSuffix(" ms"); + + CustomSeekBarPreference seek3 = findPreference("buttonscale"); + seek3.setRange(80, 250); + seek3.setValue(prefButtonSize); + seek3.setSuffix(" %"); + + CustomSeekBarPreference seek4 = findPreference("mousescale"); + seek4.setRange(25, 300); + seek4.setValue(mouseScale); + seek4.setSuffix(" %"); + + CustomSeekBarPreference seek6 = findPreference("mousespeed"); + seek6.setRange(25, 300); + seek6.setValue((int)(mouseSpeed *100f)); + seek6.setSuffix(" %"); + + + computeVisibility(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences p, String s) { + super.onSharedPreferenceChanged(p, s); + computeVisibility(); + } + + private void computeVisibility(){ + CustomSeekBarPreference seek2 = findPreference("timeLongPressTrigger"); + seek2.setVisible(!LauncherPreferences.PREF_DISABLE_GESTURES); + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceExperimentalFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceExperimentalFragment.java new file mode 100644 index 000000000..6b5ea17b8 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceExperimentalFragment.java @@ -0,0 +1,13 @@ +package net.kdt.pojavlaunch.prefs.screens; + +import android.os.Bundle; + +import net.kdt.pojavlaunch.R; + +public class LauncherPreferenceExperimentalFragment extends LauncherPreferenceFragment { + + @Override + public void onCreatePreferences(Bundle b, String str) { + addPreferencesFromResource(R.xml.pref_experimental); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceFragment.java new file mode 100644 index 000000000..531d0d30d --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceFragment.java @@ -0,0 +1,57 @@ +package net.kdt.pojavlaunch.prefs.screens; + + +import android.graphics.Color; +import android.os.*; + +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.*; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; + +import android.content.*; +import android.view.View; +import android.widget.Toast; + +import static net.kdt.pojavlaunch.Architecture.is32BitsDevice; +import static net.kdt.pojavlaunch.Tools.getTotalDeviceMemory; +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE; + +/** + * Preference for the main screen, any sub-screen should inherit this class for consistent behavior, + * overriding only onCreatePreferences + */ +public class LauncherPreferenceFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + view.setBackgroundColor(Color.parseColor("#232323")); + super.onViewCreated(view, savedInstanceState); + } + + @Override + public void onCreatePreferences(Bundle b, String str) { + addPreferencesFromResource(R.xml.pref_main); + } + + @Override + public void onResume() { + super.onResume(); + getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences p, String s) { + LauncherPreferences.loadPreferences(getContext()); + } + + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java new file mode 100644 index 000000000..9b8a41d34 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceJavaFragment.java @@ -0,0 +1,43 @@ +package net.kdt.pojavlaunch.prefs.screens; + +import static net.kdt.pojavlaunch.Architecture.is32BitsDevice; +import static net.kdt.pojavlaunch.Tools.getTotalDeviceMemory; + +import android.os.Bundle; +import android.widget.TextView; + +import androidx.preference.EditTextPreference; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.prefs.CustomSeekBarPreference; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; + +public class LauncherPreferenceJavaFragment extends LauncherPreferenceFragment { + @Override + public void onCreatePreferences(Bundle b, String str) { + int ramAllocation = LauncherPreferences.PREF_RAM_ALLOCATION; + + // Triggers a write for some reason + addPreferencesFromResource(R.xml.pref_java); + + int maxRAM; + int deviceRam = getTotalDeviceMemory(getContext()); + + CustomSeekBarPreference seek7 = findPreference("allocation"); + seek7.setMin(256); + + if(is32BitsDevice()) maxRAM = Math.min(1100, deviceRam); + else maxRAM = deviceRam - (deviceRam < 3064 ? 800 : 1024); //To have a minimum for the device to breathe + + seek7.setMax(maxRAM); + seek7.setValue(ramAllocation); + seek7.setSuffix(" MB"); + + + EditTextPreference editJVMArgs = findPreference("javaArgs"); + if (editJVMArgs != null) { + editJVMArgs.setOnBindEditTextListener(TextView::setSingleLine); + } + + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceMiscellaneousFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceMiscellaneousFragment.java new file mode 100644 index 000000000..ebb665b9a --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceMiscellaneousFragment.java @@ -0,0 +1,12 @@ +package net.kdt.pojavlaunch.prefs.screens; + +import android.os.Bundle; + +import net.kdt.pojavlaunch.R; + +public class LauncherPreferenceMiscellaneousFragment extends LauncherPreferenceFragment { + @Override + public void onCreatePreferences(Bundle b, String str) { + addPreferencesFromResource(R.xml.pref_misc); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java new file mode 100644 index 000000000..ebcbc14a2 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/prefs/screens/LauncherPreferenceVideoFragment.java @@ -0,0 +1,33 @@ +package net.kdt.pojavlaunch.prefs.screens; + +import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE; + +import android.os.Build; +import android.os.Bundle; + +import androidx.preference.PreferenceFragmentCompat; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.prefs.CustomSeekBarPreference; + +/** + * Fragment for any settings video related + */ +public class LauncherPreferenceVideoFragment extends LauncherPreferenceFragment { + @Override + public void onCreatePreferences(Bundle b, String str) { + addPreferencesFromResource(R.xml.pref_video); + + //Disable notch checking behavior on android 8.1 and below. + findPreference("ignoreNotch").setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && PREF_NOTCH_SIZE != 0); + + CustomSeekBarPreference seek5 = findPreference("resolutionRatio"); + seek5.setMin(25); + seek5.setSuffix(" %"); + + // #724 bug fix + if (seek5.getValue() < 25) { + seek5.setValue(100); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloaderTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloaderTask.java index 98b24549e..0c001a0d8 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloaderTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/MinecraftDownloaderTask.java @@ -324,7 +324,7 @@ public class MinecraftDownloaderTask extends AsyncTask p1, View p2, int p3, long p4) { - String version = p1.getItemAtPosition(p3).toString(); - mActivity.mProfile.selectedVersion = version; + mActivity.mProfile.selectedVersion = p1.getItemAtPosition(p3).toString(); PojavProfile.setCurrentProfile(mActivity, mActivity.mProfile); if (PojavProfile.isFileType(mActivity)) { @@ -89,7 +88,6 @@ public class RefreshVersionListTask extends AsyncTask true); - mActivity.mTextVersion.setText(mActivity.getString(R.string.mcl_version_msg,mActivity.mVersionSelector.getSelectedItem())); } private ArrayList filter(JMinecraftVersionList.Version[] list1, File[] list2) { @@ -145,9 +138,8 @@ public class RefreshVersionListTask extends AsyncTask + + diff --git a/app_pojavlauncher/src/main/res/layout/control_offset_preference_dialog.xml b/app_pojavlauncher/src/main/res/layout/control_offset_preference_dialog.xml new file mode 100644 index 000000000..3475e309c --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/control_offset_preference_dialog.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app_pojavlauncher/src/main/res/layout/launcher_main_v4.xml b/app_pojavlauncher/src/main/res/layout/launcher_main_v4.xml index 9a2761a29..52124ed32 100644 --- a/app_pojavlauncher/src/main/res/layout/launcher_main_v4.xml +++ b/app_pojavlauncher/src/main/res/layout/launcher_main_v4.xml @@ -33,17 +33,33 @@ android:orientation="vertical" app:layout_constraintGuide_percent="0.86" /> + + + app:layout_constraintTop_toTopOf="@+id/launchermain_account_image" /> - - - - - + app:layout_constraintWidth_default="percent" + app:layout_constraintWidth_percent="0.25" />