I have to implement Automatic Event Tracking in android
Need to automatically collect analytics data on all button clicks and page views but it has to be done in a generic way so that I don't need to write the Analytics code again for every click.
Example: I have 2 buttons on my activity each of them having a click listener. Now i want to call Analytics.track(String buttonName) so that i do not have to add this in every click listener. The data that should be passed in tracking is button Name.
A way (probably not the ultimate way) to do that could be extending Button (or View), and putting analytics code into the View#performClick() method.
As for the buttonName, it can be a a field of your custom View class, that you can set programmatically or even via an XML custom attribute.
Global implementation :
Create a custom XML attribut : create a file named attrs.xml in the ressource folder :
<resources>
<declare-styleable name="tracking">
<attr name="tracking_name" format="string" />
</declare-styleable>
</resources>
Create a custom Button (or View) class, that overwrite performClick() method and call Analytics.track() with the string gotten from your XML custom attribute or set programmatically :
public class TrackedClickButton extends Button {
private String mTrackingName;
public TrackedClickButton(Context context) {
super(context);
}
public TrackedClickButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public TrackedClickButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
#TargetApi(21)
public TrackedClickButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.tracking);
if (array.hasValue(R.styleable.tracking_name)) {
mTrackingName = array.getString(R.styleable.tracking_name);
}
}
public void setTrackingName(String trackingName) {
this.mTrackingName = trackingName;
}
#Override
public boolean performClick() {
//Make sure the view has an onClickListener that listened the click event,
//so that we don't report click on passive elements
boolean clickHasBeenPerformed = super.performClick();
if(clickHasBeenPerformed && mTrackingName != null) {
Analytics.track(mTrackingName);
}
return clickHasBeenPerformed;
}
}
Use your new class everywhere you want to track the event, for example in a layout file :
<com.heysolutions.dentsply.Activites.MainActivity.TrackedClickButton
xmlns:tracking="http://schemas.android.com/apk/res-auto"
android:id="#+id/button"
android:layout_width="50dp"
android:layout_height="50dp"
tracking:tracking_name="buttonTrackingName"/>
Once again, this is one way, may be some other easier/better/better with your implementation ways :)
Create your own clickListener in Kotlin.
In this exemple, I put a debounceTime variable to prevent double clicking :
fun View.clickAndTrack(debounceTime: Long = 500L, action: () -> Unit) {
this.setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
else {
// do your Analytics action here
action()
}
lastClickTime = SystemClock.elapsedRealtime()
}
})
}
You can use Activity#dispatchTouchEvent(android.view.MotionEvent) to intercept touch events.
Related
Hi I want to set android custom buttons' setMinHeight and setMaxHeight
I have android button widget in the Library project and user of SDK(library project) can take use of that custom Button but I want to put restriction that button's Minimum size has to be 200dp and button's maximum height can not exceed 350dp how do i achieve that from custom view Button custom class?
Tried searching lot of thread but not sure yet.
public class MyCustomButton extends android.widget.Button {
public MyCustomButton(Context context) {
super(context);
applyDefaults(null);
}
public MyCustomButton(Context context, AttributeSet attrs) {
super(context, attrs);
applyDefaults(attrs);
}
public MyCustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
applyDefaults(attrs);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MyCustomButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
applyDefaults(attrs);
}
private void applyDefaults(AttributeSet attrs) {
String height = attrs.getAttributeValue("http://schemas.android.com/apk/res/android", "layout_height");
String width = attrs.getAttributeValue("http://schemas.android.com/apk/res/android", "layout_width");
Log.e("MVYAS=======", "height===" + height);
Log.e("MVYAS=======", "width===" + width);
setUpButtonForLoginOrLogout();
setAllCaps(false);
setMinHeight(getResources().getDimensionPixelSize(R.dimen.mid_button_min_height));
setMinWidth(getResources().getDimensionPixelSize(R.dimen.mid_button_min_width));
setMaxHeight(getResources().getDimensionPixelSize(R.dimen.mid_button_max_height));
setMaxWidth(getResources().getDimensionPixelSize(R.dimen.mid_button_max_width));
}
}
and while using into layout.xml file
<com.example.library.widget.MyCustomButton
android:id="#+id/my_button"
android:layout_width="153dp"
android:layout_height="57dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_alignWithParentIfMissing="true"
android:layout_margin="10dp"
styleButton:mobileid="inverted"/>
Here user should not be able to create button height to 57dp as its lower than the desired button height.
Putting restriction by adding setMinHeight() and setMaxHeight() dose not work.
How do i achieve that. Your help appreciated.
Thanks in Advance.
Just add these below two methods in MyCustomButtom class and try it:
#Override
public void setMinimumHeight(int minHeight) {
super.setMinimumHeight(minHeight);
}
#Override
public void setMinHeight(#Px int minHeight) {
super.setMinHeight(minHeight);
}
And one more thing, add below line in applyDefaults() function.
setMinimumHeight(getResources().getDimensionPixelSize(R.dimen.mid_button_min_height));
When creating a custom view, I have noticed that many people seem to do it like this:
public MyView(Context context) {
super(context);
// this constructor used when programmatically creating view
doAdditionalConstructorWork();
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
// this constructor used when creating view through XML
doAdditionalConstructorWork();
}
private void doAdditionalConstructorWork() {
// init variables etc.
}
My first question is, what about the constructor MyView(Context context, AttributeSet attrs, int defStyle)? I'm not sure where it is used, but I see it in the super class. Do I need it, and where is it used?
There's another part to this question.
Long story short, No, but if you do override any constructor, then ensure to call super(...) with the exact same number of arguments (like, see Jin's answer for example why).
If you will add your custom View from xml also like :
<com.mypack.MyView
...
/>
you will need the constructor public MyView(Context context, AttributeSet attrs), otherwise you will get an Exception when Android tries to inflate your View.
If you add your View from xml and also specify the android:style attribute like :
<com.mypack.MyView
style="#styles/MyCustomStyle"
...
/>
the 2nd constructor will also be called and default the style to MyCustomStyle before applying explicit XML attributes.
The third constructor is usually used when you want all of the Views in your application to have the same style.
If you override all three constructors, please DO NOT CASCADE this(...) CALLS. You should instead be doing this:
public MyView(Context context) {
super(context);
init(context, null, 0);
}
public MyView(Context context, AttributeSet attrs) {
super(context,attrs);
init(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
private void init(Context context, AttributeSet attrs, int defStyle) {
// do additional work
}
The reason is that the parent class might include default attributes in its own constructors that you might be accidentally overriding. For example, this is the constructor for TextView:
public TextView(Context context) {
this(context, null);
}
public TextView(Context context, #Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
public TextView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
If you did not call super(context), you would not have properly set R.attr.textViewStyle as the style attr.
MyView(Context context)
Used when instanciating Views programmatically.
MyView(Context context, AttributeSet attrs)
Used by the LayoutInflater to apply xml attributes. If one of this attribute is named style, attributes will be looked up the the style before looking for explicit values in the layout xml file.
MyView(Context context, AttributeSet attrs, int defStyleAttr)
Suppose you want to apply a default style to all widgets without having to specify style in each layout file. For an example make all checkboxes pink by default. You can do this with defStyleAttr and the framework will lookup the default style in your theme.
Note that defStyleAttr was incorrectly named defStyle some time ago and there is some discussion about whether this constructor is really needed or not. See https://code.google.com/p/android/issues/detail?id=12683
MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
The 3rd constructor works well if you have control over the base theme of the applications. That is working for google because they ship their widgets along side the default Themes. But suppose you're writing a widget library and you want a default style to be set without your users needing to tweak their theme. You can now do this using defStyleRes by setting it to the default value in the 2 first constructors:
public MyView(Context context) {
super(context, null, 0, R.style.MyViewStyle);
init();
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs, 0, R.style.MyViewStyle);
init();
}
All in all
If you're implementing your own views, only the 2 first constructors should be needed and can be called by the framework.
If you want your Views to be extensible, you might implement the 4th constructor for children of your class to be able to use global styling.
I don't see a real use case for the 3rd constructor. Maybe a shortcut if you don't provide a default style for your widget but still want your users to be able to do so. Shouldn't happen that much.
Kotlin seems to take away a lot of this pain:
class MyView
#JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: View(context, attrs, defStyle)
#JvmOverloads will generate all required constructors (see that annotation's documentation), each of which presumably calls super(). Then, simply replace your initialization method with a Kotlin init {} block. Boilerplate code gone!
The third constructor is much more complicated.Let me hold an example.
Support-v7 SwitchCompact package supports thumbTint and trackTint attribute since 24 version while 23 version does not support them.Now you want to support them in 23 version and how will you do to achieve this?
We assume to use custom View SupportedSwitchCompact extends SwitchCompact.
public SupportedSwitchCompat(Context context) {
this(context, null);
}
public SupportedSwitchCompat(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SupportedSwitchCompat(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mThumbDrawable = getThumbDrawable();
mTrackDrawable = getTrackDrawable();
applyTint();
}
It's a traditional code style.Note we pass 0 to the third param here. When you run the code, you will find getThumbDrawable() always return null how strange it is because the method getThumbDrawable() is its super class SwitchCompact's method.
If you pass R.attr.switchStyle to the third param, everything goes well.So why?
The third param is a simple attribute. The attribute points to a style resource.In above case, the system will find switchStyle attribute in current theme fortunately system finds it.
In frameworks/base/core/res/res/values/themes.xml, you will see:
<style name="Theme">
<item name="switchStyle">#style/Widget.CompoundButton.Switch</item>
</style>
If you have to include three constructors like the one under discussion now, you could do this too.
public MyView(Context context) {
this(context,null,0);
}
public MyView(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
doAdditionalConstructorWork();
}
Here is my button:
public class ChimmerButton extends Button {
public ChimmerButton(Context context) {
super(context);
}
public ChimmerButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ChimmerButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/*
* This method is used to apply the external font
*/
public void setTypeface(Typeface tf, int style) {
if (!isInEditMode()) {
super.setTypeface(Typeface.createFromAsset(
getContext().getAssets(), "calibre-regular.ttf"));
}
}
}
How can i apply Theme.Light.NoTitleBar.Fullscreen on all chimmerButtons using above code?? Is there any solution for this?
Note that this should be called before any views are instantiated in
the Context (for example before calling setContentView(View) or
inflate(int, ViewGroup)).
from:
http://developer.android.com/reference/android/view/ContextThemeWrapper.html#setTheme%28int%29
Unfortunately, You must set the theme before displaying the Activity at all.
Therefore you can't have 'dynamic' themes driven by runtime code (though the comment above shows how to make a custom theme for your buttons)
ContextThemeWrapper themedContext;
public ChimmerButton(ContextThemeWrapper themedContext) {
This.themedContext = themedContext;
}
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ) {
themedContext = new ContextThemeWrapper( Activity.this, android.R.style.Theme_Holo_Light_Dialog_NoActionBar );
}
else {
themedContext = new ContextThemeWrapper( Activity.this, android.R.style.Theme_Light_NoTitleBar );
Button's theme is context dependent by default. So instead of setting theme for buttons, set theme for the activity that contains the button. It is a much easier solution.
Apply a theme to an activity in Android?
Just for extending CheckBoxPreference or SwitchPreference on Android Lollipop, the widget (the checkbox or the switch) won't have animation anymore.
I'd like to extend SwitchPreference to force api < 21 to use SwitchCompat instead of the default one they are using (which is obviously wrong).
I am using the new AppCompatPreferenceActivity with appcompat-v7:22.1.1 but that doesn't seem to affect the switches.
The thing is that with just extending those classes, without adding any custom layout or widget resource layout, the animation is gone.
I know I can write two instances of my preference.xml (on inside values-v21) and it will work... But I'd like to know why is this happening and if somebody knows a solution without having two preference.xml.
Code example:
public class SwitchPreference extends android.preference.SwitchPreference {
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwitchPreference(Context context) {
super(context);
}
}
This or the same for CheckBoxPreference and then using:
<com.my.package.SwitchPreference />
Will make the animation in a Lollipop device to be gone.
--
Another thing I tried for the SwitchPreference (that I can with CheckBoxPreference) is to give a layout with the default id but #android:id/switchWidgetis not public while #android:id/checkbox is. I also know I can use a <CheckBoxPreference /> and give a widget layout that is in fact a SwitchCompat, but I'd like to avoid that (confusing the names).
It seems I found a fix for your issue.
Extensive Explanation
In SwitchCompat, when toggling the the switch, it tests a few functions before playing the animation: getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown().
Full method:
#Override
public void setChecked(boolean checked) {
super.setChecked(checked);
// Calling the super method may result in setChecked() getting called
// recursively with a different value, so load the REAL value...
checked = isChecked();
if (getWindowToken() != null && ViewCompat.isLaidOut(this) && isShown()) {
animateThumbToCheckedState(checked);
} else {
// Immediately move the thumb to the new position.
cancelPositionAnimator();
setThumbPosition(checked ? 1 : 0);
}
}
By using a custom view extending SwitchCompat, I found out, that isShown() always returns false, because the at third iteration of the while, parent == null.
public boolean isShown() {
View current = this;
//noinspection ConstantConditions
do {
if ((current.mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
ViewParent parent = current.mParent;
if (parent == null) {
return false; // We are not attached to the view root
}
if (!(parent instanceof View)) {
return true;
}
current = (View) parent;
} while (current != null);
return false;
}
Interestingly, the third parent is the second attribute passed to getView(View convertView, ViewGroup parent) in Preference, means the PreferenceGroupAdapter didn't get a parent passed to its own getView(). Why this happens exactly and why this happens only for custom preference classes, I don't know.
For my testing purposes, I used the CheckBoxPreference with a SwitchCompat as widgetLayout, and I also didn't see animations.
Fix
Now to the fix: simply make your own view extending SwitchCompat, and override your isShown() like this:
#Override
public boolean isShown() {
return getVisibility() == VISIBLE;
}
Use this SwitchView for your widgetLayout style, and animations work again :D
Styles:
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
…
<item name="android:checkBoxPreferenceStyle">#style/Preference.SwitchView</item>
…
</style>
<style name="Preference.SwitchView">
<item name="android:widgetLayout">#layout/preference_switch_view</item>
</style>
Widget layout:
<de.Maxr1998.example.preference.SwitchView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#null"
android:clickable="false"
android:focusable="false" />
Sometimes Extending from a Class is not the best solution. To avoid loosing the animations you could instead Compose it, I meant creating a Class where you have a SwitchPreference field variable and apply the new logic to it. It's like a wrapper. This worked for me.
i manage to fix it like this and animations is working before it was going to the state directly without animation:
FIX:
CustomSwitchCompat.class
public class CustomSwitchCompat extends SwitchCompat {
public CustomSwitchCompat(Context context) {
super(context);
}
public CustomSwitchCompat(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomSwitchCompat(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean isShown() {
return getVisibility() == VISIBLE;
}
}
In your layout do this: preference_switch_layout.xml
<com.example.CustomSwitchCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#null"
android:clickable="false"
android:focusable="false"
app:switchMinWidth="55dp"/>
and in your preference.xml do this:
<CheckBoxPreference
android:defaultValue="false"
android:key=""
android:widgetLayout="#layout/preference_switch_layout"
android:summary=""
android:title="" />
I was having this issue, when I was using custom layout (app:layout) for SwitchPreference. At first, switch animation was triggered, but after a little scrolling it stopped and switch was jumping without animation. I tried every solution from stackoverflow, but nothing helped.
After debugging of SwitchCompat.setChecked method I found out that this condition is failing:
public void setChecked(boolean checked) {
...
if (getWindowToken() != null && ViewCompat.isLaidOut(this)) {
animateThumbToCheckedState(checked);
} else {
// Immediately move the thumb to the new position.
cancelPositionAnimator();
setThumbPosition(checked ? 1 : 0);
}
}
Concretely ViewCompat.isLaidOut(this) returned false. I guess this is a bug either in View or Preference (or subclasses). Anyway, I was able to fix this with little hack.
I created a subclass of SwitchCompat and did override setChecked method, where I call requestLayout() and in onNextLayout I call SwitchCompat's setChecked method. This guarantees that isLaidOut condition is true when changing checked state.
Full code of custom SwitchCompat:
class SwitchCompatFix #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.appcompat.R.attr.switchStyle,
): SwitchCompat(context, attrs, defStyleAttr) {
override fun setChecked(checked: Boolean) {
doOnNextLayout {
post { super.setChecked(checked) }
}
requestLayout()
}
}
public class SwitchPreference extends android.preference.SwitchPreference {
public SwitchPreference(Context context) {
this(context, null);
}
public SwitchPreference(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.checkBoxPreferenceStyle);
}
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
try {
Field canRecycleLayoutField = Preference.class.getDeclaredField("mCanRecycleLayout");
canRecycleLayoutField.setAccessible(true);
canRecycleLayoutField.setBoolean(this, true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
I have a LinearLayout with some Views in it. Then I want to treat this element as a View, so I created a new class extending from LinearLayout. Now when I dynamically add a new instance of this class into the layout I see nothing. I believe I have to get the View somehow from my class, but don't know how. Is it possible to somehow assocciate this new class with an xml?
Update:
public class Task extends LinearLayout {
public Task(Context context) {
super(context);
LayoutInflater.from(context).inflate(R.layout.task_view, this, false);
}
public Task(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.task_view, this, false);
}
public Task(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
LayoutInflater.from(context).inflate(R.layout.task_view, this, false);
}
}
Then:
Task newTask = new Task(getActivity());
someLinearLayout.addView((View) newTask); // happens nothing
You can use different approaches like:
Inflating it into a View
Using <include> tag
Inflating:
public class InflatedView extends LinearLayout
{
public InflatedView(Context c)
{
super(c);
LayoutInflater.from(this).inflate(R.layout.your_other_layout, this);
}
//override other constructors too.
}
Now you can use this in your xmls like this:
<com.your.package.InflatedView android:layout_height="etc" other:attribute="here" />
Include:
Very simple, use include tag:
<include layout="#layout/your_other_layout"/>
Here are RomainGuy's layout tricks.