From 32b080261845c7508581f9c452d48ffd2401c450 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Fri, 2 Aug 2019 12:50:57 +0200 Subject: Add skeleton build variant --- .../openvpn/views/DefaultVPNListPreference.java | 39 ++ .../de/blinkt/openvpn/views/FileSelectLayout.java | 189 ++++++ .../blinkt/openvpn/views/MultiLineRadioGroup.java | 164 +++++ .../blinkt/openvpn/views/PagerSlidingTabStrip.java | 732 +++++++++++++++++++++ .../blinkt/openvpn/views/RemoteCNPreference.java | 146 ++++ .../openvpn/views/ScreenSlidePagerAdapter.java | 79 +++ .../java/de/blinkt/openvpn/views/SeekBarTicks.java | 73 ++ .../de/blinkt/openvpn/views/SlidingTabLayout.java | 314 +++++++++ .../de/blinkt/openvpn/views/SlidingTabStrip.java | 207 ++++++ .../java/de/blinkt/openvpn/views/TabBarView.java | 16 + 10 files changed, 1959 insertions(+) create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java create mode 100644 main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java (limited to 'main/src/ui/java/de/blinkt/openvpn/views') diff --git a/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java b/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java new file mode 100644 index 00000000..e8328f5c --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/DefaultVPNListPreference.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.core.ProfileManager; + +import java.util.Collection; + +public class DefaultVPNListPreference extends ListPreference { + public DefaultVPNListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setVPNs(context); + } + + private void setVPNs(Context c) { + ProfileManager pm = ProfileManager.getInstance(c); + Collection profiles = pm.getProfiles(); + CharSequence[] entries = new CharSequence[profiles.size()]; + CharSequence[] entryValues = new CharSequence[profiles.size()];; + + int i=0; + for (VpnProfile p: profiles) + { + entries[i]=p.getName(); + entryValues[i]=p.getUUIDString(); + i++; + } + + setEntries(entries); + setEntryValues(entryValues); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java b/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java new file mode 100644 index 00000000..bc3bd5cd --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/FileSelectLayout.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.content.Intent; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.IOException; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; +import de.blinkt.openvpn.activities.FileSelect; +import de.blinkt.openvpn.core.VpnStatus; +import de.blinkt.openvpn.core.X509Utils; +import de.blinkt.openvpn.fragments.Utils; + +import static android.os.Build.VERSION; +import static android.os.Build.VERSION_CODES; + + +public class FileSelectLayout extends LinearLayout implements OnClickListener { + + + public void parseResponse(Intent data, Context c) { + + try { + String newData = Utils.getFilePickerResult(fileType, data, c); + if (newData!=null) + setData(newData, c); + + if (newData == null) { + String fileData = data.getStringExtra(FileSelect.RESULT_DATA); + setData(fileData, c); + } + + } catch (IOException | SecurityException e) { + VpnStatus.logException(e); + } + } + + public interface FileSelectCallback { + + String getString(int res); + + void startActivityForResult(Intent startFC, int mTaskId); + } + + private boolean mIsCertificate; + private TextView mDataView; + private String mData; + private FileSelectCallback mFragment; + private int mTaskId; + private Button mSelectButton; + private Utils.FileType fileType; + private String mTitle; + private boolean mShowClear; + private TextView mDataDetails; + private Button mShowClearButton; + + + public FileSelectLayout(Context context, AttributeSet attrset) { + super(context, attrset); + + TypedArray ta = context.obtainStyledAttributes(attrset, R.styleable.FileSelectLayout); + + setupViews(ta.getString(R.styleable.FileSelectLayout_fileTitle), ta.getBoolean(R.styleable.FileSelectLayout_certificate, true) + ); + + ta.recycle(); + } + + public FileSelectLayout (Context context, String title, boolean isCertificate, boolean showClear) + { + super(context); + + setupViews(title, isCertificate); + mShowClear = showClear; + + } + + private void setupViews(String title, boolean isCertificate) { + inflate(getContext(), R.layout.file_select, this); + + mTitle = title; + mIsCertificate = isCertificate; + + TextView tView = (TextView) findViewById(R.id.file_title); + tView.setText(mTitle); + + mDataView = (TextView) findViewById(R.id.file_selected_item); + mDataDetails = (TextView) findViewById(R.id.file_selected_description); + mSelectButton = (Button) findViewById(R.id.file_select_button); + mSelectButton.setOnClickListener(this); + + mShowClearButton = (Button) findViewById(R.id.file_clear_button); + mShowClearButton.setOnClickListener(this); + } + + public void setClearable(boolean clearable) + { + mShowClear = clearable; + if (mShowClearButton != null && mData !=null) + mShowClearButton.setVisibility(mShowClear? VISIBLE : GONE); + + } + + + public void setCaller(FileSelectCallback fragment, int i, Utils.FileType ft) { + mTaskId = i; + mFragment = fragment; + fileType = ft; + } + + public void getCertificateFileDialog() { + Intent startFC = new Intent(getContext(), FileSelect.class); + startFC.putExtra(FileSelect.START_DATA, mData); + startFC.putExtra(FileSelect.WINDOW_TITLE, mTitle); + if (fileType == Utils.FileType.PKCS12) + startFC.putExtra(FileSelect.DO_BASE64_ENCODE, true); + if (mShowClear) + startFC.putExtra(FileSelect.SHOW_CLEAR_BUTTON, true); + + mFragment.startActivityForResult(startFC, mTaskId); + } + + + public String getData() { + return mData; + } + + public void setData(String data, Context c) { + mData = data; + if (data == null) { + mDataView.setText(c.getString(R.string.no_data)); + mDataDetails.setText(""); + mShowClearButton.setVisibility(GONE); + } else { + if (mData.startsWith(VpnProfile.DISPLAYNAME_TAG)) { + mDataView.setText(c.getString(R.string.imported_from_file, VpnProfile.getDisplayName(mData))); + } else if (mData.startsWith(VpnProfile.INLINE_TAG)) + mDataView.setText(R.string.inline_file_data); + else + mDataView.setText(data); + if (mIsCertificate) { + mDataDetails.setText(X509Utils.getCertificateFriendlyName(c, data)); + } + + // Show clear button if it should be shown + mShowClearButton.setVisibility(mShowClear? VISIBLE : GONE); + } + + } + + @Override + public void onClick(View v) { + if (v == mSelectButton) { + Intent startFilePicker=null; + if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + startFilePicker = Utils.getFilePickerIntent(getContext(), fileType); + } + + if (startFilePicker == null || Utils.alwaysUseOldFileChooser(v.getContext())) { + getCertificateFileDialog(); + } else { + mFragment.startActivityForResult(startFilePicker, mTaskId); + } + } else if (v == mShowClearButton) { + setData(null, getContext()); + } + } + + + + + public void setShowClear() { + mShowClear = true; + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java b/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java new file mode 100644 index 00000000..8296a644 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/MultiLineRadioGroup.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2012-2018 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RadioGroup; + +import java.util.HashMap; +import java.util.Map; + +public class MultiLineRadioGroup extends RadioGroup { + private Map viewRectMap; + + public MultiLineRadioGroup(Context context) { + this(context, null); + } + + public MultiLineRadioGroup(Context context, AttributeSet attrs) { + super(context, attrs); + + viewRectMap = new HashMap(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + int widthMeasurement = MeasureSpec.getSize(widthMeasureSpec); + int heightMeasurement = MeasureSpec.getSize(heightMeasureSpec); + switch (getOrientation()){ + case HORIZONTAL: + heightMeasurement = findHorizontalHeight(widthMeasureSpec, heightMeasureSpec); + break; + case VERTICAL: + widthMeasurement = findVerticalWidth(widthMeasureSpec, heightMeasureSpec); + break; + } + setMeasuredDimension(widthMeasurement, heightMeasurement); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int count = getChildCount(); + for(int x=0; x < count; ++x) { + View button = getChildAt(x); + Rect dims = viewRectMap.get(button); + button.layout(dims.left, dims.top, dims.right, dims.bottom); + } + } + + private int findHorizontalHeight(int widthMeasureSpec, int heightMeasureSpec){ + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + int maxRight = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight(); + + // create MeasureSpecs to accommodate max space that RadioButtons can occupy + int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxRight - getPaddingLeft(), + MeasureSpec.getMode(widthMeasureSpec)); + int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + parentHeight - (getPaddingTop() + getPaddingBottom()), + MeasureSpec.getMode(heightMeasureSpec)); + + int nextLeft = getPaddingLeft(); + int nextTop = getPaddingTop(); + int maxRowHeight = 0; + viewRectMap.clear(); + // measure and find placement for each RadioButton (results to be used in onLayout() stage) + int count = getChildCount(); + for(int x=0; x < count; ++x){ + View button = getChildAt(x); + measureChild(button, newWidthMeasureSpec, newHeightMeasureSpec); + + maxRowHeight = Math.max(maxRowHeight, button.getMeasuredHeight()); + + // determine RadioButton placement + int nextRight = nextLeft + button.getMeasuredWidth(); + if(nextRight > maxRight){ // if current button will exceed border on this row ... + // ... move to next row + nextLeft = getPaddingLeft(); + nextTop += maxRowHeight; + + // adjust for next row values + nextRight = nextLeft + button.getMeasuredWidth(); + maxRowHeight = button.getMeasuredHeight(); + } + + int nextBottom = nextTop + button.getMeasuredHeight(); + viewRectMap.put(button, new Rect(nextLeft, nextTop, nextRight, nextBottom)); + + // update nextLeft + nextLeft = nextRight; + } + + // height of RadioGroup is a natural by-product of placing all the children + int idealHeight = nextTop + maxRowHeight + getPaddingBottom(); + switch(MeasureSpec.getMode(heightMeasureSpec)){ + case MeasureSpec.UNSPECIFIED: + return idealHeight; + case MeasureSpec.AT_MOST: + return Math.min(idealHeight, parentHeight); + case MeasureSpec.EXACTLY: + default: + return parentHeight; + } + } + + private int findVerticalWidth(int widthMeasureSpec, int heightMeasureSpec){ + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int maxBottom = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom(); + + // create MeasureSpecs to accommodate max space that RadioButtons can occupy + int newWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + parentWidth - (getPaddingLeft() + getPaddingRight()), + MeasureSpec.getMode(widthMeasureSpec)); + int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxBottom - getPaddingTop(), + MeasureSpec.getMode(heightMeasureSpec)); + + int nextTop = getPaddingTop(); + int nextLeft = getPaddingLeft(); + int maxColWidth = 0; + viewRectMap.clear(); + // measure and find placement for each RadioButton (results to be used in onLayout() stage) + int count = getChildCount(); + for(int x=0; x < count; ++x){ + View button = getChildAt(x); + measureChild(button, newWidthMeasureSpec, newHeightMeasureSpec); + + maxColWidth = Math.max(maxColWidth, button.getMeasuredWidth()); + + // determine RadioButton placement + int nextBottom = nextTop + button.getMeasuredHeight(); + if(nextBottom > maxBottom){ // if current button will exceed border for this column ... + // ... move to next column + nextTop = getPaddingTop(); + nextLeft += maxColWidth; + + // adjust for next row values + nextBottom = nextTop + button.getMeasuredHeight(); + maxColWidth = button.getMeasuredWidth(); + } + + int nextRight = nextLeft + button.getMeasuredWidth(); + viewRectMap.put(button, new Rect(nextLeft, nextTop, nextRight, nextBottom)); + + // update nextTop + nextTop = nextBottom; + } + + // width of RadioGroup is a natural by-product of placing all the children + int idealWidth = nextLeft + maxColWidth + getPaddingRight(); + switch(MeasureSpec.getMode(widthMeasureSpec)){ + case MeasureSpec.UNSPECIFIED: + return idealWidth; + case MeasureSpec.AT_MOST: + return Math.min(idealWidth, parentWidth); + case MeasureSpec.EXACTLY: + default: + return parentWidth; + } + } +} \ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java b/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java new file mode 100644 index 00000000..ab8598c6 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/PagerSlidingTabStrip.java @@ -0,0 +1,732 @@ +/* + * Copyright (C) 2013 Andreas Stuetz + * + * 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 de.blinkt.openvpn.views; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v4n.view.ViewPager; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Pair; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + + +import java.util.Locale; + +import de.blinkt.openvpn.R; + +public class PagerSlidingTabStrip extends HorizontalScrollView implements TabBarView { + + private static final float OPAQUE = 1.0f; + private static final float HALF_TRANSP = 0.5f; + + public interface CustomTabProvider { + public View getCustomTabView(ViewGroup parent, int position); + } + + // @formatter:off + private static final int[] ATTRS = new int[]{ + android.R.attr.textSize, + android.R.attr.textColor, + android.R.attr.paddingLeft, + android.R.attr.paddingRight, + }; + // @formatter:on + + private final PagerAdapterObserver adapterObserver = new PagerAdapterObserver(); + + //These indexes must be related with the ATTR array above + private static final int TEXT_SIZE_INDEX = 0; + private static final int TEXT_COLOR_INDEX = 1; + private static final int PADDING_LEFT_INDEX = 2; + private static final int PADDING_RIGHT_INDEX = 3; + + private LinearLayout.LayoutParams defaultTabLayoutParams; + private LinearLayout.LayoutParams expandedTabLayoutParams; + + private final PageListener pageListener = new PageListener(); + public ViewPager.OnPageChangeListener delegatePageListener; + + private LinearLayout tabsContainer; + private ViewPager pager; + + private int tabCount; + + private int currentPosition = 0; + private float currentPositionOffset = 0f; + + private Paint rectPaint; + private Paint dividerPaint; + + private int indicatorColor; + private int indicatorHeight = 2; + + private int underlineHeight = 0; + private int underlineColor; + + private int dividerWidth = 0; + private int dividerPadding = 0; + private int dividerColor; + + private int tabPadding = 12; + private int tabTextSize = 14; + private ColorStateList tabTextColor = null; + private float tabTextAlpha = HALF_TRANSP; + private float tabTextSelectedAlpha = OPAQUE; + + private int paddingLeft = 0; + private int paddingRight = 0; + + private boolean shouldExpand = false; + private boolean textAllCaps = true; + private boolean isPaddingMiddle = false; + + private Typeface tabTypeface = null; + private int tabTypefaceStyle = Typeface.BOLD; + private int tabTypefaceSelectedStyle = Typeface.BOLD; + + private int scrollOffset; + private int lastScrollX = 0; + + private int tabBackgroundResId = R.drawable.slidingtab_background; + + private Locale locale; + + public PagerSlidingTabStrip(Context context) { + this(context, null); + } + + public PagerSlidingTabStrip(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PagerSlidingTabStrip(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setFillViewport(true); + setWillNotDraw(false); + tabsContainer = new LinearLayout(context); + tabsContainer.setOrientation(LinearLayout.HORIZONTAL); + tabsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + addView(tabsContainer); + + //Default color will be 'textColorPrimary' + int colorPrimary = context.getResources().getColor(android.R.color.primary_text_dark); + setTextColor(colorPrimary); + underlineColor = colorPrimary; + dividerColor = colorPrimary; + indicatorColor = colorPrimary; + + + DisplayMetrics dm = getResources().getDisplayMetrics(); + scrollOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, scrollOffset, dm); + indicatorHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, indicatorHeight, dm); + underlineHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, underlineHeight, dm); + dividerPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerPadding, dm); + tabPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, tabPadding, dm); + dividerWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerWidth, dm); + tabTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, tabTextSize, dm); + + // get system attrs (android:textSize and android:textColor) + TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + tabTextSize = a.getDimensionPixelSize(TEXT_SIZE_INDEX, tabTextSize); + ColorStateList colorStateList = a.getColorStateList(TEXT_COLOR_INDEX); + if (colorStateList != null) { + tabTextColor = colorStateList; + } + paddingLeft = a.getDimensionPixelSize(PADDING_LEFT_INDEX, paddingLeft); + paddingRight = a.getDimensionPixelSize(PADDING_RIGHT_INDEX, paddingRight); + a.recycle(); + + //In case we have the padding they must be equal so we take the biggest + if (paddingRight < paddingLeft) { + paddingRight = paddingLeft; + } + + if (paddingLeft < paddingRight) { + paddingLeft = paddingRight; + } + + // get custom attrs + a = context.obtainStyledAttributes(attrs, R.styleable.PagerSlidingTabStrip); + indicatorColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsIndicatorColor, indicatorColor); + underlineColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsUnderlineColor, underlineColor); + dividerColor = a.getColor(R.styleable.PagerSlidingTabStrip_pstsDividerColor, dividerColor); + dividerWidth = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerWidth, dividerWidth); + indicatorHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsIndicatorHeight, indicatorHeight); + underlineHeight = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsUnderlineHeight, underlineHeight); + dividerPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsDividerPadding, dividerPadding); + tabPadding = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsTabPaddingLeftRight, tabPadding); + tabBackgroundResId = a.getResourceId(R.styleable.PagerSlidingTabStrip_pstsTabBackground, tabBackgroundResId); + shouldExpand = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsShouldExpand, shouldExpand); + scrollOffset = a.getDimensionPixelSize(R.styleable.PagerSlidingTabStrip_pstsScrollOffset, scrollOffset); + textAllCaps = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsTextAllCaps, textAllCaps); + isPaddingMiddle = a.getBoolean(R.styleable.PagerSlidingTabStrip_pstsPaddingMiddle, isPaddingMiddle); + tabTypefaceStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextStyle, Typeface.BOLD); + tabTypefaceSelectedStyle = a.getInt(R.styleable.PagerSlidingTabStrip_pstsTextSelectedStyle, Typeface.BOLD); + tabTextAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextAlpha, HALF_TRANSP); + tabTextSelectedAlpha = a.getFloat(R.styleable.PagerSlidingTabStrip_pstsTextSelectedAlpha, OPAQUE); + a.recycle(); + + rectPaint = new Paint(); + rectPaint.setAntiAlias(true); + rectPaint.setStyle(Style.FILL); + + + dividerPaint = new Paint(); + dividerPaint.setAntiAlias(true); + dividerPaint.setStrokeWidth(dividerWidth); + + defaultTabLayoutParams = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + expandedTabLayoutParams = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f); + + if (locale == null) { + locale = getResources().getConfiguration().locale; + } + } + + public void setViewPager(ViewPager pager) { + this.pager = pager; + if (pager.getAdapter() == null) { + throw new IllegalStateException("ViewPager does not have adapter instance."); + } + + pager.setOnPageChangeListener(pageListener); + pager.getAdapter().registerDataSetObserver(adapterObserver); + adapterObserver.setAttached(true); + notifyDataSetChanged(); + } + + public void notifyDataSetChanged() { + tabsContainer.removeAllViews(); + tabCount = pager.getAdapter().getCount(); + View tabView; + for (int i = 0; i < tabCount; i++) { + + if (pager.getAdapter() instanceof CustomTabProvider) { + tabView = ((CustomTabProvider) pager.getAdapter()).getCustomTabView(this, i); + } else { + tabView = LayoutInflater.from(getContext()).inflate(R.layout.padersliding_tab, this, false); + } + + CharSequence title = pager.getAdapter().getPageTitle(i); + + addTab(i, title, tabView); + } + + updateTabStyles(); + getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + @Override + public void onGlobalLayout() { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + getViewTreeObserver().removeGlobalOnLayoutListener(this); + } else { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + + currentPosition = pager.getCurrentItem(); + currentPositionOffset = 0f; + scrollToChild(currentPosition, 0); + updateSelection(currentPosition); + } + }); + } + + private void addTab(final int position, CharSequence title, View tabView) { + TextView textView = (TextView) tabView.findViewById(R.id.tab_title); + if (textView != null) { + if (title != null) textView.setText(title); + float alpha = pager.getCurrentItem() == position ? tabTextSelectedAlpha : tabTextAlpha; + ViewCompat.setAlpha(textView, alpha); + } + + tabView.setFocusable(true); + tabView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (pager.getCurrentItem() != position) { + View tab = tabsContainer.getChildAt(pager.getCurrentItem()); + notSelected(tab); + pager.setCurrentItem(position); + } + } + }); + + tabView.setPadding(tabPadding, tabView.getPaddingTop(), tabPadding, tabView.getPaddingBottom()); + tabsContainer.addView(tabView, position, shouldExpand ? expandedTabLayoutParams : defaultTabLayoutParams); + } + + private void updateTabStyles() { + for (int i = 0; i < tabCount; i++) { + View v = tabsContainer.getChildAt(i); + v.setBackgroundResource(tabBackgroundResId); + TextView tab_title = (TextView) v.findViewById(R.id.tab_title); + + if (tab_title != null) { + tab_title.setTextSize(TypedValue.COMPLEX_UNIT_PX, tabTextSize); + tab_title.setTypeface(tabTypeface, pager.getCurrentItem() == i ? tabTypefaceSelectedStyle : tabTypefaceStyle); + if (tabTextColor != null) { + tab_title.setTextColor(tabTextColor); + } + // setAllCaps() is only available from API 14, so the upper case is made manually if we are on a + // pre-ICS-build + if (textAllCaps) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + tab_title.setAllCaps(true); + } else { + tab_title.setText(tab_title.getText().toString().toUpperCase(locale)); + } + } + } + } + + } + + private void scrollToChild(int position, int offset) { + if (tabCount == 0) { + return; + } + + int newScrollX = tabsContainer.getChildAt(position).getLeft() + offset; + if (position > 0 || offset > 0) { + + //Half screen offset. + //- Either tabs start at the middle of the view scrolling straight away + //- Or tabs start at the begging (no padding) scrolling when indicator gets + // to the middle of the view width + newScrollX -= scrollOffset; + Pair lines = getIndicatorCoordinates(); + newScrollX += ((lines.second - lines.first) / 2); + } + + if (newScrollX != lastScrollX) { + lastScrollX = newScrollX; + scrollTo(newScrollX, 0); + } + } + + private Pair getIndicatorCoordinates() { + // default: line below current tab + View currentTab = tabsContainer.getChildAt(currentPosition); + float lineLeft = currentTab.getLeft(); + float lineRight = currentTab.getRight(); + + // if there is an offset, start interpolating left and right coordinates between current and next tab + if (currentPositionOffset > 0f && currentPosition < tabCount - 1) { + + View nextTab = tabsContainer.getChildAt(currentPosition + 1); + final float nextTabLeft = nextTab.getLeft(); + final float nextTabRight = nextTab.getRight(); + + lineLeft = (currentPositionOffset * nextTabLeft + (1f - currentPositionOffset) * lineLeft); + lineRight = (currentPositionOffset * nextTabRight + (1f - currentPositionOffset) * lineRight); + } + return new Pair(lineLeft, lineRight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (isInEditMode() || tabCount == 0) { + return; + } + + final int height = getHeight(); + // draw indicator line + rectPaint.setColor(indicatorColor); + Pair lines = getIndicatorCoordinates(); + canvas.drawRect(lines.first + paddingLeft, height - indicatorHeight, lines.second + paddingRight, height, rectPaint); + // draw underline + rectPaint.setColor(underlineColor); + canvas.drawRect(paddingLeft, height - underlineHeight, tabsContainer.getWidth() + paddingRight, height, rectPaint); + // draw divider + if (dividerWidth != 0) { + dividerPaint.setStrokeWidth(dividerWidth); + dividerPaint.setColor(dividerColor); + for (int i = 0; i < tabCount - 1; i++) { + View tab = tabsContainer.getChildAt(i); + canvas.drawLine(tab.getRight(), dividerPadding, tab.getRight(), height - dividerPadding, dividerPaint); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (isPaddingMiddle) { + //Make sure tabContainer is bigger than the HorizontalScrollView to be able to scroll + tabsContainer.setMinimumWidth(getWidth()); + int halfFirstTab = 0; + if (tabsContainer.getChildCount() > 0) { + halfFirstTab = (tabsContainer.getChildAt(0).getWidth() / 2); + } + //The user choose the tabs to start in the middle of the view width (padding) + paddingLeft = paddingRight = getWidth() / 2 - halfFirstTab; + //Clipping padding to false to see the tabs while we pass them swiping + setClipToPadding(false); + } + + if (scrollOffset == 0) scrollOffset = getWidth() / 2 - paddingLeft; + setPadding(paddingLeft, getPaddingTop(), paddingRight, getPaddingBottom()); + super.onLayout(changed, l, t, r, b); + } + + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + this.delegatePageListener = listener; + } + + private class PageListener implements ViewPager.OnPageChangeListener { + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + currentPosition = position; + currentPositionOffset = positionOffset; + int offset = tabCount > 0 ? (int) (positionOffset * tabsContainer.getChildAt(position).getWidth()) : 0; + scrollToChild(position, offset); + invalidate(); + if (delegatePageListener != null) { + delegatePageListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + if (state == ViewPager.SCROLL_STATE_IDLE) { + scrollToChild(pager.getCurrentItem(), 0); + } + //Full alpha for current item + View currentTab = tabsContainer.getChildAt(pager.getCurrentItem()); + selected(currentTab); + //Half transparent for prev item + if (pager.getCurrentItem() - 1 >= 0) { + View prevTab = tabsContainer.getChildAt(pager.getCurrentItem() - 1); + notSelected(prevTab); + } + //Half transparent for next item + if (pager.getCurrentItem() + 1 <= pager.getAdapter().getCount() - 1) { + View nextTab = tabsContainer.getChildAt(pager.getCurrentItem() + 1); + notSelected(nextTab); + } + + if (delegatePageListener != null) { + delegatePageListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + updateSelection(position); + if (delegatePageListener != null) { + delegatePageListener.onPageSelected(position); + } + } + + } + + private void updateSelection(int position) { + for (int i = 0; i < tabCount; ++i) { + View tv = tabsContainer.getChildAt(i); + tv.setSelected(i == position); + } + } + + private void notSelected(View tab) { + TextView title = (TextView) tab.findViewById(R.id.tab_title); + if (title != null) { + title.setTypeface(tabTypeface, tabTypefaceStyle); + ViewCompat.setAlpha(title, tabTextAlpha); + } + } + + private void selected(View tab) { + TextView title = (TextView) tab.findViewById(R.id.tab_title); + if (title != null) { + title.setTypeface(tabTypeface, tabTypefaceSelectedStyle); + ViewCompat.setAlpha(title, tabTextSelectedAlpha); + } + } + + private class PagerAdapterObserver extends DataSetObserver { + + private boolean attached = false; + + @Override + public void onChanged() { + notifyDataSetChanged(); + } + + public void setAttached(boolean attached) { + this.attached = attached; + } + + public boolean isAttached() { + return attached; + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (pager != null) { + if (!adapterObserver.isAttached()) { + pager.getAdapter().registerDataSetObserver(adapterObserver); + adapterObserver.setAttached(true); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (pager != null) { + if (adapterObserver.isAttached()) { + pager.getAdapter().unregisterDataSetObserver(adapterObserver); + adapterObserver.setAttached(false); + } + } + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + super.onRestoreInstanceState(savedState.getSuperState()); + currentPosition = savedState.currentPosition; + if (currentPosition != 0 && tabsContainer.getChildCount() > 0) { + notSelected(tabsContainer.getChildAt(0)); + selected(tabsContainer.getChildAt(currentPosition)); + } + requestLayout(); + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState savedState = new SavedState(superState); + savedState.currentPosition = currentPosition; + return savedState; + } + + static class SavedState extends BaseSavedState { + int currentPosition; + + public SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + currentPosition = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(currentPosition); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public int getIndicatorColor() { + return this.indicatorColor; + } + + public int getIndicatorHeight() { + return indicatorHeight; + } + + public int getUnderlineColor() { + return underlineColor; + } + + public int getDividerColor() { + return dividerColor; + } + + public int getDividerWidth() { + return dividerWidth; + } + + public int getUnderlineHeight() { + return underlineHeight; + } + + public int getDividerPadding() { + return dividerPadding; + } + + public int getScrollOffset() { + return scrollOffset; + } + + public boolean getShouldExpand() { + return shouldExpand; + } + + public int getTextSize() { + return tabTextSize; + } + + public boolean isTextAllCaps() { + return textAllCaps; + } + + public ColorStateList getTextColor() { + return tabTextColor; + } + + public int getTabBackground() { + return tabBackgroundResId; + } + + public int getTabPaddingLeftRight() { + return tabPadding; + } + + public void setIndicatorColor(int indicatorColor) { + this.indicatorColor = indicatorColor; + invalidate(); + } + + public void setIndicatorColorResource(int resId) { + this.indicatorColor = getResources().getColor(resId); + invalidate(); + } + + public void setIndicatorHeight(int indicatorLineHeightPx) { + this.indicatorHeight = indicatorLineHeightPx; + invalidate(); + } + + public void setUnderlineColor(int underlineColor) { + this.underlineColor = underlineColor; + invalidate(); + } + + public void setUnderlineColorResource(int resId) { + this.underlineColor = getResources().getColor(resId); + invalidate(); + } + + public void setDividerColor(int dividerColor) { + this.dividerColor = dividerColor; + invalidate(); + } + + public void setDividerColorResource(int resId) { + this.dividerColor = getResources().getColor(resId); + invalidate(); + } + + public void setDividerWidth(int dividerWidthPx) { + this.dividerWidth = dividerWidthPx; + invalidate(); + } + + public void setUnderlineHeight(int underlineHeightPx) { + this.underlineHeight = underlineHeightPx; + invalidate(); + } + + public void setDividerPadding(int dividerPaddingPx) { + this.dividerPadding = dividerPaddingPx; + invalidate(); + } + + public void setScrollOffset(int scrollOffsetPx) { + this.scrollOffset = scrollOffsetPx; + invalidate(); + } + + public void setShouldExpand(boolean shouldExpand) { + this.shouldExpand = shouldExpand; + if (pager != null) { + requestLayout(); + } + } + + public void setAllCaps(boolean textAllCaps) { + this.textAllCaps = textAllCaps; + } + + public void setTextSize(int textSizePx) { + this.tabTextSize = textSizePx; + updateTabStyles(); + } + + public void setTextColor(int textColor) { + setTextColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{textColor})); + } + + public void setTextColor(ColorStateList colorStateList) { + this.tabTextColor = colorStateList; + updateTabStyles(); + } + + public void setTextColorResource(int resId) { + setTextColor(getResources().getColor(resId)); + } + + public void setTextColorStateListResource(int resId) { + setTextColor(getResources().getColorStateList(resId)); + } + + public void setTypeface(Typeface typeface, int style) { + this.tabTypeface = typeface; + this.tabTypefaceSelectedStyle = style; + updateTabStyles(); + } + + public void setTabBackground(int resId) { + this.tabBackgroundResId = resId; + } + + public void setTabPaddingLeftRight(int paddingPx) { + this.tabPadding = paddingPx; + updateTabStyles(); + } +} \ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java b/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java new file mode 100644 index 00000000..4b477f9c --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/RemoteCNPreference.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import de.blinkt.openvpn.R; +import de.blinkt.openvpn.VpnProfile; + +public class RemoteCNPreference extends DialogPreference { + + + private Spinner mSpinner; + private EditText mEditText; + private int mDNType; + private String mDn; + private TextView mRemoteTLSNote; + //private ScrollView mScrollView; + + public RemoteCNPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setDialogLayoutResource(R.layout.tlsremote); + + } + + @Override + protected void onBindDialogView(View view) { + + super.onBindDialogView(view); + + mEditText = (EditText) view.findViewById(R.id.tlsremotecn); + mSpinner = (Spinner) view.findViewById(R.id.x509verifytype); + mRemoteTLSNote = (TextView) view.findViewById(R.id.tlsremotenote); + //mScrollView = (ScrollView) view.findViewById(R.id.tlsremotescroll); + if(mDn!=null) + mEditText.setText(mDn); + + populateSpinner(); + + } + + + + public String getCNText() { + return mDn; + } + + public int getAuthtype() { + return mDNType; + } + + public void setDN(String dn) { + mDn = dn; + if(mEditText!=null) + mEditText.setText(dn); + } + + public void setAuthType(int x509authtype) { + mDNType = x509authtype; + if (mSpinner!=null) + populateSpinner(); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult) { + String dn = mEditText.getText().toString(); + int authtype = getAuthTypeFromSpinner(); + if (callChangeListener(new Pair(authtype, dn))) { + mDn = dn; + mDNType = authtype; + } + } + } + + private void populateSpinner() { + ArrayAdapter authtypes = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_item); + authtypes.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + authtypes.add(getContext().getString(R.string.complete_dn)); + authtypes.add(getContext().getString(R.string.rdn)); + authtypes.add(getContext().getString(R.string.rdn_prefix)); + if ((mDNType == VpnProfile.X509_VERIFY_TLSREMOTE || mDNType == VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING) + && !(mDn==null || "".equals(mDn))) { + authtypes.add(getContext().getString(R.string.tls_remote_deprecated)); + mRemoteTLSNote.setVisibility(View.VISIBLE); + } else { + mRemoteTLSNote.setVisibility(View.GONE); + } + mSpinner.setAdapter(authtypes); + mSpinner.setSelection(getSpinnerPositionFromAuthTYPE()); + } + + private int getSpinnerPositionFromAuthTYPE() { + switch (mDNType) { + case VpnProfile.X509_VERIFY_TLSREMOTE_DN: + return 0; + case VpnProfile.X509_VERIFY_TLSREMOTE_RDN: + return 1; + case VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX: + return 2; + case VpnProfile.X509_VERIFY_TLSREMOTE_COMPAT_NOREMAPPING: + case VpnProfile.X509_VERIFY_TLSREMOTE: + if (mDn==null || "".equals(mDn)) + return 1; + else + return 3; + + + default: + return 0; + } + } + + private int getAuthTypeFromSpinner() { + int pos = mSpinner.getSelectedItemPosition(); + switch (pos) { + case 0: + return VpnProfile.X509_VERIFY_TLSREMOTE_DN; + case 1: + return VpnProfile.X509_VERIFY_TLSREMOTE_RDN; + case 2: + return VpnProfile.X509_VERIFY_TLSREMOTE_RDN_PREFIX; + case 3: + // This is the tls-remote entry, only visible if mDntype is a + // tls-remote type + return mDNType; + default: + return VpnProfile.X509_VERIFY_TLSREMOTE; + } + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java b/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java new file mode 100644 index 00000000..38bb54b5 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/ScreenSlidePagerAdapter.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.res.Resources; +import android.os.Bundle; +import android.support.annotation.StringRes; +import android.support.v4n.app.FragmentStatePagerAdapter; + +import java.util.Vector; + +import de.blinkt.openvpn.activities.MainActivity; + +/** +* Created by arne on 18.11.14. +*/ +public class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + + private final Resources res; + private Bundle mFragmentArguments; + + public void setFragmentArgs(Bundle fragmentArguments) { + mFragmentArguments = fragmentArguments; + } + + static class Tab { + public Class fragmentClass; + String mName; + + public Tab(Class fClass, String name){ + mName = name; + fragmentClass = fClass; + } + + } + + + private Vector mTabs = new Vector(); + + public ScreenSlidePagerAdapter(FragmentManager fm, Context c) { + super(fm); + res = c.getResources(); + } + + @Override + public Fragment getItem(int position) { + try { + Fragment fragment = mTabs.get(position).fragmentClass.newInstance(); + if (mFragmentArguments!=null) + fragment.setArguments(mFragmentArguments); + return fragment; + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabs.get(position).mName; + } + + @Override + public int getCount() { + return mTabs.size(); + } + + public void addTab(@StringRes int name, Class fragmentClass) { + mTabs.add(new Tab(fragmentClass, res.getString(name))); + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java b/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java new file mode 100644 index 00000000..347ce708 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/SeekBarTicks.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.ViewConfiguration; +import android.widget.SeekBar; + +public class SeekBarTicks extends SeekBar { + private Paint mTickPaint; + private float mTickHeight; + + private float tickHeightRatio = 0.6f; + + public SeekBarTicks(Context context, AttributeSet attrs) { + super (context, attrs); + + initTicks (context, attrs, android.R.attr.seekBarStyle); + } + + + public SeekBarTicks(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + initTicks (context, attrs, defStyle); + + /*mTickHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + tickHeightDP, + ctx.getResources().getDisplayMetrics()); */ + } + + private void initTicks(Context context, AttributeSet attrs, int defStyle) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.secondaryProgress }, defStyle, 0); + + mTickPaint = new Paint(); + //noinspection deprecation + mTickPaint.setColor( context.getResources().getColor(android.R.color.black)); + a.recycle(); + } + + + @Override + protected synchronized void onDraw(Canvas canvas) { + drawTicks(canvas); + super.onDraw(canvas); + } + + private void drawTicks(Canvas canvas) { + + final int available = getWidth() - getPaddingLeft() - getPaddingRight(); + final int availableHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + + int extrapadding = (int) ((availableHeight- (availableHeight * tickHeightRatio))/2); + + int tickSpacing = available / (getMax() ); + + for (int i = 1; i < getMax(); i++) { + final float x = getPaddingLeft() + i * tickSpacing; + + canvas.drawLine(x, getPaddingTop()+extrapadding, x, getHeight()-getPaddingBottom()-extrapadding, mTickPaint); + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java new file mode 100644 index 00000000..ea3b1c26 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabLayout.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2013 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 de.blinkt.openvpn.views; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.support.v4n.view.PagerAdapter; +import android.support.v4n.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as to + * the user's scroll progress. + *

+ * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. + *

+ * The colors can be customized in two ways. The first and simplest is to provide an array of colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + *

+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + */ +public class SlidingTabLayout extends HorizontalScrollView implements TabBarView { + + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + /** + * @return return the color of the divider drawn to the right of {@code position}. + */ + int getDividerColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + mTabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(true); + } + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final View.OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + + mTabStrip.addView(tabView); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements View.OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} diff --git a/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java new file mode 100644 index 00000000..88bfb9a3 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/SlidingTabStrip.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2013 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 de.blinkt.openvpn.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private final Paint mDividerPaint; + private final float mDividerHeight; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor, + DEFAULT_DIVIDER_COLOR_ALPHA)); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + + mDividerHeight = DEFAULT_DIVIDER_HEIGHT; + mDividerPaint = new Paint(); + mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density)); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + + // Vertical separators between the titles + int separatorTop = (height - dividerHeightPx) / 2; + for (int i = 0; i < childCount - 1; i++) { + View child = getChildAt(i); + mDividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(child.getRight(), separatorTop, child.getRight(), + separatorTop + dividerHeightPx, mDividerPaint); + } + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + private int[] mDividerColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return mDividerColors[position % mDividerColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + + void setDividerColors(int... colors) { + mDividerColors = colors; + } + } +} \ No newline at end of file diff --git a/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java b/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java new file mode 100644 index 00000000..71f03c03 --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/views/TabBarView.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2012-2016 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.views; + +import android.support.v4n.view.ViewPager; + +/** + * Created by arne on 18.11.14. + */ +public interface TabBarView { + + void setViewPager(ViewPager mPager); +} -- cgit v1.2.3