diff --git a/.idea/dictionaries/Administrator.xml b/.idea/dictionaries/Administrator.xml new file mode 100644 index 00000000..d87a1619 --- /dev/null +++ b/.idea/dictionaries/Administrator.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..d64dffbf --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..3b312839 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/Material.iml b/Material.iml index 0bb6048a..5da3c893 100644 --- a/Material.iml +++ b/Material.iml @@ -1,5 +1,5 @@ - + diff --git a/app/app.iml b/app/app.iml index 3d634f67..4d8d3d12 100644 --- a/app/app.iml +++ b/app/app.iml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/java/com/rey/material/demo/ButtonFragment.java b/app/src/main/java/com/rey/material/demo/ButtonFragment.java index 472ea988..6de9f773 100644 --- a/app/src/main/java/com/rey/material/demo/ButtonFragment.java +++ b/app/src/main/java/com/rey/material/demo/ButtonFragment.java @@ -47,7 +47,11 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa @Override public void onClick(View v) { - Toast.makeText(getActivity(), "Button Clicked!\nEvent's fired when in anim end.", Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), "Button Clicked!\nEvent's fired when in anim end.", Toast.LENGTH_SHORT).show(); + if(v instanceof FloatingActionButton){ + FloatingActionButton bt = (FloatingActionButton)v; + bt.setLineMorphingState((bt.getLineMorphingState() + 1) % 2, true); + } } }; @@ -55,7 +59,11 @@ public void onClick(View v) { @Override public void onClick(View v) { - Toast.makeText(getActivity(), "Button Clicked!\nEvent's fired when out anim end.", Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), "Button Clicked!\nEvent's fired when out anim end.", Toast.LENGTH_SHORT).show(); + if(v instanceof FloatingActionButton){ + FloatingActionButton bt = (FloatingActionButton)v; + bt.setLineMorphingState((bt.getLineMorphingState() + 1) % 2, true); + } } }; diff --git a/app/src/main/java/com/rey/material/demo/CustomViewPager.java b/app/src/main/java/com/rey/material/demo/CustomViewPager.java new file mode 100644 index 00000000..4d16a4c7 --- /dev/null +++ b/app/src/main/java/com/rey/material/demo/CustomViewPager.java @@ -0,0 +1,33 @@ +package com.rey.material.demo; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.View; + +import com.rey.material.widget.Slider; +import com.rey.material.widget.Switch; + +/** + * Created by Rey on 3/18/2015. + */ +public class CustomViewPager extends ViewPager{ + + public CustomViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomViewPager(Context context) { + super(context); + } + + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + return super.canScroll(v, checkV, dx, x, y) || (checkV && customCanScroll(v)); + } + + protected boolean customCanScroll(View v) { + if (v instanceof Slider || v instanceof Switch) + return true; + return false; + } +} diff --git a/app/src/main/java/com/rey/material/demo/FabFragment.java b/app/src/main/java/com/rey/material/demo/FabFragment.java new file mode 100644 index 00000000..c57d891c --- /dev/null +++ b/app/src/main/java/com/rey/material/demo/FabFragment.java @@ -0,0 +1,64 @@ +package com.rey.material.demo; + +import android.annotation.TargetApi; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.rey.material.widget.FloatingActionButton; + +public class FabFragment extends Fragment{ + + + public static FabFragment newInstance(){ + FabFragment fragment = new FabFragment(); + + return fragment; + } + + private Drawable[] mDrawables = new Drawable[2]; + private int index = 0; + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_fab, container, false); + + final FloatingActionButton fab_line = (FloatingActionButton)v.findViewById(R.id.fab_line); + fab_line.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + fab_line.setLineMorphingState((fab_line.getLineMorphingState() + 1) % 2, true); + } + }); + + final FloatingActionButton fab_image = (FloatingActionButton)v.findViewById(R.id.fab_image); + mDrawables[0] = v.getResources().getDrawable(R.drawable.ic_autorenew_white_24dp); + mDrawables[1] = v.getResources().getDrawable(R.drawable.ic_done_white_24dp); + fab_image.setIcon(mDrawables[index], false); + fab_image.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + index = (index + 1) % 2; + fab_image.setIcon(mDrawables[index], true); + } + }); + + return v; + } + + @Override + public void onPause() { + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + } + +} diff --git a/app/src/main/java/com/rey/material/demo/MainActivity.java b/app/src/main/java/com/rey/material/demo/MainActivity.java index 3178d7ff..70607e17 100644 --- a/app/src/main/java/com/rey/material/demo/MainActivity.java +++ b/app/src/main/java/com/rey/material/demo/MainActivity.java @@ -37,7 +37,7 @@ public class MainActivity extends ActionBarActivity implements AdapterView.OnIte private DrawerLayout dl_navigator; private FrameLayout fl_drawer; private ListView lv_drawer; - private ViewPager vp; + private CustomViewPager vp; private TabPageIndicator tpi; private DrawerAdapter mDrawerAdapter; @@ -47,7 +47,7 @@ public class MainActivity extends ActionBarActivity implements AdapterView.OnIte private ToolbarManager mToolbarManager; private SnackBar mSnackBar; - private Tab[] mItems = new Tab[]{Tab.PROGRESS, Tab.BUTTONS, Tab.SWITCHES, Tab.TEXTFIELDS, Tab.SNACKBARS, Tab.DIALOGS}; + private Tab[] mItems = new Tab[]{Tab.PROGRESS, Tab.BUTTONS, Tab.FAB, Tab.SWITCHES, Tab.SLIDERS, Tab.TEXTFIELDS, Tab.SNACKBARS, Tab.DIALOGS}; @Override protected void onCreate(Bundle savedInstanceState) { @@ -59,7 +59,7 @@ protected void onCreate(Bundle savedInstanceState) { fl_drawer = (FrameLayout)findViewById(R.id.main_fl_drawer); lv_drawer = (ListView)findViewById(R.id.main_lv_drawer); mToolbar = (Toolbar)findViewById(R.id.main_toolbar); - vp = (ViewPager)findViewById(R.id.main_vp); + vp = (CustomViewPager)findViewById(R.id.main_vp); tpi = (TabPageIndicator)findViewById(R.id.main_tpi); mSnackBar = (SnackBar)findViewById(R.id.main_sn); @@ -109,7 +109,7 @@ public void onPageScrollStateChanged(int state) {} }); - vp.setCurrentItem(0); + vp.setCurrentItem(2); } @Override @@ -153,14 +153,14 @@ public SnackBar getSnackBar(){ return mSnackBar; } - - public enum Tab { PROGRESS ("Progresses"), BUTTONS ("Buttons"), + FAB ("FABs"), SWITCHES ("Switches"), - TEXTFIELDS ("Textfields"), - SNACKBARS ("Snackbars"), + SLIDERS ("Sliders"), + TEXTFIELDS ("TextFields"), + SNACKBARS ("SnackBars"), DIALOGS ("Dialogs"); private final String name; @@ -260,8 +260,12 @@ public PagerAdapter(FragmentManager fm, Tab[] tabs) { setFragment(Tab.PROGRESS, fragment); else if(fragment instanceof ButtonFragment) setFragment(Tab.BUTTONS, fragment); + else if(fragment instanceof FabFragment) + setFragment(Tab.FAB, fragment); else if(fragment instanceof SwitchesFragment) setFragment(Tab.SWITCHES, fragment); + else if(fragment instanceof SliderFragment) + setFragment(Tab.SLIDERS, fragment); else if(fragment instanceof TextfieldFragment) setFragment(Tab.TEXTFIELDS, fragment); else if(fragment instanceof SnackbarFragment) @@ -292,9 +296,15 @@ public Fragment getItem(int position) { case BUTTONS: mFragments[position] = ButtonFragment.newInstance(); break; + case FAB: + mFragments[position] = FabFragment.newInstance(); + break; case SWITCHES: mFragments[position] = SwitchesFragment.newInstance(); break; + case SLIDERS: + mFragments[position] = SliderFragment.newInstance(); + break; case TEXTFIELDS: mFragments[position] = TextfieldFragment.newInstance(); break; diff --git a/app/src/main/java/com/rey/material/demo/SliderFragment.java b/app/src/main/java/com/rey/material/demo/SliderFragment.java new file mode 100644 index 00000000..93a0256f --- /dev/null +++ b/app/src/main/java/com/rey/material/demo/SliderFragment.java @@ -0,0 +1,41 @@ +package com.rey.material.demo; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; + +import com.rey.material.widget.RadioButton; + +public class SliderFragment extends Fragment{ + + + public static SliderFragment newInstance(){ + SliderFragment fragment = new SliderFragment(); + + return fragment; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_slider, container, false); + + return v; + } + + @Override + public void onPause() { + super.onPause(); + } + + @Override + public void onResume() { + super.onResume(); + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a49e9c70..80e44c19 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -32,7 +32,7 @@ android:clipToPadding="false" android:background="?attr/colorPrimary"/> - diff --git a/app/src/main/res/layout/fragment_button.xml b/app/src/main/res/layout/fragment_button.xml index c8affec3..a00c17de 100644 --- a/app/src/main/res/layout/fragment_button.xml +++ b/app/src/main/res/layout/fragment_button.xml @@ -10,7 +10,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:gravity="center"> + android:gravity="center" + android:padding="8dp"> + android:gravity="center" + android:padding="8dp"> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_progress.xml b/app/src/main/res/layout/fragment_progress.xml index eb474e85..0e99dd7e 100644 --- a/app/src/main/res/layout/fragment_progress.xml +++ b/app/src/main/res/layout/fragment_progress.xml @@ -9,7 +9,8 @@ + android:orientation="vertical" + android:padding="8dp"> + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_snackbar.xml b/app/src/main/res/layout/fragment_snackbar.xml index 23bb0261..e857cefc 100644 --- a/app/src/main/res/layout/fragment_snackbar.xml +++ b/app/src/main/res/layout/fragment_snackbar.xml @@ -10,7 +10,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:gravity="center"> + android:gravity="center" + android:padding="8dp"> + android:gravity="center" + android:padding="8dp"> + android:checked="false" + android:padding="16dp"/> #FFDFDFDF 4dp - @style/FloatingActionButtonIcon.Light - true + @style/FloatingActionButtonIcon.Light @style/Material.Drawable.Ripple.Touch.Light false @@ -193,8 +192,7 @@ @@ -202,8 +200,7 @@ @@ -211,8 +208,7 @@ diff --git a/build.gradle b/build.gradle index 6356aabd..4722e373 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,14 @@ buildscript { } } +def isReleaseBuild() { + return version.contains("SNAPSHOT") == false +} + allprojects { + version = VERSION_NAME + group = GROUP + repositories { jcenter() } diff --git a/gradle.properties b/gradle.properties index 1d3591c8..6dfcee51 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,19 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true + +VERSION_NAME=1.0.0 +VERSION_CODE=2 +GROUP=com.github.rey5137 + +POM_DESCRIPTION= An Android library to bring Material Design UI to pre-Lolipop Android. +POM_URL=https://github.com/rey5137/Material +POM_SCM_URL=https://github.com/rey5137/Material +POM_SCM_CONNECTION=scm:git@github.com:rey5137/Material.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:rey5137/Material.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=rey5137 +POM_DEVELOPER_NAME=Rey Pham \ No newline at end of file diff --git a/image/button_flat_touch.gif b/image/button_flat_touch.gif new file mode 100644 index 00000000..403cc3bf Binary files /dev/null and b/image/button_flat_touch.gif differ diff --git a/image/button_flat_wave.gif b/image/button_flat_wave.gif new file mode 100644 index 00000000..73c78dc5 Binary files /dev/null and b/image/button_flat_wave.gif differ diff --git a/image/button_raise_touch.gif b/image/button_raise_touch.gif new file mode 100644 index 00000000..58b74b8c Binary files /dev/null and b/image/button_raise_touch.gif differ diff --git a/image/button_raise_wave.gif b/image/button_raise_wave.gif new file mode 100644 index 00000000..50496d57 Binary files /dev/null and b/image/button_raise_wave.gif differ diff --git a/image/cb.gif b/image/cb.gif new file mode 100644 index 00000000..203cd484 Binary files /dev/null and b/image/cb.gif differ diff --git a/image/fab_image.gif b/image/fab_image.gif new file mode 100644 index 00000000..921cface Binary files /dev/null and b/image/fab_image.gif differ diff --git a/image/fab_line.gif b/image/fab_line.gif new file mode 100644 index 00000000..f68d52e7 Binary files /dev/null and b/image/fab_line.gif differ diff --git a/image/progress_circular_determinate.gif b/image/progress_circular_determinate.gif new file mode 100644 index 00000000..5e271f77 Binary files /dev/null and b/image/progress_circular_determinate.gif differ diff --git a/image/progress_circular_indeterminate.gif b/image/progress_circular_indeterminate.gif new file mode 100644 index 00000000..97159abb Binary files /dev/null and b/image/progress_circular_indeterminate.gif differ diff --git a/image/progress_linear_buffer.gif b/image/progress_linear_buffer.gif new file mode 100644 index 00000000..b78fa547 Binary files /dev/null and b/image/progress_linear_buffer.gif differ diff --git a/image/progress_linear_determinate.gif b/image/progress_linear_determinate.gif new file mode 100644 index 00000000..1614cfb1 Binary files /dev/null and b/image/progress_linear_determinate.gif differ diff --git a/image/progress_linear_indeterminate.gif b/image/progress_linear_indeterminate.gif new file mode 100644 index 00000000..6c203395 Binary files /dev/null and b/image/progress_linear_indeterminate.gif differ diff --git a/image/progress_linear_query.gif b/image/progress_linear_query.gif new file mode 100644 index 00000000..9bbe4740 Binary files /dev/null and b/image/progress_linear_query.gif differ diff --git a/image/rb.gif b/image/rb.gif new file mode 100644 index 00000000..ae77704d Binary files /dev/null and b/image/rb.gif differ diff --git a/image/slider_continuous.gif b/image/slider_continuous.gif new file mode 100644 index 00000000..f01b4e8c Binary files /dev/null and b/image/slider_continuous.gif differ diff --git a/image/slider_discrete.gif b/image/slider_discrete.gif new file mode 100644 index 00000000..d1ef3b98 Binary files /dev/null and b/image/slider_discrete.gif differ diff --git a/image/switch.gif b/image/switch.gif new file mode 100644 index 00000000..cfc8e034 Binary files /dev/null and b/image/switch.gif differ diff --git a/image/textfield.gif b/image/textfield.gif new file mode 100644 index 00000000..11b5ec95 Binary files /dev/null and b/image/textfield.gif differ diff --git a/lib/build.gradle b/lib/build.gradle index 10422b28..e706137e 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -23,3 +23,5 @@ dependencies { compile 'com.android.support:appcompat-v7:21.0.2' compile 'com.android.support:cardview-v7:21.0.2' } + +apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' \ No newline at end of file diff --git a/lib/gradle.properties b/lib/gradle.properties new file mode 100644 index 00000000..b06c0557 --- /dev/null +++ b/lib/gradle.properties @@ -0,0 +1,3 @@ +POM_NAME=Material Library +POM_ARTIFACT_ID=material +POM_PACKAGING=aar \ No newline at end of file diff --git a/lib/lib.iml b/lib/lib.iml index db72899b..461e9f32 100644 --- a/lib/lib.iml +++ b/lib/lib.iml @@ -1,5 +1,5 @@ - + @@ -62,6 +62,7 @@ + @@ -81,7 +82,9 @@ + + diff --git a/lib/src/main/AndroidManifest.xml b/lib/src/main/AndroidManifest.xml index 631bc861..536642f2 100644 --- a/lib/src/main/AndroidManifest.xml +++ b/lib/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + android:versionCode="2" + android:versionName="1.0.0"> diff --git a/lib/src/main/java/com/rey/material/widget/EditText.java b/lib/src/main/java/com/rey/material/widget/EditText.java index 9e1e6953..05685aea 100644 --- a/lib/src/main/java/com/rey/material/widget/EditText.java +++ b/lib/src/main/java/com/rey/material/widget/EditText.java @@ -160,8 +160,6 @@ private void init(Context context, AttributeSet attrs, int defStyleAttr, int def mDividerCompoundPadding = a.getBoolean(R.styleable.EditText_et_dividerCompoundPadding, true); mInputView.setPadding(0, 0, 0, dividerPadding + dividerHeight); - System.out.println("a " + mInputView.getTotalPaddingLeft() + " " + mInputView.getTotalPaddingRight()); - mDivider = new DividerDrawable(dividerHeight, mDividerCompoundPadding ? mInputView.getTotalPaddingLeft() : 0, mDividerCompoundPadding ? mInputView.getTotalPaddingRight() : 0, mDividerColors, dividerAnimDuration); mDivider.setInEditMode(isInEditMode()); mDivider.setAnimEnable(false); diff --git a/lib/src/main/java/com/rey/material/widget/FloatingActionButton.java b/lib/src/main/java/com/rey/material/widget/FloatingActionButton.java index 8e811770..b5199739 100644 --- a/lib/src/main/java/com/rey/material/widget/FloatingActionButton.java +++ b/lib/src/main/java/com/rey/material/widget/FloatingActionButton.java @@ -14,13 +14,19 @@ import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; +import android.view.View; import android.view.ViewGroup; -import android.widget.Button; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.widget.FrameLayout; import android.widget.RelativeLayout; @@ -28,13 +34,17 @@ import com.rey.material.drawable.LineMorphingDrawable; import com.rey.material.drawable.RippleDrawable; import com.rey.material.util.ThemeUtil; +import com.rey.material.util.ViewUtil; -public class FloatingActionButton extends Button { +public class FloatingActionButton extends View { private OvalShadowDrawable mBackground; - private LineMorphingDrawable mIcon; + private Drawable mIcon; + private Drawable mPrevIcon; + private int mAnimDuration; + private Interpolator mInterpolator; + private SwitchIconAnimator mSwitchIconAnimator; private int mIconSize; - private boolean mAutoSwitch; private RippleManager mRippleManager = new RippleManager(); private RippleDrawable mRipple; @@ -66,51 +76,68 @@ public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAtt init(context, attrs, defStyleAttr, defStyleRes); } - - @SuppressWarnings("deprecation") - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, defStyleRes); - - int radius = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_radius, ThemeUtil.dpToPx(context, 28)); - int elevation = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_elevation, ThemeUtil.dpToPx(context, 4)); - int bgColor = a.getColor(R.styleable.FloatingActionButton_fab_backgroundColor, ThemeUtil.colorAccent(context, 0xFFFAFAFA)); - int iconId = a.getResourceId(R.styleable.FloatingActionButton_fab_icon, 0); - mIconSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_iconSize, ThemeUtil.dpToPx(context, 24)); - mAutoSwitch = a.getBoolean(R.styleable.FloatingActionButton_fab_autoSwitch, true); - int rippleId = a.getResourceId(R.styleable.FloatingActionButton_ripple, 0); - boolean delayClick = a.getBoolean(R.styleable.FloatingActionButton_delayClick, false); - - a.recycle(); - - mBackground = new OvalShadowDrawable(radius, bgColor, elevation, elevation); - - if(iconId != 0) - setIcon(new LineMorphingDrawable.Builder(context, iconId).build()); - - mRippleManager.onCreate(this, context, null, 0, 0); - mRippleManager.setDelayClick(delayClick); - - if(rippleId != 0){ - RippleDrawable.Builder buidler = new RippleDrawable.Builder(context, rippleId); - - buidler.maskType(RippleDrawable.Mask.TYPE_OVAL) - .backgroundDrawable(null) - .left((int)mBackground.getPaddingLeft()) - .top((int)mBackground.getPaddingTop()) - .right((int)mBackground.getPaddingRight()) - .bottom((int)mBackground.getPaddingBottom()); - - mRipple = buidler.build(); - - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - setBackground(mRipple); - else - setBackgroundDrawable(mRipple); - } - + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { setClickable(true); + mSwitchIconAnimator = new SwitchIconAnimator(); + applyStyle(context, attrs, defStyleAttr, defStyleRes); } + + public void applyStyle(int resId){ + applyStyle(getContext(), null, 0, resId); + } + + private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, defStyleRes); + + int radius = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_radius, ThemeUtil.dpToPx(context, 28)); + int elevation = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_elevation, ThemeUtil.dpToPx(context, 4)); + int bgColor = a.getColor(R.styleable.FloatingActionButton_fab_backgroundColor, ThemeUtil.colorAccent(context, 0xFFFAFAFA)); + int iconSrc = a.getResourceId(R.styleable.FloatingActionButton_fab_iconSrc, 0); + int iconLineMorphing = a.getResourceId(R.styleable.FloatingActionButton_fab_iconLineMorphing, 0); + mIconSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fab_iconSize, ThemeUtil.dpToPx(context, 24)); + int rippleId = a.getResourceId(R.styleable.FloatingActionButton_ripple, 0); + boolean delayClick = a.getBoolean(R.styleable.FloatingActionButton_delayClick, false); + mAnimDuration = a.getInteger(R.styleable.FloatingActionButton_fab_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime)); + int resId = a.getResourceId(R.styleable.FloatingActionButton_fab_interpolator, 0); + if(resId != 0) + mInterpolator = AnimationUtils.loadInterpolator(context, resId); + else if(mInterpolator == null) + mInterpolator = new DecelerateInterpolator(); + + a.recycle(); + + mBackground = new OvalShadowDrawable(radius, bgColor, elevation, elevation); + mBackground.setBounds(0, 0, getWidth(), getHeight()); + + if(iconLineMorphing != 0) + setIcon(new LineMorphingDrawable.Builder(context, iconLineMorphing).build(), false); + else if(iconSrc != 0) + setIcon(context.getResources().getDrawable(iconSrc), false); + + mRippleManager.onCreate(this, context, null, 0, 0); + mRippleManager.setDelayClick(delayClick); + + if(rippleId != 0){ + RippleDrawable.Builder builder = new RippleDrawable.Builder(context, rippleId); + + builder.maskType(RippleDrawable.Mask.TYPE_OVAL) + .backgroundDrawable(null) + .left((int)mBackground.getPaddingLeft()) + .top((int)mBackground.getPaddingTop()) + .right((int)mBackground.getPaddingRight()) + .bottom((int)mBackground.getPaddingBottom()); + + mRipple = builder.build(); + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + setBackground(mRipple); + else + setBackgroundDrawable(mRipple); + } + + setClickable(true); + } public int getRadius(){ return mBackground.getRadius(); @@ -139,35 +166,46 @@ else if(mBackground.setShadow(elevation, elevation)) requestLayout(); } - public int getIconState(){ - if(mIcon != null) - return mIcon.getLineState(); + public int getLineMorphingState(){ + if(mIcon != null && mIcon instanceof LineMorphingDrawable) + return ((LineMorphingDrawable)mIcon).getLineState(); return -1; } - public void setIconState(int state, boolean animation){ - if(mIcon != null) - mIcon.switchLineState(state, animation); + public void setLineMorphingState(int state, boolean animation){ + if(mIcon != null && mIcon instanceof LineMorphingDrawable) + ((LineMorphingDrawable)mIcon).switchLineState(state, animation); } public int getBackgroundColor(){ return mBackground.getColor(); } - public LineMorphingDrawable getIcon(){ + public Drawable getIcon(){ return mIcon; } - public void setIcon(LineMorphingDrawable icon){ - if(mIcon != null){ - mIcon.setCallback(null); - unscheduleDrawable(mIcon); - } - - mIcon = icon; - mIcon.setCallback(this); - invalidate(); + public void setIcon(Drawable icon, boolean animation){ + if(icon == null) + return; + + if(animation) { + mSwitchIconAnimator.startAnimation(icon); + invalidate(); + } + else{ + if(mIcon != null){ + mIcon.setCallback(null); + unscheduleDrawable(mIcon); + } + + mIcon = icon; + float half = mIconSize / 2f; + mIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half)); + mIcon.setCallback(this); + invalidate(); + } } @Override @@ -267,18 +305,10 @@ public void dismiss(){ if(getParent() != null) ((ViewGroup)getParent()).removeView(this); } - - @Override - public boolean performClick() { - if(mIcon != null && mAutoSwitch) - mIcon.switchLineState((mIcon.getLineState() + 1) % mIcon.getLineStateCount(), true); - - return super.performClick(); - } - + @Override protected boolean verifyDrawable(Drawable who) { - return super.verifyDrawable(who) || mBackground == who || mIcon == who; + return super.verifyDrawable(who) || mBackground == who || mIcon == who || mPrevIcon == who; } @Override @@ -294,12 +324,19 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) { float half = mIconSize / 2f; mIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half)); } + + if(mPrevIcon != null){ + float half = mIconSize / 2f; + mPrevIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half)); + } } @Override public void draw(@NonNull Canvas canvas) { mBackground.draw(canvas); super.draw(canvas); + if(mPrevIcon != null) + mPrevIcon.draw(canvas); if(mIcon != null) mIcon.draw(canvas); } @@ -541,5 +578,133 @@ public int getOpacity() { } } - + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.state = getLineMorphingState(); + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + if(ss.state >= 0) + setLineMorphingState(ss.state, false); + requestLayout(); + } + + static class SavedState extends BaseSavedState { + int state; + + /** + * Constructor called from {@link Slider#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + state = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(state); + } + + @Override + public String toString() { + return "FloatingActionButton.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " state=" + state + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + class SwitchIconAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mIcon.setAlpha(0); + mPrevIcon.setAlpha(255); + } + + public boolean startAnimation(Drawable icon) { + if(mIcon == icon) + return false; + + mPrevIcon = mIcon; + mIcon = icon; + float half = mIconSize / 2f; + mIcon.setBounds((int)(mBackground.getCenterX() - half), (int)(mBackground.getCenterY() - half), (int)(mBackground.getCenterX() + half), (int)(mBackground.getCenterY() + half)); + mIcon.setCallback(FloatingActionButton.this); + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + else { + mPrevIcon.setCallback(null); + unscheduleDrawable(mPrevIcon); + mPrevIcon = null; + } + + invalidate(); + return true; + } + + public void stopAnimation() { + mRunning = false; + mPrevIcon.setCallback(null); + unscheduleDrawable(mPrevIcon); + mPrevIcon = null; + mIcon.setAlpha(255); + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mAnimDuration); + float value = mInterpolator.getInterpolation(progress); + + mIcon.setAlpha(Math.round(255 * value)); + mPrevIcon.setAlpha(Math.round(255 * (1f - value))); + + if(progress == 1f) + stopAnimation(); + + if(mRunning) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidate(); + } + + } } diff --git a/lib/src/main/java/com/rey/material/widget/RippleManager.java b/lib/src/main/java/com/rey/material/widget/RippleManager.java index 5b4b6def..284649a5 100644 --- a/lib/src/main/java/com/rey/material/widget/RippleManager.java +++ b/lib/src/main/java/com/rey/material/widget/RippleManager.java @@ -32,7 +32,7 @@ public void onCreate(View v, Context context, AttributeSet attrs, int defStyleAt mDelayClick = a.getBoolean(R.styleable.RippleView_delayClick, mDelayClick); a.recycle(); - if(resId != 0){ + if(resId != 0 && !mView.isInEditMode()){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) mView.setBackground(new RippleDrawable.Builder(context, resId).build()); else diff --git a/lib/src/main/java/com/rey/material/widget/Slider.java b/lib/src/main/java/com/rey/material/widget/Slider.java new file mode 100644 index 00000000..43dd2a2e --- /dev/null +++ b/lib/src/main/java/com/rey/material/widget/Slider.java @@ -0,0 +1,877 @@ +package com.rey.material.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.rey.material.R; +import com.rey.material.util.ColorUtil; +import com.rey.material.util.ThemeUtil; +import com.rey.material.util.TypefaceUtil; +import com.rey.material.util.ViewUtil; + +/** + * Created by Ret on 3/18/2015. + */ +public class Slider extends View{ + + private RippleManager mRippleManager = new RippleManager(); + + private Paint mPaint; + private RectF mDrawRect; + private RectF mTempRect; + private Path mLeftTrackPath; + private Path mRightTrackPath; + private Path mMarkPath; + + private int mMinValue = 0; + private int mMaxValue = 100; + private int mStepValue = 1; + + private boolean mDiscreteMode = false; + + private int mPrimaryColor; + private int mSecondaryColor; + private int mTrackSize; + private Paint.Cap mTrackCap; + private int mThumbBorderSize; + private int mThumbRadius; + private int mThumbFocusRadius; + private float mThumbPosition; + private Typeface mTypeface; + private int mTextSize; + private int mTextColor; + private int mGravity = Gravity.CENTER; + private int mTravelAnimationDuration; + private int mTransformAnimationDuration; + private Interpolator mInterpolator; + + private int mTouchSlop; + private PointF mMemoPoint; + private boolean mIsDragging; + private float mThumbCurrentRadius; + private float mThumbFillPercent; + private int mTextHeight; + private int mMemoValue; + private String mValueText; + + private ThumbRadiusAnimator mThumbRadiusAnimator; + private ThumbStrokeAnimator mThumbStrokeAnimator; + private ThumbMoveAnimator mThumbMoveAnimator; + + public Slider(Context context) { + super(context); + + init(context, null, 0, 0); + } + + public Slider(Context context, AttributeSet attrs) { + super(context, attrs); + + init(context, attrs, 0, 0); + } + + public Slider(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, 0); + } + + public Slider(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr); + + init(context, attrs, defStyleAttr, defStyleRes); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mDrawRect = new RectF(); + mTempRect = new RectF(); + mLeftTrackPath = new Path(); + mRightTrackPath = new Path(); + + mThumbRadiusAnimator = new ThumbRadiusAnimator(); + mThumbStrokeAnimator = new ThumbStrokeAnimator(); + mThumbMoveAnimator = new ThumbMoveAnimator(); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + mMemoPoint = new PointF(); + + applyStyle(context, attrs, defStyleAttr, defStyleRes); + } + + public void applyStyle(int resId){ + applyStyle(getContext(), null, 0, resId); + } + + private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mRippleManager.onCreate(this, context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider, defStyleAttr, defStyleRes); + mDiscreteMode = a.getBoolean(R.styleable.Slider_sl_discreteMode, mDiscreteMode); + mPrimaryColor = a.getColor(R.styleable.Slider_sl_primaryColor, ThemeUtil.colorControlActivated(context, 0xFF000000)); + mSecondaryColor = a.getColor(R.styleable.Slider_sl_secondaryColor, ThemeUtil.colorControlNormal(context, 0xFF000000)); + mTrackSize = a.getDimensionPixelSize(R.styleable.Slider_sl_trackSize, ThemeUtil.dpToPx(context, 2)); + int cap = a.getInteger(R.styleable.Slider_sl_trackCap, 0); + if(cap == 0) + mTrackCap = Paint.Cap.BUTT; + else if(cap == 1) + mTrackCap = Paint.Cap.ROUND; + else + mTrackCap = Paint.Cap.SQUARE; + mThumbBorderSize = a.getDimensionPixelSize(R.styleable.Slider_sl_thumbBorderSize, ThemeUtil.dpToPx(context, 2)); + mThumbRadius = a.getDimensionPixelSize(R.styleable.Slider_sl_thumbRadius, ThemeUtil.dpToPx(context, 10)); + mThumbFocusRadius = a.getDimensionPixelSize(R.styleable.Slider_sl_thumbFocusRadius, ThemeUtil.dpToPx(context, 14)); + mTravelAnimationDuration = a.getInteger(R.styleable.Slider_sl_travelAnimDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime)); + mTransformAnimationDuration = a.getInteger(R.styleable.Slider_sl_travelAnimDuration, context.getResources().getInteger(android.R.integer.config_shortAnimTime)); + int resId = a.getResourceId(R.styleable.Slider_sl_interpolator, 0); + mInterpolator = resId != 0 ? AnimationUtils.loadInterpolator(context, resId) : new DecelerateInterpolator(); + mGravity = a.getInt(R.styleable.Slider_android_gravity, Gravity.CENTER_VERTICAL); + mMinValue = a.getInteger(R.styleable.Slider_sl_minValue, mMinValue); + mMaxValue = a.getInteger(R.styleable.Slider_sl_maxValue, mMaxValue); + mStepValue = a.getInteger(R.styleable.Slider_sl_stepValue, mStepValue); + setValue(a.getInteger(R.styleable.Slider_sl_value, getValue()), false); + + String familyName = a.getString(R.styleable.Slider_sl_fontFamily); + int style = a.getInteger(R.styleable.Slider_sl_textStyle, Typeface.NORMAL); + + mTypeface = TypefaceUtil.load(context, familyName, style); + mTextColor = a.getColor(R.styleable.Slider_sl_textColor, 0xFFFFFFFF); + mTextSize = a.getDimensionPixelSize(R.styleable.Slider_sl_textSize, context.getResources().getDimensionPixelOffset(R.dimen.abc_text_size_small_material)); + setEnabled(a.getBoolean(R.styleable.Slider_android_enabled, true)); + + a.recycle(); + + mPaint.setTextSize(mTextSize); + mPaint.setTextAlign(Paint.Align.CENTER); + mPaint.setTypeface(mTypeface); + + measureText(); + } + + private void measureText(){ + Rect temp = new Rect(); + String text = String.valueOf(mMaxValue); + mPaint.setTextSize(mTextSize); + float width = mPaint.measureText(text); + float maxWidth = (float)(mThumbRadius * Math.sqrt(2) * 2 - ThemeUtil.dpToPx(getContext(), 8)); + if(width > maxWidth){ + float textSize = mTextSize * maxWidth / width; + mPaint.setTextSize(textSize); + } + + mPaint.getTextBounds(text, 0, text.length(), temp); + mTextHeight = temp.height(); + } + + private String getValueText(){ + int value = getValue(); + if(mValueText == null || mMemoValue != value){ + mMemoValue = value; + mValueText = String.valueOf(mMemoValue); + } + + return mValueText; + } + + public int getValue(){ + return Math.round(getExactValue()); + } + + public float getExactValue(){ + return (mMaxValue - mMinValue) * getPosition() + mMinValue; + } + + public float getPosition(){ + return mThumbMoveAnimator.isRunning() ? mThumbMoveAnimator.getPosition() : mThumbPosition; + } + + public void setPosition(float pos, boolean animation){ + if(animation) { + if(!mThumbMoveAnimator.startAnimation(pos)){ + if(!mIsDragging) + mThumbRadiusAnimator.startAnimation(mThumbRadius); + mThumbStrokeAnimator.startAnimation(pos == 0 ? 0 : 1); + } + } + else { + mThumbPosition = pos; + mThumbCurrentRadius = mThumbRadius; + mThumbFillPercent = mThumbPosition == 0 ? 0 : 1; + invalidate(); + } + } + + public void setValue(float value, boolean animation){ + value = Math.min(mMaxValue, Math.max(value, mMinValue)); + setPosition((value - mMinValue) / (mMaxValue - mMinValue), animation); + } + + @Override + public void setOnClickListener(OnClickListener l) { + if(l == mRippleManager) + super.setOnClickListener(l); + else{ + mRippleManager.setOnClickListener(l); + setOnClickListener(mRippleManager); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + switch (widthMode) { + case MeasureSpec.UNSPECIFIED: + widthSize = getSuggestedMinimumWidth(); + break; + case MeasureSpec.AT_MOST: + widthSize = Math.min(widthSize, getSuggestedMinimumWidth()); + break; + } + + switch (heightMode) { + case MeasureSpec.UNSPECIFIED: + heightSize = getSuggestedMinimumHeight(); + break; + case MeasureSpec.AT_MOST: + heightSize = Math.min(heightSize, getSuggestedMinimumHeight()); + break; + } + + setMeasuredDimension(widthSize, heightSize); + } + + @Override + public int getSuggestedMinimumWidth() { + return (mDiscreteMode ? (int)(mThumbRadius * Math.sqrt(2)) : mThumbFocusRadius) * 4 + getPaddingLeft() + getPaddingRight(); + } + + @Override + public int getSuggestedMinimumHeight() { + return (mDiscreteMode ? (int)(mThumbRadius * (4 + Math.sqrt(2))) : mThumbFocusRadius * 2) + getPaddingTop() + getPaddingBottom(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + mDrawRect.left = getPaddingLeft() + mThumbRadius; + mDrawRect.right = w - getPaddingRight() - mThumbRadius; + + int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + if(mDiscreteMode){ + int fullHeight = (int)(mThumbRadius * (4 + Math.sqrt(2))); + int height = mThumbRadius * 2; + switch (align) { + case Gravity.TOP: + mDrawRect.top = Math.max(getPaddingTop(), fullHeight - height); + mDrawRect.bottom = mDrawRect.top + height; + break; + case Gravity.BOTTOM: + mDrawRect.bottom = h - getPaddingBottom(); + mDrawRect.top = mDrawRect.bottom - height; + break; + default: + mDrawRect.top = Math.max((h - height) / 2f, fullHeight - height); + mDrawRect.bottom = mDrawRect.top + height; + break; + } + } + else{ + int height = mThumbFocusRadius * 2; + switch (align) { + case Gravity.TOP: + mDrawRect.top = getPaddingTop(); + mDrawRect.bottom = mDrawRect.top + height; + break; + case Gravity.BOTTOM: + mDrawRect.bottom = h - getPaddingBottom(); + mDrawRect.top = mDrawRect.bottom - height; + break; + default: + mDrawRect.top = (h - height) / 2f; + mDrawRect.bottom = mDrawRect.top + height; + break; + } + } + } + + private boolean isThumbHit(float x, float y, float radius){ + float cx = mDrawRect.width() * mThumbPosition + mDrawRect.left; + float cy = mDrawRect.centerY(); + + return x >= cx - radius && x <= cx + radius && y >= cy - radius && y < cy + radius; + } + + private double distance(float x1, float y1, float x2, float y2){ + return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); + } + + private float correctPosition(float position){ + if(!mDiscreteMode) + return position; + + int totalOffset = mMaxValue - mMinValue; + int valueOffset = Math.round(totalOffset * position); + int stepOffset = valueOffset / mStepValue; + int lowerValue = stepOffset * mStepValue; + int higherValue = Math.min(mMaxValue, (stepOffset + 1) * mStepValue); + + if(valueOffset - lowerValue < higherValue - valueOffset) + position = lowerValue / (float)totalOffset; + else + position = higherValue / (float)totalOffset; + + return position; + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + super.onTouchEvent(event); + mRippleManager.onTouchEvent(event); + + if(!isEnabled()) + return false; + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mIsDragging = isThumbHit(event.getX(), event.getY(), mThumbRadius) && !mThumbMoveAnimator.isRunning(); + mMemoPoint.set(event.getX(), event.getY()); + if(mIsDragging) + mThumbRadiusAnimator.startAnimation(mDiscreteMode ? 0 : mThumbFocusRadius); + break; + case MotionEvent.ACTION_MOVE: + if(mIsDragging) { + if(mDiscreteMode) { + float position = correctPosition(Math.min(1f, Math.max(0f, (event.getX() - mDrawRect.left) / mDrawRect.width()))); + setPosition(position, true); + } + else{ + float offset = (event.getX() - mMemoPoint.x) / mDrawRect.width(); + mThumbPosition = Math.min(1f, Math.max(0f, mThumbPosition + offset)); + mMemoPoint.x = event.getX(); + mThumbStrokeAnimator.startAnimation(mThumbPosition == 0 ? 0 : 1); + invalidate(); + } + } + break; + case MotionEvent.ACTION_UP: + if(mIsDragging) { + mIsDragging = false; + setPosition(getPosition(), true); + } + else if(distance(mMemoPoint.x, mMemoPoint.y, event.getX(), event.getY()) <= mTouchSlop){ + float position = correctPosition(Math.min(1f, Math.max(0f, (event.getX() - mDrawRect.left) / mDrawRect.width()))); + setPosition(position, true); + } + break; + case MotionEvent.ACTION_CANCEL: + if(mIsDragging) { + mIsDragging = false; + setPosition(getPosition(), true); + } + break; + } + + return true; + } + + private void getTrackPath(float x, float y, float radius){ + float halfStroke = mTrackSize / 2f; + + mLeftTrackPath.reset(); + mRightTrackPath.reset(); + + if(radius - 1f < halfStroke){ + if(mTrackCap != Paint.Cap.ROUND){ + if(x > mDrawRect.left){ + mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke); + mLeftTrackPath.lineTo(x, y - halfStroke); + mLeftTrackPath.lineTo(x, y + halfStroke); + mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke); + mLeftTrackPath.close(); + } + + if(x < mDrawRect.right){ + mRightTrackPath.moveTo(mDrawRect.right, y + halfStroke); + mRightTrackPath.lineTo(x, y + halfStroke); + mRightTrackPath.lineTo(x, y - halfStroke); + mRightTrackPath.lineTo(mDrawRect.right, y - halfStroke); + mRightTrackPath.close(); + } + } + else{ + if(x > mDrawRect.left){ + mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke); + mLeftTrackPath.arcTo(mTempRect, 90, 180); + mLeftTrackPath.lineTo(x, y - halfStroke); + mLeftTrackPath.lineTo(x, y + halfStroke); + mLeftTrackPath.close(); + } + + if(x < mDrawRect.right){ + mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke); + mRightTrackPath.arcTo(mTempRect, 270, 180); + mRightTrackPath.lineTo(x, y + halfStroke); + mRightTrackPath.lineTo(x, y - halfStroke); + mRightTrackPath.close(); + } + } + } + else{ + if(mTrackCap != Paint.Cap.ROUND){ + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); + + if(x - radius > mDrawRect.left){ + mLeftTrackPath.moveTo(mDrawRect.left, y - halfStroke); + mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mLeftTrackPath.lineTo(mDrawRect.left, y + halfStroke); + mLeftTrackPath.close(); + } + + if(x + radius < mDrawRect.right){ + mRightTrackPath.moveTo(mDrawRect.right, y - halfStroke); + mRightTrackPath.arcTo(mTempRect, -angle, angle * 2); + mRightTrackPath.lineTo(mDrawRect.right, y + halfStroke); + mRightTrackPath.close(); + } + } + else{ + float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); + + if(x - radius > mDrawRect.left){ + float angle2 = (float)(Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180); + + mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke); + mLeftTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2); + + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + mLeftTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mLeftTrackPath.close(); + } + + if(x + radius < mDrawRect.right){ + float angle2 = (float)Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke)); + mRightTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke)); + + angle2 = (float)(angle2 / Math.PI * 180); + mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke); + mRightTrackPath.arcTo(mTempRect, angle2, -angle2 * 2); + + mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); + mRightTrackPath.arcTo(mTempRect, -angle, angle * 2); + mRightTrackPath.close(); + } + } + } + } + + private Path getMarkPath(Path path, float cx, float cy, float radius, float factor){ + if(path == null) + path = new Path(); + else + path.reset(); + + float x1 = cx - radius; + float y1 = cy; + float x2 = cx + radius; + float y2 = cy; + float x3 = cx; + float y3 = cy + radius; + + float nCx = cx; + float nCy = cy - radius * factor; + + // calculate first arc + float angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI); + float nRadius = (float)distance(nCx, nCy, x1, y1); + mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius); + path.moveTo(x1, y1); + path.arcTo(mTempRect, 180 - angle, 180 + angle * 2); + + if(factor > 0.9f) + path.lineTo(x3, y3); + else{ + // find center point for second arc + float x4 = (x2 + x3) / 2; + float y4 = (y2 + y3) / 2; + + double d1 = distance(x2, y2, x4, y4); + double d2 = d1 / Math.tan(Math.PI * (1f - factor) / 4); + + nCx = (float)(x4 - Math.cos(Math.PI / 4) * d2); + nCy = (float)(y4 - Math.sin(Math.PI / 4) * d2); + + // calculate second arc + angle = (float)(Math.atan2(y2 - nCy, x2 - nCx) * 180 / Math.PI); + float angle2 = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI); + nRadius = (float)distance(nCx, nCy, x2, y2); + mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius); + path.arcTo(mTempRect, angle, angle2 - angle); + + // calculate third arc + nCx = cx * 2 - nCx; + angle = (float)(Math.atan2(y3 - nCy, x3 - nCx) * 180 / Math.PI); + angle2 = (float)(Math.atan2(y1 - nCy, x1 - nCx) * 180 / Math.PI); + mTempRect.set(nCx - nRadius, nCy - nRadius, nCx + nRadius, nCy + nRadius); + path.arcTo(mTempRect, angle + (float)Math.PI / 4, angle2 - angle); + } + + path.close(); + + return path; + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + float x = mDrawRect.width() * mThumbPosition + mDrawRect.left; + float y = mDrawRect.centerY(); + int filledPrimaryColor = ColorUtil.getMiddleColor(mSecondaryColor, isEnabled() ? mPrimaryColor : mSecondaryColor, mThumbFillPercent); + + getTrackPath(x, y, mThumbCurrentRadius); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mSecondaryColor); + canvas.drawPath(mRightTrackPath, mPaint); + mPaint.setColor(filledPrimaryColor); + canvas.drawPath(mLeftTrackPath, mPaint); + + if(mDiscreteMode){ + float factor = 1f - mThumbCurrentRadius / mThumbRadius; + + if(factor > 0){ + mMarkPath = getMarkPath(mMarkPath, x, y, mThumbRadius, factor); + mPaint.setStyle(Paint.Style.FILL); + int saveCount = canvas.save(); + canvas.translate(0, -mThumbRadius * 2 * factor); + canvas.drawPath(mMarkPath, mPaint); + mPaint.setColor(ColorUtil.getColor(mTextColor, factor)); + canvas.drawText(getValueText(), x, y + mTextHeight / 2f - mThumbRadius * factor, mPaint); + canvas.restoreToCount(saveCount); + } + + float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize; + if(radius > 0) { + mPaint.setColor(filledPrimaryColor); + canvas.drawCircle(x, y, radius, mPaint); + } + } + else{ + float radius = isEnabled() ? mThumbCurrentRadius : mThumbCurrentRadius - mThumbBorderSize; + if(mThumbFillPercent == 1) + mPaint.setStyle(Paint.Style.FILL); + else{ + float strokeWidth = (radius - mThumbBorderSize) * mThumbFillPercent + mThumbBorderSize; + radius = radius - strokeWidth / 2f; + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(strokeWidth); + } + canvas.drawCircle(x, y, radius, mPaint); + } + } + + class ThumbRadiusAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + float mStartRadius; + int mRadius; + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mStartRadius = mThumbCurrentRadius; + } + + public boolean startAnimation(int radius) { + if(mThumbCurrentRadius == radius) + return false; + + mRadius = radius; + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + else + mThumbCurrentRadius = mRadius; + + invalidate(); + return true; + } + + public void stopAnimation() { + mRunning = false; + mThumbCurrentRadius = mRadius; + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration); + float value = mInterpolator.getInterpolation(progress); + + mThumbCurrentRadius = (mRadius - mStartRadius) * value + mStartRadius; + + if(progress == 1f) + stopAnimation(); + + if(mRunning) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidate(); + } + + } + + class ThumbStrokeAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + float mStartFillPercent; + int mFillPercent; + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mStartFillPercent = mThumbFillPercent; + } + + public boolean startAnimation(int fillPercent) { + if(mThumbFillPercent == fillPercent) + return false; + + mFillPercent = fillPercent; + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + else + mThumbFillPercent = mFillPercent; + + invalidate(); + return true; + } + + public void stopAnimation() { + mRunning = false; + mThumbFillPercent = mFillPercent; + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mTransformAnimationDuration); + float value = mInterpolator.getInterpolation(progress); + + mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent; + + if(progress == 1f) + stopAnimation(); + + if(mRunning) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidate(); + } + + } + + class ThumbMoveAnimator implements Runnable{ + + boolean mRunning = false; + long mStartTime; + float mStartFillPercent; + float mStartRadius; + float mStartPosition; + float mPosition; + float mFillPercent; + int mDuration; + + public boolean isRunning(){ + return mRunning; + } + + public float getPosition(){ + return mPosition; + } + + public void resetAnimation(){ + mStartTime = SystemClock.uptimeMillis(); + mStartPosition = mThumbPosition; + mStartFillPercent = mThumbFillPercent; + mStartRadius = mThumbCurrentRadius; + mFillPercent = mPosition == 0 ? 0 : 1; + mDuration = mDiscreteMode && !mIsDragging ? mTransformAnimationDuration * 2 + mTravelAnimationDuration : mTravelAnimationDuration; + } + + public boolean startAnimation(float position) { + if(mThumbPosition == position) + return false; + + mPosition = position; + + if(getHandler() != null){ + resetAnimation(); + mRunning = true; + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + } + else { + mThumbPosition = position; + mThumbCurrentRadius = mThumbRadius; + mThumbFillPercent = mThumbPosition == 0 ? 0 : 1; + } + + invalidate(); + + return true; + } + + public void stopAnimation() { + mRunning = false; + mThumbCurrentRadius = mDiscreteMode && mIsDragging ? 0 : mThumbRadius; + mThumbFillPercent = mFillPercent; + mThumbPosition = mPosition; + getHandler().removeCallbacks(this); + invalidate(); + } + + @Override + public void run() { + long curTime = SystemClock.uptimeMillis(); + float progress = Math.min(1f, (float)(curTime - mStartTime) / mDuration); + float value = mInterpolator.getInterpolation(progress); + + if(mDiscreteMode){ + if(mIsDragging) { + mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition; + mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent; + } + else{ + float p1 = (float)mTravelAnimationDuration / mDuration; + float p2 = (float)(mTravelAnimationDuration + mTransformAnimationDuration)/ mDuration; + if(progress < p1) { + value = mInterpolator.getInterpolation(progress / p1); + mThumbCurrentRadius = mStartRadius * (1f - value); + mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition; + mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent; + } + else if(progress > p2){ + mThumbCurrentRadius = mThumbRadius * (progress - p2) / (1 - p2); + } + } + } + else{ + mThumbPosition = (mPosition - mStartPosition) * value + mStartPosition; + mThumbFillPercent = (mFillPercent - mStartFillPercent) * value + mStartFillPercent; + + if(progress < 0.2) + mThumbCurrentRadius = Math.max(mThumbRadius + mThumbBorderSize * progress * 5, mThumbCurrentRadius); + else if(progress >= 0.8) + mThumbCurrentRadius = mThumbRadius + mThumbBorderSize * (5f - progress * 5); + } + + + if(progress == 1f) + stopAnimation(); + + if(mRunning) + getHandler().postAtTime(this, SystemClock.uptimeMillis() + ViewUtil.FRAME_DURATION); + + invalidate(); + } + + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + + ss.position = getPosition(); + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + + super.onRestoreInstanceState(ss.getSuperState()); + setPosition(ss.position, false); + requestLayout(); + } + + static class SavedState extends BaseSavedState { + float position; + + /** + * Constructor called from {@link Slider#onSaveInstanceState()} + */ + SavedState(Parcelable superState) { + super(superState); + } + + /** + * Constructor called from {@link #CREATOR} + */ + private SavedState(Parcel in) { + super(in); + position = in.readFloat(); + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeFloat(position); + } + + @Override + public String toString() { + return "Slider.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " pos=" + position + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/lib/src/main/java/com/rey/material/widget/Switch.java b/lib/src/main/java/com/rey/material/widget/Switch.java index 929677b2..fa6317a3 100644 --- a/lib/src/main/java/com/rey/material/widget/Switch.java +++ b/lib/src/main/java/com/rey/material/widget/Switch.java @@ -6,7 +6,9 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.RadialGradient; import android.graphics.RectF; +import android.graphics.Shader; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; @@ -35,12 +37,11 @@ public class Switch extends View implements Checkable { private Paint mPaint; private RectF mDrawRect; private RectF mTempRect; - private Path mPath; + private Path mTrackPath; - private int mStrokeSize; - private ColorStateList mStrokeColors; - private Paint.Cap mStrokeCap; - private int mThumbBorderSize; + private int mTrackSize; + private ColorStateList mTrackColors; + private Paint.Cap mTrackCap; private int mThumbRadius; private ColorStateList mThumbColors; private float mThumbPosition; @@ -60,6 +61,15 @@ public class Switch extends View implements Checkable { private int[] mTempStates = new int[2]; + private int mShadowSize; + private int mShadowOffset; + private Path mShadowPath; + private Paint mShadowPaint; + + private static final int COLOR_SHADOW_START = 0x4C000000; + private static final int COLOR_SHADOW_END = 0x00000000; + + public Switch(Context context) { super(context); @@ -85,67 +95,75 @@ public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyl } private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ - mRippleManager.onCreate(this, context, attrs, defStyleAttr, defStyleRes); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Switch, defStyleAttr, defStyleRes); - - mStrokeSize = a.getDimensionPixelSize(R.styleable.Switch_sw_strokeSize, ThemeUtil.dpToPx(context, 2)); - mStrokeColors = a.getColorStateList(R.styleable.Switch_sw_strokeColor); - int cap = a.getInteger(R.styleable.Switch_sw_strokeCap, 0); - if(cap == 0) - mStrokeCap = Paint.Cap.BUTT; - else if(cap == 1) - mStrokeCap = Paint.Cap.ROUND; - else - mStrokeCap = Paint.Cap.SQUARE; - mThumbBorderSize = a.getDimensionPixelSize(R.styleable.Switch_sw_thumbBorderSize, ThemeUtil.dpToPx(context, 2)); - mThumbColors = a.getColorStateList(R.styleable.Switch_sw_thumbColor); - mThumbRadius = a.getDimensionPixelSize(R.styleable.Switch_sw_thumbRadius, ThemeUtil.dpToPx(context, 8)); - mMaxAnimDuration = a.getInt(R.styleable.Switch_sw_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime)); - mGravity = a.getInt(R.styleable.Switch_android_gravity, Gravity.CENTER_VERTICAL); - mChecked = a.getBoolean(R.styleable.Switch_android_checked, false); - mThumbPosition = mChecked ? 1f : 0f; - int resId = a.getResourceId(R.styleable.Switch_sw_interpolator, 0); - mInterpolator = resId != 0 ? AnimationUtils.loadInterpolator(context, resId) : new DecelerateInterpolator(); - mFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); - - a.recycle(); - - if(mStrokeColors == null){ - int[][] states = new int[][]{ - new int[]{-android.R.attr.state_checked}, - new int[]{android.R.attr.state_checked}, - }; - int[] colors = new int[]{ - ThemeUtil.colorControlNormal(context, 0xFF000000), - ThemeUtil.colorControlActivated(context, 0xFF000000), - }; - - mStrokeColors = new ColorStateList(states, colors); - } - - if(mThumbColors == null){ - int[][] states = new int[][]{ - new int[]{-android.R.attr.state_checked}, - new int[]{android.R.attr.state_checked}, - }; - int[] colors = new int[]{ - ThemeUtil.colorSwitchThumbNormal(context, 0xFF000000), - ThemeUtil.colorControlActivated(context, 0xFF000000), - }; - - mThumbColors = new ColorStateList(states, colors); - } - - mPaint = new Paint(); - mPaint.setAntiAlias(true); - mPaint.setStrokeCap(mStrokeCap); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mDrawRect = new RectF(); mTempRect = new RectF(); - mPath = new Path(); + mTrackPath = new Path(); + + mFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity(); + + applyStyle(context, attrs, defStyleAttr, defStyleRes); } + private void applyStyle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){ + mRippleManager.onCreate(this, context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Switch, defStyleAttr, defStyleRes); + + mTrackSize = a.getDimensionPixelSize(R.styleable.Switch_sw_trackSize, ThemeUtil.dpToPx(context, 2)); + mTrackColors = a.getColorStateList(R.styleable.Switch_sw_trackColor); + int cap = a.getInteger(R.styleable.Switch_sw_trackCap, 0); + if(cap == 0) + mTrackCap = Paint.Cap.BUTT; + else if(cap == 1) + mTrackCap = Paint.Cap.ROUND; + else + mTrackCap = Paint.Cap.SQUARE; + mThumbColors = a.getColorStateList(R.styleable.Switch_sw_thumbColor); + mThumbRadius = a.getDimensionPixelSize(R.styleable.Switch_sw_thumbRadius, ThemeUtil.dpToPx(context, 8)); + mShadowSize = a.getDimensionPixelSize(R.styleable.Switch_sw_thumbElevation, ThemeUtil.dpToPx(context, 2)); + mShadowOffset = mShadowSize / 2; + mMaxAnimDuration = a.getInt(R.styleable.Switch_sw_animDuration, context.getResources().getInteger(android.R.integer.config_mediumAnimTime)); + mGravity = a.getInt(R.styleable.Switch_android_gravity, Gravity.CENTER_VERTICAL); + mChecked = a.getBoolean(R.styleable.Switch_android_checked, false); + mThumbPosition = mChecked ? 1f : 0f; + int resId = a.getResourceId(R.styleable.Switch_sw_interpolator, 0); + mInterpolator = resId != 0 ? AnimationUtils.loadInterpolator(context, resId) : new DecelerateInterpolator(); + + a.recycle(); + + if(mTrackColors == null){ + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + ColorUtil.getColor(ThemeUtil.colorControlNormal(context, 0xFF000000), 0.5f), + ColorUtil.getColor(ThemeUtil.colorControlActivated(context, 0xFF000000), 0.5f), + }; + + mTrackColors = new ColorStateList(states, colors); + } + + if(mThumbColors == null){ + int[][] states = new int[][]{ + new int[]{-android.R.attr.state_checked}, + new int[]{android.R.attr.state_checked}, + }; + int[] colors = new int[]{ + 0xFAFAFA, + ThemeUtil.colorControlActivated(context, 0xFF000000), + }; + + mThumbColors = new ColorStateList(states, colors); + } + + mPaint.setStrokeCap(mTrackCap); + + buildShadow(); + } + @Override public void setOnClickListener(OnClickListener l) { if(l == mRippleManager) @@ -190,7 +208,7 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { mStartTime = SystemClock.uptimeMillis(); break; case MotionEvent.ACTION_MOVE: - float offset = (event.getX() - mMemoX) / (mDrawRect.width() - mThumbRadius * 2 - mThumbBorderSize); + float offset = (event.getX() - mMemoX) / (mDrawRect.width() - mThumbRadius * 2); mThumbPosition = Math.min(1f, Math.max(0f, mThumbPosition + offset)); mMemoX = event.getX(); invalidate(); @@ -243,29 +261,29 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override public int getSuggestedMinimumWidth() { - return (mThumbRadius * 2 + mThumbBorderSize) * 2 + getPaddingLeft() + getPaddingRight(); + return mThumbRadius * 4 + Math.max(mShadowSize, getPaddingLeft()) + Math.max(mShadowSize, getPaddingRight()); } @Override public int getSuggestedMinimumHeight() { - return mThumbRadius * 2 + mThumbBorderSize + getPaddingTop() + getPaddingBottom(); + return mThumbRadius * 2 + Math.max(mShadowSize - mShadowOffset, getPaddingTop()) + Math.max(mShadowSize + mShadowOffset, getPaddingBottom()); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mDrawRect.left = getPaddingLeft(); - mDrawRect.right = w - getPaddingRight(); + mDrawRect.left = Math.max(mShadowSize, getPaddingLeft()); + mDrawRect.right = w - Math.max(mShadowSize, getPaddingRight()); - int height = mThumbRadius * 2 + mThumbBorderSize; + int height = mThumbRadius * 2; int align = mGravity & Gravity.VERTICAL_GRAVITY_MASK; switch (align) { case Gravity.TOP: - mDrawRect.top = getPaddingTop(); + mDrawRect.top = Math.max(mShadowSize - mShadowOffset, getPaddingTop()); mDrawRect.bottom = mDrawRect.top + height; break; case Gravity.BOTTOM: - mDrawRect.bottom = h - getPaddingBottom(); + mDrawRect.bottom = h - Math.max(mShadowSize + mShadowOffset, getPaddingBottom()); mDrawRect.top = mDrawRect.bottom - height; break; default: @@ -275,11 +293,11 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) { } } - private int getStrokeColor(boolean checked){ + private int getTrackColor(boolean checked){ mTempStates[0] = isEnabled() ? android.R.attr.state_enabled : -android.R.attr.state_enabled; mTempStates[1] = checked ? android.R.attr.state_checked : -android.R.attr.state_checked; - return mStrokeColors.getColorForState(mTempStates, 0); + return mTrackColors.getColorForState(mTempStates, 0); } private int getThumbColor(boolean checked){ @@ -288,28 +306,57 @@ private int getThumbColor(boolean checked){ return mThumbColors.getColorForState(mTempStates, 0); } + + private void buildShadow(){ + if(mShadowSize <= 0) + return; + + if(mShadowPaint == null){ + mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); + mShadowPaint.setStyle(Paint.Style.FILL); + mShadowPaint.setDither(true); + } + float startRatio = (float)mThumbRadius / (mThumbRadius + mShadowSize + mShadowOffset); + mShadowPaint.setShader(new RadialGradient(0, 0, mThumbRadius + mShadowSize, + new int[]{COLOR_SHADOW_START, COLOR_SHADOW_START, COLOR_SHADOW_END}, + new float[]{0f, startRatio, 1f} + , Shader.TileMode.CLAMP)); + + if(mShadowPath == null){ + mShadowPath = new Path(); + mShadowPath.setFillType(Path.FillType.EVEN_ODD); + } + else + mShadowPath.reset(); + float radius = mThumbRadius + mShadowSize; + mTempRect.set(-radius, -radius, radius, radius); + mShadowPath.addOval(mTempRect, Path.Direction.CW); + radius = mThumbRadius - 1; + mTempRect.set(-radius, -radius - mShadowOffset, radius, radius - mShadowOffset); + mShadowPath.addOval(mTempRect, Path.Direction.CW); + } + + private void getTrackPath(float x, float y, float radius){ + float halfStroke = mTrackSize / 2f; - private void getPath(float x, float y, float radius){ - float halfStroke = mStrokeSize / 2f; - - mPath.reset(); + mTrackPath.reset(); - if(mStrokeCap != Paint.Cap.ROUND){ + if(mTrackCap != Paint.Cap.ROUND){ mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); float angle = (float)(Math.asin(halfStroke / (radius - 1f)) / Math.PI * 180); if(x - radius > mDrawRect.left){ - mPath.moveTo(mDrawRect.left, y - halfStroke); - mPath.arcTo(mTempRect, 180 + angle, -angle * 2); - mPath.lineTo(mDrawRect.left, y + halfStroke); - mPath.close(); + mTrackPath.moveTo(mDrawRect.left, y - halfStroke); + mTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mTrackPath.lineTo(mDrawRect.left, y + halfStroke); + mTrackPath.close(); } if(x + radius < mDrawRect.right){ - mPath.moveTo(mDrawRect.right, y - halfStroke); - mPath.arcTo(mTempRect, -angle, angle * 2); - mPath.lineTo(mDrawRect.right, y + halfStroke); - mPath.close(); + mTrackPath.moveTo(mDrawRect.right, y - halfStroke); + mTrackPath.arcTo(mTempRect, -angle, angle * 2); + mTrackPath.lineTo(mDrawRect.right, y + halfStroke); + mTrackPath.close(); } } else{ @@ -318,55 +365,51 @@ private void getPath(float x, float y, float radius){ if(x - radius > mDrawRect.left){ float angle2 = (float)(Math.acos(Math.max(0f, (mDrawRect.left + halfStroke - x + radius) / halfStroke)) / Math.PI * 180); - mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mStrokeSize, y + halfStroke); - mPath.arcTo(mTempRect, 180 - angle2, angle2 * 2); + mTempRect.set(mDrawRect.left, y - halfStroke, mDrawRect.left + mTrackSize, y + halfStroke); + mTrackPath.arcTo(mTempRect, 180 - angle2, angle2 * 2); mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); - mPath.arcTo(mTempRect, 180 + angle, -angle * 2); - mPath.close(); + mTrackPath.arcTo(mTempRect, 180 + angle, -angle * 2); + mTrackPath.close(); } if(x + radius < mDrawRect.right){ float angle2 = (float)Math.acos(Math.max(0f, (x + radius - mDrawRect.right + halfStroke) / halfStroke)); - mPath.moveTo((float)(mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float)(y + Math.sin(angle2) * halfStroke)); + mTrackPath.moveTo((float) (mDrawRect.right - halfStroke + Math.cos(angle2) * halfStroke), (float) (y + Math.sin(angle2) * halfStroke)); angle2 = (float)(angle2 / Math.PI * 180); - mTempRect.set(mDrawRect.right - mStrokeSize, y - halfStroke, mDrawRect.right, y + halfStroke); - mPath.arcTo(mTempRect, angle2, -angle2 * 2); + mTempRect.set(mDrawRect.right - mTrackSize, y - halfStroke, mDrawRect.right, y + halfStroke); + mTrackPath.arcTo(mTempRect, angle2, -angle2 * 2); mTempRect.set(x - radius + 1f, y - radius + 1f, x + radius - 1f, y + radius - 1f); - mPath.arcTo(mTempRect, -angle, angle * 2); - mPath.close(); + mTrackPath.arcTo(mTempRect, -angle, angle * 2); + mTrackPath.close(); } } } - - private float getInterpolation(float value){ - return 2 * value * (1f - value); - } - + @Override public void draw(@NonNull Canvas canvas) { super.draw(canvas); - - float shinkWidth = getInterpolation(mThumbPosition) * mThumbBorderSize; - - float outerRadius = mThumbRadius + mThumbBorderSize / 2f - shinkWidth; - float x = (mDrawRect.width() - outerRadius * 2) * mThumbPosition + mDrawRect.left + outerRadius; + + float x = (mDrawRect.width() - mThumbRadius * 2) * mThumbPosition + mDrawRect.left + mThumbRadius; float y = mDrawRect.centerY(); - getPath(x, y, outerRadius); - mPaint.setColor(ColorUtil.getMiddleColor(getStrokeColor(false), getStrokeColor(true), mThumbPosition)); + getTrackPath(x, y, mThumbRadius); + mPaint.setColor(ColorUtil.getMiddleColor(getTrackColor(false), getTrackColor(true), mThumbPosition)); mPaint.setStyle(Paint.Style.FILL); - canvas.drawPath(mPath, mPaint); - - float strokeWidth = mThumbBorderSize + (mThumbRadius - mThumbBorderSize / 2f) * mThumbPosition - shinkWidth; - float radius = mThumbRadius - (strokeWidth - mThumbBorderSize) / 2f - shinkWidth; - - mPaint.setColor(ColorUtil.getMiddleColor(getThumbColor(false), getThumbColor(true), mThumbPosition)); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeWidth(strokeWidth); - canvas.drawCircle(x, y, radius, mPaint); + canvas.drawPath(mTrackPath, mPaint); + + if(mShadowSize > 0){ + int saveCount = canvas.save(); + canvas.translate(x, y + mShadowOffset); + canvas.drawPath(mShadowPath, mShadowPaint); + canvas.restoreToCount(saveCount); + } + + mPaint.setColor(ColorUtil.getMiddleColor(getThumbColor(false), getThumbColor(true), mThumbPosition)); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(x, y, mThumbRadius, mPaint); } private void resetAnimation(){ diff --git a/lib/src/main/res/values/attrs.xml b/lib/src/main/res/values/attrs.xml index 32539f13..b85da2d0 100644 --- a/lib/src/main/res/values/attrs.xml +++ b/lib/src/main/res/values/attrs.xml @@ -150,21 +150,54 @@ - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -278,9 +311,11 @@ - + + - + + diff --git a/lib/src/main/res/values/styles.xml b/lib/src/main/res/values/styles.xml index 6c3caef9..61892644 100644 --- a/lib/src/main/res/values/styles.xml +++ b/lib/src/main/res/values/styles.xml @@ -189,14 +189,36 @@ + + + +