Implementazioni varie
This commit is contained in:
1
waterfall_toolbar/.gitignore
vendored
Normal file
1
waterfall_toolbar/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
43
waterfall_toolbar/build.gradle
Normal file
43
waterfall_toolbar/build.gradle
Normal file
@@ -0,0 +1,43 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
|
||||
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
// google
|
||||
implementation "com.android.support:cardview-v7:27.1.1"
|
||||
implementation "com.android.support:design:27.1.1"
|
||||
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
21
waterfall_toolbar/proguard-rules.pro
vendored
Normal file
21
waterfall_toolbar/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,26 @@
|
||||
package it.integry.plugins.waterfalltoolbar;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("it.integry.plugins.waterfall_toolbar.test", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
2
waterfall_toolbar/src/main/AndroidManifest.xml
Normal file
2
waterfall_toolbar/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="it.integry.plugins.waterfalltoolbar" />
|
||||
@@ -0,0 +1,19 @@
|
||||
package it.integry.plugins.waterfalltoolbar
|
||||
|
||||
var density: Float? = null
|
||||
|
||||
data class Dp(var value: Float) {
|
||||
fun toPx(): Px {
|
||||
val innerDensity: Float = density ?: throw NullPointerException(
|
||||
"You must set density before using DimensionUnits classes.")
|
||||
return Px((value * innerDensity + 0.5f).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
data class Px(var value: Int) {
|
||||
fun toDp(): Dp {
|
||||
val innerDensity: Float = density ?: throw NullPointerException(
|
||||
"You must set density before using DimensionUnits classes.")
|
||||
return Dp(value / innerDensity + 0.5f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package it.integry.plugins.waterfalltoolbar
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.widget.NestedScrollView
|
||||
import android.support.v7.widget.CardView
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
|
||||
/**
|
||||
* Created by Hugo Castelani
|
||||
* Date: 19/09/17
|
||||
* Time: 19:30
|
||||
*/
|
||||
|
||||
open class WaterfallToolbar : CardView {
|
||||
init {
|
||||
// set density to be able to use DimensionUnits
|
||||
// this code must run before all the signings using DimensionUnits
|
||||
if (density == null) density = resources.displayMetrics.density
|
||||
}
|
||||
|
||||
/**
|
||||
* The recycler view whose scroll is going to be listened
|
||||
*/
|
||||
var recyclerView: RecyclerView? = null
|
||||
set(value) {
|
||||
field = value
|
||||
addRecyclerViewScrollListener()
|
||||
}
|
||||
|
||||
/**
|
||||
* The scroll view whose scroll is going to be listened
|
||||
*/
|
||||
var scrollView: ScrollView? = null
|
||||
set(value) {
|
||||
field = value
|
||||
addScrollViewScrollListener()
|
||||
}
|
||||
|
||||
/**
|
||||
* The scroll view whose scroll is going to be listened
|
||||
*/
|
||||
var nestedScrollView: NestedScrollView? = null
|
||||
set(value) {
|
||||
field = value
|
||||
addNestedScrollViewScrollListener()
|
||||
}
|
||||
|
||||
/**
|
||||
* The three variables ahead are null safe, since they are always set
|
||||
* at least once in init() and a null value can't be assigned to them
|
||||
* after that. So all the "!!" involving them below are fully harmless.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The elevation with which the toolbar starts
|
||||
*/
|
||||
var initialElevation: Px? = null
|
||||
set(value) {
|
||||
if (value != null) {
|
||||
field = value
|
||||
|
||||
// got to update elevation in case this value have
|
||||
// been set in a running and visible activity
|
||||
if (isSetup) adjustCardElevation()
|
||||
|
||||
} else throw NullPointerException("This field cannot be null.")
|
||||
}
|
||||
|
||||
/**
|
||||
* The elevation the toolbar gets when it reaches final scroll elevation
|
||||
*/
|
||||
var finalElevation: Px? = null
|
||||
set(value) {
|
||||
if (value != null) {
|
||||
field = value
|
||||
|
||||
// got to update elevation in case this value have
|
||||
// been set in a running and visible activity
|
||||
if (isSetup) adjustCardElevation()
|
||||
|
||||
} else throw NullPointerException("This field cannot be null.")
|
||||
}
|
||||
|
||||
/**
|
||||
* The percentage of the screen's height that is
|
||||
* going to be scrolled to reach the final elevation
|
||||
*/
|
||||
var scrollFinalPosition: Int? = null
|
||||
set(value) {
|
||||
if (value != null) {
|
||||
val screenHeight = resources.displayMetrics.heightPixels
|
||||
field = Math.round(screenHeight * (value / 100.0f))
|
||||
|
||||
// got to update elevation in case this value have
|
||||
// been set in a running and visible activity
|
||||
if (isSetup) adjustCardElevation()
|
||||
|
||||
} else throw NullPointerException("This field cannot be null.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimension units (dp and pixel) auxiliary
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Values related to Waterfall Toolbar behavior in their default forms
|
||||
*/
|
||||
val defaultInitialElevation = Dp(0f).toPx()
|
||||
val defaultFinalElevation = Dp(4f).toPx()
|
||||
val defaultScrollFinalElevation = 12
|
||||
|
||||
/**
|
||||
* Auxiliary that indicates if the view is already setup
|
||||
*/
|
||||
private var isSetup: Boolean = false
|
||||
|
||||
/**
|
||||
* Position in which toolbar must be to reach expected shadow
|
||||
*/
|
||||
private var orthodoxPosition = Px(0)
|
||||
|
||||
/**
|
||||
* Recycler/scroll view real position
|
||||
*/
|
||||
private var realPosition = Px(0)
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context, null)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int?)
|
||||
: super(context, attrs, defStyleAttr!!) {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
private fun init(context: Context?, attrs: AttributeSet?) {
|
||||
// leave card corners square
|
||||
radius = 0f
|
||||
|
||||
if (context != null && attrs != null) {
|
||||
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaterfallToolbar)
|
||||
|
||||
val rawInitialElevation = typedArray.getDimensionPixelSize(
|
||||
R.styleable.WaterfallToolbar_initial_elevation, defaultInitialElevation.value)
|
||||
|
||||
val rawFinalElevation = typedArray.getDimensionPixelSize(
|
||||
R.styleable.WaterfallToolbar_final_elevation, defaultFinalElevation.value)
|
||||
|
||||
scrollFinalPosition = typedArray.getInteger(
|
||||
R.styleable.WaterfallToolbar_scroll_final_elevation, defaultScrollFinalElevation)
|
||||
|
||||
this.initialElevation = Px(rawInitialElevation)
|
||||
this.finalElevation = Px(rawFinalElevation)
|
||||
|
||||
typedArray.recycle()
|
||||
|
||||
} else {
|
||||
|
||||
initialElevation = defaultInitialElevation
|
||||
finalElevation = defaultFinalElevation
|
||||
scrollFinalPosition = defaultScrollFinalElevation
|
||||
}
|
||||
|
||||
adjustCardElevation() // just to make sure card elevation is set
|
||||
|
||||
isSetup = true
|
||||
}
|
||||
|
||||
private fun addRecyclerViewScrollListener() {
|
||||
recyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
// real position must always get updated
|
||||
realPosition.value = realPosition.value + dy
|
||||
mutualScrollListenerAction()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun addScrollViewScrollListener() {
|
||||
scrollView?.viewTreeObserver?.addOnScrollChangedListener {
|
||||
// real position must always get updated
|
||||
realPosition.value = scrollView!!.scrollY
|
||||
mutualScrollListenerAction()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNestedScrollViewScrollListener() {
|
||||
nestedScrollView?.viewTreeObserver?.addOnScrollChangedListener {
|
||||
realPosition.value = nestedScrollView!!.scrollY
|
||||
mutualScrollListenerAction()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These lines are common in both scroll listeners, so they are better joined
|
||||
*/
|
||||
private fun mutualScrollListenerAction() {
|
||||
// orthodoxPosition can't be higher than scrollFinalPosition because
|
||||
// the last one holds the position in which shadow reaches ideal size
|
||||
|
||||
if (realPosition.value <= scrollFinalPosition!!) {
|
||||
orthodoxPosition.value = realPosition.value
|
||||
} else {
|
||||
orthodoxPosition.value = scrollFinalPosition!!
|
||||
}
|
||||
|
||||
adjustCardElevation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed up the card elevation setting
|
||||
*/
|
||||
private fun adjustCardElevation() {
|
||||
cardElevation = calculateElevation().value.toFloat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the elevation based on given attributes and scroll
|
||||
* @return New calculated elevation
|
||||
*/
|
||||
private fun calculateElevation(): Px {
|
||||
// getting back to rule of three:
|
||||
// finalElevation = scrollFinalPosition
|
||||
// newElevation = orthodoxPosition
|
||||
var newElevation: Int = finalElevation!!.value * orthodoxPosition.value / scrollFinalPosition!!
|
||||
|
||||
// avoid values under minimum value
|
||||
if (newElevation < initialElevation!!.value) newElevation = initialElevation!!.value
|
||||
|
||||
return Px(newElevation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the view's current dynamic state in a parcelable object
|
||||
* @return A parcelable with the saved data
|
||||
*/
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
val savedState = SavedState(super.onSaveInstanceState())
|
||||
|
||||
savedState.elevation = cardElevation.toInt()
|
||||
savedState.orthodoxPosition = orthodoxPosition
|
||||
savedState.realPosition = realPosition
|
||||
|
||||
return savedState
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the view's dynamic state
|
||||
* @param state The frozen state that had previously been returned by onSaveInstanceState()
|
||||
*/
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
if (state is SavedState) {
|
||||
super.onRestoreInstanceState(state.superState)
|
||||
|
||||
// setting card elevation doesn't work until view is created
|
||||
post {
|
||||
// it's safe to use "!!" here, since savedState will
|
||||
// always store values properly set in onSaveInstanceState()
|
||||
cardElevation = state.elevation!!.toFloat()
|
||||
orthodoxPosition = state.orthodoxPosition!!
|
||||
realPosition = state.realPosition!!
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
super.onRestoreInstanceState(state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom parcelable to store this view's dynamic state
|
||||
*/
|
||||
private class SavedState : View.BaseSavedState {
|
||||
var elevation: Int? = null
|
||||
var orthodoxPosition: Px? = null
|
||||
var realPosition: Px? = null
|
||||
|
||||
internal constructor(source: Parcel) : super(source)
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
internal constructor(source: Parcel, loader: ClassLoader) : super(source, loader)
|
||||
|
||||
internal constructor(superState: Parcelable) : super(superState)
|
||||
|
||||
companion object {
|
||||
internal val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(source: Parcel): SavedState {
|
||||
return SavedState(source)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
waterfall_toolbar/src/main/res/values/attrs.xml
Normal file
8
waterfall_toolbar/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="WaterfallToolbar">
|
||||
<attr format="dimension" name="initial_elevation" />
|
||||
<attr format="dimension" name="final_elevation" />
|
||||
<attr format="integer" name="scroll_final_elevation" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
3
waterfall_toolbar/src/main/res/values/strings.xml
Normal file
3
waterfall_toolbar/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">Waterfall Toolbar</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package it.integry.plugins.waterfalltoolbar;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user