android
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
android [2015/02/21 16:25] – [ScrolView] skipidar | android [2022/09/03 22:42] (current) – skipidar | ||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ==== Linux environment ==== | ||
+ | How to set up a headless build environment. | ||
+ | |||
+ | |OS|Ubuntu 20.04| | ||
+ | |Android SDK|Android 11 (API level 30) as of 26.11.2021| | ||
+ | |||
+ | |||
+ | ==== Install android-sdk on Ubuntu ==== | ||
+ | |||
+ | <sxh bash> | ||
+ | # install openjdk8 - tested android sdk and its compatible | ||
+ | sudo apt-get install openjdk-8-jdk | ||
+ | # activate the 8th version if you have multiple using: | ||
+ | # sudo update-alternatives --config java | ||
+ | |||
+ | |||
+ | # install android-sdk under / | ||
+ | sudo apt update && sudo apt install android-sdk | ||
+ | |||
+ | |||
+ | |||
+ | # download the commandlinetools and add them to sdk | ||
+ | # < | ||
+ | wget -P /tmp/ https:// | ||
+ | |||
+ | # check https:// | ||
+ | sudo mkdir -p / | ||
+ | |||
+ | sudo unzip / | ||
+ | sudo mv / | ||
+ | |||
+ | # open bashrc and modify the environment variables | ||
+ | vim ~/.bashrc | ||
+ | export ANDROID_HOME="/ | ||
+ | export PATH=$PATH: | ||
+ | |||
+ | # apply changes in bashrc | ||
+ | source ~/.bashrc | ||
+ | |||
+ | |||
+ | # setting the rights | ||
+ | sudo chown $USER:$USER $ANDROID_HOME -R | ||
+ | |||
+ | |||
+ | |||
+ | # can successfully access the sdkmanager | ||
+ | sdkmanager --version | ||
+ | |||
+ | # install the platform tools https:// | ||
+ | sdkmanager " | ||
+ | |||
+ | # install the build tools https:// | ||
+ | sdkmanager " | ||
+ | |||
+ | |||
+ | # confirm all the licenses for the android tools otherwise they wont work | ||
+ | yes | sdkmanager --licenses | ||
+ | |||
+ | |||
+ | # check for android sdk updates | ||
+ | sdkmanager --update | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | # you can build android project now | ||
+ | cd < | ||
+ | ./gradlew build | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ==== HOWTO ==== | ||
+ | |||
+ | === Plan app's Layout === | ||
+ | * use Fragments for top level elements which may be reused, since fragments **can not be nested**. | ||
+ | * implement Views with custom Layout, implemented in **xml** to use these blocks on deeper levels. \\ These views can be nested. \\ implement layout in xml and assign the xml layout to the view (see Create a View class which will wrap the XML Layout) | ||
+ | |||
+ | ==== Declaring custom XML attributes ==== | ||
+ | |||
+ | An in detail describtion about declaring custom XML attributes for custom views can be found here: | ||
+ | [[http:// | ||
+ | |||
+ | <fc # | ||
+ | |||
+ | - Nutze diesen Namespace, statt den der eigenen App: < | ||
+ | ==== Fading Background ==== | ||
+ | Is described here: http:// | ||
+ | |||
+ | ==== ListView ==== | ||
+ | |||
+ | == Select Item == | ||
+ | <sxh java> | ||
+ | | ||
+ | </ | ||
+ | == Iterate Items== | ||
+ | <sxh java> | ||
+ | | ||
+ | |||
+ | for(int i=0; i< | ||
+ | View v = (View) contentListAdapter.getItem(i); | ||
+ | Log.d(" | ||
+ | </ | ||
+ | |||
+ | ==== Using JAR Libs ==== | ||
+ | A Jar can be added to android, by putting it into the **libs** folder! | ||
+ | |||
+ | |||
+ | **ACHTUNG: | ||
+ | |||
+ | Adroid kann nur JARs nutzen, die mit dem Compiler 1.7 gebaut wurden. | ||
+ | Falls dies nicht der Fall ist - wird eine NoClassDefFound exception geworfen. | ||
+ | |||
+ | {{http:// | ||
+ | |||
+ | |||
+ | ==== Tools ==== | ||
+ | The tool monitor.bat can be used as a Standalone logger! Details are [[http:// | ||
+ | |||
+ | {{http:// | ||
+ | |||
+ | |||
+ | === Aapt === | ||
+ | Inspect the APK is done via | ||
+ | |||
+ | < | ||
+ | D: | ||
+ | </ | ||
+ | |||
+ | === Unpackaging the APK is done via === | ||
+ | https:// | ||
+ | |||
+ | < | ||
+ | apktool d HelloWorld.apk ./ | ||
+ | </ | ||
+ | |||
+ | === Genimotion | ||
+ | Genimotion is a nice and fast Android VM. | ||
+ | |||
+ | It may support ARM Architecture, | ||
+ | How to do that is described here: http:// | ||
+ | ==== Maven ==== | ||
+ | |||
+ | == Dependncies == | ||
+ | |||
+ | **ACHTUNG: | ||
+ | For some reason the dependencies form parent pom are not inherited to the child pom. | ||
+ | So always add dependencies directly to children | ||
+ | |||
+ | Dependency to my own apklib: | ||
+ | |||
+ | The apklib | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | The dependency | ||
+ | < | ||
+ | <!-- Barcode Helper Lib --> | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ==Package type== | ||
+ | |||
+ | |||
+ | The apk is an App. | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | The apklib - is the android lib project | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==Signieren== | ||
+ | Wie man Maven einrichtet, um eine signierte APK bauen zu können steth hier: \\ | ||
+ | http:// | ||
+ | |||
+ | Danach kann eine signierte **apk** z.B. durch erzeugt werden: | ||
+ | < | ||
+ | mvn clean install | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==ZipAlign== | ||
+ | TO do the zipalign - use the code as described here: http:// | ||
+ | |||
+ | DO not forget the **< | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | <sdk> | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | ==Release== | ||
+ | Bauene in release mode, so dass die Variable **BuildConfig.DEBUG==false** ist: | ||
+ | ist beim plugin **android-maven-plugin** am besten über einen Parameter **-Dandroid.release=true** steuerbar. | ||
+ | |||
+ | Die Details sind hier beschrieben: | ||
+ | |||
+ | < | ||
+ | clean install -Dandroid.release=true | ||
+ | </ | ||
+ | ==== In-App Payments ==== | ||
+ | |||
+ | ^Method ^ Describtion ^ Methods ^ Transaction fee ^ | ||
+ | |Native | ||
+ | |||| | ||
+ | |||
+ | ==== Certificates ==== | ||
+ | Check Signature | ||
+ | < | ||
+ | jarsigner -verify -verbose -certs my_application.apk | ||
+ | </ | ||
+ | |||
+ | |||
+ | The Certificate can be extracted from an APK. | ||
+ | Just Exctract the File META-INF\ADT.RSA from the apk file, since an apk can be entered as a normal zip. | ||
+ | Then use the following code to convert the ADT.RSA to the certificate. | ||
+ | < | ||
+ | openssl.exe pkcs7 -in ADT.RSA -print_certs -inform DER -out ADT.RSA.CER | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==== Services ==== | ||
+ | Services may run, after the app was stopped. | ||
+ | |||
+ | <sxh java> | ||
+ | public class MyService extends IntentService { | ||
+ | |||
+ | public MyService() { | ||
+ | super(" | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | protected void onHandleIntent(Intent intent) { } | ||
+ | |||
+ | |||
+ | |||
+ | /** | ||
+ | * Services has to use Handlers to toast messages | ||
+ | * @param context - the app context | ||
+ | * @param message - the message to toast | ||
+ | * @param handler - the handler to reuse | ||
+ | */ | ||
+ | public static void toastFromService(final Context context, | ||
+ | final String message, Handler handler) { | ||
+ | handler.post(new Runnable() { | ||
+ | @Override | ||
+ | public void run() { | ||
+ | Toast.makeText(context, | ||
+ | LOG.info(" | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | |||
+ | /** Start the instance of IntentService service by sending an Intent */ | ||
+ | public static final void start(Context context) { | ||
+ | Intent intent = new Intent(context, | ||
+ | context.startService(intent); | ||
+ | } | ||
+ | |||
+ | /** to toast stuff use the handler */ | ||
+ | void toastMessage(final String message){ | ||
+ | handler.post(new Runnable() { | ||
+ | @Override | ||
+ | public void run() { | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Automatic Testing ==== | ||
+ | - Create a new Test Project, as [http:// | ||
+ | |||
+ | == android.test.ActivityInstrumentationTestCase2< | ||
+ | Use this clss to test activities. | ||
+ | |||
+ | * Every method in this class will be executed as a JUnitTest. | ||
+ | * Before every test - the method **setUp()** is executed | ||
+ | * test methods names have to start with **" | ||
+ | |||
+ | |||
+ | === Espresso - UI tests === | ||
+ | |||
+ | <sxh java> | ||
+ | package digital.alf.geosound; | ||
+ | |||
+ | import static androidx.test.espresso.Espresso.onView; | ||
+ | import static androidx.test.espresso.assertion.ViewAssertions.matches; | ||
+ | import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; | ||
+ | import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; | ||
+ | import static androidx.test.espresso.matcher.ViewMatchers.withText; | ||
+ | import static org.hamcrest.CoreMatchers.allOf; | ||
+ | |||
+ | import android.Manifest; | ||
+ | import android.app.Activity; | ||
+ | import android.os.Bundle; | ||
+ | |||
+ | import androidx.navigation.NavController; | ||
+ | import androidx.navigation.Navigation; | ||
+ | import androidx.test.espresso.action.ViewActions; | ||
+ | import androidx.test.espresso.matcher.ViewMatchers; | ||
+ | import androidx.test.ext.junit.rules.ActivityScenarioRule; | ||
+ | import androidx.test.ext.junit.runners.AndroidJUnit4; | ||
+ | import androidx.test.filters.LargeTest; | ||
+ | import androidx.test.rule.GrantPermissionRule; | ||
+ | |||
+ | import org.junit.Rule; | ||
+ | import org.junit.Test; | ||
+ | import org.junit.runner.RunWith; | ||
+ | |||
+ | @RunWith(AndroidJUnit4.class) | ||
+ | @LargeTest | ||
+ | public class JustOpenAllWindows { | ||
+ | |||
+ | @Rule | ||
+ | public ActivityScenarioRule< | ||
+ | |||
+ | @Rule | ||
+ | public GrantPermissionRule mRuntimePermissionRuleFine = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION); | ||
+ | |||
+ | @Rule | ||
+ | public GrantPermissionRule mRuntimePermissionRuleCoarse = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION); | ||
+ | |||
+ | @Rule | ||
+ | public GrantPermissionRule mRuntimePermissionRuleBackground = GrantPermissionRule.grant(Manifest.permission.ACCESS_BACKGROUND_LOCATION); | ||
+ | |||
+ | |||
+ | @Test | ||
+ | public void openPreferences() { | ||
+ | |||
+ | activityRule.getScenario().onActivity(activity -> { | ||
+ | Bundle args = new Bundle(); | ||
+ | NavController navController = Navigation.findNavController(activity, | ||
+ | navController.navigate(R.id.action_fragment_maps_to_fragment_preferences, | ||
+ | }); | ||
+ | |||
+ | onView(allOf(ViewMatchers.withText(R.string.dialog_preference_label), | ||
+ | .check(matches(isDisplayed())); | ||
+ | |||
+ | onView(ViewMatchers.withText(R.string.dialog_default_onout_volume_title)) | ||
+ | .perform(ViewActions.click()); | ||
+ | |||
+ | //Click on cancel button | ||
+ | onView(ViewMatchers.withId(android.R.id.button2)) | ||
+ | .perform(ViewActions.click()); | ||
+ | |||
+ | |||
+ | onView(ViewMatchers.withText(R.string.dialog_default_onout_volume_title)) | ||
+ | .perform(ViewActions.click()); | ||
+ | |||
+ | //Click on cancel button | ||
+ | onView(ViewMatchers.withId(android.R.id.button2)) | ||
+ | .perform(ViewActions.click()); | ||
+ | |||
+ | System.out.println(" | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | === Execution in CodeBuild === | ||
+ | https:// | ||
+ | |||
+ | |||
+ | ==== Snippets ==== | ||
+ | |||
+ | === Preference Item with value in Summary === | ||
+ | |||
+ | Declare an own xml attribute for custom Views. Add this to **res/ | ||
+ | This attribute will contain the Mask for the summary, as known by // | ||
+ | < | ||
+ | < | ||
+ | <attr name=" | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Use own attribute | ||
+ | < | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | app: | ||
+ | </ | ||
+ | |||
+ | Implement own preference view | ||
+ | < | ||
+ | public class PreferenceSummaryValue extends EditTextPreference { | ||
+ | |||
+ | private String summaryFormat = " | ||
+ | |||
+ | public PreferenceSummaryValue(Context context) { | ||
+ | super(context); | ||
+ | init(null, | ||
+ | } | ||
+ | |||
+ | public PreferenceSummaryValue(Context context, AttributeSet attrs) { | ||
+ | super(context, | ||
+ | init(attrs, | ||
+ | } | ||
+ | |||
+ | public PreferenceSummaryValue(Context context, AttributeSet attrs, | ||
+ | int defStyle) { | ||
+ | super(context, | ||
+ | init(attrs, | ||
+ | } | ||
+ | |||
+ | private void init(AttributeSet attributes, Context context){ | ||
+ | if(attributes != null){ | ||
+ | TypedArray tarr = context.obtainStyledAttributes(attributes, | ||
+ | String attribute = tarr.getString(R.styleable.PreferenceSummaryValue_summaryFormat); | ||
+ | if(attribute != null) summaryFormat = attribute; | ||
+ | tarr.recycle(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void setText(String text) { | ||
+ | super.setText(text); | ||
+ | setSummary(String.format(summaryFormat, | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void setSummary(CharSequence summary) { | ||
+ | super.setSummary(summary); | ||
+ | } | ||
+ | |||
+ | public void setSummaryFormat(String summaryFormat) { | ||
+ | this.summaryFormat = summaryFormat; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ==== Layouts ==== | ||
+ | |||
+ | === TableLayout === | ||
+ | |||
+ | * Kann wirklich nur gleichmaeßige Zellen darstellen. | ||
+ | * Kann kein RowSpan | ||
+ | * Die Rows existieren als ein besonderes Objekt " | ||
+ | * Die Columns als besondere Objekte existieren nicht - es werden die CHildViews einfach gleich groß gehalten werden | ||
+ | * Nur gleiche layouts sollten als Columns benutzt werden. TableLayout mit 1 TabeRow und LinearLayout als Table Column klappt gut. | ||
+ | * Nutze Lieber GridLayout | ||
+ | |||
+ | |||
+ | <sxh java> | ||
+ | <?xml version=" | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | < | ||
+ | < | ||
+ | | ||
+ | <View android: | ||
+ | | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | < | ||
+ | < | ||
+ | | ||
+ | <View android: | ||
+ | | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | < | ||
+ | < | ||
+ | | ||
+ | <View android: | ||
+ | | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | ||
+ | |||
+ | | ||
+ | |||
+ | | ||
+ | | ||
+ | |||
+ | | ||
+ | </ | ||
+ | |||
+ | <!-- display this button in 3rd column via layout_column(zero based) --> | ||
+ | |||
+ | <!-- display this button in 2nd column via layout_column(zero based) --> | ||
+ | </ | ||
+ | |||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | === GridLayout === | ||
+ | * Has row-span which tableLayout does not have | ||
+ | * **CAN NOT DISTRIBUTE HORIZONTAL SPACE** e.g. when you need multiple columns which occupy the parent \\{{http:// | ||
+ | |||
+ | How to use GridLayout is described in this video: | ||
+ | {{youtube> | ||
+ | |||
+ | === LinearLayout === | ||
+ | * Can order items in a row. | ||
+ | * Children must have either a **given size** or a **weight** | ||
+ | * You can also set the view with some content to **wrap_content**. Then LinearLayout will **respect the size of content** for this view when distributing space via **weight**. | ||
+ | * CAN NOT make a child take all the **remaning space**. (Use **RelativeLayout** for that!) | ||
+ | |||
+ | Achieving that is easy, when the width of children is given: \\ | ||
+ | {{http:// | ||
+ | < | ||
+ | <?xml version=" | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Trying to make the table in the middle filling the remaning space fails, because the table \\ | ||
+ | {{http:// | ||
+ | |||
+ | Example about how to use weight, width, wrap_content: | ||
+ | |||
+ | {{http:// | ||
+ | |||
+ | {{http:// | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | === FlowLayout === | ||
+ | |||
+ | There is a custom FlowLayout here https:// | ||
+ | * Automatical **breaking the line to wrap the content**, when the space is exceeded. No default layout is able to do that. | ||
+ | |||
+ | Maven dependency: | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | {{http:// | ||
+ | {{http:// | ||
+ | |||
+ | |||
+ | ==== Dialogs ==== | ||
+ | The Dialogs are quite nice in Android. | ||
+ | They are customizable and may have a complete custom layout. | ||
+ | |||
+ | |||
+ | < | ||
+ | // Create custom dialog object with layout R.layout.dialog | ||
+ | |||
+ | final Dialog dialog = new Dialog(CustomDialog.this); | ||
+ | // Include dialog.xml file | ||
+ | dialog.setContentView(R.layout.dialog); | ||
+ | // Set dialog title | ||
+ | dialog.setTitle(" | ||
+ | |||
+ | // set values for custom dialog components - text, image and button | ||
+ | TextView text = (TextView) dialog.findViewById(R.id.textDialog); | ||
+ | text.setText(" | ||
+ | ImageView image = (ImageView) dialog.findViewById(R.id.imageDialog); | ||
+ | image.setImageResource(R.drawable.image0); | ||
+ | |||
+ | dialog.show(); | ||
+ | |||
+ | Button declineButton = (Button) dialog.findViewById(R.id.declineButton); | ||
+ | // if decline button is clicked, close the custom dialog | ||
+ | declineButton.setOnClickListener(new OnClickListener() { | ||
+ | @Override | ||
+ | public void onClick(View v) { | ||
+ | // Close dialog | ||
+ | dialog.dismiss(); | ||
+ | } | ||
+ | }); | ||
+ | </ | ||
+ | |||
+ | |||
+ | However it is difficult to style the dialog completele! | ||
+ | E.g. you can not make the DialogDivider in native DIalogs to have a custom color. | ||
+ | |||
+ | == android-styled-dialogs == | ||
+ | Allows to style the dialogs completele. | ||
+ | |||
+ | |On GitHub | ||
+ | |On Maven |< | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | < | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | {{https:// | ||
+ | |||
+ | |||
+ | == ListView AlerDialog == | ||
+ | |||
+ | ListView dialogs with custom Views in the list may be very useful! | ||
+ | They look native too so use them if possible! | ||
+ | |||
+ | To achieve that do the follwing: | ||
+ | |||
+ | == 1. Create a custom ListView Item== | ||
+ | * It should Inherit from a ViewGroup | ||
+ | * It should have **blockdescedants** attribute set. Otherwise you wont be able to choose it in the list | ||
+ | * Achtung: you may not use the methods, which make the items clickable, focuasble ... Otherwise the items in the list will not be clickable. < | ||
+ | //cant use this for items in lists using android.content.DialogInterface.OnClickListener | ||
+ | item.setClickable(true); | ||
+ | item.setFocusable(true); | ||
+ | item.setFocusableInTouchMode(true); | ||
+ | </ | ||
+ | |||
+ | See this thread: http:// | ||
+ | |||
+ | < | ||
+ | <?xml version=" | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | <Button | ||
+ | android: | ||
+ | style=" | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | </ | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | </ | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | </ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | == 2. Create a View class which will wrap the XML Layout == | ||
+ | <sxh java> | ||
+ | |||
+ | import android.content.Context; | ||
+ | import android.util.AttributeSet; | ||
+ | import android.view.LayoutInflater; | ||
+ | import android.widget.Button; | ||
+ | import android.widget.LinearLayout; | ||
+ | import android.widget.TextView; | ||
+ | |||
+ | public class DialogueTripItem extends LinearLayout{ | ||
+ | |||
+ | Context mContext; | ||
+ | |||
+ | TextView dialogTripsDestinations; | ||
+ | TextView dialogTripsTimes; | ||
+ | Button buttonChooseTrip; | ||
+ | |||
+ | public DialogueTripItem(Context context) { | ||
+ | super(context); | ||
+ | init(context); | ||
+ | } | ||
+ | public DialogueTripItem(Context context, AttributeSet attrs) { | ||
+ | super(context, | ||
+ | init(context); | ||
+ | } | ||
+ | public DialogueTripItem(Context context, AttributeSet attrs, int defStyle) { | ||
+ | super(context, | ||
+ | init(context); | ||
+ | } | ||
+ | |||
+ | private void init(Context context){ | ||
+ | this.mContext = context; | ||
+ | LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||
+ | inflater.inflate(R.layout.dialog__select_current_trip_item, | ||
+ | |||
+ | dialogTripsDestinations = (TextView) findViewById(R.id.dialogTripsDestinations); | ||
+ | dialogTripsTimes = (TextView) findViewById(R.id.dialogTripsTimes); | ||
+ | buttonChooseTrip = (Button) findViewById(R.id.buttonChooseTrip); | ||
+ | } | ||
+ | |||
+ | public void setDestinations(String dialogTripsDestinations){ | ||
+ | this.dialogTripsDestinations.setText(dialogTripsDestinations); | ||
+ | } | ||
+ | public void setDialogTripsTimes(String dialogTripsTimes){ | ||
+ | this.dialogTripsTimes.setText(dialogTripsTimes); | ||
+ | } | ||
+ | public Button getButton(){ | ||
+ | return buttonChooseTrip; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | == 3. Create the AlerDialog == | ||
+ | |||
+ | <sxh java> | ||
+ | ListAdapter adapter = new ListAdapter(); | ||
+ | final DialogueTripItem item = new DialogueTripItem(activity); | ||
+ | adapter.content.add(item); | ||
+ | |||
+ | AlertDialog.Builder builder = new IvuColorsAlertDialogBuilder(activity); | ||
+ | builder.setAdapter(adapter, | ||
+ | @Override | ||
+ | public void onClick(DialogInterface dialog, int which) { | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | } | ||
+ | }); | ||
+ | builder.setTitle(" | ||
+ | builder.setIcon(activity.getResources().getDrawable(R.drawable.icon_xhdpi)); | ||
+ | builder.setNegativeButton(" | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==== Installing an Application programmatically ==== | ||
+ | An APK installation may be started programmatically. | ||
+ | The use case was to put a second (library) application into the assets folder of the main applicaiton. Install the library application on demand. | ||
+ | |||
+ | Important: | ||
+ | * The applicaiton may not be started from the **assets** folder. It has to be copied to the external storage | ||
+ | |||
+ | <sxh java> | ||
+ | private void installApk() { | ||
+ | Intent intent = new Intent(Intent.ACTION_VIEW); | ||
+ | |||
+ | // the name of the apk in the assests folder | ||
+ | String assetFileName = " | ||
+ | |||
+ | // location where the app will be temporary copied to | ||
+ | File cacheFolder = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"// | ||
+ | |||
+ | // the ful path to the cache file | ||
+ | File fileCache = new File(cacheFolder, | ||
+ | |||
+ | // copy the file to the cache | ||
+ | try { | ||
+ | // needs android.permission.WRITE_EXTERNAL_STORAGE | ||
+ | fileCache.getParentFile().mkdirs(); | ||
+ | fileCache.createNewFile(); | ||
+ | |||
+ | // delete old file | ||
+ | if (fileCache.exists()) { | ||
+ | fileCache.delete(); | ||
+ | } | ||
+ | |||
+ | copyAsset(assetFileName, | ||
+ | |||
+ | } catch (Exception e) { | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | Toast.LENGTH_LONG); | ||
+ | } | ||
+ | |||
+ | if (!fileCache.exists()) { | ||
+ | // if (!assetExists(this.getAssets(), | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | Toast.LENGTH_LONG).show(); | ||
+ | return; | ||
+ | } | ||
+ | intent.setDataAndType(Uri.fromFile(fileCache)," | ||
+ | startActivity(intent); | ||
+ | } | ||
+ | |||
+ | private void copyAsset(String assetFileName, | ||
+ | AssetManager assetManager = getAssets(); | ||
+ | String[] files = null; | ||
+ | try { | ||
+ | files = assetManager.list("" | ||
+ | } catch (IOException e) { | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | Toast.LENGTH_LONG).show(); | ||
+ | } | ||
+ | for(String filename : files) { | ||
+ | if(!filename.equals(assetFileName)){ | ||
+ | continue; | ||
+ | } | ||
+ | |||
+ | InputStream in = null; | ||
+ | OutputStream out = null; | ||
+ | try { | ||
+ | in = assetManager.open(filename); | ||
+ | File outFile = new File(copyToFolderPath, | ||
+ | out = new FileOutputStream(outFile); | ||
+ | copyFile(in, | ||
+ | in.close(); | ||
+ | in = null; | ||
+ | out.flush(); | ||
+ | out.close(); | ||
+ | out = null; | ||
+ | } catch(IOException e) { | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | Toast.LENGTH_LONG).show(); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private void copyFile(InputStream in, OutputStream out) throws IOException { | ||
+ | byte[] buffer = new byte[1024]; | ||
+ | int read; | ||
+ | while ((read = in.read(buffer)) != -1) { | ||
+ | out.write(buffer, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | private static boolean assetExists(AssetManager assets, String name) { | ||
+ | try { | ||
+ | // using File to extract path / filename | ||
+ | // alternatively use name.lastIndexOf("/" | ||
+ | File f = new File(name); | ||
+ | String parent = f.getParent(); | ||
+ | if (parent == null) | ||
+ | parent = ""; | ||
+ | String fileName = f.getName(); | ||
+ | // now use path to list all files | ||
+ | String[] assetList = assets.list(parent); | ||
+ | if (assetList != null && assetList.length > 0) { | ||
+ | for (String item : assetList) { | ||
+ | if (fileName.equals(item)) | ||
+ | return true; | ||
+ | } | ||
+ | } | ||
+ | } catch (IOException e) { | ||
+ | // Log.w(TAG, e); // enable to log errors | ||
+ | } | ||
+ | return false; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==== Debugging HTTP Communication ==== | ||
+ | |||
+ | To Capture HTTP Communication of Android, | ||
+ | to simulate slow connection etc. use the following Tools: | ||
+ | |||
+ | * Genymotion Simulator - http:// | ||
+ | * Fiddler - http:// | ||
+ | * Connectify as alternative to Genymotion | ||
+ | |||
+ | |||
+ | In your virtual device, | ||
+ | |||
+ | * Go to Android settings menu | ||
+ | * In Wireless & Networks section, select Wi-Fi | ||
+ | * Press and hold for 2 seconds WiredSSID network in the list | ||
+ | * Choose Modify Network | ||
+ | * Check Show advanced options | ||
+ | * Select Manual for Proxy settings menu entry | ||
+ | * Enter the proxy address: the Fiddler-running PC's IPAddress and Port 8888 | ||
+ | * Press the Save button | ||
+ | |||
+ | In Fiddler, | ||
+ | |||
+ | * Click Tools menu > Fiddler Options > Connections | ||
+ | * Tick the Allow Remote Computers to connect box | ||
+ | * Restart Fiddler. | ||
+ | |||
+ | == As an alternative to Genymotion== | ||
+ | Use the Connectify, to connect your Phone with the Net via the PC with Fiddler. \\ | ||
+ | Configure FIddler as described above. | ||
+ | |||
+ | |||
+ | ==== Fragments ==== | ||
+ | |||
+ | == Fallpits == | ||
+ | * Fragments can not be nested. So use Fragments only for top level pieces when planning app's layout | ||
+ | |||
+ | |||
+ | Fragments can be defined in XML by using a | ||
+ | * a **fragment** tag | ||
+ | * a **class** attribute | ||
+ | |||
+ | < | ||
+ | < | ||
+ | class=" | ||
+ | android: | ||
+ | android: | ||
+ | </ | ||
+ | |||
+ | The minimal fragment class looks as following | ||
+ | |||
+ | <sxh java> | ||
+ | public class FragmentMainContent extends Fragment { | ||
+ | |||
+ | @Override | ||
+ | public View onCreateView(LayoutInflater inflater, ViewGroup container, | ||
+ | Bundle savedInstanceState) { | ||
+ | return inflater.inflate(R.layout.fragment_maincontent_layout, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | where the **fragment_maincontent_layout** may look as whatever you like. For example: | ||
+ | < | ||
+ | <?xml version=" | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | </ | ||
+ | |||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | * The function, which makes Android restore the Fragment without information loss // | ||
+ | |||
+ | // ACHTUNG: when using setRetainInstance(true) on a fragment to restore it's vars on configchange - check whether the fragment already exists before recreating it | ||
+ | this.fragmentDetails = (Fragment1Details) fragmentManager.findFragmentByTag(" | ||
+ | if(this.fragmentDetails==null) { | ||
+ | // create the fragment here and tag it with " | ||
+ | } | ||
+ | </ | ||
+ | * The function // | ||
+ | |||
+ | ^Class^Usage^Details^ | ||
+ | |FragmentManager|< | ||
+ | |FragmentTransaction|< | ||
+ | |||
+ | Fragments may be either created in xml or programmatically. | ||
+ | |||
+ | In XML | ||
+ | < | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | Programmatically. Programmatically fragment operations are done via **FragmentManager** which create **FragmentTranscation** \\ | ||
+ | **ACHTUNG: | ||
+ | < | ||
+ | // get Fragment manager | ||
+ | FragmentManager fragmentManager = getFragmentManager(); | ||
+ | |||
+ | // ACHTUNG: when using setRetainInstance(true) on a fragment to restore it's vars on configchange - check whether the fragment already exists before recreating it | ||
+ | this.fragmentDetails = (Fragment1Details) fragmentManager.findFragmentByTag(" | ||
+ | |||
+ | if(this.fragmentDetails==null) { | ||
+ | // start a transaction | ||
+ | FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); | ||
+ | // create a fragment | ||
+ | this.fragmentDetails = new Fragment1Details(); | ||
+ | // set the id to the fragment | ||
+ | // The Fragment should not be recreated when the Application is killed | ||
+ | this.fragmentDetails.setRetainInstance(true); | ||
+ | // add the fragment to the view | ||
+ | fragmentTransaction.add(R.id.fragmentContainer, | ||
+ | // //make transaction reversable by undoing the action on the fragmentmanager | ||
+ | // fragmentTransaction.addToBackStack(null); | ||
+ | // commit the transaction | ||
+ | fragmentTransaction.commit(); | ||
+ | |||
+ | // init the value from model of fragmentDetails. | ||
+ | myActivityModel.setText(myActivityModel.getText()); | ||
+ | }else{ | ||
+ | Log.d(TAG," | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ==== ScrolView ==== | ||
+ | To scroll the View continous prgrammatic scrolling. | ||
+ | To scroll continously the view has to be scrolled by a little delta in a regular time period. \\ | ||
+ | The period of time should be nearly 24 Frames per second, which is **every 41 ms**. \\ | ||
+ | The distance-change in px per ms depend from the speed you wish to achieve. | ||
+ | |||
+ | |0.1 - 0.2 PX per MS| Slow movement| | ||
+ | |0.4 - 0.7 PX per MS| Middle speed| | ||
+ | |1 PX - 1.4 PX per MS| Fast speed| | ||
+ | |||
+ | |12PFS| Seems to be not enough eince you can see the picture jerking | ||
+ | |24PFS| Seems to be ok | {{http:// | ||
+ | |36PFS| No difference to 24FPS | {{http:// | ||
+ | |||
+ | |||
+ | COde to test differend scroll speeds | ||
+ | |||
+ | <sxh java> | ||
+ | |||
+ | return new Thread(new Runnable() { | ||
+ | @Override | ||
+ | public void run() { | ||
+ | |||
+ | // scroll time | ||
+ | int scrollTimeMs = 4000; | ||
+ | |||
+ | // SPEED IN PX / MS, IF THE IMAGE WULD MOVE CONTIONOUSLY | ||
+ | double pxPerMsStart = 0.2; | ||
+ | double pxPerEnd = 0.8; | ||
+ | double pxPerMsChangeBy = 0.1; | ||
+ | |||
+ | // FPS | ||
+ | int fpsStart = 24; //per sec | ||
+ | int fpsEnd = 24; | ||
+ | int fpsChangeBy = 12; | ||
+ | |||
+ | |||
+ | // iterate pause time | ||
+ | for(double scrollspeedPxPerMs = pxPerMsStart; | ||
+ | |||
+ | // iterate steps | ||
+ | for(int fps=fpsStart; | ||
+ | |||
+ | // pauses in ms are computed from fps | ||
+ | pauseMs | ||
+ | |||
+ | /* the speed is given for the case, when the image moves contingously | ||
+ | because there is a redraw pause between movements - we have to multiply the pause in MS with the speed | ||
+ | to know to which amount the image has to be moved after every pause | ||
+ | */ | ||
+ | steppx = (int) Math.round((double)pauseMs * scrollspeedPxPerMs); | ||
+ | |||
+ | |||
+ | // timestemp for next iteration | ||
+ | timestampWhenToStop = System.currentTimeMillis() + scrollTimeMs; | ||
+ | |||
+ | // annouce settings | ||
+ | final String message = String.format(" | ||
+ | Log.d(" | ||
+ | |||
+ | // jump to the top | ||
+ | ViewGroupAnimatedActivity6.this.runOnUiThread(new Runnable() { | ||
+ | public void run() { | ||
+ | Toast.makeText(getApplicationContext(), | ||
+ | scrollView.setSmoothScrollingEnabled(false); | ||
+ | scrollView.scrollTo(0, | ||
+ | scrollView.setSmoothScrollingEnabled(true); | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | // scroll to the bottom | ||
+ | boolean stopIteration = false; | ||
+ | while (!stopIteration){ | ||
+ | |||
+ | ViewGroupAnimatedActivity6.this.runOnUiThread(new Runnable() { | ||
+ | public void run() { | ||
+ | scrollView.smoothScrollBy(0, | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | if(timestampWhenToStop < System.currentTimeMillis()){ | ||
+ | stopIteration = true; | ||
+ | continue; | ||
+ | } | ||
+ | try { | ||
+ | Thread.sleep(pauseMs); | ||
+ | } catch (InterruptedException e) { | ||
+ | e.printStackTrace(); | ||
+ | } | ||
+ | |||
+ | if(!isRunning){ | ||
+ | return; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | }// for fps | ||
+ | } // for pxPerEnd | ||
+ | |||
+ | isRunning = false; | ||
+ | } | ||
+ | }); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ==== Gradle ==== | ||
+ | |||
+ | To execute gradle tasks - a gradle wrapper file is used. | ||
+ | The wrapper is a shell / batch script, which downloads the gradle and so does not require gradle to be installed previously. | ||
+ | The wrapper uses it's own gradle version | ||
+ | The name of the file is **gradlew.bat** | ||
+ | |||
+ | To generate the gradle wrapper use gradle and add the wrapper : | ||
+ | < | ||
+ | gradle wrapper | ||
+ | </ | ||
+ | |||
+ | |||
+ | ==== AIDL==== | ||
+ | **aidl** | ||
+ | |||
+ | - all aidl have to be located in th same packages | ||
+ | - can pass Parcels or primitives | ||
+ | - implement aidl to be able to retrieve a common interface to communicate between different applicaitons, | ||
+ | < | ||
+ | public void onServiceConnected(ComponentName name, IBinder service) { | ||
+ | IServiceReadTicketBinder serviceReadTicketBinder = IServiceReadTicketBinder.Stub.asInterface(service); | ||
+ | // IServiceReadTicketBinder serviceReadTicketBinder = (IServiceReadTicketBinder)service; | ||
+ | </ | ||
+ | |||
+ | - add in out inout as stated here http:// | ||
+ | - use Bundle.class to exchange data with the Service. It provides possibility to restore the stored values typesafe. Bundle is like a Hashmap which can store different types of values result.getString(" | ||
+ | |||
+ | |||
+ | ==== Bindings ==== | ||
+ | |||
+ | The bare minimum in code | ||
+ | <sxh java> | ||
+ | @Override | ||
+ | public View onCreateView(LayoutInflater inflater, ViewGroup container, | ||
+ | | ||
+ | |||
+ | final ViewDataBinding binding = DataBindingUtil.inflate( | ||
+ | inflater, R.layout.fragment_poidialog, | ||
+ | final View view = binding.getRoot(); | ||
+ | return view; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The binding in xml | ||
+ | |||
+ | https:// | ||
+ | < | ||
+ | <?xml version=" | ||
+ | <layout | ||
+ | xmlns: | ||
+ | xmlns: | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | > | ||
+ | |||
+ | <include | ||
+ | layout=" | ||
+ | app: | ||
+ | /> | ||
+ | |||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | <?xml version=" | ||
+ | <layout | ||
+ | xmlns: | ||
+ | > | ||
+ | |||
+ | < | ||
+ | // declare fields | ||
+ | < | ||
+ | name=" | ||
+ | type=" | ||
+ | </ | ||
+ | |||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | |||
+ | </ | ||
+ | |||
+ | </ | ||
+ | |||
+ | |||
+ | |||
+ | ==== Style ==== | ||
+ | |||
+ | === Styles, attributes === | ||
+ | |||
+ | {{https:// | ||
+ | |||
+ | |||
+ | === TextAppearance === | ||
+ | The text size in android is controlled by " | ||
+ | |||
+ | |||
+ | Material Design provides 13 type “styles” that are applied to all the text in your app. Each of these have a design term (eg. “Body 1”) along with a corresponding type attribute that can be overridden in your app theme (eg. textAppearanceBody1). There are default “baseline” values (text size, letter spacing, capitalization, | ||
+ | |||
+ | https:// | ||
+ | |||
+ | {{https:// | ||
+ | |||
+ | |||
+ | == Usage in your view == | ||
+ | |||
+ | At the end, | ||
+ | to express which size the text in your custom view should have | ||
+ | instead of influencing " | ||
+ | which will lead to difficulties with consistancy across app and consistancy with android native textSizes | ||
+ | |||
+ | you should set " | ||
+ | |||
+ | and the value should be one of androids own textAppearance **attributes**: | ||
+ | * textAppearanceHeadline1 | ||
+ | * textAppearanceHeadline2 | ||
+ | * textAppearanceHeadline3 | ||
+ | * textAppearanceHeadline4 | ||
+ | * textAppearanceBody1 | ||
+ | * textAppearanceBody2 | ||
+ | |||
+ | The same attributes are used by android too, so your view will be consistant with android natives | ||
+ | |||
+ | In your someAppPartLayout.xml | ||
+ | < | ||
+ | <!-- My title in my app view --> | ||
+ | < | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | android: | ||
+ | </ | ||
+ | |||
+ | |||
+ | == Changing the value of attributes in AppTheme == | ||
+ | |||
+ | In your theme, maybe defined in your styles.xml | ||
+ | you can change single android attributes. | ||
+ | |||
+ | In my theme, which is defined in this style - overriding the single attributes " | ||
+ | referencing MY styles, which can interfere and override the default values for textAppearance. | ||
+ | |||
+ | **Remark: | ||
+ | Those textAppearance* attributes are also used in android native views, | ||
+ | like buttons | ||
+ | see https:// | ||
+ | So overriding those in a theme - changes the behavior also for Android native elements | ||
+ | |||
+ | THose attributes are not just strings, but are introduced as typed attributes in attrs.xml | ||
+ | with format=" | ||
+ | format=" | ||
+ | In the value-style element - the items a la android: | ||
+ | |||
+ | USAGE of attributes | ||
+ | The attribute can be USED in views, by refering to the attribute in the view | ||
+ | e.g. | ||
+ | < | ||
+ | |||
+ | In your styles.xml pointing textAppearanceHeadline1 attribute of type " | ||
+ | to my own style. | ||
+ | |||
+ | The style inherits from androids own style but I still can override some values in my inherited style. | ||
+ | |||
+ | < | ||
+ | <!-- Base application theme. --> | ||
+ | <style name=" | ||
+ | <!-- Customize your theme here. --> | ||
+ | <item name=" | ||
+ | <item name=" | ||
+ | <item name=" | ||
+ | |||
+ | |||
+ | <item name=" | ||
+ | @style/ | ||
+ | </ | ||
+ | </ | ||
+ | |||
+ | |||
+ | == Introducing own styles, to refer from overridden attributes textAppearance* == | ||
+ | |||
+ | THe value of an overridden attribute **textAppearance*** is a style | ||
+ | |||
+ | |||
+ | in your styles.xml | ||
+ | parallel to the AppTheme - introduce styles, to refer from attributes. | ||
+ | < | ||
+ | |||
+ | <!-- here I can change attributes of the STYLES. | ||
+ | by Inheriting from " | ||
+ | but get the possibility to override them here | ||
+ | --> | ||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | |||
+ | <style name=" | ||
+ | <item name=" | ||
+ | </ | ||
+ | </ | ||